我需要在现有文件的中间原子地写入 64 kB 的数据。就是这样,或者什么都不应该写。如何在 Linux/C 中实现这一点?
4 回答
我认为这是不可能的,或者至少没有任何接口可以保证作为其合同的一部分写入是原子的。换句话说,如果现在有一种原子方式,那是一个实现细节,依靠它保持这种方式是不安全的。您可能需要为您的问题找到另一种解决方案。
但是,如果您只有一个写入进程,并且您的目标是其他进程看到完整的写入或根本没有写入,您可以只在文件的临时副本中进行更改,然后使用rename
原子替换它。任何已经打开旧文件的文件描述符的阅读器都会看到旧内容;任何按名称新打开它的读者都会看到新内容。任何读者都不会看到部分更新。
有几种方法可以“原子地”修改文件内容。虽然从技术上讲,修改本身从来都不是真正的原子,但有一些方法可以使它对所有其他进程看起来都是原子的。
我在 Linux 中最喜欢的方法是使用
fcntl(fd, F_SETLEASE, F_WRLCK)
.fd
只有当它是文件的唯一打开描述符时才会成功;也就是说,没有其他人(甚至这个进程也没有)打开文件。此外,该文件必须由运行该进程的用户拥有,或者该进程必须以 root 身份运行,或者该进程必须具有CAP_LEASE
内核授予租约的能力。SIGIO
成功后,只要另一个进程打开或截断文件,租用所有者进程就会收到一个信号(默认情况下)。opener 将被内核阻止最多/proc/sys/fs/lease-break-time
几秒钟(默认为 45 秒),或者直到租约所有者释放或降级租约或关闭文件,以较短者为准。因此,租用所有者有几十秒的时间来完成“原子”操作,而没有任何其他进程能够看到文件内容。有几个皱纹需要注意。一是内核允许租用所需的特权或所有权。另一个是对方打开或截断文件只会延迟;租赁所有者无法替换(硬链接或重命名)文件。(嗯,它可以,但打开器将始终打开原始文件。)此外,重命名、硬链接和取消链接/删除文件不会影响文件内容,因此完全不受文件租约的影响。
还要记住,您需要处理生成的信号。你可以
fcntl(fd, F_SETSIG, signum)
用来改变信号。我个人使用一个简单的信号处理程序——一个空主体——来捕获信号,但也有其他方法。实现半原子性的一种可移植方法是使用内存映射,使用
mmap()
. 想法是使用memmove()
或类似的方法尽快替换内容,然后使用msync()
将更改刷新到实际存储介质。如果文件中的内存映射偏移量是页面大小的倍数,则映射的页面反映了页面缓存。也就是说,以任何方式读取文件的任何其他进程 -
mmap()
或其read()
派生类 - 将立即看到memmove()
.msync()
只需要确保更改也存储在磁盘上,以防系统崩溃 - 它基本上等同于fsync()
.为了避免抢占(由于当前时间片启动而导致内核中断操作)和页面错误,我首先读取映射数据以确保页面在内存中,然后
sched_yield()
在memmove()
. 读取映射数据应该将页面错误地放入页面缓存,并sched_yield()
释放剩余的时间片,从而极有可能memmove()
不会被内核以任何方式中断。(如果您不确定页面已经出现故障,内核可能会memmove()
分别中断每个页面。您不会在进程中看到这一点,但其他进程会看到修改发生在页面大小的块中。)这并不完全是原子的,但它是实用的:它没有给你任何保证,只会让比赛窗口非常非常短;因此我称之为semi-atomic。
请注意,此方法与文件租约兼容。人们可以尝试对文件进行写租约,但如果在某个可接受的时间段内(比如一两秒)内没有授予租约,则回退到无租约内存映射。我将使用
timer_create()
andtimer_settime()
创建超时计时器,以及相同的空体信号处理程序来捕获SIGALRM
信号;这样当超时发生时fcntl()
被中断(返回-1
)errno == EINTR
- 定时器间隔设置为一些小的值(比如 25000000 纳秒或 0.025 秒),因此它会在此之后经常重复,如果初始中断错过则中断系统调用任何原因。大多数用户空间应用程序创建原始文件的副本,修改副本的内容,然后用副本替换原始文件。
打开文件的每个进程只会看到完整的更改,而不会看到新旧内容的混合。但是,任何保持文件打开的人都只会看到其原始内容,而不会意识到任何更改(除非他们自己检查)。大多数文本编辑器都会检查,但守护进程和其他进程不会打扰。
请记住,在 Linux 中,文件名和它的内容是两个不同的东西。您可以打开一个文件,取消链接/删除它,并且只要您打开文件,就可以继续阅读和修改内容。
还有其他方法。我不想建议任何具体的方法,因为最佳方法在很大程度上取决于具体情况:其他进程是否保持文件打开,或者在阅读内容之前是否总是(重新)打开它?原子性是首选还是绝对需要?数据是纯文本、XML 结构还是二进制?
编辑添加:
请注意,无法事先保证文件将被原子地成功修改。理论上没有,实践中也没有。
例如,您可能会遇到磁盘已满的写入错误。或者驱动器可能会在错误的时刻打嗝。我只列出了三种实用的方法来使它在典型的用例中看起来是原子的。
我最喜欢写租约的原因是我总是可以fcntl(fd,F_GETLEASE,&ptr)
用来检查租约是否仍然有效。如果不是,那么写入不是原子的。
如果之前已经读取了相同的数据(因此它很可能在页面缓存中),那么高系统负载不太可能导致 64k 写入的租约中断。如果进程具有超级用户权限,您可以使用setpriority(PRIO_PROCESS,getpid(),-20)
临时将进程优先级提高到最大,同时获取文件租用和修改文件。如果要覆盖的数据刚刚被读取,极不可能移动到swap;因此也不应该发生交换。
换句话说,虽然租用方法很可能失败,但实际上它几乎总是成功的——即使没有本附录中提到的额外技巧。
就个人而言,我只是检查修改是否不是原子的,在修改之后,在/fcntl()
之前使用调用(确保数据命中磁盘以防断电);这给了我一个绝对可靠、简单的方法来检查修改是否是原子的。msync()
fsync()
对于配置文件和其他敏感数据,我也推荐 rename 方法。(实际上,我更喜欢用于 NFS 安全文件锁定的硬链接方法,这相当于同一件事,但使用临时名称来检测命名竞争。)但是,它的问题是任何保持文件打开的进程都必须检查并自愿重新打开文件以查看更改的内容。
如果没有抽象层,磁盘写入不能是原子的。如果写入中断,您应该保留日志并恢复。
据我所知,低于 PIPE_BUF 大小的写入是原子的。但是我从不依赖这个。如果访问文件的程序是你自己写的,可以使用flock()实现独占访问。该系统调用在文件上设置一个锁,并允许其他知道该锁的进程访问或不访问。