디스크 I/O란
지금까지 만든 OS에는 프로세스, 메모리 관리, 시스템 콜이 있지만, 모든 데이터가 RAM 위에만 존재한다. 전원을 끄면 전부 사라진다. 영속적인 데이터 저장을 위해서는 디스크에 접근할 수 있어야 하고, 그러려면 디바이스 드라이버커널과 하드웨어 장치 사이의 추상화 계층. 커널의 상위 코드(파일 시스템 등)는 드라이버가 제공하는 통일된 인터페이스만 호출하면 되고, 장치별 레지스터 조작은 드라이버 내부에 캡슐화된다.가 필요하다.
이 글에서는 QEMU의 가상 디스크 장치인 virtio-blk을 위한 드라이버를 구현한다.
Virtio란
Virtio는 가상 장치를 위한 디바이스 인터페이스 표준이다. 실제 하드웨어(SATA, NVMe 등)마다 인터페이스가 제각각인데, 가상 환경에서는 Virtio라는 통일된 프로토콜을 사용해서 드라이버를 단순화한다. QEMU, Firecracker 등에서 널리 쓰인다.
Virtio에는 Legacy와 Modern 두 가지 인터페이스가 있다. 이 구현에서는 좀 더 단순한 Legacy 인터페이스를 사용한다.
Virtqueue — 드라이버와 장치가 통신하는 방법
Virtio 장치는 virtqueue드라이버(OS)와 장치(QEMU) 사이에서 공유하는 메모리 영역 기반의 큐. 드라이버가 요청을 넣고 장치가 처리 결과를 돌려주는 구조다.라는 자료구조를 통해 드라이버와 통신한다. virtqueue는 세 영역으로 구성된다.
| 이름 | 주체 | 내용 |
|---|---|---|
| Descriptor Table | 드라이버 | 요청의 메모리 주소·크기를 기록하는 테이블 |
| Available Ring원형 큐를 Ring이라고 부름. | 드라이버 | 장치에 처리할 요청들을 등록하는 큐 |
| Used Ring | 장치 | 장치가 처리한 요청들을 기록한 큐 |
하나의 요청(예: 디스크 읽기)은 여러 디스크립터장치에게 전달할 메모리 영역을 기술하는 엔트리. 주소, 크기, 속성(읽기 전용/쓰기 가능), 다음 디스크립터 인덱스를 담고 있다. 하나의 요청을 여러 디스크립터로 나누면 영역마다 다른 속성을 줄 수 있다.를 체인으로 엮어서 표현한다. 이렇게 하면 디스크립터마다 다른 속성(읽기 전용/쓰기 가능)을 줄 수 있고, 메모리에 흩어진 데이터를 하나의 요청으로 묶을 수 있다(Scatter-Gather I/O).
virtqueue의 디스크 쓰기 요청의 전체 흐름은 이렇다.
- 드라이버가 Descriptor Table에 요청 내용을 기록
- 디스크립터 체인의 헤드 인덱스를 Available Ring에 추가
- 드라이버가 장치에 “새 요청 있어"라고 알림 (MMIO 레지스터에 쓰기)
- 장치가 Available Ring에서 요청을 꺼내 처리
- 장치가 결과를 Used Ring에 기록하고 드라이버에게 알림
이 때, 우리 OS는 하드웨어 인터럽트 처리 방식이 구현되어 있지 않아서 busy-wait 방식으로 드라이버가 신호를 받는다. 실제 OS와의 차이점은 다음과 같다.
실제 OS: 장치가 Used Ring 기록 -> 인터럽트 발생 -> 드라이버 핸들러 호출
우리 OS: 장치가 Used Ring 기록 -> (아무 알림 없음) -> 드라이버가 루프 돌며 직접 확인QEMU에 virtio-blk 장치 연결
먼저 테스트용 파일을 준비한다.
echo "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In ut magna consequat, cursus velit aliquam, scelerisque odio. Ut lorem eros, feugiat quis bibendum vitae, malesuada ac orci. Praesent eget quam non nunc fringilla cursus imperdiet non tellus. Aenean dictum lobortis turpis, non interdum leo rhoncus sed. Cras in tellus auctor, faucibus tortor ut, maximus metus. Praesent placerat ut magna non tristique. Pellentesque at nunc quis dui tempor vulputate. Vestibulum vitae massa orci. Mauris et tellus quis risus sagittis placerat. Integer lorem leo, feugiat sed molestie non, viverra a tellus." > lorem.txtQEMU 실행 스크립트에 virtio-blk 장치를 추가한다.
run.sh
$QEMU -machine virt -bios default -nographic -serial mon:stdio --no-reboot \
-kernel kernel.elf
$QEMU -machine virt -bios default -nographic -serial mon:stdio --no-reboot \
-drive id=drive0,file=lorem.txt,format=raw,if=none \
-device virtio-blk-device,drive=drive0,bus=virtio-mmio-bus.0 \
-kernel kernel.elf
추가된 옵션은 다음과 같다.
-drive id=drive0: drive0QEMU 내부에서 디스크를 식별하기 위한 이름. -device에서 drive=drive0으로 참조해서 장치와 디스크를 연결한다. 이름은 양쪽이 일치하기만 하면 아무거나 써도 된다.이라는 이름의 디스크를 정의한다.lorem.txt를 디스크 이미지로 사용하며, 이미지 형식은 raw이다.-device virtio-blk-device:drive0디스크를 사용하는virtio-blk장치를 추가. bus=virtio-mmio-bus.0QEMU의 virt 머신에 미리 정의된 virtio 장치용 MMIO 버스. .0은 첫 번째 슬롯이며, 물리 주소 0x10001000에 매핑되어 있다. 코드에서 VIRTIO_BLK_PADDR을 0x10001000으로 정의한 이유가 이것이다. 옵션을 통해 해당 장치를 virtio-mmio 버스에 매핑함.
C 매크로 및 구조체 정의
kernel.h에 virtio 관련 정의를 추가한다. 양이 많지만 대부분 virtio 사양서에 정의된 상수와 자료구조를 그대로 옮긴 것이다.
kernel.h
#define SECTOR_SIZE 512
#define VIRTQ_ENTRY_NUM 16
#define VIRTIO_DEVICE_BLK 2
#define VIRTIO_BLK_PADDR 0x10001000
#define VIRTIO_REG_MAGIC 0x00
#define VIRTIO_REG_VERSION 0x04
#define VIRTIO_REG_DEVICE_ID 0x08
#define VIRTIO_REG_PAGE_SIZE 0x28
#define VIRTIO_REG_QUEUE_SEL 0x30
#define VIRTIO_REG_QUEUE_NUM_MAX 0x34
#define VIRTIO_REG_QUEUE_NUM 0x38
#define VIRTIO_REG_QUEUE_PFN 0x40
#define VIRTIO_REG_QUEUE_READY 0x44
#define VIRTIO_REG_QUEUE_NOTIFY 0x50
#define VIRTIO_REG_DEVICE_STATUS 0x70
#define VIRTIO_REG_DEVICE_CONFIG 0x100
#define VIRTIO_STATUS_ACK 1
#define VIRTIO_STATUS_DRIVER 2
#define VIRTIO_STATUS_DRIVER_OK 4
#define VIRTQ_DESC_F_NEXT 1
#define VIRTQ_DESC_F_WRITE 2
#define VIRTQ_AVAIL_F_NO_INTERRUPT 1
#define VIRTIO_BLK_T_IN 0
#define VIRTIO_BLK_T_OUT 1
struct virtq_desc {
uint64_t addr; // 데이터가 있는 물리 주소
uint32_t len; // 데이터 크기
uint16_t flags; // NEXT, WRITE 등
uint16_t next; // 체인의 다음 디스크립터 인덱스
} __attribute__((packed));
struct virtq_avail {
uint16_t flags; // 인터럽트 제어 플래그
uint16_t index; // 다음에 쓸 위치
uint16_t ring[VIRTQ_ENTRY_NUM]; // 디스크립터 헤드 인덱스 배열
} __attribute__((packed));
struct virtq_used_elem {
uint32_t id; // 처리한 디스크립터 체인의 헤드 인덱스
uint32_t len; // 장치가 실제로 쓴 바이트 수
} __attribute__((packed));
struct virtq_used {
uint16_t flags;
uint16_t index; // 장치가 다음에 쓸 위치
struct virtq_used_elem ring[VIRTQ_ENTRY_NUM];
} __attribute__((packed));
struct virtio_virtq {
struct virtq_desc descs[VIRTQ_ENTRY_NUM]; // 디스크립터 테이블
struct virtq_avail avail; // Available Ring
struct virtq_used used __attribute__((aligned(PAGE_SIZE))); // Used Ring (페이지 정렬 필요)
int queue_index; // 이 큐의 번호
volatile uint16_t *used_index; // used.index를 가리키는 포인터
uint16_t last_used_index; // 드라이버가 마지막으로 확인한 used index
} __attribute__((packed));
struct virtio_blk_req {
uint32_t type; // 요청 타입 (IN=읽기, OUT=쓰기)
uint32_t reserved; // 사양에서 예약된 필드 (0)
uint64_t sector; // 접근할 섹터 번호
uint8_t data[512]; // 읽기/쓰기할 데이터 (1섹터 = 512바이트)
uint8_t status; // 장치가 기록하는 처리 결과 (0=성공)
} __attribute__((packed));매크로
SECTOR_SIZE: 디스크의 최소 읽기/쓰기 단위. 한 번에 512바이트씩 접근VIRTQ_ENTRY_NUM: virtqueue의 디스크립터 슬롯 개수, 최대 16개까지VIRTIO_REG_*: MMIO 베이스 주소에서의 오프셋을 뜻하는 값
구조체
virtq_desc: 디스크립터 테이블 엔트리 구조체virtq_avail: Available Ring 구현 구조체virtq_used와virtq_used_elem: Used Ring 구현 구조체virtio_virtq: 디스크립터 테이블, Available Ring, Used Ring을 합친 전체 virtqueue 구조체virtio_blk_req: 드라이버가 장치에 요청하는 request 구조체__attribute__((packed)):컴파일러가 구조체 멤버 사이에 패딩을 삽입하면 드라이버와 장치가 서로 다른 오프셋에서 값을 읽게 되기 때문이다.
MMIO 레지스터 접근 유틸리티
MMIO 레지스터란 장치의 제어/상태 정보가 담긴 메모리 주소로, 일반 RAM처럼 lw/sw로 접근하지만 실제로는 장치가 응답한다. 이 레지스터에 접근하기 위한 유틸리티 함수를 kernel.c에 추가한다.
uint32_t virtio_reg_read32(unsigned offset) {
return *((volatile uint32_t *) (VIRTIO_BLK_PADDR + offset));
}
uint64_t virtio_reg_read64(unsigned offset) {
return *((volatile uint64_t *) (VIRTIO_BLK_PADDR + offset));
}
void virtio_reg_write32(unsigned offset, uint32_t value) {
*((volatile uint32_t *) (VIRTIO_BLK_PADDR + offset)) = value;
}
void virtio_reg_fetch_and_or32(unsigned offset, uint32_t value) {
virtio_reg_write32(offset, virtio_reg_read32(offset) | value);
}포인터에 volatile이 반드시 필요하다. 일반 메모리와 달리 MMIO에서는 읽기/쓰기 자체가 부수 효과(장치에 명령 전송 등)를 일으킨다. volatile이 없으면 컴파일러가 “같은 주소를 두 번 읽는 건 불필요하다"고 판단해서 두 번째 읽기를 제거할 수 있다. 장치 상태가 변할 수 있는 MMIO에서는 치명적이다.
MMIO 영역을 페이지 테이블에 매핑
커널이 MMIO 레지스터에 접근하려면 해당 물리 주소를 페이지 테이블에 매핑해야 한다.
kernel.c — create_process
for (paddr_t paddr = (paddr_t) __kernel_base;
paddr < (paddr_t) __free_ram_end; paddr += PAGE_SIZE)
map_page(page_table, paddr, paddr, PAGE_R | PAGE_W | PAGE_X);
for (paddr_t paddr = (paddr_t) __kernel_base;
paddr < (paddr_t) __free_ram_end; paddr += PAGE_SIZE)
map_page(page_table, paddr, paddr, PAGE_R | PAGE_W | PAGE_X);
map_page(page_table, VIRTIO_BLK_PADDR, VIRTIO_BLK_PADDR, PAGE_R | PAGE_W);
Virtqueue 초기화
struct virtio_virtq *virtq_init(unsigned index) {
paddr_t virtq_paddr = alloc_pages(align_up(sizeof(struct virtio_virtq), PAGE_SIZE) / PAGE_SIZE);
struct virtio_virtq *vq = (struct virtio_virtq *) virtq_paddr;
vq->queue_index = index;
vq->used_index = (volatile uint16_t *) &vq->used.index;
// 큐 선택
virtio_reg_write32(VIRTIO_REG_QUEUE_SEL, index);
// 디스크립터 개수 설정
virtio_reg_write32(VIRTIO_REG_QUEUE_NUM, VIRTQ_ENTRY_NUM);
// 큐의 페이지 프레임 번호를 장치에 알림
virtio_reg_write32(VIRTIO_REG_QUEUE_PFN, virtq_paddr / PAGE_SIZE);
return vq;
}virtq_init은 virtqueue를 위한 메모리를 할당하여 할당된 물리 페이지 번호를 장치에게 알려주는 역할을 하는 함수이다. 과정은 다음과 같다.
virtqueue용 메모리 할당
캐스팅된
vq의 멤버를 초기화MMIO 레지스터에 큐 정보 쓰기
Virtio 장치 초기화
장치 초기화는 Virtio 사양에 정의된 핸드셰이크 절차를 따른다.
struct virtio_virtq *blk_request_vq;
struct virtio_blk_req *blk_req;
paddr_t blk_req_paddr;
uint64_t blk_capacity;
void virtio_blk_init(void) {
if (virtio_reg_read32(VIRTIO_REG_MAGIC) != 0x74726976)
PANIC("virtio: invalid magic value");
if (virtio_reg_read32(VIRTIO_REG_VERSION) != 1)
PANIC("virtio: invalid version");
if (virtio_reg_read32(VIRTIO_REG_DEVICE_ID) != VIRTIO_DEVICE_BLK)
PANIC("virtio: invalid device id");
// 1. 장치 리셋
virtio_reg_write32(VIRTIO_REG_DEVICE_STATUS, 0);
// 2. ACKNOWLEDGE: 장치를 발견함
virtio_reg_fetch_and_or32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_ACK);
// 3. DRIVER: 이 장치를 제어할 수 있음
virtio_reg_fetch_and_or32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_DRIVER);
// 페이지 크기 설정
virtio_reg_write32(VIRTIO_REG_PAGE_SIZE, PAGE_SIZE);
// Virtqueue 초기화
blk_request_vq = virtq_init(0);
// 6. DRIVER_OK: 장치 사용 준비 완료
virtio_reg_write32(VIRTIO_REG_DEVICE_STATUS, VIRTIO_STATUS_DRIVER_OK);
// 디스크 용량 확인
blk_capacity = virtio_reg_read64(VIRTIO_REG_DEVICE_CONFIG + 0) * SECTOR_SIZE;
printf("virtio-blk: capacity is %d bytes\n", (int)blk_capacity);
// 요청 버퍼 할당
blk_req_paddr = alloc_pages(align_up(sizeof(*blk_req), PAGE_SIZE) / PAGE_SIZE);
blk_req = (struct virtio_blk_req *) blk_req_paddr;
}코드 과정은 다음과 같다.
- 전역 변수 세팅
- 장치 검증
- 핸드 셰이크
- 디바이스 리셋
- ACK
- 드라이버 상태 비트 세팅
- virtqueue 초기화
- 드라이버 OK
- 디스크 용량 read, req 버퍼 할당
전체적인 패턴은 네트워크 핸드셰이크와 비슷하다. 매직 넘버·버전·디바이스 ID를 확인해서 올바른 장치인지 검증하고, 상태 비트를 순서대로 설정(리셋 -> ACK -> DRIVER -> DRIVER_OK)해서 장치를 활성화한다. OS는 장치 내부에서 실제로 어떤 일이 일어나는지 신경 쓸 필요 없이, 사양에 정의된 MMIO 레지스터에 값을 쓰기만 하면 된다.
kernel_main에 초기화를 추가한다.
kernel.c — kernel_main
void kernel_main(void) {
memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);
WRITE_CSR(stvec, (uint32_t) kernel_entry);
...
void kernel_main(void) {
memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);
WRITE_CSR(stvec, (uint32_t) kernel_entry);
virtio_blk_init();
...
I/O 요청 보내기
이제 실제로 디스크를 읽고 쓰는 함수를 구현한다.
kernel.c
void virtq_kick(struct virtio_virtq *vq, int desc_index) {
vq->avail.ring[vq->avail.index % VIRTQ_ENTRY_NUM] = desc_index;
vq->avail.index++;
__sync_synchronize();
virtio_reg_write32(VIRTIO_REG_QUEUE_NOTIFY, vq->queue_index);
vq->last_used_index++;
}
bool virtq_is_busy(struct virtio_virtq *vq) {
return vq->last_used_index != *vq->used_index;
}
void read_write_disk(void *buf, unsigned sector, int is_write) {
if (sector >= blk_capacity / SECTOR_SIZE) {
printf("virtio: tried to read/write sector=%d, but capacity is %d\n",
sector, blk_capacity / SECTOR_SIZE);
return;
}
// 요청 구성
blk_req->sector = sector;
blk_req->type = is_write ? VIRTIO_BLK_T_OUT : VIRTIO_BLK_T_IN;
if (is_write)
memcpy(blk_req->data, buf, SECTOR_SIZE);
// 3개의 디스크립터로 체인 구성
struct virtio_virtq *vq = blk_request_vq;
vq->descs[0].addr = blk_req_paddr;
vq->descs[0].len = sizeof(uint32_t) * 2 + sizeof(uint64_t); // type(4바이트) + reserved(4바이트) + sector(8바이트)
vq->descs[0].flags = VIRTQ_DESC_F_NEXT;
vq->descs[0].next = 1;
vq->descs[1].addr = blk_req_paddr + offsetof(struct virtio_blk_req, data);
vq->descs[1].len = SECTOR_SIZE; // 512
vq->descs[1].flags = VIRTQ_DESC_F_NEXT | (is_write ? 0 : VIRTQ_DESC_F_WRITE);
vq->descs[1].next = 2;
vq->descs[2].addr = blk_req_paddr + offsetof(struct virtio_blk_req, status);
vq->descs[2].len = sizeof(uint8_t); // 1
vq->descs[2].flags = VIRTQ_DESC_F_WRITE;
// 장치에 알리고 완료 대기
virtq_kick(vq, 0);
while (virtq_is_busy(vq))
;
if (blk_req->status != 0) {
printf("virtio: warn: failed to read/write sector=%d status=%d\n",
sector, blk_req->status);
return;
}
if (!is_write)
memcpy(buf, blk_req->data, SECTOR_SIZE);
}virtq_kick
virtq_kick 함수는 장치에 새 요청이 있다는걸 알리는 함수로 코드 흐름은 다음과 같다.
- Available Ring에 디스크립터 헤드 인덱스 push and idx++
- 메모리 배리어
__sync_synchronize() - notify 레지스터에
queue_index(virtqueue 번호) 등록 last_used_index++(virtq_is_busy에서 장치가 처리 끝냈는지 비교할 기준값)
virtq_kick에서 __sync_synchronize()GCC/Clang의 빌트인 메모리 배리어 함수. 이 호출 이전의 모든 메모리 쓰기가 이 호출 이후의 메모리 접근보다 반드시 먼저 완료되도록 보장한다.를 호출하는 이유는, Available Ring에 대한 쓰기가 NOTIFY 레지스터에 대한 쓰기보다 반드시 먼저 장치에게 보여야 하기 때문이다. 이 배리어가 없으면 CPU나 컴파일러의 재배치(reordering)로 인해 장치가 아직 업데이트되지 않은 Available Ring을 읽을 수 있다.
virtq_is_busy
virtq_is_busy 함수는 장치가 처리 중 인지 체크하는 함수이다.
vq->last_used_index != *vq->used_index;에서 last_used_index와 vq->used_index가 가리키는 값(역참조)이 같은지 체크하여 장치가 처리 중 인지를 체크한다.
read_write_disk
read_write_disk 함수는 실제 디스크 읽기/쓰기를 담당하는 함수로 코드 흐름은 다음과 같다.
- 범위 검사: 요청한 섹터 번호가 디스크 용량을 초과하는지 체크
blk_req채우기- 디스크립터 체인 구성하기 (3개로)
- descs[0]: type + sector (장치가 읽기만 함)
- descs[1]: data[512] (읽기 시 WRITE 플래그 -> 장치가 여기에 씀)
- descs[2]: status (장치가 결과를 씀)
virtq_kick로 장치에 요청이 있다고 알리기virtq_is_busy로 처리가 완료될 때까지 대기하기- 결과 확인:
blk_req->status가 0이 아니면 에러 - 읽기인 경우 데이터 복사:
blk_req->data를buf로
요청은 3개의 디스크립터 체인으로 구성된다. virtio_blk_req 구조체의 각 영역을 디스크립터 하나씩에 매핑하는 것이다.
그림으로 보면 다음과 같다.

왜 하나의 구조체를 3개로 나누는가? 디스크립터마다 **다른 속성(flags)**을 줄 수 있어야 하기 때문이다. 요청 헤더(type, sector)는 장치가 읽기만 하면 되지만, 데이터 영역은 읽기 작업 시 장치가 써야(VIRTQ_DESC_F_WRITE) 한다.
현재 구현은 요청을 보낸 뒤 busy-wait장치가 처리를 끝낼 때까지 CPU가 무한 루프를 돌며 확인하는 방식. 단순하지만 CPU를 낭비한다. 실제 OS에서는 인터럽트를 사용해서 CPU를 다른 프로세스에 양보한다.으로 완료를 기다린다. 매번 링의 처음 3개 디스크립터만 쓰는 것도 이 때문이다. 실제 OS에서는 인터럽트 기반으로 비동기 처리하면서 동시에 여러 요청을 처리할 수 있도록 자유 디스크립터를 추적해야 한다. 즉, 위 코드에는 0, 1, 2를 지정했지만, 실제로는 빈 디스크립터 3개를 찾아서 next로 연결짓는 코드가 필요한 것이다.
동작 확인
kernel.c - kernel_main
void kernel_main(void) {
memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);
WRITE_CSR(stvec, (uint32_t) kernel_entry);
virtio_blk_init();
idle_proc = create_process(NULL, 0);
idle_proc->pid = 0;
current_proc = idle_proc;
create_process(_binary_shell_bin_start, (size_t) _binary_shell_bin_size);
yield();
PANIC("switched to idle process");
}
void kernel_main(void) {
memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);
WRITE_CSR(stvec, (uint32_t) kernel_entry);
virtio_blk_init();
char buf[SECTOR_SIZE];
read_write_disk(buf, 0, false);
printf("first sector: %s\n", buf);
strcpy(buf, "hello from kernel!!!\n");
read_write_disk(buf, 0, true);
idle_proc = create_process(NULL, 0);
idle_proc->pid = 0;
current_proc = idle_proc;
create_process(_binary_shell_bin_start, (size_t) _binary_shell_bin_size);
yield();
PANIC("switched to idle process");
}
$ ./run.sh
virtio-blk: capacity is 1024 bytes
first sector: Lorem ipsum dolor sit amet, consectetur adipiscing elit. In ut magna consequat, cursus velit aliquam, scelerisque odio. Ut lorem eros, feugiat quis bibendum vitae, malesuada ac orci. Praesent eget quam non nunc fringilla cursus imperdiet non tellus. Aenean dictum lobortis turpis, non interdum leo rhoncus sed. Cras in tellus auctor, faucibus tortor ut, maximus metus. Praesent placerat ut magna non tristique. Pellentesque at nunc quis dui tempor vulputate. Vestibulum vitae massa orci. Mauris et tellus quis ri��디스크 이미지인 lorem.txt의 내용이 출력된다. 쓰기 후에는 lorem.txt 파일의 첫 섹터가 덮어씌워진 것도 확인할 수 있다.
마지막에 깨진 문자 ��를 볼 수 있는데 이는 buf는 512 바이트인데, lorem.txt는 그보다 길어서 buf 범위를 넘어서 \0이 나올때까지 printf가 읽는 UB가 발생한다. 다른 스터디원은 Page Fault가 발생했다고 한다.
head lorem.txt
hello from kernel!!!
amet, consectetur adipiscing elit. In ut magna consequat, cursus velit aliquam, scelerisque odio. Ut lorem eros, feugiat quis bibendum vitae, malesuada ac orci. Praesent eget quam non nunc fringilla cursus imperdiet non tellus. Aenean dictum lobortis turpis, non interdum leo rhoncus sed. Cras in tellus auctor, faucibus tortor ut, maximus metus. Praesent placerat ut magna non tristique. Pellentesque at nunc quis dui tempor vulputate. Vestibulum vitae massa orci. Mauris et tellus quis risus sagittis placerat. Integer lorem leo, feugiat sed molestie non, viverra a tellus.정리
디바이스 드라이버는 OS와 디바이스를 연결해주는 매개체 역할을 한다. 드라이버가 직접 디스크 헤드를 움직이는 것이 아니라, MMIO 레지스터와 공유 메모리(virtqueue)를 통해 장치에게 “이 섹터 읽어줘"라고 부탁하고, 장치가 나머지 물리적 작업을 수행한다.
이번 챕터에서 구현한 전체 흐름을 애니메이션으로 보면 다음과 같다.
virtio-blk I/O Flow 시뮬레이션
[1] Data (NEXT|W)
[2] Status (WRITE)
ring[0]: ?
ring[0]: ?
다음 글에서는 이 read_write_disk를 활용해서 tar 기반 파일 시스템을 구현한다.