일반적인 인식에서는 OS의 I/O 작업은 프로세스가 종료되면 정지하는 것으로 받아들여지고 있지 않을까요.
그러나 Linux 커널에서의 I/O 작업 라이프사이클은 반드시 프로세스의 라이프사이클과 일치하지 않을 수 있습니다.
특히 Linux 커널 5.1에서 도입된 io_uring을 사용한 경우, 프로세스가 종료된 후에도 I/O 작업이 커널 내에서 계속되어 스토리지에 도달할 가능성이 데이터베이스 플랫폼 개발자인 예브게니 이바노프 씨에 의해 보고되고 있습니다.
Is There I/O After Death?
What Happens to io_uring When a Process Dies | by Evgenii Ivanov | Sep, 2026 | YDB.
테크 블로그 https://blog.ydb.tech/is-there-i-o-after-death-what-happens-to-io-uring-when-a-process-dies-92c65354873f
번역 대상:
postPublishedType=repub기존의 Linux 네이티브 AIO나 SQPOLL 모드의 io_uring을 이용해 스토리지에 쓰기를 수행하는 경우에는, 프로세스에 대해 waitpid()가 호출되면 프로세스와 관련된 I/O 작업도 종료되고, 쓰기 완료를 기다린 뒤 waitpid()가 값을 반환합니다.
그러나 일반적인 io_uring에서는 프로세스 종료 후에도 이전에 전송된 쓰기가 커널 내에 유지되어, 나중에 디바이스에 도달하는 경우가 있습니다.
즉, waitpid()는 단순히 프로세스의 종료를 알릴 뿐이며, I/O 작업의 완료를 보장하는 것이 아니라는 것입니다.
양자의 동작 차이는 일반적으로는 미미하다고 할 수 있지만, 페일 패스트형, 즉 이상이나 오류를 감지했을 때 즉시 처리를 중단·정지시키는 설계 방침에 기반한 애플리케이션의 경우에는 중요한 차이가 됩니다.
애플리케이션 프로세스의 I/O 작업이 아직 처리 중인 단계에서 후속 프로세스에 의한 복구·스토리지 검사·쓰기가 시작되어 버릴 가능성이 있기 때문입니다.
I/O 처리의 동작을 실험하기 위해, 이바노프 씨는 부모-자식 프로세스로 이루어진 간단한 테스트 프로그램을 만들었습니다.
자식 프로세스는 쓰기 프로세스로서, 동작 중에 1024개·4KiB의 블록을 순환하면서 “자식 프로세스의 PID”와 “쓰기 횟수”를 계속 덮어씁니다.
부모 프로세스는 다음과 같은 처리를 수행합니다.
·1.
번역 대상:
자식 프로세스에 SIGKILL을 전송한다·2.
waitpid()로 자식 프로세스의 종료를 기다린다·3.
자식 프로세스의 쓰기 대상인 1024개의 블록을 모두 읽는다·4.
잠시 후 다시 한번 1024개의 블록을 모두 읽는다·5.
블록별로 내용을 비교한다 만약 두 개의 스냅샷에서 차이가 발생했다면, waitpid()가 반환된 후에 종료되었어야 할 자식 프로세스가 스토리지에 쓰기를 수행했다는 것이 됩니다.
•실험 1: 그대로 실행 먼저 유휴 상태의 NVMe 디바이스상에서 실험합니다.
SIGKILL at +1000.
061 ms
waitpid took 7.
514 ms
first snapshot: 1024/1024 blocks from writer
second snapshot: 1024/1024 blocks from writer
번역 대상:
all 1024 positions unchanged
언뜻 보면 두 스냅샷 간에 차이가 없어 보이지만, 그렇다고 해서 자식 프로세스 종료 후에 스토리지에 대한 쓰기가 이루어지지 않았음을 보여주는 것은 아닙니다. 유휴 상태의 NVMe는 단순히 너무 빠르기 때문에, 자식 프로세스 종료 시점에 미처리된 쓰기가 부모가 첫 번째 스냅샷을 실행하기 전, 혹은 실행 중에 완료되었을 가능성이 있습니다.
·실험 2: dm-delay에 의한 쓰기 지연
Linux에는 dm-delay라는 편리한 디바이스 매퍼가 존재하여 스토리지의 응답이 느린 상태를 재현할 수 있으므로, 3초간의 쓰기 지연을 설정했습니다. 자식 프로세스는 지연 매핑을 통해 천천히 쓰는 반면, 부모 프로세스는 직접 읽어들일 수 있습니다.
SIGKILL at +1000.064 ms
waitpid took 2.951 ms
first snapshot:
0/1024 blocks from current writer
second snapshot:
1024/1024 blocks from current writer
1024/1024 positions changed
I/O로부터의 잔재 감지가 자식 프로세스가 waitpid() 약 3밀리초 후에 복귀하고, 거의 3초 후에 쓰기가 발생했습니다. 즉, waitpid() 완료 이후에도 I/O가 진행 중임을 시사합니다.
• 실험 3: fio를 사용하여 랜덤 읽기를 dm-delay로 인한 인위적인 지연을 사용하지 않고, 네이티브 NVMe 큐에서 동일한 동작을 확인하기 위해 fio를 사용하여 랜덤 읽기 워로드를 실행시켰습니다.
fio --name=grave_readload \
--filename=/dev/nvme2n1p2 \
--readonly \
--rw=randread \
--bs=4096 \
--direct=1 \
--ioengine=io_uring \
--iodepth=2048 \
--numjobs=32 쓰기 프로세스 실행 중에 시그널 kill이 발생한 결과, 역시 이전의 쓰기가 프로세스 종료 후에 스토리지까지 도달하는 것이 관찰되었으며, io_uring의 비동기 I/O가 프로세스 종료 이후에도 지속될 수 있음을 시사했습니다.
SIGKILL at +1000.059 ms
waitpid는 8.028 ms 소요되었습니다.
첫 번째 snímka는 252.735 ms를 기록했습니다.
두 번째 snímka는 177.544 ms를 기록했습니다.
627/1024 위치가 변경되었습니다.
마이오(I/O) 검출이 된 I/O에 대한 대응 프로세스가 종료된 후 I/O 작업으로 인한 영향을 배제하는 간단한 대응책은, 쓰기 측 프로세스가 장치를 열 때 배타적 잠금(바리어 메커니즘)을 설정하는 것임을 이와노프 氏は 밝혔습니다. 쓰기 측 프로세스는 open() 함수를 사용하여 장치를 열고, 파일 디스크립터를 사용하여 flock() 함수로 배타적 잠금을 설정합니다. int fd = open(path, O_RDWR | O_DIRECT);
flock(fd, LOCK_EX); 쓰기 측 프로세스를 종료시킨 후, 후계 프로세스는 동일한 잠금을 획득해야 합니다. int fd = open(path, O_RDWR | O_DIRECT);
for (;;) {
if (flock(fd, LOCK_EX | LOCK_NB) == 0) {
break; // 잠금 획득 성공
}
if (errno != EWOULDBLOCK && errno != EAGAIN) {
perror("flock");
abort();
}
usleep(1000); // 1ms 후에 다시 잠금 획득을 시도
} 프로세스 종료 후에도 미완료의 I/O 요청이 파일 디스크립터에 대한 참조를 유지하기 때문에, 후속 프로세스는 잠금을 획득할 수 없습니다. 따라서 위와 같은 구현을 수행하면 I/O 완료될 때까지 대기시키는 실질적인 장벽으로 기능한다는 의미입니다. 다음 표는 Linux 6.6.79에서 I/O 요청과 장벽 메커니즘에 대한 동작을 요약한 것입니다. 쓰기 방법 waitpid()가 미처리된 쓰기를 지우는가? waitpid() 후에 후속 프로세스가 즉시 잠금을 획득하는가? 일반적인 io_uring을 하지 않는 직후에는 EWOULDBLOCK 오류, 이후 조금 더 나중에 SQPOLL 모드의 io_uring을 하는 직후에는 성공, Linux 네이티브 AIO를 하는 직후에는 성공, 일반적인 io_uring에서는 쓰기 프로세스가 종료된 후에 스토리지의 잠금 획득 결과가 변화하는 것을 확인할 수 있었습니다. 반면에 SQPOLL 모드의 io_uring 및 Linux 네이티브 AIO에서는 쓰기가 모두 완료될 때까지 waitpid()는 반환하지 않았습니다. 결론적으로, Linux 커널에서 프로세스의 라이프사이클과 I/O의 라이프사이클이 항상 일치하는 것은 아니라는 것을 확인할 수 있었습니다. 특히 일반적인 io_uring에서는 waitpid()가 프로세스의 종료를 알림에도 I/O가 미완료 상태일 수 있습니다. 복구 프로토콜 등으로 waitpid()를 종료 프로세스에 의한 I/O 작업의 장벽으로 사용하는 경우 일반적인 io_uring이 나타내는 동작은 문제로 이어질 수 있으므로, I/O 경로의 동작을 이해하거나 배타적 잠금에 의한 명시적인 장벽 메커니즘의 도입이 필요합니다. 참고로, 비-SQPOLL io_uring의 동작은 Linux 커널 7.3에서 변경될 예정이며, 비동기 I/O가 프로세스 종료 후에 지속되는 동작은 사라질 가능성이 있습니다. Google 우선 소스에 설정 복사본 기사 제목과 URL
겉으로 보기에는 두 스냅샷에서 차이가 없어 보이지만, 이것이 자 프로세스 종료 후 저장소에 쓰기 작업이 수행되지 않았다는 것을 의미하지는 않습니다. 아이돌 상태의 NVMe는 단순히 너무 빠르기 때문에, 자 프로세스 종료 시점에 처리되지 않은 쓰기가 부모 프로세스가 첫 번째 스냅샷을 실행하기 전, 혹은 실행 중에 완료될 수 있습니다.
* 실험 2: dm-delay를 통한 쓰기 지연
Linux에는 dm-delay라는 유용한 장치 매퍼가 존재하며, 저장소의 응답이 느린 상태를 재현할 수 있습니다. 따라서 3초간의 쓰기 지연을 설정했습니다. 자 프로세스는 지연 매핑에 의해 느리게 쓰기를 수행하는 반면, 부모 프로세스는 직접적으로 읽기를 수행할 수 있습니다.
SIGKILL at +1000.064 ms
waitpid took 2.951 ms
첫 번째 스냅샷:
0/1024 블록에서 현재 작성자
두 번째 스냅샷:
1024/1024 블록에서 현재 작성자
1024/1024 위치가 변경됨
I/O로부터의 잔재 감지가 자식 프로세스가 waitpid() 약 3밀리초 후에 복귀하고, 거의 3초 후에 쓰기가 수행되었습니다. 즉, waitpid() 완료 이후에도 I/O가 진행 중임을 시사합니다.
• 실험 3: fio를 사용하여 임의 읽기를 dm-delay를 통한 인위적인 지연을 사용하지 않고, 네이티브 NVMe 큐에서 동일한 동작을 확인하기 위해 fio를 사용하여 임의 읽기 워로드를 실행했습니다.
fio --name=grave_readload \
--filename=/dev/nvme2n1p2 \
--readonly \
--rw=randread \
--bs=4096 \
--direct=1 \
--ioengine=io_uring \
--iodepth=2048 \
--numjobs=32 쓰기 프로세스 실행 중에 시그널 kill이 발생한 결과, 역시 이전의 쓰기가 프로세스 종료 후에 스토리지까지 도달하는 것이 관찰되었으며, io_uring의 비동기 I/O가 프로세스 종료 이후에도 지속될 수 있음을 시사했습니다.
SIGKILL at +1000.059 ms
waitpid는 8.028 ms 소요되었습니다.
첫 번째 snímka는 252.735 ms를 기록했습니다.
두 번째 snímka는 177.544 ms를 기록했습니다.
627/1024 위치가 변경되었습니다.
마이오(I/O) 검출된 I/O에 대한 대응 프로세스 종료 후 I/O 작업으로 인한 영향을 배제하는 간단한 대응은, 쓰기 측 프로세스가 장치를 열 때 배타적 잠금(바리어 메커니즘)을 설정하는 것임을 이와노프 氏は설명했습니다. 쓰기 측 프로세스는 open() 함수를 사용하여 장치를 열고, 파일 디스크립터를 사용하여 flock() 함수로 배타적 잠금을 설정합니다. int fd = open(path, O_RDWR | O_DIRECT);
flock(fd, LOCK_EX); 쓰기 측 프로세스를 종료시킨 후, 후계 프로세스는 동일한 잠금을 획득해야 합니다. int fd = open(path, O_RDWR | O_DIRECT);
for (;;) {
if (flock(fd, LOCK_EX | LOCK_NB) == 0) {
break; // 잠금 획득 성공
}
if (errno != EWOULDBLOCK && errno != EAGAIN) {
perror("flock");
abort();
}
usleep(1000); // 1ms 후에 다시 잠금 획득을 시도
} 프로세스 종료 후에도 미완료의 I/O 요청이 파일 디스크립터에 대한 참조를 유지하기 때문에, 후속 프로세스는 잠금을 획득할 수 없습니다. 따라서 위와 같은 구현을 수행하면 I/O 완료될 때까지 대기시키는 실질적인 장벽으로 기능한다는 의미입니다. 다음 표는 Linux 6.6.79에서 I/O 요청과 장벽 메커니즘에 대한 동작을 요약한 것입니다. 쓰기 방법 waitpid()가 미처리된 쓰기를 지우는가? waitpid() 후에 후속 프로세스가 즉시 잠금을 획득하는가? 일반적인 io_uring을 하지 않는 직후에는 EWOULDBLOCK 오류, 이후에 성공하는 SQPOLL 모드 io_uring을 하는 직후에 성공하는 Linux 네이티브 AIO를 하는 직후에 성공하는 일반적인 io_uring에서는 쓰기 프로세스가 종료된 후에 저장소의 잠금 획득 결과가 변화하는 것을 확인할 수 있었습니다. 반면에 SQPOLL 모드 io_uring 및 Linux 네이티브 AIO에서는 쓰기가 모두 완료될 때까지 waitpid()는 반환되지 않았습니다. 결론적으로, Linux 커널에서 프로세스의 라이프사이클과 I/O의 라이프사이클이 항상 일치하는 것은 아니라는 것을 확인할 수 있었습니다. 특히 일반적인 io_uring에서는 waitpid()가 프로세스의 종료를 알림에도 I/O가 미완료일 수 있습니다. 복구 프로토콜 등으로 waitpid()를 종료 프로세스에 의한 I/O 작업의 장벽으로 활용하는 경우 일반적인 io_uring이 나타내는 동작은 문제로 이어질 수 있으므로, I/O 경로의 동작을 이해하거나 배타적 잠금에 의한 명시적인 장벽 메커니즘의 도입이 필요합니다. 참고로, 비-SQPOLL io_uring의 동작은 Linux 커널 7.3에서 변경될 예정이며, 비동기 I/O가 프로세스 종료 후에 지속되는 동작은 사라질 가능성이 있다는 것입니다. Google 우선 소스에 설정 복사본 기사 제목과 URL
자식 프로세스가 종료된 후 waitpid()가 약 3밀리초 후에 반환되고, 거의 3초 후에 쓰기가 수행되었습니다. 즉, waitpid() 완료 이후에도 I/O가 진행되고 있음을 시사합니다.
실험 3: fio를 사용하여 랜덤 읽기를 dm-delay를 통한 인위적인 지연을 사용하지 않고 네이티브 NVMe 큐에서 동일한 동작을 확인하기 위해, fio를 사용하여 랜덤 읽기 워크로드를 실행시켰습니다.
fio --name=grave_readload \
--filename=/dev/nvme2n1p2 \
--readonly \
--rw=randread \
--bs=4096 \
--direct=1 \
--ioengine=io_uring \
--iodepth=2048 \
--numjobs=32 쓰기 프로세스 실행 중에 시그널 kill을 발생시킨 결과, 역시 이전의 쓰기가 프로세스 종료 후에 스토리지까지 도달하는 것이 관찰되었으며, io_uring의 비동기 I/O가 프로세스 종료 이후에도 지속될 수 있음을 시사했습니다.
SIGKILL at +1000.059 ms
waitpid took 8.028 ms
첫 번째 샷 랩 스냅샷은 252.735 ms
두 번째 샷 랩 스냅샷은 177.544 ms
627/1024 위치 변경
I/O로부터의 심각한 오류가 감지되었습니다. 대응 프로세스 종료 후 I/O 작업으로 인한 영향을 배제하는 간단한 대책은 쓰기 측 프로세스가 장치를 여는 시점에 배타적 잠금(바리어 메커니즘)을 설정하는 것입니다라고 이와노프 氏は述べています. 쓰기 측 프로세스는 open() 함수를 사용하여 장치를 열고, 파일 디스크립터를 사용하여 flock()을 통한 배타적 잠금을 설정합니다. 파일 디스크립터 fd는 open(path, O_RDWR | O_DIRECT)를 통해 생성되며, flock(fd, LOCK_EX)를 통해 배타적 잠금을 설정합니다. 쓰기 측 프로세스를 종료시킨 후, 후계 프로세스는 동일한 잠금을 획득해야 합니다. 파일 디스크립터 fd는 open(path, O_RDWR | O_DIRECT)를 통해 다시 생성됩니다.
for (;;) {
if (flock(fd, LOCK_EX | LOCK_NB) == 0) {
break; // 잠금 획득 성공
}
if (errno != EWOULDBLOCK && errno != EAGAIN) {
perror("flock");
abort();
}
usleep(1000); // 1ms 후에 다시 잠금 획득을 시도
} 프로세스 종료 후에도 미완료의 I/O 요청이 파일 디스크립터에 대한 참조를 유지하기 때문에, 후속 프로세스는 잠금을 획득할 수 없습니다. 따라서 위와 같은 구현을 수행하면 I/O 완료될 때까지 대기시키는 실질적인 장벽으로 기능한다는 의미입니다. 다음 표는 Linux 6.6.79에서 I/O 요청과 장벽 메커니즘에 대한 동작을 요약한 것입니다. 쓰기 방법 waitpid()가 미처리된 쓰기를 지우는가? waitpid() 후에 후속 프로세스가 즉시 잠금을 획득하는가? 일반적인 io_uring을 하지 않는 직후에는 EWOULDBLOCK 오류, 이후에 성공하는 SQPOLL 모드 io_uring을 하는 직후에 성공하는 Linux 네이티브 AIO를 하는 직후에 성공하는 일반적인 io_uring에서는 쓰기 프로세스가 종료된 후에 저장소의 잠금 획득 결과가 변화하는 것을 확인할 수 있었습니다. 반면에 SQPOLL 모드 io_uring 및 Linux 네이티브 AIO에서는 쓰기가 모두 완료될 때까지 waitpid()가 반환되지 않았습니다. 결론적으로, Linux 커널에서 프로세스의 라이프사이클과 I/O의 라이프사이클이 항상 일치하는 것은 아니라는 것을 확인할 수 있었습니다. 특히 일반적인 io_uring에서는 waitpid()가 프로세스의 종료를 알리더라도 I/O가 미완료일 수 있습니다. 복구 프로토콜 등으로 waitpid()를 종료 프로세스에 의한 I/O 작업의 장벽으로 활용하는 경우 일반적인 io_uring이 나타내는 동작은 문제로 이어질 수 있으므로, I/O 경로의 동작을 이해하거나 배타적 잠금에 의한 명시적인 장벽 메커니즘의 도입이 필요합니다. 참고로, 비-SQPOLL io_uring의 동작은 Linux 커널 7.3에서 변경될 예정이며, 비동기 I/O가 프로세스 종료 후에 지속되는 동작은 사라질 가능성이 있다는 것입니다. Google 우선 소스에 설정 클립보드 기사 제목과 URL을 복사 X Facebook Bluesky Discord Threads
작성 과정 실행 중 시그널 킬(SIGKILL)이 발생하여, 역시 이전 작성 내용이 프로세스 종료 후에도 스토리지까지 도달하는 것이 관찰되었고, io_uring의 비동기 I/O가 프로세스 종료 후에도 지속될 수 있음을 시사했습니다. waitpid는 8.028ms 소요되었습니다.
first snapshot took 252.735 ms
second snapshot took 177.544 ms
627/1024개의 위치가 변경되었습니다.
I/O (입출력)이 무덤에서 감지되었다고 합니다. 대응 프로세스 종료 후 I/O 작업으로 인한 영향을 배제하는 간단한 대책은, 쓰기 측 프로세스가 장치를 여는 시점에 배타적 잠금(락)을 걸어 ‘장벽 메커니즘’을 마련하는 것이라고 이와노프 씨가 밝혔습니다. 쓰기 측 프로세스는 open() 함수를 사용하여 장치를 열고, 파일 디스크립터를 사용하여 flock() 함수를 통해 배타적 잠금을 설정합니다. int fd = open(path, O_RDWR | O_DIRECT);
flock(fd, LOCK_EX); 쓰기 측 프로세스를 종료시킨 후, 후계 프로세스는 동일한 잠금을 획득해야 합니다. int fd = open(path, O_RDWR | O_DIRECT);
for (;;) {
if (flock(fd, LOCK_EX | LOCK_NB) == 0) {
break; // 잠금 획득 성공
}
if (errno != EWOULDBLOCK && errno != EAGAIN) {
perror("flock");
abort();
}
usleep(1000); // 1ms 후에 다시 잠금 획득을 시도
} 프로세스 종료 후에도 미완료의 I/O 요청이 파일 디스크립터에 대한 참조를 유지하기 때문에, 후속 프로세스는 잠금을 획득할 수 없습니다. 따라서 위와 같은 구현을 수행하면 I/O 완료될 때까지 대기시키는 실질적인 장벽으로 기능한다는 의미입니다. 다음 표는 Linux 6.6.79에서 I/O 요청과 장벽 메커니즘에 대한 동작을 요약한 것입니다. 쓰기 방법 waitpid()가 미처리된 쓰기를 지우는가? waitpid() 후에 후속 프로세스가 즉시 잠금을 획득하는가? 일반적인 io_uring을 하지 않는 직후에는 EWOULDBLOCK 오류, 이후에 성공하는 SQPOLL 모드 io_uring을 하는 직후에 성공하는 Linux 네이티브 AIO를 하는 직후에 성공하는 일반적인 io_uring에서는 쓰기 프로세스가 종료된 후에 저장소의 잠금 획득 결과가 변화하는 것을 확인할 수 있었습니다. 반면에 SQPOLL 모드 io_uring 및 Linux 네이티브 AIO에서는 쓰기가 모두 완료될 때까지 waitpid()가 반환되지 않았습니다. 결론적으로, Linux 커널에서 프로세스의 라이프사이클과 I/O의 라이프사이클이 항상 일치하는 것은 아니라는 것을 확인할 수 있었습니다. 특히 일반적인 io_uring에서는 waitpid()가 프로세스의 종료를 알리더라도 I/O가 미완료일 수 있습니다. 복구 프로토콜 등으로 waitpid()를 종료 프로세스에 의한 I/O 작업의 장벽으로 활용하는 경우 일반적인 io_uring이 나타내는 동작은 문제로 이어질 수 있으므로, I/O 경로의 동작을 이해하거나 배타적 잠금에 의한 명시적인 장벽 메커니즘의 도입이 필요합니다. 참고로, 비-SQPOLL io_uring의 동작은 Linux 커널 7.3에서 변경될 예정이며, 비동기 I/O가 프로세스 종료 후에 지속되는 동작은 사라질 가능성이 있습니다. Google 우선 소스에 설정 복사본 기사 제목과 URL
방제 프로세스 종료 후 I/O 작업으로 인한 영향을 배제하는 간단한 방제는 쓰기 측 프로세스가 장치를 여는 시점에 배타적 잠금(바리어 메커니즘)을 설정하는 것임을 이와노프氏は 설명합니다.
쓰기 측 프로세스는 open()을 사용하여 장치를 열고, 파일 디스크립터를 사용하여 flock()을 통한 배타적 잠금을 설정합니다. int fd = open(path, O_RDWR | O_DIRECT);
flock(fd, LOCK_EX); 쓰기 측 프로세스를 종료시킨 후, 후계 프로세스는 동일한 잠금을 획득해야 합니다. int fd = open(path, O_RDWR | O_DIRECT);
for (;;) {
if (flock(fd, LOCK_EX | LOCK_NB) == 0) {
break; // 락 획득 성공
}
if (errno != EWOULDBLOCK && errno != EAGAIN) {
perror("flock");
abort();
}
usleep(1000); // 1ms 후에 다시 락 획득을 시도
} 프로세스 종료 후에도 미완료된 I/O 요청이 파일 디스크립터에 대한 참조를 유지하기 때문에, 후속 프로세스는 락을 획득할 수 없습니다. 따라서, 위와 같은 구현을 수행하면 I/O 완료될 때까지 대기시키는 실질적인 장벽으로 기능한다는 의미입니다. 다음 표는 Linux 6.6.79에서 I/O 요청과 바리어 메커니즘에 대한 동작을 요약한 것입니다. 쓰기 방법 waitpid()가 미처리된 쓰기를 지우는가? waitpid() 후에 후속 프로세스가 즉시 락을 획득하는가? 일반적인 io_uring을 하지 않는 직후에는 EWOULDBLOCK 오류, 이후에 성공하는 SQPOLL 모드 io_uring을 하는 직후에 성공하는 Linux 네이티브 AIO를 하는 직후에 성공하는 일반적인 io_uring에서는 쓰기 프로세스가 종료된 후에 스토리지의 락 획득 결과가 변화하는 것을 확인할 수 있었습니다. 반면에, SQPOLL 모드 io_uring 및 Linux 네이티브 AIO에서는 쓰기가 모두 완료될 때까지 waitpid()는 반환하지 않았습니다. 결론적으로, Linux 커널에서 프로세스의 라이프사이클과 I/O의 라이프사이클은 항상 일치하지 않는다는 것을 확인했습니다. 특히 일반적인 io_uring에서는 waitpid()가 프로세스의 종료를 알림에도 I/O가 미완료일 수 있습니다. 복구 프로토콜 등으로 waitpid()를 종료 프로세스에 의한 I/O 작업의 바리어로 사용하는 경우 일반적인 io_uring이 나타내는 동작은 문제로 이어질 수 있으므로, I/O 경로의 동작을 이해하거나 배타적 락에 의한 명시적인 바리어 메커니즘의 도입이 필요합니다. 참고로, 비-SQPOLL io_uring의 동작은 Linux 커널 7.3에서 변경될 예정이며, 비동기 I/O가 프로세스 종료 후에 지속되는 동작은 사라질 가능성이 있습니다. Google 우선 소스에 설정 복사본 기사 제목과 URL
쓰기 측 프로세스를 종료한 후, 후계 프로세스는 동일한 잠금을 획득해야 합니다. int fd = open(path, O_RDWR | O_DIRECT);
for (;;) {
if (flock(fd, LOCK_EX | LOCK_NB) == 0) {
break; // 잠금 획득 성공
}
if (errno != EWOULDBLOCK && errno != EAGAIN) {
perror("flock");
abort();
}
usleep(1000); // 1ms 후에 다시 잠금 획득을 시도
} 프로세스 종료 후에도 미완료의 I/O 요청이 파일 디스크립터에 대한 참조를 유지하기 때문에, 후속 프로세스는 잠금을 획득할 수 없습니다. 따라서 위와 같은 구현을 수행하면 I/O 완료될 때까지 대기시키는 실질적인 장벽으로 기능한다는 의미입니다. 다음 표는 Linux 6.6.79에서 I/O 요청과 장벽 메커니즘에 대한 동작을 요약한 것입니다. 쓰기 방법 waitpid()가 미처리된 쓰기를 지우는가? waitpid() 후에 후속 프로세스가 즉시 잠금을 획득하는가? 일반적인 io_uring을 하지 않는 직후에는 EWOULDBLOCK 오류, 이후에 성공하는 SQPOLL 모드 io_uring을 하는 직후에 성공하는 Linux 네이티브 AIO를 하는 직후에 성공하는 일반적인 io_uring에서는 쓰기 프로세스가 종료된 후에 저장소의 잠금 획득 결과가 변화하는 것을 확인할 수 있었습니다. 반면에 SQPOLL 모드 io_uring 및 Linux 네이티브 AIO에서는 쓰기가 모두 완료될 때까지 waitpid()가 반환되지 않았습니다. 결론적으로, Linux 커널에서 프로세스의 라이프사이클과 I/O의 라이프사이클이 항상 일치하는 것은 아니라는 것을 확인할 수 있었습니다. 특히 일반적인 io_uring에서는 waitpid()가 프로세스의 종료를 알림에도 I/O가 미완료일 수 있습니다. 복구 프로토콜 등으로 waitpid()를 종료 프로세스에 의한 I/O 작업의 장벽으로 사용하는 경우 일반적인 io_uring이 나타내는 동작은 문제로 이어질 수 있으므로, I/O 경로의 동작을 이해하거나 배타적 잠금에 의한 명시적 장벽 메커니즘의 도입이 필요합니다. 참고로, 비-SQPOLL io_uring의 동작은 Linux 커널 7.3에서 변경될 예정이며, 비동기 I/O가 프로세스 종료 후에 지속되는 동작은 사라질 가능성이 있습니다. Google 우선 소스에 설정 클립보드 기사 제목과 URL을 복사 X Facebook Bluesky Discord Threads
프로세스 종료 후에도 미완료된 I/O 요청이 파일 디스크립터에 대한 참조를 유지하기 때문에, 후속 프로세스는 잠금을 획득할 수 없습니다. 따라서 위와 같은 구현을 수행하면 I/O 완료까지 대기시키는 실질적인 장벽으로 기능한다는 의미입니다. 다음 표는 Linux 6.6.79에서 I/O 요청과 장벽 메커니즘에 대한 동작을 요약한 것입니다.
일반적인 io_uring에서는 쓰기 프로세스가 종료된 후 저장소의 잠금 획득 결과가 변화하는 현상을 확인할 수 있었습니다. 반면에 SQPOLL 모드의 io_uring 및 Linux 네이티브 AIO에서는 쓰기가 모두 완료될 때까지 waitpid()가 반환되지 않았습니다. 결론적으로, Linux 커널에서 프로세스의 라이프사이클과 I/O의 라이프사이클은 항상 일치하지 않는다는 것을 확인할 수 있었습니다. 특히 일반적인 io_uring에서는 waitpid()가 프로세스의 종료를 통지해도 I/O가 미완료 상태일 가능성이 있습니다. 복구 프로토콜 등으로 waitpid()를 종료 프로세스에 의한 I/O 작업의 장벽으로 활용할 때 일반적인 io_uring이 나타내는 동작은 문제로 이어질 수 있으므로, I/O 경로의 동작을 이해하거나 배타 잠금에 의한 명시적인 장벽 메커니즘의 도입이 필요합니다. 참고로, 비-SQPOLL io_uring의 동작은 Linux 커널 7.3에서 변경될 예정이며, 비동기 I/O가 프로세스 종료 후 지속되는 동작은 사라질 가능성이 있습니다.
원문 보기 | 출처: Gigazine