Research

What is IO_uring? (Inside IO_uring)

smileostrich 2023. 5. 29. 22:12

예전에 SSD, HDD에 대한 을 살펴본 것 처럼, I/O 처리 방식은 시스템 성능에 큰 영향을 미칩니다.

 

Coding for SSDs – Part 1: Introduction and Table of Contents | Code Capsule

Translations: This article was translated to Simplified Chinese by Xiong Duo and to Korean by Matt Lee (이 성욱). Introduction I want to make solid-state drives (SSDs) the optimal storage solution for my key-value store project. For that reason, I had t

codecapsule.com

이전까지는 select, poll, epoll과 같은 방식을 통해 비동기 I/O를 처리하곤 했습니다.

다만, 최근에는 더욱 향상된 성능과 효율성을 제공하는 새로운 모델, IO_uring이 등장했습니다.

이 새로운 비동기 I/O 처리 모델인 IO_uring에 대해 알아봅시다 : )

 

TLDR

IO_uring은 리눅스 커널 5.1부터 도입된 새로운 비동기 I/O 처리 모델입니다. 기존의 비동기 I/O 모델들과 달리, IO_uring은 더욱 강력한 기능과 향상된 성능을 제공합니다.

기본적으로 IO_uring은 사용자 공간(user-space)과 커널 공간(kernel-space) 사이에 공유된 두 개의 링 버퍼(ring buffer)를 이용합니다. 하나는 submission queue로, 사용자 공간에서 생성된 I/O 요청들을 커널에 전달하기 위해 사용되며, 다른 하나는 completion queue로, 완료된 I/O 작업들을 커널에서 사용자 공간으로 전달하기 위해 사용됩니다. 이러한 구조는 시스템 콜의 횟수를 줄이고, context switch를 최소화하여 성능을 향상시킵니다.

 

IO_uring 의 기본 구조

IO_uring은 기본적으로 사용자 공간(user-space)과 커널 공간(kernel-space) 사이에 두 개의 링 버퍼를 유지합니다:

  1. Submission Queue (SQ) : 사용자 공간에서 발생하는 I/O 작업을 커널에 전달합니다.
  2. Completion Queue (CQ) : 완료된 I/O 작업의 결과를 커널에서 사용자 공간으로 반환합니다.

각 큐는 큐 헤드(queue head), 큐 테일(queue tail), 큐 링(queue ring), 그리고 배열(entries array)로 구성되어 있습니다. 사용자는 테일에 새로운 항목을 추가하고, 커널은 헤드를 통해 항목을 가져옵니다. 이러한 설계를 통해, 커널은 큐의 상태를 지속적으로 확인하지 않고도 요청을 처리할 수 있습니다.

 

IO_uring 의 작동 방식

  1. 먼저, 사용자 공간에서는 io_uring_setup 시스템 콜 통해 IO_uring 인스턴스를 초기화하고, 필요한 크기를 지정합니다.
  2. 다음으로, 사용자는 io_uring_register 시스템 콜 통해 파일 설명자, 이벤트, 버퍼 등을 등록합니다.
  3. 이후에는 사용자가 io_uring_enter 시스템 콜 통해 I/O 작업을 submit 합니다. 이때 submission queue entry (SQE) 생성하여 해당 작업을 설명하며, SQE submission queue 추가됩니다.
  4. 커널은 SQE들을 검사하고 해당하는 I/O 작업을 수행합니다. 완료된 작업은 completion queue entry (CQE) 변환되어 completion queue 추가됩니다.
  5. 마지막으로, 사용자는 다시 io_uring_enter 호출하여 completion queue에서 완료된 작업을 가져옵니다.

 

IO_uring의 장점

  1. 확장성 : IO_uring은 기존의 비동기 I/O 모델보다 더욱 높은 확장성을 제공합니다. 여러 I/O 작업을 하나의 시스템 콜로 submit 할 수 있으며, 이를 통해 시스템 콜의 오버헤드를 크게 줄일 수 있습니다.
  2. 효율성 : IO_uring은 사용자 공간과 커널 공간 사이의 context switch를 최소화하며, 이를 통해 성능 향상이 가능해집니다. 또한, 사용자 공간에서 직접 접근 가능한 submission queue와 completion queue를 제공함으로써, 불필요한 복사 작업을 줄입니다.
  3. 유연성 : IO_uring은 파일 I/O 뿐만 아니라, 소켓 I/O, 타이머, 이벤트 fd, 시그널 등 다양한 형태의 비동기 작업에 사용할 수 있습니다. 이는 IO_uring이 높은 수준의 유연성을 가지고 있음을 의미합니다.

 

자, 이제 어느 정도 IO_uring 이 이해됐으니, 예제를 한번 살펴볼까요?

const BUF_SIZE: usize = 1024;

#[tokio::main]
async fn main() -> io::Result<()> {
    // Create a new io_uring instance.
    let mut ring = IoUring::new(256).unwrap();

    // Open the file for reading.
    let file = File::open("testfile").unwrap();
    let fd = file.as_raw_fd();

    // Allocate a buffer for reading.
    let mut buf = vec![0; BUF_SIZE];

    // Prepare a ReadV operation.
    let read_e = opcode::Readv::new(opcode::types::Fd(fd), &buf[..], 0)
        .build()
        .user_data(0x42);

    // Submit the ReadV operation to the ring.
    unsafe {
        let mut queue = ring.submission().available();
        queue.push(read_e).unwrap();
        ring.submit().unwrap();
    }

    // Wait for the operation to complete.
    let cqe = ring.completion().wait().unwrap();
    assert_eq!(cqe.user_data(), 0x42);

    // The operation completed successfully, print number of bytes read.
    let bytes_read = cqe.result().unwrap() as usize;
    println!("Read {} bytes", bytes_read);

    Ok(())
}

 

  1. io_uring 인스턴스 생성 : IoUring::new(256).unwrap()을 호출하여 새로운 io_uring 인스턴스를 생성합니다. ( 인스턴스는 256개의 엔트리를 가진 큐를 설정)
  2. 파일 열기 : "testfile" 파일을 열고, FD 가져옵니다.
  3. 버퍼 할당 : BUF_SIZE(1024) 크기를 가진 버퍼를 생성합니다. (파일 읽기 결과를 저장하기 위해 사용)
  4. ReadV prepare : opcode::Readv::new(opcode::types::Fd(fd), &buf[..], 0).build().user_data(0x42)을 호출해서 ReadV 작업을 준비합니다. 작업은 파일 디스크립터 fd에서 읽어오며, 읽어온 데이터는 buf 저장됩니다.
  5. ReadV submit : ring.submission().available()를 호출하여 submit 큐를 가져온다음, queue.push(read_e).unwrap()을 호출하여 ReadV 작업을 submit 큐에 추가합니다. 그리고 ring.submit().unwrap()을 호출하여 큐에 있는 작업들을 submit 합니다.
  6. 마무리 : 이후 ReadV 작업이 완료되기를 기다렸다가, 작업이 완료되면 출력합니다.

 

마무리

IO_uring은 리눅스 커널 5.1 이상에서 사용할 수 있다는 제약조건이 있긴 하지만, 기존의 비동기 I/O 모델보다 더욱 강력하고 유연한 기능을 제공합니다. 다만, CVE-2021–20226, CVE-2022-29582 등과 같은 보안 이슈들이 존재했던것처럼(지금은 둘 다 해결됨) 보안에 대한 우려를 가지고 계신분들도 많으실 겁니다.(실제로 타당한 걱정일 수 있구요)
그래도 성능적으로 매력적인 선택지이기에 앞으로 지켜볼만한 선택지일것 같습니다 :)

 

CVE-2021–20226 a reference counting bug which leads to local privilege escalation in io_uring.

Hello, I’m Shiga( @Ga_ryo_ ), a security engineer at Flatt Security Inc.

flattsecurity.medium.com

 

+ 사진자료가 없어서 찾아보다가, 예전에 리트윗 해둔 0xor0one님의 트윗에서 잘 정리된 그림을 찾았고 추가할 수 있었다.(이 자리를 빌어 감사의 말씀을)

 

트위터에서 즐기는 0xor0ne

“Interesting writeup by @Ga_ryo_ on CVE-2021–20226 (Local Privilege Escalation in Linux kernel through io_uring) https://t.co/aEfB2M5CbM #Linux #kernel #exploit #infosec #cybersecurity”

twitter.com

 

혹시 틀린 부분이 있다면, 언제든 편하게 지적해주세요!

 

이메일 ian.ilminmoon@gmail.com