为什么 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) 内核函数有时无法在 1 次调用中完成。我发现多线程可能存在问题。如果线程正在 open()ing、write()ing 和 close()ing 文件描述符,我亲眼目睹了中间关闭中的文件描述符在 open() 的另一个线程中被重用。然后 close() 完成,当我们尝试写入时,我们得到一个错误的文件描述符。

这是一个功能还是一个错误?我知道多个线程共享文件描述符表,因此当我们尽快处理文件描述符表时,我们可能会遇到未定义的行为(见下文),但我很好奇在 close() 之前如何释放文件描述符做完了。

每个线程的代码都写入它打开的文件描述符,因为我们不知道当线程打开时调用的clone()实际上在所有线程之间共享整个文件描述符表。此外,我们没有预见到一个线程的 open()/write()/close() 操作集可能会由于一个线程在另一个线程的 close() 中间执行 open() 而产生冲突,其中所述close() 在完成清理和返回之前使文件描述符可用。

以下是 strace 的一些输出,说明了该行为。这里我们有两个线程,58506 和 58508。PID 58506 在时间戳结束 532769 处移动到关闭 12。在 532791 - 25 微秒后 - 关闭完成。但在时间戳 532775- 532769- PID 58508 之后的 8 个麦克风打开一个文件。在 532803(开始打开后 28 微秒),打开完成,文件描述符 12(现已关闭)被分配给 PID 58508。在 532949,由于文件描述符错误,写入不成功。这是 532803 之后 146 微秒,此时 PID 58508 获取了文件描述符。

这是在本地文件系统上的 Dell R640 服务器上,该服务器具有由一对 SSD 支持的硬件 RAID 卡(就其价值而言)。

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() 完成之前释放文件描述符让我感到紧张。

相关内容