原子系统调用。 Input/Output 操作

Atomic syscall. Input/Output operations

我想使用无锁队列编写多线程安全记录器。记录线程会将消息推送到队列,记录器会将它们弹出并发送到输出。我考虑如何解决这个问题——发送到输出。 我想尽可能避免使用 mutex/locks。 因此,假设我将使用 C++ 流写入 file/console。我们可以假设目标系统是 Linux。

好的,写入流必须只是 Unix 提供的系统调用的包装器(也许是高级包装器)write。据我所知,系统调用是原子的(只有一个进程可以同时执行系统调用)。因此,不使用锁来安全地写入文件是很诱人的。 但是 write 是一个系统调用但它不保证写入 "whole output"。它 returns 成功写入文件的字节数。

基本上,我的问题是: 如何解决?是否可以避免互斥? (我认为这是不可能的)。请标记我的注意事项,我错了吗?

Igor 是对的:只用一个线程完成所有日志写入。请记住,内核必须进行锁定以同步对打开的文件描述符(它跟踪文件位置)的访问,因此通过从多个内核进行写入会导致内核内部争用。更糟糕的是,您正在从多个内核进行系统调用,这意味着内核的代码/数据访问会弄脏您在多个内核上的缓存。

有关在系统调用完成后进行系统调用对用户space 代码性能的影响的更多信息,请参见this paper。 (以及关于内核内部不频繁系统调用的数据/指令缓存未命中)。让一个线程执行所有系统调用(至少是所有写入系统调用)绝对有意义,以将进程占用空间的那一部分隔离到一个核心。以及内核内部的锁争用。

那篇 FlexSC 论文是关于批处理系统调用以减少用户-> 内核-> 用户转换的想法,但它们也测量了普通同步系统调用方法的开销。更重要的是关于进行系统调用的缓存污染的讨论。


或者,如果您可以让多个线程写入您的日志文件,您可以只这样做而根本不使用队列。

不能保证大型写入会不间断地完成,但中小型写入应该(几乎?)在大多数操作系统上始终复制其整个缓冲区。特别是如果您正在写入文件而不是管道。 IDK Linux write() 在被抢占时的行为方式,但我希望它通常会恢复以完成写入,而不是在没有写入所有请求的字节的情况下返回。当被信号中断时,部分写入的可能性更大。

保证两个write()系统调用的字节不会混在一起;来自一个的所有字节将在来自另一个的字节之前或之后。不过,您是正确的,部分写入是一个潜在的问题。我忘记了 glibc 系统调用包装器是否会在 EINTR 为您恢复呼叫。尽管在那种情况下,这意味着实际上没有写入任何字节,否则它会返回字节计数成功。

您应该对此进行测试,以了解部分写入和性能。 kernel-space 锁定可能比无锁队列的开销更小,但是从生成日志消息的每个线程进行系统调用可能会降低性能。 (当你测试它时,确保你在你的 user-space 进程中进行了一些真实的工作,而不仅仅是 only 调用 write 的循环。)