Lobsters

Thread-identity switcheroo for io_uring

io_uring을 위한 스레드 정체성 바꿔치기

io_uring이 실제로 블록될 때만 워커 스레드 비용을 부담하도록 task_struct의 정체성을 교환하는 RFC 패치가 제안됐습니다. tmpfs fsync()에서 약 700% 향상이 관찰됐지만, 커널 참조 무결성과 shadow stack 호환성 같은 위험도 큽니다.

AI 요약

io_uring은 애플리케이션이 비동기 실행을 기대하는 서브시스템이므로, 명시적으로 요청하지 않은 이상 호출이 블록되지 않아야 합니다. 사용자는 submission ring에 Submission Queue Entry(SQE)를 기록한 뒤 io_uring_enter()를 호출하고, 커널은 해당 항목들을 처리합니다. page cache에 데이터가 이미 있고 사용자 공간 버퍼도 RAM에 상주하는 경우처럼 작업을 즉시 끝낼 수 있다면 io_uring_enter() 안에서 바로 실행할 수 있습니다. 그러나 커널 내부의 모든 경로가 비동기 실행을 전제로 설계된 것은 아니기 때문에, 실제로 블록될 가능성이 있는 작업을 어떻게 처리할지가 계속 문제가 되어 왔습니다.

■ 기존 방식의 비용

현재 커널은 io_uring_enter()를 블록시킬 수 있는 작업을 별도의 워커 스레드에 넘깁니다. fdatasync(), statx(), 일부 openat() 경로가 대표적인 예이며, O_NONBLOCK 플래그가 있어도 블록 가능성이 남는 경로가 있습니다. 워커 스레드로 넘기면 제출 스레드는 계속 진행할 수 있고, 워커가 블록되더라도 제출 스레드가 멈추지는 않습니다. 하지만 워커 스레드를 깨우고 컨텍스트 스위치를 수행하는 비용이 발생합니다.

작업이 실제로 블록된다면 이 비용이 전체 실행 시간에서 큰 비중을 차지하지 않을 수 있습니다. 반대로 작업이 운 좋게 블록 없이 완료되면, 블록을 피하기 위해 수행한 스레드 전환 자체가 요청 처리 비용의 상당 부분이 됩니다. 커널의 모든 시스템 콜 경로를 비블로킹으로 다시 작성하는 방법도 있지만, 이는 오래 걸리고 쉽지 않은 작업입니다. Jens Axboe가 제안한 RFC는 모든 경로를 고치는 대신, 우선 제출 스레드에서 작업을 실행한 뒤 실제로 잠들어야 하는 순간에만 다른 방식으로 처리하는 접근을 취합니다.

■ 스케줄러가 블록 시점을 알리는 구조

패치 세트는 task_struct의 플래그 필드에 PF_IO_HANDOFF를 추가합니다. 이 플래그가 설정된 스레드가 어떤 이유로든 잠들기 직전이 되면 스케줄러가 새로운 io_uring_task_sleeping() 함수를 호출합니다. io_uring은 특정 파일 시스템이나 시스템 콜 경로를 일일이 수정하지 않고도, 커널 어디에서든 해당 작업이 실제로 블록되는 순간을 전달받을 수 있게 됩니다.

io_uring이 블록 사실을 알게 된 뒤 작업을 처음부터 워커 스레드에서 재시작하는 것은 일반적으로 불가능합니다. 이미 커널 내부에서 상당한 작업이 진행된 뒤일 수 있고, 어느 경로에서 블록될지 알 수 없기 때문입니다. 따라서 해당 작업은 현재 실행 중인 흐름에서 끝까지 진행해야 합니다. 여기서 RFC가 제안하는 방법이 “thread identity handoff”입니다.

■ task_struct 정체성 교환

제출 스레드가 잠들기 직전이라는 알림을 받으면 io_uring은 스레드 풀에서 워커 스레드 하나를 선택합니다. 진행 중인 작업 자체를 워커에게 넘기는 대신, 두 스레드의 정체성을 교환합니다. 원래 제출 스레드에서 사용하던 task_struct의 정체성은 워커 스레드에 부여됩니다. 여기에는 스레드 ID와 시그널 처리 설정 등이 포함됩니다. 그 결과 워커 스레드는 원래 제출 스레드인 것처럼 submission ring의 처리를 이어가고, 최종적으로 사용자 공간으로 돌아갑니다.

동시에 원래 제출 스레드는 워커 스레드의 정체성을 갖게 된 뒤, 원래 진행하던 작업에서 블록됩니다. 깨어난 뒤에는 해당 작업을 완료하고 워커 스레드 풀의 구성원으로 들어갑니다. 사용자 공간에서 보면 io_uring_enter()를 반환한 스레드가 호출 당시와 다른 task_struct를 사용하게 되지만, 그 밖의 상태는 동일하게 보이도록 만드는 것이 목표입니다. 작업이 블록되지 않으면 전체 처리를 제출 스레드에서 수행하므로 워커 스레드를 깨우는 비용이 발생하지 않고, 실제 블록이 발생한 경우에만 정체성 교환과 워커 활용 비용을 부담합니다.

■ 가장 큰 위험: task_struct 참조

이 방식은 task_struct가 커널 내부에서 널리 참조된다는 점 때문에 위험합니다. 두 스레드가 정체성을 교환하기 전에 다른 커널 코드가 해당 task_struct를 참조하고 있지 않다는 사실을 확인해야 합니다. 참조가 남아 있는 상태에서 교환하면, 이후 그 참조를 통해 잘못된 task_struct에 작업이 적용될 수 있습니다. 이는 메모리 안전성과 커널 안정성 문제로 이어질 수 있으므로 반드시 피해야 합니다.

패치는 thread_handoff_allowed()와 thread_handoff_compatible()에 여러 조건을 두어, 정체성 교환이 가능한 경우에만 이 최적화를 사용하도록 합니다. 예를 들어 ptrace()로 추적되는 스레드는 추적자가 해당 task_struct를 참조하므로 교환할 수 없습니다. 반대로 해당 스레드가 다른 작업을 추적하고 있는 경우에도 추적 대상이 task_struct를 참조할 수 있으므로 허용되지 않습니다. perf event를 사용 중인 경우, 커널에서 futex 소유권 추적을 사용하는 경우, 실시간 스케줄러로 실행 중인 경우, core scheduling cookie를 보유한 경우, vfork()를 수행 중인 경우 등도 제외됩니다.

이 조건 목록이 이 RFC에서 특히 취약하고 복잡한 부분으로 지적됩니다. task_struct를 참조하는 경로를 모두 현재 시점에 찾아냈다고 하더라도, 이후 io_uring을 고려하지 않은 다른 커널 개발자가 새로운 참조를 추가하면 정체성 교환 조건이 다시 깨질 수 있기 때문입니다. Peter Zijlstra는 shadow stack을 사용하는 스레드도 handoff를 막는 조건 중 하나이며, 이 상태라면 대부분의 배포 시스템에서 기능을 사용할 수 없게 된다고 지적했습니다. Axboe는 shadow stack 역시 스레드 정체성의 나머지 요소와 함께 이동시킬 수 있다고 보고 있습니다.

■ 벤치마크 결과와 한계

RFC cover letter에 포함된 벤치마크에서는 작업 유형에 따라 큰 성능 차이가 나타났습니다. 빠르게 끝나는 비블로킹 작업에서는 개선 폭이 컸으며, tmpfs에서 실행한 fsync() 벤치마크는 거의 700% 향상됐습니다. 반면 항상 블록되는 테스트에서는 성능이 저하된 경우도 있었습니다.

특히 여러 작업을 한 번에 제출하는 높은 queue depth에서 회귀가 두드러졌습니다. 기존 방식은 각 작업을 즉시 워커 스레드로 넘겨 병렬 처리할 수 있지만, 새 방식은 제출 스레드에서 각 작업을 블록 지점까지 순차적으로 진행하므로 그 전처리가 직렬화됩니다. Axboe는 이 문제를 해결할 아이디어가 있다고 밝혔지만, 이런 작업을 수행하는 애플리케이션은 애초에 높은 queue depth를 사용하는 경우가 많지 않다고 보고 있어 추가 개선이 실제로 가치가 있을지는 확신하지 못하고 있습니다.

Axboe는 이 패치 세트를 가까운 시일 안에 병합하려는 것이 아니라, 접근 방식 자체가 실현 가능한지 확인하는 데 초점을 두고 있습니다. Gabriel Krisman Bertazi는 이 아이디어가 “정말 멋지고 아주 위험한 일처럼 보인다”고 평가했습니다. 현재 논의는 아직 초기 단계이며, shadow stack을 비롯한 모든 커널 상태와 참조를 안전하게 함께 이동할 수 있는지가 핵심 검증 대상입니다. 이 위험을 해결할 수 있다면 일부 io_uring 워크로드에서는 실제로 블록되지 않는 작업의 성능을 크게 높일 가능성이 있습니다.

■ Lobsters 반응

• @1amzave — 저처럼 task_struct를 그대로 둔 채 나머지를 모두 교환하는 대신, task_struct 사이에서 스택만 교환하는 편이 더 간단하지 않을까 생각하셨다면, 메일링 리스트 논의에서도 이 문제가 다뤄졌습니다: https://lwn.net/ml/all/[email protected]/

원문: LWN.net / 번역·요약: Trawling