close(2)가 완료되지 않는 이유는 무엇입니까? 멀티스레딩에서 발생할 수 있는 문제

close(2)가 완료되지 않는 이유는 무엇입니까? 멀티스레딩에서 발생할 수 있는 문제

RedHat Linux 7.9, 커널 3.10.0-1160.90.1.el7.x86_64, glibc-2.17-326.el7_9.x86_64. 예, 오래되었습니다. 죄송합니다.

Linux 커널에서 close(2) 커널 함수가 때때로 한 번의 호출로 완료되지 않는 이유는 무엇입니까? 멀티스레딩에 문제가 있을 수 있다는 것을 알았습니다. 스레드가 파일 설명자를 열고()하고, 쓰고()하고, 닫는 경우, 닫기 도중에 파일 설명자가 open()의 다른 스레드에서 재사용되는 것을 개인적으로 목격했습니다. 그런 다음 close()가 완료되고 쓰기를 시도하면 잘못된 파일 설명자가 나타납니다.

이것은 기능인가요 아니면 버그인가요? 여러 스레드가 파일 설명자 테이블을 공유한다는 것을 알고 있으므로 파일 설명자 테이블을 최대한 빨리 처리할 때 정의되지 않은 동작(아래 참조)이 발생할 수 있지만 close()가 완료되기 전에 파일 설명자가 어떻게 해제되는지 궁금합니다.

스레드가 열릴 때 호출되는 clone()이 실제로 모든 스레드 간에 전체 파일 설명자 테이블을 공유한다는 사실을 모르기 때문에 각 스레드의 코드는 자신이 여는 파일 설명자에 씁니다. 또한 한 스레드의 open()/write()/close() 작업 집합이 다른 스레드의 close() 중간에 open()을 수행하기 때문에 충돌이 발생할 수 있다고 예상하지 않습니다. 정리가 완료되고 반환되기 전에 사용할 수 있는 파일 설명자입니다.

다음은 이 동작을 보여주는 strace의 일부 출력입니다. 여기에는 58506과 58508이라는 두 개의 스레드가 있습니다. PID 58506은 타임스탬프 532769의 끝에서 마감 12로 이동합니다. 532791 - 25마이크로초 후 - 종료가 완료됩니다. 그러나 타임스탬프 532775-532769-PID 58508 이후 8개의 마이크가 파일을 엽니다. 532803(열기가 시작된 후 28마이크로초)에 열기가 완료되고 파일 설명자 12(현재 닫힘)가 PID 58508에 할당됩니다. 532949에서 파일 설명자가 잘못되어 쓰기에 실패했습니다. 이는 PID 58508이 파일 설명자를 얻은 532803 이후 146마이크로초입니다.

이는 한 쌍의 SSD로 지원되는 하드웨어 RAID 카드가 있는 Dell R640 서버의 로컬 파일 시스템에 있습니다(가치가 있는 만큼).

58506 08:58:34.532769 close(12 <unfinished ...>

58508 08:58:34.532775 open("/path/to/dir1/file1", O_WRONLY|O_CREAT|O_TRUNC, 0664 <unfinished ...>

58506 08:58:34.532791 <... close resumed>) = 0

58508 08:58:34.532803 <... open resumed>) = 12

58506 08:58:34.532808 close(12)         = 0

58508 08:58:34.532936 write(12, “datadatadatadata"..., 1572 <unfinished ...>

58506 08:58:34.532943 <... write resumed>) = 258

58508 08:58:34.532949 <... write resumed>) = -1 EBADF (Bad file descriptor)

58506 08:58:34.532963 stat("/path/to/dir2", {st_mode=S_IFDIR|0755, st_size=4096, ...})
 = 0
58506 08:58:34.532995 open("/path/to/dir2/file2", O_WRONLY|O_CREAT|O_TRUNC, 0664) = 12


58508 08:58:34.533286 close(12)         = 0
58508 08:58:34.533311 close(12)         = -1 EBADF (Bad file descriptor)

58508 08:58:34.533595 write(1, "[2023-10-04 08:58:34.533588 E] P"..., 216) = 216

58506 08:58:34.533634 write(12, "moredatadatadatadata"..., 6139) = -1 EBADF (Bad file descriptor)
58506 08:58:34.533660 close(12)         = -1 EBADF (Bad file descriptor)

58508 08:58:34.533681 nanosleep({tv_sec=0, tv_nsec=100000000},  <unfinished ...>

58506 08:58:34.533688 close(12)         = -1 EBADF (Bad file descriptor)
58506 08:58:34.533709 stat("/path/to/dir2/file2", {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
58506 08:58:34.533760 write(1, "[2023-10-04 08:58:34.533754 E] P"..., 233) = 233
58506 08:58:34.533786 nanosleep({tv_sec=0, tv_nsec=100000000},  <unfinished ...>

타임스탬프 533709에서 개발자는 파일의 길이가 0인지 확인합니다. 파일이 실제로 생성되었지만 쓰기가 실패하여 파일에 데이터가 없습니다. 따라서 코드는 루프에 빠지고 파일을 다시 작성하려고 시도합니다.

POSIX에서는 열린 파일 설명자를 하위 스레드에 복사하려면 pthread()가 필요합니다(https://linux.die.net/man/7/pthreads), 그러나 하위 열린 파일 설명자는 언급되지 않습니다.

나는 close()가 "운영 체제의 관점에서 볼 때 이 함수를 실행하는 동안 스레드를 취소하는 것이 안전하다는 것을 의미하는" 취소 지점이라는 점에 약간 불안합니다(https://stackoverflow.com/questions/27374707/what-exactly-is-a-cancellation-point, 허용되는 답변을 참조하세요). 하지만 close() 스레드가 취소되면 다른 스레드에 잘못된 파일 설명자가 있을 수도 있습니다...? 운영 체제는 당시에 올바른 파일 설명자 테이블을 가지고 있는지 확인합니까? 그럴 것 같지만 close()가 완료되기 전에 파일 설명자를 해제하면 긴장됩니다.

관련 정보