IO_URING Submission Queue Polling


제출 대기열 주기적 확인

시스템 호출 횟수를 줄이는 것이 'io_uring'의 주요 목표입니다.   이를 위해 'io_uring'을 사용하면 단일(single) 시스템 호출을 하지 않고도 I/O 요청을 제출할 수 있습니다.   이는 'io_uring'이 지원하는 특수 제출 대기열 주기적 확인(special submission queue polling) 기능을 통해 수행됩니다.   이 모드에서는 프로그램이 폴링(polling:주기적 확인) 모드를 설정한 직후 'io_uring'이 프로그램이 추가할 수 있는 항목에 대해 공유 제출 대기열을 주기적으로 확인(폴링)하는 특수 커널 스레드를 시작합니다.   그렇게 하면 항목을 공유 대기열에 제출하기만 하면 커널 스레드가 이를 확인하고 프로그램이 일반적으로 'liburing'에 의해 처리되는 io_uring_enter() 시스템 호출을 수행할 필요 없이 제출 대기열 항목을 선택합니다.   이는 사용자 공간과 커널 간에 공유되는 대기열을 갖는 이점입니다.

이 모드를 사용하는 방법은 무엇입니까? 아이디어는 간단합니다.   io_uring_params 구조의 플래그 멤버에 IORING_SETUP_SQPOLL 플래그를 설정하여 이 모드를 사용하고 싶다고 'io_uring'에 알립니다.   프로세스와 함께 시작되는 커널 스레드가 일정 기간 동안 제출을 확인하지 못하면 종료되며 프로그램은 다시 깨우기 위해 io_uring_enter() 시스템 호출을 한 번 더 호출해야 합니다.   이 기간은 :c:struct'io_uring_params' 구조의 sq_thread_idle 멤버를 통해 구성할 수 있습니다.   그러나 계속해서 제출이 들어오면 커널 폴러(poller) 스레드는 절대로 잠들지 않아야 합니다.

⚛ Polling 설명 (chatgpt)
"폴링(polling)"은 컴퓨터 과학 및 네트워킹에서 주기적으로 어떤 상태나 이벤트를 확인하는 과정을 의미합니다.   예를 들어, 특정 장치나 시스템이 새로운 데이터를 받았는지, 작업이 완료되었는지 등을 주기적으로 확인하는 것을 말합니다.   이는 대기 상태에서 계속해서 확인하는 방식으로, 이벤트가 발생했을 때 이를 인식하고 처리하기 위해 사용됩니다.   따라서 "제출 대기열 폴링"은 제출된 작업이나 요청이 대기열에 쌓여 있을 때, 이를 주기적으로 확인하고 처리하는 과정을 의미합니다.

Note - 주의1
'liburing'을 사용할 때 io_uring_enter() 시스템 호출을 직접 호출하지 마십시오.   이는 일반적으로 'liburing'의 io_uring_submit() 함수로 처리됩니다.   이는 폴링(주기적 확인) 모드를 사용하고 있는지 여부를 자동으로 결정하고 사용자가 걱정할 필요 없이 프로그램이 io_uring_enter()를 호출해야 하는 시기를 처리합니다.

Note - 주위2
커널의 폴러 스레드는 많은 CPU를 차지할 수 있습니다.   이 기능을 사용할 때는 주의가 필요합니다.   매우 큰 sq_thread_idle 값을 설정하면 프로그램에서 제출이 발생하지 않는 동안 커널 스레드가 계속 CPU를 소비하게 됩니다.   실제로 많은 양의 I/O를 처리할 것으로 예상되는 경우 이 기능을 사용하는 것이 좋습니다.   그리고 그렇게 하는 경우에도 폴러 스레드의 유휴 값을 최대 몇 초로 설정하는 것이 좋습니다.

그러나 이 기능을 사용해야 하는 경우 io_uring_register_files()와 함께 사용해야 합니다.   이를 사용하여 파일 설명자 배열에 대해 미리 커널에 알립니다.   이는 I/O를 시작하기 전에 여는 파일 설명자의 일반적인 배열입니다.   제출하는 동안 일반적으로 io_uring_prep_read() 또는 io_uring_prep_write()와 같은 호출처럼 파일 설명자를 전달하는 대신 'SQE'의 플래그 필드에 IOSQE_FIXED_FILE 플래그를 설정하고 배열에서 이전에 설정한 파일 설명자의 인덱스를 전달해야 합니다.

📦 How it works - 작동 원리

이 예제 프로그램은 이전에 본 고정 버퍼 예제와 매우 유사합니다.   고정 버퍼를 처리하기 위해 io_uring_prep_read_fixed() 및 io_uring_prep_write_fixed()와 같은 특수 함수를 사용한 반면, io_uring_prep_read(), io_uring_prep_readv(), io_uring_prep_write() 또는 io_uring_prep_writev()와 같은 일반 함수를 사용합니다.   그러나 제출을 설명하는 데 사용되는 'SQE'에서는 io_uring_prep_readv() 및 io_uring_prep_writev()와 같은 호출에서 파일 설명자 자체가 아닌 파일 설명자 배열의 파일 설명자 인덱스를 사용하는 동안 IOSQE_FIXED_FILE 플래그를 설정합니다.

프로그램이 시작되면 io_uring 인스턴스를 설정하기 전에 제출 대기열 주기적 확인(폴링)을 수행하는 커널 스레드의 실행 상태를 출력합니다.   이 스레드의 이름은 'io_uring-sq'입니다. print_sq_poll_kernel_thread_status() 함수는 이 상태를 출력합니다.   물론 제출 큐 주기적 확인(폴링)을 사용하는 다른 프로세스가 있는 경우 이 커널 스레드가 실제로 실행 중임을 확인할 수 있습니다.   모든 커널 스레드의 상위는 init 직후에 시작되는 kthreadd 커널 스레드입니다. 이 스레드의 프로세스 ID는 1입니다.   결과적으로 'kthreadd'의 PID는 2이며 이 사실을 활용하여 간단한 최적화로 커널 스레드만 필터링할 수 있습니다.
system("ps --ppid 2 | grep io_uring-sq" )

io_uring을 초기화하려면 일반적인 io_uring_queue_init() 대신 io_uring_queue_init_params()를 사용합니다.   이는 io_uring_params 구조체에 대한 포인터를 인수로 사용하기 때문입니다.   해당 인수에서 플래그 필드의 일부로 'IORING_SETUP_SQPOLL'을 지정하고 'sq_thread_idle'을 제출 큐 주기적 확인(폴러) 커널 스레드의 유휴 시간인 2000ms으로 설정합니다.   이러한 밀리초 동안 제출이 없으면 스레드가 종료되고 io_uring_enter() 시스템 호출은 커널 스레드를 다시 실행하기 위해 'liburing'을 통해 내부적으로 수행되어야 합니다.

제출 대기열 주기적 확인은 고정 파일과의 조합에서만 작동하므로 먼저 처리하려는 단일(lone) 파일 설명자를 등록합니다.   더 많은 파일을 처리하는 경우 io_uring_register_files() 함수를 사용하여 해당 파일을 열고 등록하는 곳입니다.   각 제출마다 io_sqe_set_flags() 도우미 함수를 사용하여 IOSQE_FIXED_FILE 플래그를 설정하고 실제 파일 설명자 자체가 아닌 등록된 파일 배열에서 열린 파일의 인덱스를 io_uring_prep_read() 또는 io_uring_prep_write()와 같은 함수에 제공해야 합니다.

이 예에는 4개의 버퍼가 있습니다.   처음 2개는 2개의 쓰기 작업에서 각각 한 줄을 파일에 쓰는 데 사용됩니다.   나중에 우리는 2개의 추가 읽기 작업이 포함된 3번째와 4번째 버퍼를 사용하여 작성된 2개의 라인을 읽고 출력합니다.   쓰기 작업 후에는, 이제 실행 중인 io_uring-sq 커널 스레드의 상태를 인쇄합니다.

📦 Verifying polling by the kernel - 커널에 의한 폴링 확인

하지만 io_uring_submit()을 호출합니다. 이전 예제에서 이로 인해 io_uring_enter() 시스템 호출이 발생하는 것을 확인했습니다. 하지만 IORING_SETUP_SQPOLL 플래그를 설정한 경우에는 그렇지 않습니다. 'liburing'은 프로그램에 대한 지속적인 인터페이스를 유지하면서 이를 완전히 숨깁니다. 그런데 이것을 검증할 수 있을까요? 예, 'eBPF'를 사용하는 bpftrace 프로그램을 통해 시스템을 엿볼 수 있습니다. 여기서는 'io_uring'이 설정한 커널의 추적점을 사용하여 'IORING_SETUP_SQPOLL'을 설정하고 I/O 요청을 제출할 때 io_uring_submit() 함수를 호출하더라도 프로그램이 io_uring_enter() 시스템 호출을 수행하지 않음을 증명합니다. 이전에 논의한 것처럼 처리량이 높은 프로그램의 경우 시스템 호출을 최대한 피하는 것이 좋습니다. 아래 프로그램에서는 io_uring의 'io_uring_submit_sqe' 추적점에 연결합니다. 이 추적점은 SQE가 커널에 제출될 때마다 트리거됩니다. 이 추적점이 트리거될 때마다 'bpftrace'를 사용하여 명령 이름과 PID를 인쇄합니다. 먼저 한 터미널에서 bpftrace 명령을 실행하는 동시에 다른 터미널에서는 고정 버퍼 예제를 실행해 보겠습니다. 다음은 내 컴퓨터의 샘플 출력입니다. 'fixed_buffers'가 SQE를 제출하는 것임을 알 수 있습니다. Let’s repeat the previous exercise, but now by running the current example. You can see that the SQE submission happens via the io_uring_sq kernel thread. We thus avoid system calls.

⚛ 원문


Email 답글이 올라오면 이메일로 알려드리겠습니다.