본문 바로가기
Kernel/理解

Block IO 분석

by 暻煥 2024. 3. 24.

Kernel의 Block Layer 내용 정리


Storage IO Framework

  • File System Layer
    • Disk와 Memory 사이의 Mapping 작업 수행
    • Search 및 Access를 위한 Data 저장 未 구성
  • Block Layer
    • File System 요청에 따라 Data를 전달
    • IO Scheduling 기능을 이용하여 요청을 병합/분할
  • SCSI Layer
    • Data 전송 기능을 수행하며 SCSI Disk 식별
    • 일반적인 임베디드 환경에서는 SCSI Layer 도움 없이 Device로 바로 연결
  • LLDD (Low Level Device Dirver)
    • Data 전송 기능을 수행하며 특정 Device 구동 기능 제공
  • IO submit/complete
    • 각 Layer는 하위에 요청을 전달할때 그에 상응하는 Call Back 함수를 등록
    • 일반적인 경우 Hardware의 Interrupt에 의하여 Call Back 시작 (Timer, Polling 방식을 통해서도 구현)
    • 예시
      • SCSI Layer : 요청 = scsi_queue_rq() ↔ 완료 = scsi_done()
      • libSAS Layer : 요청 = sas_queuecommand() ↔ 완료 = sas_scsi_tak_done()

bio data structure

  • bio
    • bi_iter 및 bi_vec 포함
    • bio_vec
      • IO Data의 Memory 內 위치 설명
      • bv_page : Memory 上 Page를 가리킴
      • bv_len : 연속된 Memory의 길이
      • bv_offset : Page 內 Offer
    • bi_iter
      • IO Data의 Disk 內 위치 설명
      • 이전 까지 완료된 IO 상태 설명
      • bi_sector : 해당하는 Disk의 Sector 번호
      • bi_size : 완료되지 않은 IO 크기
      • bi_idx : 현재 처리중인 bio_vec의 index 번호
      • bi_bvec_done : 처리 완료한 bio_vec의 Byte 크기
    • bi_next
      • 다음번에 처리할 bio
    • bi_pool
      • bio 구조체의 mempool
    • bi_inline_vecs[]
      • BIOSET_NEED_BVECS 설정 時 활용됨
      • 연속적인 IO Data의 갯수가 많지 않다고 예상될 경우, bio_vec 할당 필요가 없으므로 성능 개선

bio_set & biovec_slab

  •  bio_set
    • bio 구조체 할당을 위한 Cache(SLAB)
    • 할당 크기 : front_pad + sizeof(struct bio) + back_pad
    • back_pad : BIOSET_NEED_BVECS 설정 시, bio_vec에서 활용
  • biovec_slab
    • bio_vec 구조체 할당을 위한 Cache(SLAB)
    • 4 종류의 크기 제공
      • biovec-16 : 16개의 bio_vec, 16개 이하의 연속 Memory IO 필요 時 사용
      • biovec-64 : 16개의 bio_vec, 64개 이하의 연속 Memory IO 필요 時 사용
      • biovec-128 : 16개의 bio_vec, 128개 이하의 연속 Memory IO 필요 時 사용
      • biovec-BIO_MAX_VECS : 256개의 bio_vec, 256개 이하의 연속 Memory IO 필요 時 사용
    • 만약 IO 요청된 연속 물리 Memory 갯수가 4개 이하인 경우 bi_inline_vecs[] 사용

blk-mq

  • Kernel v5.0부터 Multi-Queue를 Default 사용
  • File System Layer에서 요청된 bio → request에 포함되어 하위 Layer로 전달
  • Software Queue (struct blk_mq_ctx)
    • 각 CPU 마다 존재
    • bio는 1차적으로 해당 queue에 request 형태로 삽입
  • Hardware Dispatch Queue (struct blk_mq_hw_ctx)
    • 각 Block Device마다 존재
    • Software Queue에 삽입된 request를 해당하는 Hardware Queue에 Mapping

IO 전달 개요

  • current->bio_list : 本 task가 요청한 bio
  • 요청된 bio가 Device까지 전달에는 크게 3가지 경로 존재
    1. plug기능 사용 時, plug pool 내부에 충분한 갯수의 bio 누적후 IO scheduler에게 전달
    2. plug기능 미사용 時, IO Scheduler에게 전달 (혹은, block mq context에게 전달)
    3. IO Scheduler 미사용 時, Device Driver에게 바로 전달

  1. 만약 이미 blk_mq_submit_bio() 수행중인 경우, bio를 current->bio_list에 삽입
  2. current->bio_list의 bio를 순회하면서 하나씩 처리
  3. 만약 IO요청된 Page가 HighMem인 경우 LowMem 영역의 하나 할당하여 Bounce Mechanism 사용
  4. IO 요청된 크기가 너무 큰 경우 분할
  5. blk_plug->mq_list 및 blk_mq_ctx->rq_lists 혹은 IO Scheduler list에 병합 시도
  6. 모든 병합시도 실패 時, 새로운 request 할당
  7. plug 기능 사용 時, fulsh를 위한 갯수조건 충족 시 request flush
  8. IO Scheduler 사용 時, Scheduler lists에게 request 전달
  9. IO Scheduler 미사용 時, Device Driver에게 request 전달
  • 일반적으로 빠른 Disk 사용 시, IO Scheduler를 사용하지 않는것이 성능상 이득
    • Scheduler Logic에 의한 약간의 부하 존재

request & bio

  • 하나의 request는 여러개의 bio로 구성
  • 동일한 request내의 bio는 물리적으로 연속된 Disk의 위치를 가르킴
  • __sector 변수 : request中 disk의 시작위치

bio split

각 block device request_queue의 limitation 존재

  • request_queue->limits.max_segments
    • 하나의 request 內 segment의 최댓값
    • blk_queue_max_segments() 함수를 통해 접근
    • Default : BLK_MAX_SEGMENTS = 128
  • request_queue->limits.max_segment_size
    • 하나의 segment 최대크기
    • blk_queue_max_segment_size() 함수를 통해 접근
    • Default : BLK_MAX_SEGMENT_SIZZ = 65536 = 64 * 4K
  • request_queue->limits.max_sectors
    • 한번에 전송하는 request 內 sector의 최댓수. 즉, IO request의 최대 크기
    • blk_queue_max_hw_sectors() 함수를 통해 접근
    • Default : BLK_DEF_MAX_SECTORS = 2560

만약 상기의 limitation 초과하는 request 발생 時, bio_split()에 의하여 bio 분할.

bio_split()을 통해 첫번째 sector의 bio를 분할하여 반환.

 

만약, 아래 그림과 같이 split 이전에 bi_idx가 0을 가르키고 있는 상황이라면

새로 생성된 bio의 크기는 bio_vec 0번의 크기와 동일

bio_vec는 이전 bio와 공유하며, bvc_iter를 통해서 구분


bio merge

  • Disk 상에서 인접한 bio는 merge 가능
  • 일반적으로 bio merge를 통해서 request 횟수를 줄여 성능 향상
  • 만약 random access가 훨씬 많은 상황 時, merge 자주 시도하는 것 자체가 성능에 불이익

  • Sector의 연결방향에 따라서 2가지 종류의 merge 가능
    • Front Merge
    • Back Merge

  • 먼저 blk_mq_ctx->rq_list 내부에서 merge 시도
  • 이후에 IO scheduler를 통한 merge를 시도
  • IO Scheduler가 비활성화 상태이면 blk_mq_ctx->rq_lists 내부에서 merge 시도

bio bounce

  • IO 대상 Memory가 HIGH_MEM인 경우, DMA 불가
  • HIGH_MEM 대상으로 IO를 수행하기 위해 bounce buffer 활용
  • bounce buffer 向 Data Copy 後 IO 수행

  • bio의 segment 中 HIGH_MEM 존재 시, Bounce Buffer 필요
  • HIGH_MEM Page가 존재하고 segment의 갯수가 최대를 초과하면, bio_split() 後 sbumit_bio() 수행
  • new bio할당 후, 이전의 관련정보 복사
  • bio의 segment에 대하여 각 HIGH_MEM에 대응하는 새로운 bounce page 할당, 만약 write 동작이라면 page 내부의 data 복사 필요
  • bio->bi_end_io 등록, read operation 종료 후 원래의 page로 data 복사


get request

  • block layer에서 request는 sbitmap을 통한 tag로 관리
  • 하나의 tag는 하나의 request에 대응

( sbitmap의 구조 및 원리에 대해서는 다른 글을 통해서 기술하도록 한다. 여기서는, 성능 향상을 위해서 request 할당에 bitmap이 아닌 sbitmap을 사용했다는 내용만 말한다.)

  • request를 위한 Memory는 부팅 초반에 할당되며 sbitmap으로 관리
  • blk_mq_alloc_tag_set() 함수를 통해서 부팅 초반에 sbitmap 할당
    • 해당 함수는 request를 할당하는것 외에도 hctx와 CPU를 mapping하는 기능도 수행
  • 각 hctx마다 nr_tags 와 nr_reserved_tags를 이용해서 tag의 get/put 관리

  • blk_mq_tag_set : 전역 변수, 각 hctx에 대응되는 blk_mq_tags 관리
  • blk_mq_tags->static_rqs : 부팅초반에 준비된 request 관리
    • SCSI Command와 각 driver가 사용하게될 private data 포함
  • 미리 할당된 request 갯수 = nr_hw_queues * queue_depth

plug & unplug

  • blk_plug->mq_list를 통해서 IO 전달되는 경우 PLUG/UNPLUG 방법 활용
    • plug -> IO 요청을 수집 -> unplug -> 수집한 IO를 한번에 전달
  • plug 시점
    • blk_start_plug() : task_struct->blk_plug 초기화
    • blk_add_rq_to_plug() : request를 blk_plug->mq_list에 삽입
    • 만약 mq_list가 여러개의 request_queue로 구성된 경우 multiple_queues 설정
  • unplug 시점
    • 3가지 종류의 조건에 의하여 unplug
      • 누적된 IO갯수가 BLK_MAX_REQUEST_COUNT를 초과하거나, IO 요청된 크기가 BLK_PLUG_FLUSH_SIZE를 초과하는 경우
      • blk_finish_plug()를 호출하는 경우
      • scheduler에 의하여 sche_submit_work()가 호출되어 비동기적으로 flush 하는 경우
    • unplug core function : blk_flush_plug_list()
    • 만약 multiple_queues 설정된 경우, mq_ctx & mq_hctx & sector_number 기준으로 정렬 後 flush
      • 동일한 ctx 및 hctx 속하는 IO는 묶어서 blk_mq_sched_insert_requests() 호출

동기 및 비동기 IO

  • 동기 IO
    • queue_rq() 함수를 바로 호출하여 IO 수행
    • IO 완료까지 대기 후, 다음 IO를 이어서 수행
  • 비동기 IO
    • blk_mq_hw_ctx->run_work를 통해서 수행
      • 여러개의 work를 각 CPU 나누어서 수행
      • 각 work는 blk_mq_hw_ctx->cpumask 中 polling 방식으로 선택
      • 각 CPU에서 수행되는 work의 갯수 : BLK_MQ_CPU_WORK_BATCH
      • run_work는 최종적으로 queue_rq() 호출

Direct IO

  • q->mq_ops->queue_rq()를 통해서 IO 전달
    • System Resource 부족 時 (ex. tag 부족), hctx->dispatch에 삽입
    • blk_mq_run_hw_queue() 함수를 통해서 동기식 IO 수행

IO with scheduler

  • IO scheduler를 통합 정렬 및 병합이 필요한 경우, blk_mq_sched_insert_request() ghcnf
  • 만약, IO scheduler가 없으면 IO를 blk_mq_ctx->rq_lists에 삽입 (각 CPU마다 존재하는 list)
  • 최종적으로는 blk_mq_run_hw_queue() 호출

blk_mq_run_hw_queue

  • 동기 IO 및 비동기 IO 모두 최종적으로는 blk_mq_run_hw_queue() 함수 호출

  • 만약 비동기 IO인 경우, workqueue->run_work 수행
  • 동기 IO인 경우, hctx->dispatch 먼저 검사하여 완료되지 못한 IO 먼저 수행
    • resource 부족인 경우 request는 dispatch list에 존재
  • IO scheduler가 존재하는 경우 scheduler를 통해서 IO 수행
  • 만약 scheduler가 없는 경우 ctx->rq_lists 통해서 IO 수행

Core Functions

  • blk_mq_dispatch_rq_list(struct blk_mq_hw_ctx *hctx, struct list_head *list, unsigned int nr_budgets)
    • 동일한 hctx 內 rq_list의 request 수행
    • 자원부족 등으로 인한 실패 時, true return
    • IO 완료 時, false return
  • blk_mq_do_dispatch_sched(struct blk_mq_hw_ctx *hctx)
    • IO scheduler로부터 max_dispatch 갯수의 IO 수행
    • 수행되는 IO가 서로 다른 hctx에 속한다면 hctx idx 순서에 따라 IO 수행
    • 동일한 hctx에 속한다면 blk_mq_dispatch_rq_list() 함수 호출
  • blk_mq_do_dispatch_ctx(struct blk_mq_hw_ctx *hctx)
    • Round Robin 방식에 따라서 여러개의 ctx에 속한 IO를 hctx에 전달


IO Complete

  • 각각의 IO request에 대해서 완료 時, blk_mq_complete_request() 호출
  • IO 완성의 3가지 경로
    • 현재의 CPU에서 request 완료
    • soft irq를 통한 request 완료
    • ipi를 통해서 다른 CPU에서 request 완료

참고 및 출처

.

'Kernel > 理解' 카테고리의 다른 글

Scatter Gather List  (0) 2024.04.10
Device Tree 분석 (Kernel Source)  (0) 2024.02.02
Device Tree 문법  (0) 2024.02.02
ZRAM 분석  (0) 2024.02.02
ELF 실행 & execve  (0) 2024.02.02