关于O_APPEND模式write的原子性

上上周的事情了,端午小长假将近,还是按往常一样,最后一天一定要搞一个“课题”,场面不大,一天就能搞定的东西,如果说系统学习vim或者Emacs之类的,那就算了...还好,问题呼之即来,那就是write系统调用是不是原子的,答案很显然,不是!但大师说带有APPEND标志的write是原子的,很多软件的日志都是O_APPEND打开,然后在不加锁的情况下直接write的,不会出现问题,此事如何证实?本文给出答案。
       曾经纠结于Linux的write系统调用是不是原子的,答案是显然的,不是!为什么不是呢?这个问题可不是那么好回答,本文试图用一种简单的方式解释一下。另外,本文也将说明一下O_APPEND方式的write为什么是原子的,同样是简单的方式,只做实验或者思想试验,不讲代码。但是作为基础,我给出重要结构体的伪实现:
1.inode结构
表示一个文件实体,每一个磁盘中的文件只有一个inode对象与之对应。
2.file结构
表示一个文件实体在进程中的代表,需要操作某个文件(即某个inode)并独立打开它的每一个进程都有一份独立的对应该inode的file对象。该对象拥有一个pos指针,表示一个file的当前位置,不管是read还是write均从这里开始。
3.task结构
操作file的主体。
       提到write操作,最基本的就是从哪里开始写的问题,即文件当前的position。一个write系统调用的语义就是,从position开始,写入长度为len的参数buff,仅此而已,具体的写入很简单,就是内存拷贝,缓存管理,最后交给块设备即可,所以关键就是,position的定位。定位方式分为3种:
1.调用lseek手工定位;
2.根据历史write操作自动定位;
3.根据O_APPEND标志自动定位;
lseek手工定位很简单,即设置file的pos指针,根据历史write操作自动定位最好理解,比如你写入了n个字节,那么file的pos就向前推进n,在write操作的最开始处得到file的pos,然后开始write,write完毕后根据实际写入的数量重新设置file的pos。O_APPEND方式是完全和pos无关的,因为它根本就不用file的pos来定位写入开始的位置,而是根据inode的大小来定位,也就是将write的开始位置设置到文件的末尾。
       好了,到此为止,我们完成了当前位置的定位,接下来就开始write了,现在的问题是,一次write是不是可以被另一次的write影响,为了更简单的分析问题,我假设每次都将buffer一次性写完(因为一个buffer分多次写在多进程环境下肯定是会出现交叉的,毫无疑问!),即write的count参数是多少,write的返回值就是多少。首先我将一个write操作流程化,假设每次写入的数据长度均为100,线程A写100个A,线程B写100个B:
L1.get_pos
L2.write_buffer
L3.update_pos
以下分几个场景来讨论。

场景1:

线程A处在L2,线程B进入L1,无疑两个线程将获得相同的pos,当线程B紧随线程A其后进入L2的时候,线程B很有很能会将线程A的刚刚写入的数据抹掉。

场景1-1:

我在L2按照时间流逝的方向定义三个时间点,L2刚刚开始的时间(马上就要写第一个字节的那个点),中间的某个时间,L2结束的时间(写完第100字节的那个点,100是我们的假设),分别为,t1,t2,t3。
       线程A在时间t2被从CPU调度出去,不再运行,原因可能是有RT进程来袭,也可能时间片用尽...不管怎样,它不再运行了,线程B进入t1,此时线程A已经写入了若干个A,假设是40个,然后线程B一口气跑到了t3,此时写入的100字节全部都是B。线程B脱离L2,此时线程A被重新拉回CPU,从第41个字节开始,写入了60字节的A结束L2,此时文件的内容是前面40个B,后面60个A。

分析:

毫无疑问,上面的场景得到的结论就是,在一次性的write中,不会出现交叉,而只能出现覆盖,而具体如何覆盖是不确定的,有完全覆盖,也有上述场景1-1中描述的不完全覆盖,但是一般而言是不会出现不完整覆盖的情况的,甚至说在多个线程每次写入文件的字节数量相等的情况下,是100%不会出现!为什么呢?这是一个很关键的设计,即L2的过程是不会被打断的,即它是原子的。不管什么模式的write,write本身都是原子的,比如你要写X字节的数据,但是由于某种原因只写了X-y个字节,那么写X-y字节数据的过程是原子的,所谓的write非原子性场景指的是pos定位和write之间的那段,单独的pos定位和write随便一个,都是原子的。
       为了下面论述的方便,我重新流程化了write操作:
L1.get_pos
L2-0.lock_inode
L2-1.write_buffer
L2-2.unlock_inode
L3.update_pos
因此,所谓的非原子性write导致的事故只会发生在L1和L2以及L2和L3之间!

场景2:

线程A比线程B先进入L2,但是在L2和L3之间中让出CPU,导致线程B覆盖了线程A的数据,进而线程B先走出L3,按照自己的写入长度设置了pos,导致线程A被重新拉回CPU后,pos又被设置了回去。
       端午节假期前的最后一个工作日,同事在纠结于一个问题,为何ngx或者apache写日志的时候都是直接写的,为何不lock,write既然是非原子的,难道就不怕乱掉吗?确实没有乱掉,也真的没有lock,到底原因何在?按照上面的分析,频繁写的时候,应该会乱才对!由于我对ngx的代码不熟,也就没有去细看,我觉得它好像用了O_APPENDB标志打开的文件。O_APPEND是何方神圣?为了揭示它,我为O_APPEND模式进一步扩充上面write的流程:
L1.get_pos
L2-0.lock_inode
L2-1.change_pos_to_inode->size
L2-2.write_buffer
L2-3.update_inode->size
L2-4.unlock_inode
L3.update_pos
我想到此为止,不用多说,也应该知道为何O_APPEND模式打开的文件会是原子操作了,多个线程或者进程随便写入,不会交叉,不会覆盖。不过要再次重申,如果一次write没有写完一个buffer,分了好几次写,那么即便是O_APPEND模式的文件write,也会出现交叉,因为两次write之间是没有任何机制保护的。
       通过上述的分析,我们可以看出,真正写的过程是绝对lock的,但是write系统调用除了真正的写,还包括pos的定位,这个定位发生在lock之后还是之前决定了本次调用的write是原子的还是非原子的。
注解:场景2模拟代码
说实话,在现代CPU上重现场景2造成的现象特别难,几十行的代码你看得很累,对于CPU而言,弹指一挥间就执行完了,因此必须模拟实现,在mm/filemap.c的generic_file_aio_write函数中的mutex_unlock后面加入以下的代码即可(你也可以用jprobe在里面耽搁一下):

if (!strcmp(current->comm, "child")) {
#include <linux/sched.h>
    struct task_struct *pp = current->real_parent;
    while(pp && !strcmp(pp->comm, "parent")) {
        schedule_timeout(1);
    }
}

加入这些代码是为了模拟线程A被调度出去的情景,既然我知道调度出去并且线程B赶超线程A之后肯定会有问题,并且这确实会发生,我只是不知道它什么时候发生而已,因此我就制造一个它发生的假象。
       至于怎么设计对应的应用程序,唉...fork+exec。
Linus的应付之道
就事论事的Linus解决原子write的方式超级优美,看一下他的风格:
重新定义两个带有lock机制的pos_read/write,总的来讲就是为pos设置一把锁:

+static inline loff_t file_pos_read_lock(struct file *file)
 {
+	if (file->f_mode & FMODE_LSEEK)
+		mutex_lock(&file->f_pos_lock);
 	return file->f_pos;
 }
+static inline void file_pos_write_unlock(struct file *file, loff_t pos)
 {
 	file->f_pos = pos;
+	if (file->f_mode & FMODE_LSEEK)
+		mutex_unlock(&file->f_pos_lock);
 }

修改sys_write系统调用:

 	file = fget_light(fd, &fput_needed);
 	if (file) {
-		loff_t pos = file_pos_read(file);
+		loff_t pos = file_pos_read_lock(file);
 		ret = vfs_write(file, buf, count, &pos);
-		file_pos_write(file, pos);
+		file_pos_write_unlock(file, pos);
 		fput_light(file, fput_needed);
 	}

这种短平快的风格一针见血指出了问题的解决之道,事实上,大多数的复杂性都是优化的副产品!

关于O_APPEND模式write的原子性

时间: 2024-08-19 03:15:21

关于O_APPEND模式write的原子性的相关文章

大钟的ios开发之旅(2)————简单说说ios中ARC与非ARC模式下的property的变量修饰词

/******************************************************************************************** * author:[email protected]大钟 * E-mail:[email protected] *site:http://www.idealpwr.com/ *深圳市动力思维科技发展有限公司 * http://blog.csdn.net/conowen * 注:本文为原创,仅作为学习交流使用,转

Google GFS文件系统深入分析

Google GFS文件系统深入分析 现在云计算渐成潮流,对大规模数据应用.可伸缩.高容错的分布式文件系统的需求日渐增长.Google根据自身的经验打造的这套针对大量廉价客户机的Google GFS文件系统已经广泛的在Google内部进行部署,对于有类似需求的企业而言有相当的参考价值. AD:51CTO移动APP安全沙龙!马上要爆满,手慢没座位! 51CTO编辑注:本文是一篇论文,英文原文标题为The Google File System,在Google Labs上公布,由blademaster

The Google File System

摘要 我们设计并实现了Google GFS文件系统,一个面向大规模数据密集型应用的.可伸缩的分布式文件系统.GFS虽然运行在廉价的普遍硬件设备上,但是它依然了提供灾难冗余的能力,为大量客户机提供了高性能的服务. 虽然GFS的设计目标与许多传统的分布式文件系统有很多相同之处,但是,我们的设计还是以我们对自己的应用的负载情况和技术环境的分析为基础 的,不管现在还是将来,GFS和早期的分布式文件系统的设想都有明显的不同.所以我们重新审视了传统文件系统在设计上的折衷选择,衍生出了完全不同的设计 思路.

谷歌三大核心技术(一)Google File System中文版

The Google File System中文版 译者:alex 摘要 我们设计并实现了Google GFS文件系统,一个面向大规模数据密集型应用的.可伸缩的分布式文件系统.GFS虽然运行在廉价的普遍硬件设备上,但是它依然了提供灾难冗余的能力,为大量客户机提供了高性能的服务. 虽然GFS的设计目标与许多传统的分布式文件系统有很多相同之处,但是,我们的设计还是以我们对自己的应用的负载情况和技术环境的分析为基础 的,不管现在还是将来,GFS和早期的分布式文件系统的设想都有明显的不同.所以我们重新审

关Java的内存模型(JMM)

JMM的关键技术点都是围绕着多线程的原子性.可见性和有序性来建立的 一.原子性(Atomicity) 原子性是指一个操作是不可中断的.即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰. 比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A给他赋值1,线程B给它赋值为-1.那么不管这2个线程以何种方式.何种步调工作,i的值要么是1,要么是-1.线程A和线程B之间是没有干扰的.这就是原子性的一个特点,不可被中断. 但如果我们不使用int型而使用long型的话,可能

《Linux系统编程》笔记 第三章(二)

3.6 定位流 标准库提供了与系统调用lseek()类似的函数来定位流中的读写位置. #include <stdio.h> int fseek (FILE *stream, long offset, int whence); long ftell(FILE *stream); 与lseek()用法类似,whence提供了如下选择: SEEK_CUR-将流的读写位置设置为当前位置加上pos个字节,pos可以是正数或负数. SEEK_END-将流的读写位置设置为文件尾加上pos个字节,pos可以是

golang文件上传和下载

[代码]golang 实现的文件服务(包括上传,下载的server端和client端) (2013-09-20 02:03:52) 转载▼ 标签: golang go 文件服务器 it 分类: GO相关 //下载(支持断电续传)(client) package main import ( "http"     "os"     "io"     "strconv" ) const (     UA = "Golang

使用 Python 进行稳定可靠的文件操作

点这里 阅读目录 截断-写 写-替换 追加 Spooldir 原子性 一致性 隔离性 程序需要更新文件.虽然大部分程序员知道在执行I/O的时候会发生不可预期的事情,但是我经常看到一些异常幼稚的代码.在本文中,我想要分享一些如何在Python代码中改善I/O可靠性的见解. 考虑下述Python代码片段.对文件中的数据进行某些操作,然后将结果保存回文件中: ? 1 2 3 4 5 with open(filename) as f:    input = f.read() output = do_so

APUE读书笔记-第三章 文件I/O

今天看得挺快的,一下子就把第二章看完了,不过第二章也确实看得不仔细,这一章其实在程序设计中还是非常重要的,因为这一章的内容决定了程序的可移植性. 好了,回到这一章的主题文件I/O. 3.2节主要对文件描述符的概念进行了简单的介绍.根据APUE:文件描述符是一个非负整数.当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符.我也简单地翻了一下LKD和<深入理解linux内核>,其中对于文件描述符的讲解不是很多,所以对于文件描述符也谈不出来太深入理解,各大家还是分享一篇blog吧.