ch14. block device driver
블록 디바이스 드라이버,
느린 디스크를 빠른 시스템처럼
CPU는 나노초 단위로 동작하지만 디스크는 물리적 구동 탓에 밀리초 단위로 움직입니다. 커널은 이 거대한 속도 차이를 극복하기 위해 I/O 요청을 모으고, 정렬하고, 병합하여 가장 최적화된 시점에 드라이버로 전달합니다. 이 강의 노트에서는 단순한 read() 호출이 실제 디스크 제어 명령으로 변환되기까지의 전 과정을 상세히 살펴봅니다.
큰 그림: 블록 디바이스 드라이버는 "느린 디스크를 빠른 시스템처럼 보이게 하는 기술"WHY BLOCK I/O IS COMPLEX
CPU는 명령어를 나노초 단위로 처리합니다. 반면, 하드디스크는 데이터를 읽기 위해 물리적으로 헤드를 움직이고 원하는 위치를 찾아가야 하므로 밀리초 단위의 시간이 소모됩니다. 이 장의 핵심은 바로 '디스크가 매우 느리다'는 근본적인 한계에서 출발합니다.
CPU 및 버스의 속도와 디스크 하드웨어의 접근 시간 사이에는 엄청난 간극이 존재합니다. 디스크는 한 번 정확한 위치를 찾으면 빠르게 데이터를 전송할 수 있지만, 탐색 자체에 드는 비용이 막대하므로 이를 보완하기 위한 커널의 I/O 구조 역시 정교하고 복잡해질 수밖에 없습니다. 따라서 커널은 발생한 요청을 즉시 처리하지 않고 잠시 큐(Queue)에 모아둔 뒤, 순서를 재배치하고 병합하여 드라이버에 넘겨줍니다.
CPU와 디스크의 속도 차이는 마치 책상 위 메모지를 집어 드는 일과, 도서관 지하 서고까지 직접 내려가 책을 찾아오는 일의 차이와 같습니다. 커널의 블록 I/O 하위 시스템은 이 엄청난 간극을 최소화하기 위해 마련된 고도로 정교한 완충 장치입니다.
이번 학습을 통해 다음 세 가지 핵심 질문에 답할 수 있어야 합니다. 첫째, 사용자의 파일 읽기 요청이 어떻게 디스크의 특정 섹터 요청으로 변환되는가? 둘째, 커널은 왜 요청을 즉시 처리하지 않고 큐에 쌓아두는가? 셋째, 블록 디바이스 드라이버는 정확히 어느 시점에 물리적 하드웨어에 접근하는가?
이 장의 목적은 단순히 "개별 디스크 드라이버 코드를 짜는 법"을 배우는 것이 아닙니다. 리눅스 커널이 드라이버 구현을 쉽게 하도록 제공하는 bio, request, request_queue, gendisk, block_device와 같은 공통 자료구조들이 어떻게 유기적으로 맞물려 돌아가는지 그 생태계를 이해하는 것이 더 중요합니다.
핵심 O/X 퀴즈
1. 블록 디바이스 처리 과정의 가장 큰 과제는 CPU와 디스크 접근 시간 사이의 엄청난 속도 차이를 극복하는 것이다.
2. 사용자 프로그램에서 read()를 호출하면 그 즉시 디스크 컨트롤러로 명령이 전달된다.
3. 이 장은 특정 디스크의 물리적 동작 원리보다, 리눅스 블록 I/O 시스템을 구성하는 공통 커널 구조체들의 연결성을 이해하는 데 중점을 둔다.
read() 요청의 여행: VFS에서 디스크 컨트롤러까지Figure 14-1 WALKTHROUGH
어떤 프로세스가 파일 내부의 데이터를 가져오기 위해 read() 시스템 콜을 호출했다고 가정해 봅시다. 마치 우리가 온라인으로 물건을 주문할 때 단순히 '결제' 버튼만 누르면 되는 것 같지만, 이면에서는 재고 파악, 창고 분류, 배송 경로 최적화, 기사 배정 등 복잡한 물류 시스템이 가동되는 것과 동일한 이치입니다.
bio 구조체를 생성합니다.핵심 O/X 퀴즈
1. VFS는 리눅스가 여러 종류의 파일시스템을 단일한 인터페이스로 취급할 수 있도록 도와주는 계층이다.
2. 디스크 캐시에 이미 데이터가 존재하더라도 데이터의 무결성을 위해 무조건 실제 디스크를 한 번은 읽어야 한다.
3. 매핑 계층의 주된 역할은 파일 수준의 블록 번호를 실제 장치 수준의 논리 블록 번호로 변환하는 것이다.
섹터, 블록, 페이지, 세그먼트: 같은 데이터를 바라보는 네 개의 눈Figure 14-2
섹터, 블록, 페이지, 세그먼트 — 이들은 모두 커널이 다루는 '데이터의 조각'을 뜻하지만, 처리하는 주체에 따라 기준이 다릅니다. 이 용어들의 미묘한 차이를 명확하게 정립해 두어야 이후 I/O 흐름을 이해하기 수월해집니다.
- 섹터 (sector)
- 하드웨어 컨트롤러가 데이터를 전송하는 가장 기본적인 단위입니다. 리눅스 커널 내부에서는 관례적으로 이 크기를 512바이트로 고정하여 사용합니다. 실제 하드웨어가 4KB 섹터를 사용하더라도, 하위 드라이버 단에서 적절히 변환하여 처리합니다.
- 블록 (block)
- VFS 및 파일시스템이 데이터를 관리하고 취급하는 논리적 단위입니다. 크기는 반드시 2의 거듭제곱이어야 하며, 시스템 페이지 프레임 크기를 초과할 수 없고 섹터 크기의 정수배여야 합니다. (예: 512, 1024, 2048, 4096 바이트)
- 페이지 (page)
- 메모리 관리 하위 시스템과 디스크 캐시가 동작하는 단위입니다. 예를 들어 4KB 페이지 하나에는 1KB 크기의 블록 버퍼 네 개가 꽉 차게 들어갈 수 있습니다.
- 세그먼트 (segment)
- 블록 디바이스 드라이버가 DMA 전송을 수행할 때 바라보는 메모리 조각입니다. 보통 하나의 메모리 페이지 전체이거나 그 일부이며, 디스크 상에서 물리적으로 인접한 섹터들의 데이터를 담게 됩니다.
하나의 동일한 아파트 단지를 두고 행정기관은 '동' 단위로 파악하고, 택배 회사는 '배송 권역'으로 관리하며, 거주자는 개별 '호수'로 인식하는 것과 같습니다. 하드웨어는 섹터, 파일시스템은 블록, 캐시는 페이지, 드라이버는 세그먼트 단위로 대상을 바라보며, Generic Block Layer는 이 각기 다른 시선들을 하나로 이어주는 번역기 역할을 합니다.
Figure 14-2를 상기해 보면, 동일한 RAM 영역일지라도 계층에 따라 어떻게 해석되는지 잘 알 수 있습니다. 4KB 페이지 하나가 파일시스템 관점에서는 1KB 블록 버퍼 네 개로 나뉘어 보이지만, 그중 뒤쪽 3KB 영역이 단일 세그먼트로 묶여 컨트롤러 관점에서는 512바이트 크기의 섹터 여섯 개로 다뤄지게 됩니다.
핵심 O/X 퀴즈
1. 리눅스의 블록 I/O 하위 시스템에서 섹터의 크기는 관례적으로 512바이트 단위로 고정하여 표기한다.
2. 하나의 하드디스크 내에 존재하는 모든 파티션은 예외 없이 동일한 파일시스템 블록 크기를 가져야만 한다.
3. Generic Block Layer는 섹터, 블록, 페이지, 세그먼트라는 서로 다른 관점의 단위들을 매끄럽게 연결해주는 중추적인 역할을 수행한다.
DMA와 세그먼트: 드라이버는 블록을 모른다, 전송할 메모리 조각을 본다SCATTER-GATHER DMA
대부분의 디스크 I/O 작업은 '디스크상의 인접한 섹터 영역'과 '메모리(RAM)상의 특정 영역' 간의 데이터 교환으로 이루어집니다. 이때 CPU가 일일이 바이트 단위로 데이터를 옮긴다면 막대한 시스템 오버헤드가 발생하므로, 컨트롤러가 메모리에 직접 접근하여 전송을 대신 처리하는 DMA (Direct Memory Access) 기술이 필수적으로 쓰입니다.
- 단순 DMA
- 메모리상에서도 물리적으로 완전히 연속된 공간에 대해서만 전송이 가능한 구형 방식입니다. 구식 컨트롤러 환경에서 주로 발견됩니다.
- Scatter-gather DMA
- 디스크의 연속된 섹터에서 데이터를 읽어오되, 이를 메모리의 여러 흩어진(비연속적인) 영역에 나누어 담거나 반대로 모아 쓸 수 있는 최신 기법입니다. 드라이버는 컨트롤러에 시작 섹터와 총 섹터 수, 그리고 데이터를 담을 메모리 주소 및 길이의 '목록'을 한 번에 전달합니다.
블록 디바이스 드라이버 입장에서는 파일시스템의 블록 크기나 블록 버퍼의 존재를 굳이 알 필요가 없습니다. 상위 계층에서는 페이지 하나를 잘게 쪼개어 여러 블록 버퍼로 관리하더라도, 가장 아래쪽의 드라이버는 단순히 "이 메모리 세그먼트들을 하드웨어로 밀어 넣으면 된다"는 사실에만 집중합니다. 논리적 파일시스템의 세계와 물리적 하드웨어 전송의 세계가 이 지점에서 완벽하게 분리됩니다.
여기서 짚고 넘어가야 할 또 다른 중요 개념이 세그먼트 병합(Segment Merging)입니다. 만약 RAM의 특정 페이지 프레임들이 물리적으로 연속되어 있고, 대응하는 디스크의 대상 영역도 인접해 있다면, Generic Block Layer는 잘게 나뉜 세그먼트들을 하나의 더 큰 물리적 세그먼트(physical segment)로 통합해 전송 효율을 극대화합니다.
핵심 O/X 퀴즈
1. DMA는 디스크 컨트롤러가 귀중한 CPU 자원을 소모하지 않고 메모리에 직접 접근해 데이터를 복사하는 효율적인 메커니즘이다.
2. Scatter-gather DMA 방식은 메모리상에 흩어져 있는 여러 비연속적인 영역들을 단 한 번의 컨트롤러 전송 작업으로 묶어서 처리할 수 있게 해준다.
3. 블록 디바이스 드라이버가 I/O 요청을 올바르게 수행하려면 해당 파티션의 파일시스템 블록 크기를 반드시 사전에 숙지하고 있어야 한다.
Generic Block Layer와 bio: 블록 I/O 요청의 표준 포장지THE bio STRUCTURE
커널 내부에서 수많은 I/O 요청이 이리저리 이동할 때 단순히 구두로 "이거 읽어줘"라고 지시할 수는 없습니다. 택배 화물에 도착지 주소와 내용물 정보를 명시한 표준 송장이 부착되듯이, 블록 I/O 요청 역시 처리 규격에 맞춘 단일화된 포장지가 필요합니다. 그 핵심 구조체가 바로 bio입니다.
- bi_sector
- 접근하고자 하는 디스크 상의 첫 시작 섹터 번호.
- bi_bdev
- 요청의 대상이 되는 논리적 블록 디바이스 객체를 가리키는 포인터.
- bi_rw
- 데이터의 전송 방향을 나타내는 플래그 (READ 또는 WRITE).
- bi_io_vec
- 개별 세그먼트 정보를 담은
bio_vec배열을 가리키는 포인터. - bi_vcnt
- 배열에 포함된
bio_vec세그먼트의 총 개수. - bi_idx
- 요청 처리 중 아직 전송되지 않은 첫 번째 세그먼트의 인덱스를 지속적으로 추적.
- bi_end_io
- 모든 전송 작업이 성공 혹은 실패로 완전히 끝났을 때 커널이 호출해 줄 콜백 함수.
- bv_page
- 해당 세그먼트의 실제 데이터가 존재하는 물리적 메모리 페이지 프레임 포인터.
- bv_len
- 이 세그먼트가 차지하는 데이터 길이 (바이트 단위).
- bv_offset
- 페이지의 시작점에서 이 세그먼트가 위치한 실제 오프셋.
bio가 개별적인 I/O 요구 사항을 담은 원재료라면, 이를 하나로 묶어 다루기 쉽게 만든 배송 패키지가 request입니다. bio는 성능을 위해 주로 슬랩 할당자(slab allocator)를 통해 매우 빠르게 생성(bio_alloc())되며, 극단적인 메모리 부족 상황에서도 시스템이 멈추지 않게끔 작고 독립적인 메모리 풀을 예비로 유지합니다. 참조 카운트가 0이 되면 bio_put()을 통해 안전하게 해제됩니다.
중요한 점은 하나의 bio 요청이 반드시 한 번의 하드웨어 전송으로 곧장 완료되지는 않는다는 것입니다. 전송이 쪼개어 일어날 경우 bi_idx 필드가 다음 전송 타깃을 가리키며 갱신되고, 드라이버는 bio_for_each_segment 같은 매크로를 이용해 남은 세그먼트 조각들을 하나씩 성실하게 처리합니다.
핵심 O/X 퀴즈
1. bio 구조체는 타겟 디스크의 섹터 위치 정보와 메모리에 매핑될 세그먼트 정보를 단일 구조 안에 모두 포함한다.
2. 단일 세그먼트를 설명하는 bio_vec 구조체는 메모리 페이지 포인터, 길이, 그리고 해당 페이지 내의 오프셋 정보를 담고 있다.
3. 한 번 생성된 bio 구조체 내부의 값들은 I/O 작업이 완전히 종료될 때까지 어떠한 수정도 가해지지 않는 불변의 상태를 유지한다.
gendisk와 파티션: 커널이 물리적 디스크를 인지하고 관리하는 방식DISK DESCRIPTOR
단순한 bio 구조체 하나만으로는 온전한 블록 I/O 처리가 불가능합니다. 시스템에는 여러 디스크가 존재하므로 "이 요청이 정확히 어느 장치로 가야 하는가?", "이 디스크의 주(major) 번호는 무엇인가?", "요청을 쌓아둘 전용 큐는 어디에 있는가?" 같은 전반적인 메타데이터가 필요합니다. 디스크 전체의 거시적 정보를 대변하는 객체가 바로 gendisk입니다.
- major
- 이 장치를 대표하는 드라이버의 주(major) 번호.
- first_minor / minors
- 이 디스크가 사용할 수 있는 부(minor) 번호의 범위.
- disk_name
- 시스템 장치 노드에서 흔히 보이는 관습적인 문자열 이름 (예: "sda", "hda").
- capacity
- 디스크의 총 용량을 512바이트 섹터 단위 개수로 표현한 값.
- queue
- 해당 디스크를 위해 대기 중인 요청들이 쌓일 I/O 큐(request_queue) 포인터.
- fops
- 블록 장치에 특화된 연산 메서드(open, release, ioctl 등)가 정의된 테이블 포인터.
- part
- 디스크 내부의 개별 파티션 정보(hd_struct)를 가리키는 배열.
비유하자면 gendisk는 하드디스크라는 기기 전체를 나타내는 '신분증'이고, hd_struct는 그 기기 안을 쪼개놓은 파티션별 '주민등록표'와 같습니다. 만약 gendisk 객체에 GENHD_FL_UP 플래그가 세워져 있다면 커널에 의해 정상적으로 활성화되었음을 뜻하고, GENHD_FL_REMOVABLE이 켜져 있다면 언제든 탈착이 가능한 USB나 CD-ROM 같은 장치임을 나타냅니다.
하나의 통짜 하드디스크는 보통 여러 논리 파티션으로 분할되어 사용됩니다. 통상 디스크 전체와 각 파티션은 같은 major 번호를 공유하되 minor 번호를 연속적으로 할당받아 구분됩니다. hd_struct 구조체 안에는 파티션이 시작되는 절대 섹터 번호(start_sect)와 총 크기(nr_sects), 그리고 통계 수치 등이 포함됩니다.
새로운 물리적 장치가 연결되어 커널이 이를 감지하면, 드라이버는 alloc_disk() 함수를 호출해 gendisk 공간을 확보하고 파티션 배열도 초기화합니다. 모든 세팅이 끝나면 마지막으로 add_disk()를 호출함으로써 비로소 이 장치가 시스템 블록 I/O 생태계에 정식 등록됩니다.
핵심 O/X 퀴즈
1. gendisk 구조체는 실제 존재하는 물리적 디스크 장치뿐 아니라 디스크처럼 에뮬레이션되는 논리적 장치 영역을 표현하는 데에도 사용된다.
2. 디스크의 파티션 세부 정보는 전송을 위한 메모리 조각 리스트인 bio_vec 배열 내에 보관된다.
3. hd_struct 구조체는 개별 파티션의 시작 물리 섹터 번호와 포함된 섹터의 개수 등 위치 정보를 보관한다.
요청 제출: bio_alloc()에서 generic_make_request()까지REQUEST SUBMISSION PATH
매핑 계층이 "필요한 파일 데이터가 디스크 물리 공간 어디에 존재하는지"를 정확히 계산해 냈다면, 커널은 본격적으로 Generic Block Layer에 해당 작업을 발주해야 합니다. 이 발주 프로세스를 관장하는 중추적인 진입 함수가 바로 generic_make_request()입니다.
bio_alloc()을 호출해 빈 구조체를 확보한 뒤 요청 명세를 작성합니다. 시작 섹터, 타깃 디바이스, 데이터 세그먼트 배열, 방향 플래그(READ/WRITE), 콜백 함수 등을 꼼꼼히 채워 넣습니다.generic_make_request()는 넘어온 bio의 요청 범위가 대상 장치의 물리적 끝을 넘어서지 않는지 검증합니다. 한계를 벗어났다면 요청을 즉각 거부하고 에러 상태로 콜백을 발생시킵니다.bio에 기록된 타깃 장치를 추적해 연결된 gendisk 객체를 찾고, 그에 종속된 request_queue 포인터를 획득합니다.blk_partition_remap()을 거쳐 전체 물리 디스크 관점의 절대 섹터 번호로 환산합니다. 이후의 하위 계층들은 파티션의 존재를 완전히 잊고 통짜 디스크로만 취급합니다.__make_request())로 넘깁니다. 이 시점부터 I/O 스케줄러의 복잡한 정렬 및 병합 마법이 시작됩니다.핵심 O/X 퀴즈
1. generic_make_request()는 생성된 I/O 요청을 Generic Block Layer 하부로 본격적으로 밀어 넣는 공식적인 진입점이다.
2. 파티션을 대상으로 발생한 요청은 하드웨어 컨트롤러에 도달할 때까지 파티션 기준의 로컬 오프셋 번호를 끝까지 유지한다.
3. bio 구조체 내부의 bi_end_io 포인터는 해당 I/O 작업의 성공이나 실패가 확정되었을 때 커널이 사후 처리를 위해 호출해 줄 콜백 함수를 가리킨다.
I/O Scheduler의 존재 이유: 기다림의 미학, 미루는 것이 오히려 더 빠르다WHY SCHEDULING MATTERS
커널은 기껏 전달받은 디스크 I/O 요청들을 즉시 하드웨어 컨트롤러로 쏘아 보내지 않는 경우가 허다합니다. 이는 시스템이 처리할 능력이 없어서 밀리는 것이 아니라, 오히려 전체적인 처리량을 극대화하기 위해 다분히 의도적으로 요청들을 대기시키는 것입니다.
자기 디스크(HDD)의 특성상 목표한 섹터로 물리적 헤드를 이동시키는 작업(Seek)은 어마어마한 시간이 소모됩니다. 무작위 순서로 A, Z, B, Y 구역의 요청이 차례로 들어올 때 이를 곧이곧대로 처리하면 헤드가 디스크 전체를 정신없이 왕복해야 합니다. 그러나 잠시 큐에 담아두고 기다리면 커널이 이를 A, B, Y, Z와 같이 물리적으로 가까운 동선 순으로 매끄럽게 재배열할 수 있습니다.
I/O 스케줄러가 '엘리베이터(Elevator)'라는 별칭으로 불리는 이유도 이와 같습니다. 건물 엘리베이터가 먼저 버튼을 누른 층 순서대로 움직이지 않고, 현재 이동하는 방향의 경로상에 있는 층들을 묶어서 한 번에 처리하는 효율성을 본뜬 것입니다.
또한 스케줄러는 단순히 순서만 바꾸는 데 그치지 않고, 새로 유입된 요청의 대상 영역이 기존 대기 중인 요청 영역과 연속되어 있다면 두 요청을 아예 커다란 하나의 덩어리로 합쳐(Merge) 버립니다. 이렇게 하면 추가적인 탐색(Seek) 비용 없이 한 번의 큰 전송만으로 여러 개의 요청을 동시에 소화해낼 수 있습니다. 실무 I/O 패턴은 순차적 접근이 주를 이루기 때문에 이 병합 전략은 극적인 성능 향상을 가져옵니다.
결국 I/O Scheduler는 물리적인 디스크 헤드의 불필요한 움직임을 극한으로 줄이기 위해 요청을 분류, 정렬, 병합하는 커널 내의 교통 관제탑입니다. 블록 I/O는 이 스케줄러를 거치며 비동기적으로 제어되고, 물리 디스크마다 고유의 독립된 request queue와 스케줄러 인스턴스를 유지합니다.
핵심 O/X 퀴즈
1. I/O 스케줄러의 궁극적인 설계 목표는 무작위로 쏟아지는 요청들을 물리적 위치 순으로 정렬하고 병합하여 디스크의 값비싼 탐색(Seek) 비용을 최소화하는 것이다.
2. 사용자의 응답성을 최상으로 끌어올리기 위해서는 디스크 요청이 발생하는 즉시 일말의 지연도 없이 하드웨어에 전달하는 설계가 가장 이상적이다.
3. 블록 디바이스 드라이버는 스케줄링된 요청 전송을 하드웨어에 지시한 뒤 결과를 끝까지 기다리지 않고 반환하며, 실제 완료 처리는 인터럽트에 의해 비동기적으로 이루어진다.
request_queue와 request: 스케줄러가 조율하는 물리적 대기열 구조QUEUE STRUCTURES
I/O 스케줄러가 실제로 어떤 그릇을 가지고 작업을 조율하는지 살펴보겠습니다. 핵심적인 커널 구조체는 두 개입니다. 요청들이 담기는 거대한 통인 request_queue, 그리고 그 큐 안에서 개별 작업 단위를 형성하는 request 디스크립터입니다.
- elevator
- 이 특정 큐에 바인딩되어 현재 동작 중인 I/O 스케줄러 알고리즘 객체 포인터.
- request_fn
- 스케줄링이 완료된 요청들을 최종적으로 드라이버로 내려보낼 때 호출되는 전략 루틴(Strategy routine) 진입점.
- make_request_fn
- 새롭게 생성된 요청을 이 큐로 처음 밀어 넣고자 할 때 호출되는 함수.
- back/front_merge_fn
- 새로 들어온 bio 객체가 이미 큐에서 대기 중인 request의 앞부분이나 뒷부분과 연속되어 하나로 병합될 수 있는지 판별하는 로직.
- unplug_fn
- 잠시 멈춰둔(Plugging) 큐의 문을 다시 열어 드라이버 전송을 본격적으로 재개시킬 때 호출되는 함수.
- max_sectors 등
- 하드웨어 컨트롤러가 한 번에 감당할 수 있는 최대 요청 크기나 DMA 세그먼트 개수 등의 물리적 제약 정보.
- bio / biotail
- 이 request 묶음에 병합되어 있는 첫 번째 bio와 마지막 bio를 가리키는 연결 리스트 포인터.
- flags (REQ_RW)
- 현재 묶음이 디스크 읽기인지 쓰기인지 등 I/O의 성격과 방향성을 나타내는 플래그.
- sector
- 현재 이 request 덩어리 안에서 다음 차례로 전송되어야 할 타깃 섹터 번호.
- nr_sectors
- 이 request 내에 아직 전송이 완료되지 않고 남아있는 잔여 섹터의 총합.
- queuelist
- 이 request 객체 자체를 앞뒤 다른 request들과 이어주는 스케줄러 큐 내부용 이중 연결 리스트 노드.
단일 bio를 배송할 개별 택배 상자라고 비유한다면, request는 도착지 동선이 같은 상자들을 한데 모아 실은 대형 화물 트럭입니다. request_queue는 트럭들이 질서정연하게 출발을 기다리는 터미널에 해당하며, I/O 스케줄러는 어느 트럭에 어떤 상자를 추가로 싣고 어떤 트럭을 가장 먼저 출발시킬지 결정하는 노련한 배차 소장입니다.
핵심 O/X 퀴즈
1. 단일 request 구조체 안에는 연속적인 위치를 가리키는 여러 개의 bio 구조체들이 연결 리스트 형태로 묶여 포함될 수 있다.
2. request_queue 구조체 내부의 request_fn 포인터는 일반적으로 드라이버 개발자가 직접 작성한 디바이스 전략 루틴(Strategy routine)을 가리킨다.
3. request 내부의 REQ_RW 플래그는 해당 요청이 어느 파티션에서 유래했는지를 구분 짓기 위해 존재하는 태그이다.
큐 혼잡, plugging, unplugging: 성급하게 보내지도, 너무 오래 막지도 않는 절묘한 줄다리기CONGESTION & TIMING
요청 큐는 요술 주머니가 아닙니다. 물리 메모리 기반의 유한한 공간이므로 무한정 I/O 요청을 수용할 수는 없습니다. 시스템에 과부하가 걸려 디스크 I/O 작업이 폭주하면, 새로운 요청을 큐에 삽입하려는 상위 프로세스들은 빈 슬롯을 얻지 못한 채 블록(Block) 상태에 빠지게 됩니다. 커널은 기본적으로 하나의 큐에 대해 Read와 Write 각각 최대 128개까지의 대기열(pending request)을 허용하며, 이 임계치를 초과하면 해당 큐는 포화 상태(full)로 간주되어 더 이상 요청을 받지 않습니다.
- plug (마개 막기)
- 큐에 처리할 요청이 들어와도 하단 드라이버가 이를 즉시 가져가지 못하도록 임시로 문을 걸어 잠급니다. 이는 후속 요청들이 충분히 들어와 유의미한 규모의 '병합' 덩어리가 형성되길 기다리는 전략적 지연입니다. 커널은
QUEUE_FLAG_PLUGGED상태로 전환하고 짧은 타이머를 시작시킵니다. - unplug (마개 뽑기)
- 큐에 충분한 양(기본 4개)의 요청이 쌓였거나, 타이머가 만료(약 3ms)되었거나, 당장 완료를 기다리는 동기식(Sync) 요청이 도착했을 때 문을 다시 엽니다. 이 순간
kblockd워크큐 스레드나 관련 로직이 개입해 드라이버의 전략 루틴을 호출하여 억눌려있던 I/O 전송을 폭발적으로 시작합니다.
너무 성급하게 요청 하나마다 컨트롤러를 깨우면 병합의 마법을 부릴 기회를 허무하게 날리게 됩니다. 반대로 병합만을 위해 마냥 큐를 닫고 기다리면 전체적인 시스템 지연 시간(Latency)이 심각하게 늘어납니다. Plugging과 Unplugging은 커널이 이 두 모순된 목표 사이에서 최적의 타이밍을 찾아내는 정교한 밸런싱 기법입니다.
또한 큐가 가득 차기 직전까지 가면 커널은 큐가 혼잡(congested) 상태에 진입했다고 판단하여 상위 계층의 페이지 캐시나 VFS 단에서 새로운 I/O 요청의 생성 자체를 조절하도록 간접적인 브레이크 신호를 보냅니다. 통상적으로 대기열이 113개를 넘어서면 혼잡 상태로 표기하고, 다시 111개 이하로 안정화되면 해제 신호를 발생시킵니다.
핵심 O/X 퀴즈
1. Plugging 상태로 전환된 큐는 처리할 요청을 보유하고 있더라도 병합 효율을 위해 드라이버 하단으로 요청을 넘기지 않고 대기하는 상태를 뜻한다.
2. Unplugging이라는 용어는 에러가 발생한 I/O 큐 자체를 커널의 메모리 영역에서 영구적으로 폐기하고 삭제한다는 의미이다.
3. 큐 혼잡(Congestion) 신호는 VFS나 캐시 관리자와 같은 상위 시스템이 I/O 발생 속도를 능동적으로 조절하게 만드는 피드백 장치로 기능한다.
네 가지 I/O Scheduler: 각기 다른 철학을 지닌 엘리베이터들ELEVATOR ALGORITHMS
리눅스 2.6 기반 커널 환경에서는 워크로드 특성에 맞춰 선택할 수 있는 네 가지 종류의 스케줄러(Elevator)를 제공합니다. '엘리베이터'라는 명칭은 디스크 헤드가 마치 건물 승강기처럼 안쪽 트랙과 바깥쪽 트랙을 유연하게 오가며 대기열을 처리하는 모습에서 유래했습니다. 알고리즘마다 공정성을 중시할지, 대기 지연을 중시할지 그 철학이 확연히 다릅니다.
복잡한 정렬 알고리즘 없이 새로 들어온 요청을 대기열의 끝이나 앞단에 단순 병합 및 삽입만 합니다. 스마트한 하드웨어 RAID 컨트롤러나 자체적인 최적화 로직을 갖춘 고성능 SSD 환경에서 오히려 뛰어난 퍼포먼스를 발휘합니다.
프로세스마다 별도의 큐(총 64개)를 배정하고, Round-robin 방식으로 골고루 기회를 분배하여 특정 무거운 I/O 프로세스가 전체 디스크 대역폭을 독점하는 사태를 원천 차단합니다. 데스크톱 환경의 기본값으로 널리 쓰입니다.
섹터 위치 기반의 정렬 큐 2개와 타이머 기반의 마감 시한(Deadline) 큐 2개를 병렬로 운영합니다. 특히 읽기(Read) 요청에 500ms의 짧은 시한을 주어(쓰기는 5초), 결과를 애타게 기다리는 프로세스들이 무한정 멈춰있는 현상을 우선적으로 구제합니다.
특정 프로세스의 읽기 요청을 막 끝냈을 때 즉시 다음 위치로 떠나지 않습니다. 통계적으로 해당 프로세스가 아주 가까운 인접 섹터에 연속적인 후속 요청을 던질 확률이 높으므로, 디스크 헤드를 멈추고 약 7ms 정도 허공에서 '예측 대기'를 수행하여 막대한 탐색 비용을 극적으로 아낍니다.
결국 스케줄러는 단순히 짐을 쌓아두는 창고 관리자가 아니라, 디스크 물리 구동 한계와 프로세스들의 응답성 요구 사이에서 아슬아슬한 타협점을 찾아내는 정책 관리자입니다. 플래시 메모리 시대가 도래하며 Noop이나 Deadline 계열의 비중이 커졌지만, 여전히 이들이 제안한 정렬과 병합의 사상은 I/O 하위 시스템의 근간을 이룹니다.
핵심 O/X 퀴즈
1. CFQ 스케줄러의 핵심 철학은 어떤 단일 프로세스도 디스크 전체 대역폭을 독점하지 못하도록 공정하게 I/O 기회를 배분하는 것이다.
2. Deadline 스케줄러는 성능을 다소 양보하더라도 특정 I/O 요청이 영원히 처리되지 않고 방치되는 Starvation 현상을 막기 위해 고안되었다.
3. Anticipatory 알고리즘은 단 1밀리초의 지연도 허용하지 않는 엄격함으로, 현재 요청이 끝나자마자 어떠한 멈춤도 없이 즉각 다음 큐를 꺼내 처리한다.
__make_request()와 병합의 기술: 묶을 것인가, 새로 만들 것인가MERGE LOGIC
generic_make_request()가 기본적인 문지기 역할을 끝내면, 실제 큐 삽입의 뼈대 로직인 __make_request()가 바통을 넘겨받습니다. 여기서 맞닥뜨리는 핵심 과제는 단순명료합니다. "방금 도착한 이 bio를 새로운 request 트럭에 실을 것인가? 아니면 이미 대기 중인 트럭의 화물칸에 이어서 밀어 넣을 것인가?"
elv_merge() 알고리즘을 호출해 판정을 받습니다. 결과는 세 가지로 나뉩니다. 기존 요청 덩어리의 뒤에 붙이기(BACK_MERGE), 앞에 붙이기(FRONT_MERGE), 또는 병합 불가 판정(NO_MERGE).bio를 기존 request의 한쪽 끝에 성공적으로 붙였다면, 그로 인해 길어진 화물 덩어리가 또 다른 request 덩어리와 맞닿아 아예 통째로 합쳐질 가능성이 생기므로 추가적인 연쇄 병합 심사를 이어갑니다.request 구조체를 할당받고, bio를 단독 적재하여 큐에 진입시킵니다. 만약 즉각적인 동기적 처리가 필요한 작업(BIO_RW_SYNC)이라면 큐의 마개를 바로 뽑아버리기도 합니다.연쇄 병합 로직은 마치 이 빠진 테트리스를 맞추는 것과 같습니다. 비어있던 단 하나의 블록(bio)을 끼워 넣는 순간, 왼쪽 조각 모음과 오른쪽 조각 모음이 완전히 맞물리며 단숨에 거대한 하나의 라인(단일 request)으로 합쳐지는 쾌감이 블록 I/O 시스템 내부에서 끊임없이 벌어지고 있습니다.
한 가지 특이한 케이스로, BIO_RW_AHEAD 플래그가 찍힌 미래 예측용 '미리 읽기(Read-ahead)' 요청의 경우 시스템 메모리가 넉넉하지 않은 상황이라면 커널은 미련 없이 이를 즉시 포기(Drop)하고 에러 콜백을 반환합니다. 어차피 당장 급한 데이터가 아닌 일종의 보험성 요청이므로 굳이 자원을 무리하게 소모할 필요가 없기 때문입니다.
핵심 O/X 퀴즈
1. __make_request() 내부 흐름의 가장 중요한 분기점은 새로 유입된 bio를 기존 대기열의 요청 덩어리들과 병합할 수 있는지 판별하는 과정이다.
2. ELEVATOR_BACK_MERGE 판정이 내려지면 이는 새 bio를 기존 대기 요청 덩어리의 가장 첫머리(Front) 부분에 밀어 넣어야 함을 의미한다.
3. 처리 결과 반환을 기다려야 하는 동기식 요청(BIO_RW_SYNC)이 대기열에 진입하면, 커널은 성능 저하를 감수하고서라도 지연 없이 큐 마개를 뽑아(Unplug) 드라이버를 즉시 구동시키려 시도한다.
Bounce buffer: 구형 하드웨어를 위한 커널의 헌신적인 임시 우회로MEMORY CONSTRAINTS
이상적인 세계라면 하드웨어 컨트롤러가 시스템에 장착된 모든 물리 RAM 공간을 마음껏 들여다보고 데이터를 쏴줄 수 있어야 합니다. 하지만 현실에서는 버스 대역 제한(예: 구형 ISA 버스의 24비트 16MB 한계) 탓에 접근 범위가 지극히 제한된 레거시 하드웨어들이 존재합니다. 커널은 이들을 포기하지 않고 우회로인 바운스 버퍼(Bounce buffer)를 구성해 줍니다.
blk_queue_bounce() 함수가 타깃 큐의 주소 제한 임계값을 검사하여, 원본 bio가 요구하는 메모리 영역이 하드웨어의 도달 불가능 구역(High memory 등)에 속해있는지 판단합니다.bio를 만들어 이 임시 페이지를 목적지로 가리키게 합니다.건물 경비실 택배 보관소와 똑같습니다. "보안 규칙상 택배기사(구형 하드웨어)님은 건물 10층(High memory)까지 직접 배달하실 수 없습니다. 1층 경비실 보관소(Bounce buffer)에 짐을 두고 가시면, 나중에 관리인(커널)이 수레를 끌고 10층 원주인에게 가져다드리겠습니다." 과정의 낭비가 심하지만 호환성 유지를 위해 감수해야만 하는 필연적 우회로입니다.
복제 과정을 거치며 새롭게 탄생한 바운스 bio는 자신이 진짜 원본이 아님을 잊지 않기 위해 BIO_BOUNCED 플래그를 이마에 붙이고, 사후 뒤처리를 위해 bi_private 필드 주머니 속에 몰래 원본 bio의 포인터 주소를 간직한 채 큐를 향해 흘러갑니다.
핵심 O/X 퀴즈
1. 바운스 버퍼(Bounce buffer) 기법은 하드웨어 컨트롤러가 시스템의 특정 물리 메모리 주소 대역에 DMA 접근 권한이나 능력이 없을 때 발동되는 호환성 유지 수단이다.
2. 디스크에 데이터를 기록(Write)하는 흐름에서 바운스 버퍼가 개입하면, 커널은 원본 버퍼의 데이터를 컨트롤러가 접근 가능한 안전한 임시 버퍼 공간으로 일일이 복사하는 추가적인 수고를 감내해야 한다.
3. 바운스 처리를 위해 새로 파생된 임시 bio는 원래의 원본 bio와 연결고리가 완전히 단절되어 독립적으로만 소모되고 사라진다.
block_device와 bdev 파일시스템: 논리적 껍데기가 통일성을 유지하는 비결Figure 14-3
앞서 배운 gendisk가 '이런 디스크가 시스템에 장착되어 있다'를 선언하는 물리적 신분증이라면, block_device는 '커널 내 어떤 구성요소가 지금 이 블록 장치 영역을 활발하게 다루고 있다'를 추적하기 위한 동적인 논리 상태 객체에 가깝습니다. 이는 통짜 디스크 전체일 수도 있고, 특정 조각난 파티션일 수도 있습니다.
- bd_dev
- 이 장치를 유일하게 식별해 내는 주/부(major/minor) 번호의 조합.
- bd_inode
- 커널 내부에 숨겨진 특수 파일시스템인 bdev 파일시스템의 inode 객체와 연결된 포인터.
- bd_openers
- 현재 시스템 전체에서 이 장치 공간이 몇 번이나 개방(Open)되어 사용 중인지를 집계하는 참조 카운터.
- bd_contains
- 이 객체가 파티션이라면 부모가 되는 '전체 디스크'의
block_device주소를 담고, 원래부터 전체 디스크 객체라면 자기 자신의 주소를 품어 정체성을 확립합니다. - bd_part
- 파티션의 경우
hd_struct구조체를 연결해 주며, 통짜 디스크라면 NULL로 비워둡니다. - bd_holder
- 이 장치를 독점적이거나 특별한 권한으로 점유하고 있는 주체(예: 마운트 로직을 수행한 특정 파일시스템)의 서명을 기록합니다.
왜 굳이 bdev라는 숨겨진 가상 파일시스템이 필요할까요? 관리자의 실수나 의도에 따라 똑같은 하드디스크 장치를 가리키는 디바이스 파일(예: /dev/sda, /tmp/my_disk_link)이 여러 경로와 이름으로 수십 개씩 존재할 수 있기 때문입니다. 경로가 다르더라도 major/minor 번호가 동일하다면, bdget() 함수가 bdev 파일시스템의 장부를 뒤져서 시스템 전역에서 오직 단 하나만 존재하는 고유한 block_device 객체를 찾아냅니다. 덕분에 커널은 파편화된 장치 파일 이름들에 현혹되지 않고 일관된 상태 관리를 이뤄냅니다.
장치를 배타적으로 획득하고자 할 때는 bd_claim()을 호출하여 bd_holder 자리에 이름을 남기고, 볼 일이 끝나면 bd_release()로 소유권을 반납합니다. 계층 구조를 도식화한 Figure 14-3을 되짚어보면, 전체 디스크용 block_device, 개별 파티션용 block_device, 물리적 gendisk, hd_struct 배열들, 그리고 이를 떠받치는 단일 request_queue가 어떻게 하나의 거대한 그물망으로 엮여 있는지 확인할 수 있습니다.
핵심 O/X 퀴즈
1. 특정 파티션을 관할하는 block_device 객체의 bd_contains 필드는 자신의 모체가 되는 전체 통짜 디스크의 block_device 객체 주소를 가리키고 있다.
2. bd_holder 필드는 실제 전기적 신호를 주고받으며 디스크 헤드를 움직이는 최하단 디바이스 드라이버 로직 자체를 의미한다.
3. bdev 가상 파일시스템이 도입된 주된 목적은, 아무리 다양한 경로와 이름의 장치 파일들이 산재해 있어도 동일한 물리적 기기(major/minor 기준)라면 오직 단 하나의 통합된 block_device 디스크립터로 매핑해주기 위함이다.
드라이버 등록과 초기화 과정: 코드가 커널의 공식 기관으로 인정받기까지DRIVER INITIALIZATION
단순한 C 언어 코드가 어엿한 커널의 일원인 '블록 디바이스 드라이버'로 거듭나는 일련의 의식(Initialization) 과정을 따라가 보겠습니다. 이해를 돕기 위해 foo라는 가상의 신규 드라이버를 모델로 삼았습니다. 중요한 것은 각각의 커널 객체들이 어떤 논리적 순서를 밟으며 생성되고 서로 결합되는지 파악하는 것입니다.
foo_dev_t 구조체 메모리를 할당받고, 제어할 I/O 포트 영역, 사용될 IRQ 번호, 동기화를 위한 락(spinlock), gendisk 연결 포인터 등을 빈틈없이 세팅합니다.register_blkdev(FOO_MAJOR, "foo")를 호출하여 시스템에 주(major) 번호를 선점하고 드라이버의 간판(이름)을 건넸습니다. 이제 /proc/devices 통계표에 새로운 항목이 위풍당당하게 나타납니다.alloc_disk(16)을 통해 최대 15개(자신 포함 16개)의 파티션을 거느릴 수 있는 gendisk 객체를 커널로부터 받아옵니다. major, minor, 용량(capacity), 장치 이름 등의 빈칸을 꼼꼼히 작성해 넣습니다.gendisk의 fops 필드에 명확히 걸어둡니다.blk_init_queue()를 호출해 이 드라이버만을 위한 전속 요청 큐를 개설하고, 배차를 통제할 핵심 전략 루틴(foo_strategy)을 진입점(request_fn)으로 단단히 결속시킵니다. 아울러 최대 섹터 수 등 하드웨어 스펙 한계치도 함께 커널에 통보합니다.request_irq()를 호출하여, 하드웨어가 작업 완료를 보고할 때 번개처럼 튀어 나갈 인터럽트 핸들러 함수를 공식 지정합니다.add_disk(foo.gd) 선언으로 커널에 알립니다. 비로소 GENHD_FL_UP 깃발이 꽂히고 시스템 전역에 이 디스크의 존재가 방송되며, /sys/block/foo 경로에도 번듯한 객체가 노출되어 실질적인 I/O 요청을 수신하기 시작합니다.핵심 O/X 퀴즈
1. alloc_disk() 함수는 물리 디스크 전체를 대변하는 gendisk 구조체를 커널 메모리에 할당해 주며, 인자로 넘긴 숫자를 바탕으로 파티션 배열 크기까지 함께 확보한다.
2. blk_init_queue()를 호출하면 빈 큐가 생성됨과 동시에, 해당 큐에 요청이 차올랐을 때 이를 끄집어내어 하드웨어로 보낼 드라이버의 핵심 전략 루틴(Strategy routine)이 request_fn으로 확정 바인딩된다.
3. 코드 상위에서 register_blkdev()와 alloc_disk() 호출만 정상적으로 끝나면, add_disk()를 생략해도 디스크는 커널에서 완벽히 활성화되어 상위 프로세스들의 읽기/쓰기 요청을 즉각 수용한다.
Strategy routine: 막후에서 하드웨어로 진격 신호를 쏘아 올리는 순간HARDWARE TRANSFER
Strategy routine (전략 루틴)은 겹겹이 쌓인 소프트웨어 계층의 가장 깊숙한 끝자락에서 대기열의 요청 덩어리(Request)를 뽑아들어 물리적 하드웨어 컨트롤러에 직접 전기적 신호를 세팅하는 핵심 함수입니다. 우리의 가상 드라이버 예시에서는 foo_strategy()가 이 막중한 역할을 맡고 있습니다.
elv_next_request(q)를 호출해 I/O 스케줄러가 심혈을 기울여 정렬해 놓은 가장 첫 번째 request 트럭을 배차받습니다. 만약 큐가 완전히 비어있다면 조용히 루틴을 종료합니다.blk_fs_request(req)로 이 요청이 일반적인 파일시스템 레벨의 데이터 읽기/쓰기가 맞는지 확인합니다. 저수준의 컨트롤러 진단 패킷이나 특수 명령이라면 우회 경로로 빠집니다.blk_rq_map_sg()를 통해 이 거대한 요청 덩어리에 포함된 파편화된 메모리 주소 리스트(Scatter-list)를 일거에 구성하여 컨트롤러 레지스터에 한 방에 쏟아 넣습니다.rq_for_each_bio 와 bio_for_each_segment 이중 매크로 지옥을 순회하며, 세그먼트 조각 하나하나를 kmap_atomic()으로 매핑하고 개별적인 DMA 전송 명령을 수동으로 반복 타달해야 합니다.전략 루틴 코딩의 철칙은 "명령만 내리고 칼같이 돌아온다"입니다. 여기서 결과를 보겠다며 루틴 안에서 Sleep 하거나 블로킹되면 시스템 I/O 전체가 숨통이 막혀버리게 되며, 여러 요청을 병렬로 동시 소화할 수 있는 강력한 최신 컨트롤러의 진가도 전부 무용지물이 됩니다. 데이터 이동의 고단한 과정은 하드웨어 엔진에 전적으로 위임하고, 완료의 순간은 훗날 인터럽트가 날카롭게 알려줄 것입니다.
핵심 O/X 퀴즈
1. 드라이버의 전략 루틴(Strategy routine)은 스케줄러 큐의 가장 앞에서 대기 중인 요청을 가져와 실제 물리적 하드웨어의 전송을 촉발시키는 방아쇠 역할을 담당한다.
2. 가장 성능이 우수하고 효율적으로 설계된 블록 디바이스 드라이버일수록, 전송 명령을 내린 직후 디스크가 작업을 끝낼 때까지 전략 루틴 안에서 묵묵히 동기적으로 대기(Wait)하는 끈기를 보여준다.
3. 컨트롤러가 하드웨어 레벨의 Scatter-gather DMA를 지원한다면, 드라이버는 여러 메모리 세그먼트 조각 정보들을 한데 엮은 리스트 구조를 만들어 단 한 번의 명령으로 우아하게 전송을 지시할 수 있다.
Interrupt handler: 영수증 처리와 새로운 시작, 세밀한 진척도 추적의 기술COMPLETION PROCESSING
하드웨어 컨트롤러가 고된 DMA 전송 노동을 성공적으로(혹은 실패로) 마치면 마침내 CPU에 전기적 펄스, 즉 인터럽트(Interrupt) 신호를 날립니다. 부름을 받고 튀어나온 드라이버의 인터럽트 핸들러가 해야 할 미션은 명확합니다. "방금 끝난 하드웨어 전송으로 인해, 애당초 거대하게 뭉쳐있던 request 덩어리 중 정확히 '어느 지점'까지 작업이 클리어되었는가?"를 커널 장부에 세밀히 기록하고 다음 타자를 준비시키는 일입니다.
bio와 세그먼트 체인을 훑어가며 남은 바이트 수(bi_idx 등)를 지워나갑니다. 만약 특정 bio 하나가 온전히 다 처리되었다면 기쁜 마음으로 bio_endio() 콜백을 날려 상위 프로세스의 블록 상태를 해제시켜 줍니다.request 덩어리는 완벽히 처리 종료된 것입니다.blkdev_dequeue_request()의 손길을 거쳐 스케줄러 큐의 이중 연결 리스트에서 영원히 퇴출당합니다.request 빈 껍데기 디스크립터를 시스템 메모리로 반환합니다.rq→request_fn(rq)(전략 루틴)의 시동을 걸어 끊임없는 데이터의 흐름을 이어 나갑니다.전략 루틴(Strategy routine)이 거대한 화물차를 목적지로 출발시키는 배차 관리자라면, 인터럽트 핸들러(Interrupt handler)는 트럭이 기지로 복귀했을 때 길고 긴 배송 송장 목록표를 일일이 대조하며 빨간 줄을 긋는 꼼꼼한 검수자입니다. 어떤 상자는 배송 완료 처리되어 전산망에 등록되고(bio_endio), 누락되거나 다 싣지 못한 화물이 발견되면 같은 차를 그 자리에서 다시 출동시키며, 리스트가 완벽히 비워지면 비로소 다음 배송을 위한 트럭을 새롭게 호출합니다.
이 일련의 과정에서 가장 돋보이는 커널의 우아함은, 거대한 request 덩어리가 하나의 I/O 작업으로 뭉뚱그려 처리되는 것이 아니라 하부에 속한 bio와 개별 세그먼트 단위로 매우 촘촘하게 진척도가 쪼개어 관리된다는 점입니다. 어마어마한 양의 단일 요청이 컨트롤러의 한계 탓에 여러 번의 자잘한 DMA 작업으로 파편화되어 쪼개져도, 리눅스 커널은 단 한 바이트의 미아도 허용하지 않고 오차 없이 정확하게 남은 위치를 추적해 냅니다.
핵심 O/X 퀴즈
1. 드라이버의 인터럽트 핸들러는 컨트롤러로부터 DMA 완료 통보를 받는 즉시, 방금 끝난 전송량을 토대로 현재 요청 덩어리 내에 아직 처리해야 할 데이터가 얼마나 남았는지 잔여 범위를 정확하게 갱신하는 책무를 지닌다.
2. end_that_request_first() 함수는 하나의 거대한 request 내에서 특정 bio 객체의 처리가 완전히 끝났음을 감지하면 지체 없이 bio_endio() 콜백을 유발해 상위 시스템에 성공을 타전한다.
3. 처리율을 높이기 위해 I/O 요청이 완벽히 종료된 이후에도 해당 request 디스크립터 자체는 다음 재사용을 위해 현재의 스케줄러 큐 내부 구조에 영구 보존되어야만 한다.
블록 디바이스 파일 열기: 단순한 파일 오픈을 넘어선 커널 생태계의 융합blkdev_open()
리눅스 환경에서 블록 디바이스를 지칭하는 특수 파일(예: /dev/sda1)이 열리는(Open) 상황은 크게 세 가지입니다. 관리자가 특정 파일시스템을 마운트(Mount)하려 할 때, 가상 메모리 확보를 위해 스왑(Swap) 파티션을 활성화시킬 때, 또는 dd나 디스크 유틸리티 같은 사용자 프로세스가 저수준 제어를 위해 명시적으로 open() 시스템 콜을 던졌을 때입니다. 이 순간 VFS 계층의 dentry_open()은 일반 파일과는 달리 객체의 연산 테이블(f_op)을 블록 장치 전용인 def_blk_fops로 탈바꿈시켜 특수한 처리 경로를 개통합니다.
block_device 주소를 찾아 나섭니다. 이미 VFS inode의 i_bdev 필드에 값이 세팅되어 있다면 운 좋게 재활용하지만, 초기 상태라면 bdget()을 기동시켜 디바이스의 major/minor 번호를 들고 은밀한 bdev 파일시스템을 수색하여 유일무이한 객체를 찾아내거나 신규 발급받습니다.gendisk 객체를 탐색해 연결합니다. 대상이 파티션이라면 이 과정에서 몇 번째 조각에 해당하는지 인덱스 값도 함께 확보됩니다.bd_disk)를 연결하고 드라이버가 등록해 둔 본연의 오픈 메서드(disk→fops→open)를 직접 호출해 줍니다. 부가적으로 매체 탈착이 가능한 CD-ROM 류의 장치라면 파티션 테이블을 처음부터 다시 스캔(rescan_partitions)하는 기민함도 보여줍니다.block_device 객체까지 덩달아 찾아내고 오픈 절차를 밟습니다. 그런 뒤 자식 객체의 bd_contains 포인터가 통짜 디스크 객체를 가리키도록 연결망을 짜고, hd_struct 구조체도 정확히 결속시킵니다.bd_claim()을 호출하여 bd_holder 명부에 자신의 이름을 당당히 새겨 넣습니다. 만약 한발 앞서 누군가 점유하고 있다면 단호하게 -EBUSY 에러를 뱉어내고 쫓아냅니다.블록 디바이스 드라이버는 절대 우주에 홀로 떠 있는 작은 외딴섬이 아닙니다. 상공의 VFS 레이어, 데이터의 온기가 머무는 페이지 캐시 공간, 주소를 번역하는 매핑 계층, 표준 포장지 bio를 찍어내는 Generic Block Layer, 엘리베이터의 지휘자인 I/O Scheduler, 전송의 일꾼 DMA 모듈, 타이밍의 마술사 Interrupt handler, 그리고 신분증 위조를 막는 bdev 파일시스템까지 — 이 모든 거대하고 치밀한 커널 파이프라인 톱니바퀴들이 오차 없이 맞물려 돌아간 끝에 비로소 호출되는 최후의 실행자에 불과합니다. 이 웅장한 연결의 큰 그림을 머릿속에 간직한다면, 코드 속 흩뿌려진 bio, request, gendisk, block_device 같은 차가운 이름들이 파이프라인의 생동감 넘치는 주인공들로 재탄생해 보일 것입니다.