Chinaunix

标题: 关于多线程对同一个文件进行写操作,记得坛子里面讨论过,不需要互斥,原因什么来着? [打印本页]

作者: rain_fish    时间: 2010-10-18 13:46
标题: 关于多线程对同一个文件进行写操作,记得坛子里面讨论过,不需要互斥,原因什么来着?
记不清楚了,也没有找到,请达人释疑。
作者: epegasus    时间: 2010-10-18 13:54
APUE 5
读写函数原子的执行
作者: chenzhanyiczy    时间: 2010-10-18 14:58
O_APPEND就可以了
作者: 思一克    时间: 2010-10-18 15:40
APPEND方式写是原子操作。
作者: gtkmm    时间: 2010-10-18 17:28
write就是原子。。.
lseek也是原子。。。
append只不过是把lseek和write合成了一个原子。
作者: liwangli1983    时间: 2010-10-18 19:47
5楼正解,用pwrite也可以,不过不更改文件偏移。
作者: pengjianbokobe    时间: 2010-10-18 20:27
pread/pwrite
O_APPEND
作者: 思一克    时间: 2010-10-18 23:35
write不是原子写。
append方式的write才是。

write中间会被打断后给CPU其他进程,比如你write一个极大数据块时候。
append方式下的write也会被打断,但空间保留了,回来再继续写不影响原子性。
作者: chenzhanyiczy    时间: 2010-10-19 00:17
write不是原子写。
append方式的write才是。

write中间会被打断后给CPU其他进程,比如你write一个极大数 ...
思一克 发表于 2010-10-18 23:35



     gcc 加了-pthread选项,应该调用的是线程安全的write()
作者: 思一克    时间: 2010-10-19 14:23
回复 9# chenzhanyiczy


不行吧。线程库是将应用的函数用加锁的方法保证的线程安全。
对系统调用有效吗?
作者: chenzhanyiczy    时间: 2010-10-19 15:17
回复  chenzhanyiczy


不行吧。线程库是将应用的函数用加锁的方法保证的线程安全。
对系统调用有效吗 ...
思一克 发表于 2010-10-19 14:23



是的,看了一下read()源码, -pthread是不能对系统调用有效的   

read()源码:
asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)
{
     struct file *file;
     ssize_t ret = -EBADF;
     int fput_needed;
      file = fget_light(fd, &fput_needed);
     if (file) {
         loff_t pos = file_pos_read(file);
         ret = vfs_read(file, buf, count, &pos);  这里已经加锁了
         file_pos_write(file, pos);
         fput_light(file, fput_needed);
     }

     return ret;
}

所以read(),write()本身就是线程安全的.
作者: 思一克    时间: 2010-10-19 15:40
回复 11# chenzhanyiczy



不是那个意思。
sys_write, sys_read里的锁是不分进程线程的。
这些函数在内核是要被打断的,所以,他们不是原子的。
作者: epegasus    时间: 2010-10-19 15:43
write不是原子写。
append方式的write才是。

write中间会被打断后给CPU其他进程,比如你write一个极大数据块时候。
append方式下的write也会被打断,但空间保留了,回来再继续写不影响原子性。
思一克 发表于 2010-10-18 23:35



LINUX有强制锁.保证写同一区域的操作不会相互打断
作者: chenzhanyiczy    时间: 2010-10-19 16:00
本帖最后由 chenzhanyiczy 于 2010-10-19 16:05 编辑
回复  chenzhanyiczy



不是那个意思。
sys_write, sys_read里的锁是不分进程线程的。
这些函数在内 ...
思一克 发表于 2010-10-19 15:40



恩。
这有两个概念: 线程安全和原子写

我说的是线程安全,当然中途是可以被中断的。
而你说的是原子操作

总结一下:
write()是线程安全的,但不是原子操作。中途可以被中断,但即使如此,在多线程下:
线程a   write("aaa")
线程b   write("bbb")

不会出现如下情况:
abaabb
aabbab
。。。

只会出现:
aaa
bbb
bbb
aaa
。。。
作者: 思一克    时间: 2010-10-19 16:16
恩。
这有两个概念: 线程安全和原子写

我说的是线程安全,当然中途是可以被中断的。
而你说的是原子操作

总结一下:
write()是线程安全的,但不是原子操作。中途可以被中断,但即使如此,在多线程下:
线程a   write("aaa")
线程b   write("bbb")

不会出现如下情况:
abaabb
aabbab
。。。

只会出现:
aaa
bbb
bbb
aaa
。。。
--------------
你自己编写个线程程序测试一下,看数据会交错否?
我记得是不行的(一定是交错的)。
作者: liwangli1983    时间: 2010-10-19 16:17
write只有在写管道终端之类挂起时才会被中断吧,写常规文件应该是不会被中断吧?
作者: 思一克    时间: 2010-10-19 16:18
恩。
这有两个概念: 线程安全和原子写

我说的是线程安全,当然中途是可以被中断的。
而你说的是原子操作

总结一下:
write()是线程安全的,但不是原子操作。中途可以被中断,但即使如此,在多线程下:
线程a   write("aaa")
线程b   write("bbb")

不会出现如下情况:
abaabb
aabbab
。。。

只会出现:
aaa
bbb
bbb
aaa
。。。
--------------
你自己编写个线程程序测试一下,看数据会交错否?
我记得是不行的(一定是交错的)。
作者: 思一克    时间: 2010-10-19 16:18
LINUX有强制锁.保证写同一区域的操作不会相互打断
---------
sys_write 这个锁没有。O_APPEND方式下才可以。其余应该不可。
要锁文件,需要用专门另外的系统调用和相应的库函数。
作者: chenzhanyiczy    时间: 2010-10-19 16:28
恩。
这有两个概念: 线程安全和原子写

我说的是线程安全,当然中途是可以被中断的。
而你说的是原子操 ...
思一克 发表于 2010-10-19 16:16



有时间测试一下。
能不能说一下为什么会交错呢?
作者: 思一克    时间: 2010-10-19 16:35
有时间测试一下。
能不能说一下为什么会交错呢?
-------------
sys_write磁盘文件不具有原子性。
write O_APPEND方式具有原子性。
write socket不具有原子性。
write pipe 在数据尺寸小于一个固定的大小(比如4096,有个参数,我忘记是啥名字了)的时候具有原子性。

OS的设计就是如此的。
如果sys_write要保证原子性,那系统的时间片分配就有问题了。一个进程write一个大段数据,其它的都憋在那里?这个不行。

大概是这个意思吧
作者: epegasus    时间: 2010-10-19 16:58
回复 15# 思一克


    晕 确实应该交叉
从WRITE的语义上 传入的N是不被保证写完的
那么期望锁的区间要么没有 要么就是待写的这段
如果是后者空洞就造成大麻烦
问题是追加方式如果是如上所说可以留出空间然后并发写 也会有空洞问题啊 因为一般情况下你不会知道该留多少
作者: 思一克    时间: 2010-10-19 17:00
OS提供了专门的文件锁,在应用里是lockf, flock之类的函数。
从这个也可以知道write不是原子的。要原子地写,需要先用这类的锁起作用。
作者: 思一克    时间: 2010-10-19 17:16
晕 确实应该交叉
从WRITE的语义上 传入的N是不被保证写完的
那么期望锁的区间要么没有 要么就是待写的这段
如果是后者空洞就造成大麻烦
问题是追加方式如果是如上所说可以留出空间然后并发写 也会有空洞问题啊 因为一般情况下你不会知道该留多少
-----------
不是没写完的问题。传入的N写完也会交叉。
作者: 思一克    时间: 2010-10-19 17:17
晕 确实应该交叉
从WRITE的语义上 传入的N是不被保证写完的
那么期望锁的区间要么没有 要么就是待写的这段
如果是后者空洞就造成大麻烦
问题是追加方式如果是如上所说可以留出空间然后并发写 也会有空洞问题啊 因为一般情况下你不会知道该留多少
----------
APPEND知道留多少。根据write的块大小参数就知道了。
作者: epegasus    时间: 2010-10-19 17:36
回复 24# 思一克
    用关键字 '写文件'搜了C版 有数十帖问这个问题 所有的都看了遍

找到个帖子好象靠谱 当然可能漏了同一问题其他关键字帖子
http://bbs.chinaunix.net/viewthread.php?tid=908716
十楼


   
追加模式不能实现互斥,但能保证你要写的数据在一个页面内不交错
isnowran 发表于 2007-03-13 15:15


http://bbs.chinaunix.net/viewthr ... p;extra=&page=2
20楼
man 2 pread
man 2 pwrite
JohnBull 发表于 2007-06-19 23:12


  那么大致认为加锁才是真正安全的

追加方式不见得合理 比如WRITE不完全的时候 或许对日志要求不高时适用

最后的问题就是上面 追加方式在WRITE不完全的时候回形成空洞吗?
>>append方式下的write也会被打断,但空间保留了,回来再继续写不影响原子性。

按这个说法是会的.
作者: chenzhanyiczy    时间: 2010-10-19 17:45
有时间测试一下。
能不能说一下为什么会交错呢?
-------------
sys_write磁盘文件不具有原子性。
writ ...
思一克 发表于 2010-10-19 16:35



感觉还是有点问题。

请看:

asmlinkage ssize_t sys_write(unsigned int fd, const char __user * buf, size_t count)
{
     struct file *file;
     ssize_t ret = -EBADF;
     int fput_needed;
      file = fget_light(fd, &fput_needed);
     if (file) {
         loff_t pos = file_pos_read(file);
         ret = vfs_write(file, buf, count, &pos);
         file_pos_write(file, pos);
         fput_light(file, fput_needed);
     }
     return ret;
}

ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
     struct inode *inode = file->f_dentry->d_inode;
     ssize_t ret;

     if (!(file->f_mode & FMODE_WRITE))
         return -EBADF;
     if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))
         return -EINVAL;
     ret = locks_verify_area(FLOCK_VERIFY_WRITE, inode, file, *pos, count);
     if (!ret) {
         ret = security_file_permission (file, MAY_WRITE);
         if (!ret) {
              if (file->f_op->write)
                   ret = file->f_op->write(file, buf, count, pos);
              else
                   ret = do_sync_write(file, buf, count, pos);
              if (ret > 0)
                   dnotify_parent(file->f_dentry, DN_MODIFY);
         }
     }

     return ret;
}

从上面源码可以看出:
1.locks_verify_area是对文件某部分的区域锁,如果两次write()的时候,不是在文件同一个区域的话,应该就不会有问题,即不会出现交叉出错的情况
2.如果两次write()的时候,是在文件同一个区域的话,那么应该会出现交叉出错或者覆盖的情况。
         loff_t pos = file_pos_read(file);        
      ret = vfs_write(file, buf, count, &pos);
         file_pos_write(file, pos);
    可以从粗体的部分可以看出。

不知道是不是这样?
作者: 思一克    时间: 2010-10-19 18:21
从上面源码可以看出:
1.locks_verify_area是对文件某部分的区域锁,如果两次write()的时候,不是在文件同一个区域的话,应该就不会有问题,即不会出现交叉出错的情况
2.如果两次write()的时候,是在文件同一个区域的话,那么应该会出现交叉出错或者覆盖的情况。
         loff_t pos = file_pos_read(file);        
      ret = vfs_write(file, buf, count, &pos);
         file_pos_write(file, pos);
    可以从粗体的部分可以看出。

不知道是不是这样?
------------------------
是的。不在同一区域不会交叉。因为应用都没有交叉。
出现AABBABBBBBA是在应用重叠的时候。
如果写是原子的,就能确保不会交叉,无论应用层如何写。
作者: 思一克    时间: 2010-10-19 18:35
可以编写一个程序,5个线程,4个往同一打开的文件的同一位置(SEEK = 0)写一块(比如1024字节)数据。
数据是111111.。。     2222222222.。。。。  3333333333.。。。。。。。 44444444

一个线程读出全部数据。
如果读出的不全是一样的字节,就是交叉了。
作者: chenzhanyiczy    时间: 2010-10-19 23:19
从上面源码可以看出:
1.locks_verify_area是对文件某部分的区域锁,如果两次write()的时候,不是在文件同 ...
思一克 发表于 2010-10-19 18:21



    恩,看来终于讲到一起去了,

send()会是怎样呢? 感觉更加复杂,因为关系到tcp的一些流量控制等。
作者: chenzhanyiczy    时间: 2010-10-19 23:20
可以编写一个程序,5个线程,4个往同一打开的文件的同一位置(SEEK = 0)写一块(比如1024字节)数据。
数据 ...
思一克 发表于 2010-10-19 18:35



  为什么加个O_APPEND就是原子操作? 源码中哪里有体现吗?
作者: chenzhanyiczy    时间: 2010-10-20 00:00
恩,看来终于讲到一起去了,

send()会是怎样呢? 感觉更加复杂,因为关系到tcp的一 ...
chenzhanyiczy 发表于 2010-10-19 23:19



    看了一下源码,send()好像是不会出现write()一样的情况,即它不会出现交叉出错的情况
作者: 思一克    时间: 2010-10-20 08:43
回复 31# chenzhanyiczy

看代码不容易看出来。
send和write一样,write socket几下子就调用send(之类)了。
也是要交错的。
所以,send不能多了进程(线程)同时写一个socket,否则接收端全是乱套的。

我为这个专门给LINUX做了个补丁,使得send(write socket)在一定大小(用户可调)的块的时候也确保是原子的。
实验了,是好用的。
作者: 思一克    时间: 2010-10-20 08:45
这个问题原来有过讨论,这个给射个精,以后容易查到。
作者: assiss    时间: 2010-10-20 09:14
这个问题原来有过讨论,这个给射个精,以后容易查到。
思一克 发表于 2010-10-20 08:45



    我还记得当年曾经讨论过,也是你主导讨论的。呵呵。
作者: chenzhanyiczy    时间: 2010-10-20 09:29
回复  chenzhanyiczy

看代码不容易看出来。
send和write一样,write socket几下子就调用send(之类)了 ...
思一克 发表于 2010-10-20 08:43



还是看源代码吧,请看:

778 int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
779                 size_t size)
780 {
781         struct iovec *iov;
782         struct tcp_opt *tp = tcp_sk(sk);
783         struct sk_buff *skb;
784         int iovlen, flags;
785         int mss_now;
786         int err, copied;
787         long timeo;
788
789         lock_sock(sk);790         
             TCP_CHECK_TIMER(sk);
。。。省略很多
971 out:
972         if (copied)
973                 tcp_push(sk, tp, flags, mss_now, tp->nonagle);
974         TCP_CHECK_TIMER(sk);
975         release_sock(sk);976        
        return copied;

粗体部分!
可以看到这个加了锁。它的意思是锁住整个套接口。我不明白为什么会出现交叉出错?
作者: 思一克    时间: 2010-10-20 09:30
回复 34# assiss


不是我主导的,是别人提出的。
我一般也是临时研究才有一些结论的。
实际上我是用论坛学习的。没那些问题,我就不会动脑子。
作者: cskyrain    时间: 2010-10-20 09:31
mark,c版这段很少有讨论这么热烈的贴了
作者: 思一克    时间: 2010-10-20 09:35
粗体部分!
可以看到这个加了锁。它的意思是锁住整个套接口。我不明白为什么会出现交叉出错?
------------
锁住的意思不是防止交叉。
在加锁和解锁之间会有休眠的。然后锁让给了别人。当然会交叉了。
还有,设计者不认为交叉是“出错”。而认为是正常的。是用户使用不当。
我给LINUX内核补丁维护的写过信,那些权威人就如此坚持说(我不完全同意)。
作者: chenzhanyiczy    时间: 2010-10-20 09:41
粗体部分!
可以看到这个加了锁。它的意思是锁住整个套接口。我不明白为什么会出现交叉出错?
---------- ...
思一克 发表于 2010-10-20 09:35



    如果不是为了防止交叉,那这锁有什么用吗?
作者: 思一克    时间: 2010-10-20 09:47
如果不是为了防止交叉,那这锁有什么用吗?
----------
防止2个进程/线程同时写。
交叉不是“同时写”引起的。
比如2进程AB个竞争同一个SOCKET,有这个锁,假设A先用了,B就等待了。B不会写。
但A不能用的太过分(发送太大的块)。
在A发送大块的时候,A会休眠(让给其他人)。然后B得到锁,B再发送自己的数据。
结果就交叉了。

有点像“分时合理分配“的意思。
就是不能让一个人无理独占过长的时间,让其他人等待。要保证公平。
作者: 思一克    时间: 2010-10-20 09:50
在用户程序中的”同时“在OS里面可以是同时可以是不同时。
要区分开不同地方的含义。
那个SOCKET锁保证OS中不同时写同一个SOCKET.
如果无那个锁,OS中就会出现同时写一个SOCKET的情况了。
作者: chenzhanyiczy    时间: 2010-10-20 10:00
如果不是为了防止交叉,那这锁有什么用吗?
----------
防止2个进程/线程同时写。
交叉不是“同时写”引 ...
思一克 发表于 2010-10-20 09:47



1.也就是说休眠的时候会让出锁?
2.对于大文件写来说,这个粗体部分(lock_sock(sk))的锁根本没什么作用了,反正锁不锁都有可能发生交叉的情况
作者: 思一克    时间: 2010-10-20 10:19
1.也就是说休眠的时候会让出锁?
2.对于大文件写来说,这个粗体部分(lock_sock(sk))的锁根本没什么作用了,反正锁不锁都有可能发生交叉的情况
------
如果不锁,2个进程在OS中不同时写了吗?
那不是交叉问题,而是全乱套问题。
作者: chenzhanyiczy    时间: 2010-10-20 10:35
1.也就是说休眠的时候会让出锁?
2.对于大文件写来说,这个粗体部分(lock_sock(sk))的锁根本没什么作用了, ...
思一克 发表于 2010-10-20 10:19



你的意思是交叉错误的话,只是数据乱了而已,但“全乱套”的话就不只数据乱了,而且其他也乱了(比如:TCP发送窗口更新之类的)?
作者: 思一克    时间: 2010-10-20 10:52
你的意思是交叉错误的话,只是数据乱了而已,但“全乱套”的话就不只数据乱了,而且其他也乱了(比如:TCP发送窗口更新之类的)?
----------
是的。我估计不死机就不错了。
这个结果可以实验。
你将内核SEND那里的锁头去掉,编译内核,启动用网络看看啥结果?
作者: chenzhanyiczy    时间: 2010-10-20 11:14
你的意思是交叉错误的话,只是数据乱了而已,但“全乱套”的话就不只数据乱了,而且其他也乱了(比如:TCP发 ...
思一克 发表于 2010-10-20 10:52



    还有问题:
1.释放锁的时机除了“休眠”,中断或者时间片用完有可能释放锁吗? 如果可以,就有问题2了
2.因为中断或者时间片用完是随时都有可能发生的,那么“全乱套”的问题还有可能发生了
作者: 思一克    时间: 2010-10-20 11:38
还有问题:
1.释放锁的时机除了“休眠”,中断或者时间片用完有可能释放锁吗? 如果可以,就有问题2了
2.因为中断或者时间片用完是随时都有可能发生的,那么“全乱套”的问题还有可能发生了
--------------
只要每个时刻只有一个进程在用,就不会发生全乱的情况。
作者: zylthinking    时间: 2010-10-20 11:40
还有问题:
1.释放锁的时机除了“休眠”,中断或者时间片用完有可能释放锁吗? 如果可以,就有问 ...
chenzhanyiczy 发表于 2010-10-20 11:14


感觉应该会释放吧, 但也不会全乱套; 和休眠释放锁不一样吗?
作者: chenzhanyiczy    时间: 2010-10-20 11:51
还有问题:
1.释放锁的时机除了“休眠”,中断或者时间片用完有可能释放锁吗? 如果可以,就有问题2了
2. ...
思一克 发表于 2010-10-20 11:38



不太理解,不过我还是觉得有机率出现全乱的情况(当然,这是不可能的,内核不可能设计成这样)。只是现在没有找到答案来推翻我的这个疑虑。

另外:
”休眠“的时候,释放锁了,再醒来的时候,它还会不会检查相关状态并更新?没看到(TCP有很多状态,有缓冲区,有窗口,每个都是检查一遍的话很费时的),如果没有的话岂不是还是乱套了?
作者: rain_fish    时间: 2010-10-20 14:00
还有问题:
1.释放锁的时机除了“休眠”,中断或者时间片用完有可能释放锁吗? 如果可以,就有问题2了
2. ...
思一克 发表于 2010-10-20 11:38



    看了一遍大家的讨论,没得出到底要不要加锁的啊?请克哥给予提示?
作者: 思一克    时间: 2010-10-20 14:04
回复 50# rain_fish


write磁盘文件不具有原子性。
write O_APPEND方式具有原子性。
write socket不具有原子性。
write pipe 在数据尺寸小于一个固定的大小(比如4096,有个参数,我忘记是啥名字了)的时候具有原子性。更大的不原子了。

所以,多线程/进程write同一个磁盘文件需要加锁。而APPEND方式write(日志多是如此)不需要加锁。
作者: chenzhanyiczy    时间: 2010-10-20 14:31
回复  rain_fish


write磁盘文件不具有原子性。
write O_APPEND方式具有原子性。
write socket不具有 ...
思一克 发表于 2010-10-20 14:04



还是有很多没有搞清楚的(比如:O_APPEND为什么就具有原子性, 休眠和TCP乱套问题等等),看来目前我只能死记硬背了先了
作者: 思一克    时间: 2010-10-20 14:41
还是有很多没有搞清楚的(比如:O_APPEND为什么就具有原子性, 休眠和TCP乱套问题等等),看来目前我只能死记硬背了先了
-----------------
我猜测的:
因为APPEND方式永远是无重叠写入的。
其实是内部seek机制的原子性导致的。
比如有2个进程竞争write fd, APPEND写之前要seek到文件尾。
当前文件尾是S;
A: write(fd, buf, 1000);
B: write(fd, buf1, 200);
内部A就要写入位置S--->S+1000
B就要写入S+1000 ---> S + 1200

1000个字节write 完成之前,S值已经是S+1000了。
作者: chenzhanyiczy    时间: 2010-10-20 15:01
还是有很多没有搞清楚的(比如:O_APPEND为什么就具有原子性, 休眠和TCP乱套问题等等),看来目前我只能死记硬 ...
思一克 发表于 2010-10-20 14:41



    恩。虽然没找到对应的源码,但有道理!
作者: rain_fish    时间: 2010-10-20 16:56
回复  rain_fish


write磁盘文件不具有原子性。
write O_APPEND方式具有原子性。
write socket不具有 ...
思一克 发表于 2010-10-20 14:04



    非常感谢,如果是fopen就是fopen("path", 'a')的打开方式是原子的,对吧
作者: epegasus    时间: 2010-10-21 10:32
本帖最后由 epegasus 于 2010-10-21 10:39 编辑

回复 51# 思一克


    25楼的作废。

对这个问题可讨论的是
1,相关标准给出了什么,比如POSIX
2,具体操作系统实现给出了什么,主要针对但不限于linux。

首先看中文APUE第3章相关描述write:
其返回值通常与参数nbytes的值不同,否则表示出错。
如果在打开该文件时,指定了
O_APPEND选择项,则在每次写操作之前,将文件位移量设置在文件的当前结尾处。在一次成功写之后,该文件位移量增加实际写的字节数。

可以理解为write只有成功和失败2种,成功只有一种返回值,其他都是失败的。
最后一句应该是说写成功后才去增加文件偏移,没成功估计是不增加了。这个问题的关键是没有成功时文件长度是否变化了。按道理说应该没变化。
并且知道O_APPEND而设置文件偏移是在本次写前做出的。增加文件长度应该在写成功后做出的,或许同时也增加了一次偏移。如此大概是2步。
在讲原子的,首先区分以下2个原子:
1,O_APPEND写第一步和第2步中间不被打断。
2,第2步写本身不被打断。
对于2,这有几种保护方式:
写本身是互斥;
写一个有重叠区间是互斥的;
将区间分固定页块,写同一个页块是互斥的。
如果是第一种则O_APPEND写的2步是真正原子的。
试想2个进程同时写,如果不是第一种,则后写的进程面临的问题是哪里才是文件的结尾?因为先写的并没有完成。这个时候要么等写完。要么使用未增加的文件结尾。问题就是一个正在被写的文件结尾是否有效果。
对于非真正原子情况的考虑,这里还有个写失败的问题。就不谈了。
如果按以上描述,一个write真正的写部分是不能被另一个打断的。
比如一个写aaaa 一个写bbbb结果可能是aabbbb 但不可能是abab。
而追加方式下一定是aaaabbbb)(假设2个都是O_APPEND方式打开)


具体看linux实现
2.4   2.6.13中的ext2(ext3不是)都是如此
对一个普通文件的写实际被
sys_write()->
generic_file_write()->down(&inode->i_sem)互斥
所以结论还是write数据过程本身是原子的,对同一个inode的操作不会产生交错。符合以上APUE的定义。

2.6.21中ext2的write改为do_sync_write
也是在generic_file_aio_write里mutex_lock(&inode->i_mutex)保护,仍然符合
作者: 思一克    时间: 2010-10-21 10:53
回复 56# epegasus


O_APPEND的原子性估计和2个因素有关,
1)文件系统。好像说NFS(还有NTFS?)不行。而linux下的EXT2,3都可以
2)写的块的大小。一页内的应该可以(?)。这个我没有确定。

我的应用写LOG都是用不加锁的,LOG行长肯定是小于4096的。完全没有交错问题。从来没有。

普通的write系统调用中间是要被打断的。也就是其实不一定是一次写完的内容。如果写的位置重叠就有可能交错。
aaaabbbb变成aaabbbba类似的。

我没有测试过O_APPEND写的块大小超过4096的情况如何。
作者: epegasus    时间: 2010-10-21 11:11
回复 57# 思一克


    我所说的不能交错是指2个进程各只执行一次write.
测试的交错是由于执行多次看到的结果
区别是:如果一次write下不能交错,则不会发生前面提到的追加方式写失败时出现文件空洞.
作者: chenzhanyiczy    时间: 2010-10-21 11:32
还是比较赞同思一克兄的观点。

to epegasus

可以不用看具体文件系统的实现,我觉得只看到vfs那层就行了
作者: 思一克    时间: 2010-10-21 11:54
回复 57# 思一克


    我所说的不能交错是指2个进程各只执行一次write.
测试的交错是由于执行多次看到的结果
区别是:如果一次write下不能交错,则不会发生前面提到的追加方式写失败时出现文件空洞.
--------------------------
各执行一次也可能将aaaaaaaaaaabbbbbbbbbbbbb搞成aaaaaabbaaaaaaabbbbbbbbb样子。
只要数据块足够长就可能发生。

一般应用比如写LOG都是执行很多次,不断地执行。
作者: epegasus    时间: 2010-10-21 12:39
本帖最后由 epegasus 于 2010-10-21 13:13 编辑

回复 60# 思一克


    各执行一次也可能将aaaaaaaaaaabbbbbbbbbbbbb搞成aaaaaabbaaaaaaabbbbbbbbb样子。
只要数据块足够长就可能发生。
---------------------------------------------
不可能发生. 这里指类似ext2 这样的磁盘文件系统.
原因上面已经说了. 标准不允许(大概), 且实现确实不允许.
作者: 思一克    时间: 2010-10-21 13:15
回复 60# 思一克


    各执行一次也可能将aaaaaaaaaaabbbbbbbbbbbbb搞成aaaaaabbaaaaaaabbbbbbbbb样子。
只要数据块足够长就可能发生。
---------------------------------------------
不可能发生. 这里指类似ext2 这样的磁盘文件系统.
原因上面已经说了. 标准不允许(大概), 且实现确实不允许.
----------------------------------------------
没有太明白你的意思。
你说能交错成啥样?
作者: 思一克    时间: 2010-10-21 13:16
如果按以上描述,一个write真正的写部分是不能被另一个打断的。
比如一个写aaaa 一个写bbbb结果可能是aabbbb 但不可能是abab。
而追加方式下一定是aaaabbbb)(假设2个都是O_APPEND方式打开)
---------------
我就是这里没有看明白意思
作者: epegasus    时间: 2010-10-21 13:26
本帖最后由 epegasus 于 2010-10-21 13:29 编辑

回复 63# 思一克


    2进程每个只write一次
一个write aaaa
一个write bbbb

write aaaa 在释放inode相关锁前,write bbbb不能写如任何东西
所以出现aabbbb 仅仅是第一个写完了, 第2个因为偏移原因产生的一次覆盖(追加方式下不可能发生).注意这个覆盖最多仅发生一次
而绝不会出现abab的情况.
因为这个情况发生在第一个write了aa后被切换或者在多处理器上由另一个并行写b.
然后再切换回来或者再被这个处理器写a.
注意如果这样发生的就是当一个写没处理完 另一个写就发生. 而这是不允许的.
作者: 思一克    时间: 2010-10-21 13:44
2进程每个只write一次
一个write aaaa
一个write bbbb
write aaaa 在释放inode相关锁前,write bbbb不能写如任何东西
所以出现aabbbb 仅仅是第一个写完了, 第2个因为偏移原因产生的一次覆盖(追加方式下不可能发生).注意这个覆盖最多仅发生一次
而绝不会出现abab的情况.
因为这个情况发生在第一个write了aa后被切换或者在多处理器上由另一个并行写b.
然后再切换回来或者再被这个处理器写a.
注意如果这样发生的就是当一个写没处理完 另一个写就发生. 而这是不允许的.
-----------------------
明白了。
你的意思是,覆盖是因为seek的原因(?)
如果这样,那么多个进程不断写同一个文件的同一位置(比如0---1024)能如何交错法?
作者: 思一克    时间: 2010-10-21 13:53
2进程每个只write一次
一个write aaaa
一个write bbbb

write aaaa 在释放inode相关锁前,write bbbb不能写如任何东西
所以出现aabbbb 仅仅是第一个写完了, 第2个因为偏移原因产生的一次覆盖(追加方式下不可能发生).注意这个覆盖最多仅发生一次
-----------------------------------
如果按你这样分析,多个进程写同一个pipefd, 数据会交叉吗?
如何交叉?
作者: sumland    时间: 2010-10-21 16:44
回复 2# epegasus
请问大侠 apue中的例程你敲过吗?我现在在官网上下的包apue.linux.tar.gz怎么装不上呢?想安装它的头文件,才能运行书中的例子,但是我一直没能装上去,很头疼。。。
作者: epegasus    时间: 2010-10-21 17:01
回复 65# 思一克


    如果这样,那么多个进程不断写同一个文件的同一位置(比如0---1024)能如何交错法?
---------------------------------

应该不会有任何交错.一切符合上面说的.包括跨页块的写
说"应该"是因为我只能确定ext2,对于ext3我还有不确定的地方.即写本身到磁盘分成好几步.它的写是通过一种异步实现的,所以没搞清楚

测试是否交错有一种方法:
一个重复的写2个字符AB
另一个重复写CD
2个都是从0偏移开始
不管用不用追加方式.都不应该看到单独的被拆分的A 或 B或C或D出现.
不过这个条件建立在一个write要么写2个字符成功.要么一个都写不进
如果出现单独的说明 写如一个就终止的,这种情况APUE没有明说 linux系统编程(by robert love)倒是说了可能存在.




如果按你这样分析,多个进程写同一个pipefd, 数据会交叉吗?
如何交叉?
---------------------------------------------------------------------------
pipe和普通磁盘文件系统是一样 也通过mutex_lock(&inode->i_mutex)保护
所以结论和上面的ext2一样 但是对于pipe或许出现写入字节数小于传入字节数的可能性更大.----要确定这个要看pipe是如何定义和实现的.我不能确定.
而pipe写还可能已经类似普通文件一样默认就是O_APPEND方式,所以我认为写它真的不需要加锁.
作者: epegasus    时间: 2010-10-21 17:02
回复 67# sumland


    没完过.我没有UNIX-LINUX环境
作者: chenzhanyiczy    时间: 2010-10-21 17:27
回复  思一克


    如果这样,那么多个进程不断写同一个文件的同一位置(比如0---1024)能如何交错法? ...
epegasus 发表于 2010-10-21 17:01



    "应该不会有任何交错.一切符合上面说的.包括跨页块的写"
请问,有代码证据吗?

比如:
线程a write 5个字节(11111)
线程b write 1个字节(2)

如果两次write成功的话,那么你觉得应该不会出现:12111 或者 11211 等等,而可能出现11112,21111的情况?
如果是的话,那么请看下面源码:
asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)
{
     struct file *file;
     ssize_t ret = -EBADF;
     int fput_needed;
      file = fget_light(fd, &fput_needed);
     if (file) {
         loff_t pos = file_pos_read(file);               
         ret = vfs_read(file, buf, count, &pos);      --------> b阻塞在这,a也阻塞在这,但a是执行的,b是等待a执行完再执行的
         file_pos_write(file, pos);                  ----->假设a执行到,现在才更新pos,而b现在开始执行在vfs_read(),pos是原来的pos
         fput_light(file, fput_needed);
     }

     return ret;
}
作者: 瑷的思念    时间: 2010-10-21 17:47
提示: 作者被禁止或删除 内容自动屏蔽
作者: epegasus    时间: 2010-10-21 19:10
回复 70# chenzhanyiczy


    这段到没注意到.
不过并不矛盾
恰恰是我(6;4)楼说的因为偏移导致的错乱 而不是因为并行交叉写
所以我上面好几个帖子提到测试方法都强调要2个进程写入通样长度的.
你2个不一样长.发生了错乱就无法分辨到底是什么引起的
作者: 思一克    时间: 2010-10-21 19:14
回复 68# epegasus

我对这个问题的细节也不是100%熟悉,所以是在讨论。
你的分析结论好像是不对的。

2个进程同时写同一个文件的同一位置也有交错发生,很容易编程实验出来。
还有,交错不是因为没有写完(write返回值不等于参数数值)。而是完全写完的时候发生的交错。
也就是说,write完全写完的时候,写不是原子的。

还有,pipefs是用inode锁的,并且是不可SEEK的(排除了SEEK错误引起的覆盖)。
pipe不会重叠,但多个进程写一个pipe在块大小超过PIPE_BUF时候不是原子的,数据交错的乱七八糟。但数据没有丢一个字节。

所以,你说写文件用inode->i_mutex互斥了,只有第一个write写完,才进行第二个write应该不正确。

请再详细研究看。
作者: chenzhanyiczy    时间: 2010-10-21 20:53
回复  chenzhanyiczy


    这段到没注意到.
不过并不矛盾
恰恰是我(6;4)楼说的因为偏移导致的错乱 而 ...
epegasus 发表于 2010-10-21 19:10



这个跟长短没有关系,只要是同一个页块内就会发生交叉。
作者: qingfenghao    时间: 2010-10-22 10:23
回复 74# chenzhanyiczy
首先阐明两个概念:原子操作,可被打断(中断)和数据完整性(data integrity)。
不可被打断的操作一定是原子操作,但原子操作不一定非得不可被打断,而只是指不可被对同一数据资源的操作打断。
也就是说:原子操作只是说对本数据或资源的操作是原子的。而由其他进/线程执行的对其他数据或资源的操作是可以打断本操作的。
就像你用mutex锁定对一段数据的访问,但这并不会不被其他线程打断。
所以原子操作应该和一段数据/资源的访问相提才是有意义的。而数据完整性是指数据操作的结果和所期望的是一致的。
多线程环境下,原子操作可以保证数据完整性,否则不可以。

再来看read和write函数。所以问题就是:write函数本身是否可以保证多线程对同一文件写的数据完整性?
答案是NO。

因为内核对文件的读写事实上是按块(扇区)为单位进行的,一般每个扇区大小默认512字节,当然程序可调。
而内核的write实现并没有保证它是原子的,而只是保证它对单个扇区的读写是原子的,因为硬盘的读写就是以扇区为单位的。
这也提高了效率,否则如果write操作写的是两个不同的文件,那么如果write是原子的性能就很低。

所以只要你让多个线程同时写入一个以上字节的数据到同一文件,就可能导致数据的不完整。
假设两个线程t1,t2:
  1. write(fd, "ab");
  2. write(fd, "cd");
复制代码
那么a可能写入到扇区x,b写入到扇区x+1。
c写入到扇区x,d写入到扇区x+1。
这样写入顺序就有6种:
  1. a, b, c, d-------结果-------->cd
  2. a, c, b, d------结果--------->bd
  3. a, c, d, b------结果--------->db
  4. c, d, a, b------结果--------->ab
  5. c, a, d, b------结果--------->db
  6. c, a, b, d------结果--------->bd
复制代码
结果有4种:ab,db,bd,cd
这4种结果只有2种是数据完整的:ab和cd,假如你不关心写入顺序的话。其他都被写乱了。
所以数据完整性应该由调用者自己来保证。
作者: qingfenghao    时间: 2010-10-22 10:31
回复 75# qingfenghao

为什么实际会比较少产生数据不完整的情况?因为发生的概率比较低,多个线程对同一文件而且要对相同的一个以上的扇区同时写入,才有可能。为了确保这一点,
多个线程都要对文件的同一个position,而且要写入512个字节以上才可确保冲突存在。而冲突存在也不定会发生,这还和当时线程调度的具体情况有关。当然,写入的数据
越多,冲突的可能性越大。写入1024字节就比写入512字节发生冲突的可能性高。有兴趣可以写代码试试。
作者: 思一克    时间: 2010-10-22 16:10
回复 61# epegasus

epegasus 说的是对的。详细看了程序,是那样的。
generic_file_aio_write 被调用,它自己对inode上锁了,不完成write其它进程不能写同一个文件。

mutex_lock(&inode->i_mutex);
ret = __generic_file_aio_write(iocb, iov, nr_segs, &iocb->ki_pos);
mutex_unlock(&inode->i_mutex);

至于我们编程测试为什么有交错?估计是sys_read不加锁的缘故(?)。
可以进一步研究看。
作者: epegasus    时间: 2010-10-22 16:19
本帖最后由 epegasus 于 2010-10-22 16:32 编辑

回复 77# 思一克


    这个不能完全确定.目前还在探索中...

这个问题很早就提出来了。真的有必要把这个牛皮癣问题彻底讨论清楚。
或许一个搞懂文件系统的人已经知道原因了。只是没说出来。

这里已经有2个关于这个问题的精华帖子:
http://bbs.chinaunix.net/thread-804742-1-2.html
http://bbs2.chinaunix.net/viewth ... p;extra=&page=1


75楼的提法有道理.
简单说就是读写函数不是同步IO的(这里的IO指文件IO,而不是磁盘设备IO。这里的同步和异步系统调用是的2个领域的概念)
不过这里的能原子保证的可能不是扇区,而是特定文件系统下的逻辑块。
不过有待确定.

同时看看O_SYNC选项到底有没有真正互斥的效果
作者: epegasus    时间: 2010-10-29 13:14
回复 77# 思一克


    这个问题结论差不多是pipe和普通磁盘文件有时可能必须加锁.
先说说之前对77楼这段代码理解有错误.
2.6中上面普通文件一般先调用 fs\read_write.c : do_sync_write
  1.                  for (;;) {
  2.                 ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos);
  3.                 if (ret != -EIOCBRETRY)
  4.                         break;
  5.                 wait_on_retry_sync_kiocb(&kiocb);
  6.         }
复制代码
其中aio_write中一般是通过 mm\filemap.c : generic_file_aio_write完成真正的写入
  1.         mutex_lock(&inode-

  2. >i_mutex);
  3.         ret = __generic_file_aio_write_nolock(iocb, iov, nr_segs,
  4.                         &iocb->ki_pos);
  5.         mutex_unlock(&inode->i_mutex);
复制代码
这个函数中的这个代码保证了一个原子性,但是通过第一段代码可想而知道一个不完全的

write导致整个write变成了非原子的.并且结合O_APPEND的方式就形成了交错写.但是数据不会因覆盖而丢失.

然后是基本结论和前一个精华贴子一楼的差不多
http://bbs2.chinaunix.net/viewth ... p;extra=&page=1
关于pipe已经有结论.POSIX标准也是这样描述,写入数据小于等于PIPE_BUF保证写入是原子的.否则会出现交错.对于O_NONBLOCK 方式.这种原子

性的含义是要么都写进去.要么一个都写不进去.不是这样方式倒没有严格定义.即有可能写一部分而返回.

关于4K是否就是PIPE_BUF?是的.但是不能假定从最开始写的4K里面是原子.而是保证在每次写时传入的参数. 因为pipe在内存中的数据的方式,

不同于普通文件,普通文件是静态的文件地址偏移空间,可以用确定的文件页.所以既不能按总字节数去确定边界也不能用pipe中数据长算边界.

2.4的pipe只有一个页的内存.所以其按其规则一个大于4K的write必定不能一次执行完.必然写入4K后返回.O_NONBLOCK方式允许交叉.并在等待

前释放了SEM旗语,因而交错

2.6有16个页,一次可以写多个页,只要总容量允许.如果容量不够也会阻塞并释放旗语从而导致交错


然后是74楼提到的问题不影响一次__generic_file_aio_write_nolock的原子性. 理由是write是针对内存中的文件映像操作.从读或写的层面考

虑不管文件内容在不在磁盘中都必须先在内存中对应的文件的中的页.然后是对这个页的写.一但写入完成则对其他read write都是可见的.所以

一致性在这里已经得到保障.不过从mutex_unlock(&inode->i_mutex); 这段代码下面的几行可以看出O_SYNC的同步并不受保护.也就是一次原子

性的写入和写得结果存入磁盘不是原子的.其实也不必要保证,反正同步到磁盘的是较新的文件映像就是了.绝不会用旧的覆盖新的.
在加上O_APPEND后的写由以上分析不会产生覆盖和文件空洞.因为2个真实的原子的写文件印象绝不可能同时操作,即使他们交错执行了.
但是对于上面的返回-EIOCBRETRY的错误非常少见。目前还没确定具体是什么地方发生。而返回非-EIOCBRETRY的时候多半就是内存耗尽反而不用加锁了因为只执行一次原子写。
一个普通磁盘文件write总是可以阻塞的,而这个阻塞发生在fs\buffer.c中 也就是发生在写文件的未调入内存的部分时从磁盘获取内容的时候发生。但是这个阻塞不会破坏上面的原子性。
作者: 思一克    时间: 2010-10-29 16:17
TO epegasus,

问题我基本上知道了。
那个写的锁头保证了写同一个文件的顺序性,一个完成了下一个再写。
但无法保证读写的原子性。
因为读文件sys_read没有什么锁。
所以,读的线程可以读到一个刚写了一半的数据。结果就造成了那乱的结果。

这个好比4个进程赋值一个INT, 一个进程读这个INT.
如果INT的赋值不是一条指令的,虽然有写锁控制赋值的顺序性(一个赋值完成了下一个才来),但读进程读到的数据可以是乱的,因为读进程不管那么个锁的状态。
作者: epegasus    时间: 2010-10-29 17:48
回复 80# 思一克


    2.4 2.6 pipe read都是加锁的.混乱原因如上面分析.代码和标准中都得到确认.
不过你这确实引如一个新问题.在普通文件中的read可能出现的和write同步的问题.但问题只发生在并行执行write read
不能解释所有write执行完后 再read是否发生混乱
作者: shujunz    时间: 2010-10-29 20:49
感觉还是使用互斥锁方便
作者: 思一克    时间: 2010-10-29 23:39
回复 81# epegasus


   
    2.4 2.6 pipe read都是加锁的.混乱原因如上面分析.代码和标准中都得到确认.
不过你这确实引如一个新问题.在普通文件中的read可能出现的和write同步的问题.但问题只发生在并行执行write read
不能解释所有write执行完后 再read是否发生混乱
---------
是的。
一个进程写,一个读,不会乱的。
2个以上在写,一个在读就乱了。因为读不考虑写锁i_mutex的状态的。
这个设计是合理的。因为多数时候是大量的进程同时读一个文件按(比如配置文件),而写的机会很少。
所以读不能在内核加锁头。如果有必要,用户自己加。

pipe的读写在块大小小于PIPE_BUF时候是无需锁的。绝对不会乱。这个有准确的说明,并且有很多精彩的程序就根据这个设计的。
作者: epegasus    时间: 2010-10-30 01:20
回复 83# 思一克


    先把什么叫"乱"讲清楚.以后就直接针对普通文件讨论
之前说的乱都是指2个写的交错执行. 导致"最终文件可读结果" 是2次或多次写结果的交错
定义一个交错就是一次write调用写入的2个部分被另一次write写入打断. 这里说的写入都是指对文件的最终结果产生影响,能由一次最终读看到.
当然首先就假设了这次最终读是不能被新的写入打断的.如果打断了将不会看到上面说的最终写的结果.这个与read加锁不加锁没关系.

再看read和write发生多进程操作的问题
首先一次read肯定读到的是他即将想要读的那部分的最终结果,这个最终结果至少存在于他开始读的那一时刻.而他读的时候至少有个原子区域是禁止修改的(按页的操作必定在使用一个页的时候锁定这个页)
当多个写发生时,而且都加了append方式.则read读的内容也是这多个写的最终结果,因为没有写会去修改别人写过的部分.也就是读永远发生在所有的写之后.而非append方式因为对于某个页,读可能发生在某个写前.而其他页则发生在这个写之后所以就导致读的的内容有2个最终可读结果的交错.这完全可理解为读写不同步-不能保证读总是在写后的原因导致的,本质上和多个write是否交错没什么关系
作者: 思一克    时间: 2010-10-30 14:00
回复 84# epegasus


乱了就是:2个进程写同一文件同一位置,一个写全“0”, 一个写全“1”。
如果不乱,读到的结果要么是00000000,要么是11111111. 不可能是000111111或1111111000
乱了就出现混合了。




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2