原子上写64kB

时间:2012-08-04 20:47:52

标签: c linux gcc

我需要在现有文件的中间原子地写出类似64 kB的数据。这就是全部,或者什么也不写。如何在Linux / C中实现?

4 个答案:

答案 0 :(得分:4)

我不认为这是可能的,或者至少没有任何接口可以保证作为其合同的一部分,写入将是原子的。换句话说,如果现在存在一种原子的方式,那就是一个实现细节,依靠它保持这种方式是不安全的。您可能需要找到解决问题的另一种方法。

但是,如果您只有一个写入过程,并且您的目标是其他进程看到完全写入或根本没有写入,您只需在文件的临时副本中进行更改,然后使用{{1}以原子方式替换它。任何已经打开旧文件的文件描述符的读者都会看到旧内容;任何按名称新打开它的读者都会看到新内容。任何读者都不会看到部分更新。

答案 1 :(得分:4)

有几种修改文件内容的方法"原子地"。虽然从技术上讲,修改本身并不是真正的原子,但是有一些方法可以使看起来像原子到所有其他进程。

  1. Linux中我最喜欢的方法是使用fcntl(fd, F_SETLEASE, F_WRLCK)获取写入租约。只有fd是文件的唯一打开描述符时才会成功;也就是说,没有其他人(甚至不是这个过程)打开文件。此外,该文件必须由运行该进程的用户拥有,或者该进程必须以root身份运行,或者该进程必须具有CAP_LEASE功能,以便内核授予该租约。

    成功时,只要另一个进程打开或截断文件,租约所有者进程就会收到一个信号(默认为SIGIO)。内核将阻止开启者最多/proc/sys/fs/lease-break-time秒(默认为45),或直到租约所有者释放或降级租约或关闭文件,以较短者为准。因此,租赁所有者有几十秒的时间来完成" atomic"操作,没有任何其他进程能够看到文件内容。

    需要注意一些皱纹。一个是内核允许租用所需的特权或所有权。另一个事实是,打开或截断文件的另一方只会被延迟;租赁所有者不能替换(硬链接或重命名)文件。 (好吧,它可以,但是开启者将始终打开原始文件。)此外,重命名,硬链接和取消链接/删除文件不会影响文件内容,因此根本不受影响通过档案租赁。

    还要记住,您需要处理生成的信号。您可以使用fcntl(fd, F_SETSIG, signum)更改信号。我个人使用一个简单的信号处理程序 - 一个空体 - 来捕捉信号,但还有其他方法。

  2. 实现半原子性的可移植方法是使用mmap()的内存映射。我们的想法是使用memmove()或类似内容尽快替换内容,然后使用msync()将更改刷新到实际的存储介质。

    如果文件中的内存映射偏移量是页面大小的倍数,则映射的页面会反映页面缓存。也就是说,以任何方式阅读文件的任何其他进程 - mmap()read()或其衍生产品 - 都会立即看到memmove()所做的更改。 msync()只需要确保更改也存储在磁盘上,以防系统崩溃 - 它基本上等同于fsync()

    为了避免抢占(内核因当前时间片上升而中断操作)和页面错误,我首先读取映射数据以确保页面在内存中,然后调用sched_yield() ,在memmove()之前。读取映射的数据应该使页面错误进入页面缓存,sched_yield()释放剩余的时间片,使得memmove()极有可能不被内核以任何方式中断。 (如果你没有确定页面已经出现故障,内核可能会分别打断每个页面的memmove()。你不会在这个过程中看到它,但是其他进程看到修改发生了在页面大小的块中。)

    这不完全是原子的,但它很实用:它不会给你任何保证,只会使比赛窗口非常短;因此我称之为半原子

    请注意,此方法与文件租约兼容。可以尝试对文件进行写入租约,但如果在某个可接受的时间段(例如一秒或两秒)内未授予租约,则可以回退到无租用内存映射。我使用timer_create()timer_settime()来创建超时计时器,并使用相同的空体信号处理程序来捕获SIGALRM信号;这样,fcntl()在发生超时时被中断(返回-1errno == EINTR) - 定时器间隔设置为某个小值(比如25000000纳秒,或0.025秒),所以它在此之后经常重复,如果因任何原因错过了初始中断,则会中断系统调用。

  3. 大多数用户空间应用程序创建原始文件的副本,修改副本的内容,然后用副本替换原始文件。

    打开文件的每个进程只会看到完整的更改,而不会混合使用旧内容和新内容。但是,任何保持文件打开的人只会看到他们的原始内容,并且不知道任何更改(除非他们自己检查)。大多数文本编辑都会检查,但守护进程和其他进程不会打扰。

    请记住,在Linux中,文件名及其内容是两个不同的东西。您可以打开文件,取消链接/删除它,并且只要您打开文件,仍然可以继续阅读和修改内容。

  4. 还有其他方法。我不想建议任何具体方法,因为最佳方法在很大程度上取决于具体情况:其他进程是否保持文件打开,或者在阅读内容之前是否总是(重新)打开它?原子性是首选还是绝对需要?数据是纯文本,结构如XML还是二进制文件?

    编辑添加:

    请注意,没有办法事先保证文件会以原子方式成功修改。不是在理论上,也不是在实践中。

    例如,您可能会遇到磁盘已满的写入错误。或者驱动器可能在错误的时刻打嗝。我只列出了三种实用方法,使其在典型用例中似乎原子。

    写租约是我最喜欢的原因是我总是可以使用fcntl(fd,F_GETLEASE,&ptr)来检查租约是否仍然有效。如果没有,则写入不是原子的。

    如果之前已读取相同的数据(因此可能在页面缓存中),则高系统负载不太可能导致64k写入的租约被中断。如果进程具有超级用户权限,则可以使用setpriority(PRIO_PROCESS,getpid(),-20)在获取文件租约和修改文件时临时将进程优先级提高到最大值。如果刚刚读取了要覆盖的数据,则极不可能将其移动到交换;因此,不应该发生交换。

    换句话说,虽然租赁方法很可能会失败,但实际上它几乎总是成功的 - 即使没有本附录中提到的额外技巧。

    就我个人而言,我只是在fcntl() / msync()之前使用修改后的fsync()调用来检查修改是否不是原子的(确保数据在磁盘上有效)发生停电);这给了我一个绝对可靠,简单的方法来检查修改是否是原子的。

    对于配置文件和其他敏感数据,我也推荐使用重命名方法。 (实际上,我更喜欢用于NFS安全文件锁定的硬链接方法,这相当于使用临时名称来检测命名比赛。但是,它存在的问题是保持文件打开的任何进程都必须检查并自动重新打开文件,以查看更改的内容。

答案 2 :(得分:2)

如果没有抽象层,磁盘写入就不可能是原子的。如果写入被中断,您应该保留日志并恢复。

答案 3 :(得分:1)

据我所知,低于PIPE_BUF大小的写入是原子的。但是我从不依赖于此。如果访问该文件的程序是由您编写的,则可以使用flock()来实现独占访问。此系统调用会对文件设置锁定,并允许其他了解锁定的进程获取访问权限。