原来的贴
http://bbs.chinaunix.net/viewthread.php?tid=804742&extra=page%3D2
讨论的问题是许多进程同时写,会不会交叉,需要不需要LOCK的问题.
根据质疑我又研究了.
初步结果:
1) 如果多个写同时WRITE pipe, 如果每次写的大小是小于4096(一页), LINUX确保写是原子的, 数据绝对不会交叉,也就是不需要LOCK. 如果数据再大, 需要LOCK. 这个原子写的最大值是PIPE_BUF所定义. 该结论是可靠的.
2) 如果写文件, 每次写小于4096, 也是原子的, 数据不交叉, 也不需要LOCK. 结论用程序测试是对的,但没有得到可靠的理论根据. 数据大于4096, 不是原子的. 对于写LOG file, 一行一行的写, 因为每行<<4096,所以程序的结果是原子的.
原来贴子中的我的观点是所有的都是原子的(无论数据大小),是错误的.
这个问题还要继续讨论, 一直到结论完全正确圆满为止.
[ 本帖最后由 思一克 于 2006-9-18 08:38 编辑 ]
harly 回复于:2006-09-18 00:13:29
引用:1) 如果多个写同时WRITE pipe, 如果每次写的大小是4096(一页), LINUX确保写是原子的, 数据绝对不会交叉,也就是不需要LOCK. 如果数据再大, 需要LOCK. 这个原子写的最大值是PIPE_BUF所定义. 该结论是可靠的.
2) 如果写文件, 每次写小于4096, 也是原子的, 数据不交叉, 也不需要LOCK. 结论用程序测试是对的,但没有得到可靠的理论根据. 数据大于4096, 不是原子的. 对于写LOG file, 一行一行的写, 因为每行<<4096,所以程序的结果是原子的.
对第二点心存怀疑。我想如果不被信号中断等应该会写完的,即使被中断,那么如果不重启的化应该这次的写失败了,还不至于继续交叉写吧,我也没有理论根据,继续关注这个问题。
圆点坐标 回复于:2006-09-18 09:08:56
要从文件系统的角度去分析这个问题比较好,之所以是4096,那是因为在linux中一个page的大小是4096,如果超过4096,在内核会产生多个page,但是当page产生时,会有一些比如dirty啊,lock等标记,如果lock标记没有去掉,这个paged的东西时不会被修改的。
sunlan 回复于:2006-09-18 20:19:34
为验证多进程并发写可能出现的问题,我做了个测试。测试的环境是SCO OSR5.0.6,单CPU
测试所用代码如下:
p.c
#include <stdio.h>
main( )
{
FILE *fp;
int i;
fp=fopen( "aaa.txt", "a" );
for( i=0; i<50000; i++ )
{
fprintf( fp, "pid=[%d] aaaaaaaaaaaaaaaaaaaaaaaaaa\n", getpid() );
}
fclose( fp );
}
cc -o p p.c
程序里没有直接使用write,而是使用了fprintf,这是因为在写日志时更多的使用的是带格式的数据,很少有直接使用write的。
测试用的脚本p.sh
p&
p&
p&
p&
p&
使用5个进程同时对aaa.txt进行写操作。
对于aaa.txt中的大部分记录,都是下面的格式:
pid=[1900] aaaaaaaaaaaaaaaaaaaaaaaaaa
但也很少的几条记录被写成了下面这样:
pid=[1900] aaaaaaaaaaaaaaaaaaaaaaaaaa
pid=[1900pid=[1901] aaaaaaaaaaaaaaaaaaaaaaaaaa
pid=[1901] aaaaaaaaaaaaaaaaaaaaaaaaaa
感觉是在进程切换时发生的。
由于是在单CPU环境下做的测试,因此并不能真实模拟多进程并发时的情况。如果谁手头有多CPU的机器,可以试一下看会发生什么样的情况。
coldwarm 回复于:2006-09-18 21:09:21
不应当使用fprintf,使用fprintf根本验证不出来。它的I/O是默认是带缓冲的,跟write的行为是不同的。
如果使用setbuf关闭stdout的缓冲区,fprintf应当被转换成一系列的write调用,每次写一个字节。
read和write针对不同的操作对象在某些情况下不具备原子性(比如超过PIPE_BUF,nfs操作等)。
我觉得关键是在于在用户缓冲区和系统的缓冲区之间是否存在缓冲行为。如果存在缓冲,那么它必然不是原子的。
这是因为在写日志时更多的使用的是带格式的数据,很少有直接使用write的
snprintf格式化,用write写也一样。
思一克 回复于:2006-09-19 08:38:06
用sprintf到一个buffer中,
然后一起将buffer write 就是原子了。buffer长度控制在4096内(对于LOG的一行,足够)。
在测试看
linuxiang 回复于:2006-09-19 14:09:56
一次小小的提问,时隔很长时间,没想到思一克版主这么执着,小弟真是很佩服。
对于第二个问题,我认为跟文件系统有关系,写小于等于一个page时是原子的(一般linux的page是4096),写大于一个page时,可能就不是原子的。
具体细节我也不清楚,希望高手深入解释一下。
sunlan 回复于:2006-09-19 16:38:36
下面是The Open Group Base Specifications Issue 6里对write的部分说明
If the value of nbyte is greater than {SSIZE_MAX}, the result is implementation-defined.
After a write() to a regular file has successfully returned:
Any successful read() from each byte position in the file that was modified by that write shall return the data specified by the write() for that position until such byte positions are again modified.
Any subsequent successful write() to the same byte position in the file shall overwrite that file data.
Write requests to a pipe or FIFO shall be handled in the same way as a regular file with the following exceptions:
There is no file offset associated with a pipe, hence each write request shall append to the end of the pipe.
Write requests of {PIPE_BUF} bytes or less shall not be interleaved with data from other processes doing writes on the same pipe. Writes of greater than {PIPE_BUF} bytes may have data interleaved, on arbitrary boundaries, with writes by other processes, whether or not the O_NONBLOCK flag of the file status flags is set.
If the O_NONBLOCK flag is clear, a write request may cause the thread to block, but on normal completion it shall return nbyte.
If the O_NONBLOCK flag is set, write() requests shall be handled differently, in the following ways:
The write() function shall not block the thread.
A write request for {PIPE_BUF} or fewer bytes shall have the following effect: if there is sufficient space available in the pipe, write() shall transfer all the data and return the number of bytes requested. Otherwise, write() shall transfer no data and return -1 with errno set to [EAGAIN].
A write request for more than {PIPE_BUF} bytes shall cause one of the following:
When at least one byte can be written, transfer what it can and return the number of bytes written. When all data previously written to the pipe is read, it shall transfer at least {PIPE_BUF} bytes.
When no data can be written, transfer no data, and return -1 with errno set to [EAGAIN].
When attempting to write to a file descriptor (other than a pipe or FIFO) that supports non-blocking writes and cannot accept the data immediately:
If the O_NONBLOCK flag is clear, write() shall block the calling thread until the data can be accepted.
If the O_NONBLOCK flag is set, write() shall not block the thread. If some data can be written without blocking the thread, write() shall write what it can and return the number of bytes written. Otherwise, it shall return -1 and set errno to [EAGAIN].
Upon successful completion, where nbyte is greater than 0, write() shall mark for update the st_ctime and st_mtime fields of the file, and if the file is a regular file, the S_ISUID and S_ISGID bits of the file mode may be cleared.
For regular files, no data transfer shall occur past the offset maximum established in the open file description associated with fildes.
redac 回复于:2007-02-25 16:05:20
继续关注这个问题!
Edengundam 回复于:2007-02-25 19:53:20
引用:原帖由 思一克 于 2006-9-17 23:48 发表
原来的贴
http://bbs.chinaunix.net/viewthread.php?tid=804742&extra=page%3D2
讨论的问题是许多进程同时写,会不会交叉,需要不需要LOCK的问题.
根据质疑我又研究了.
初步结果:
1) ...
sunlan给出的描述和版主的第一个结论完全相符合.
但是这个描述中对普通文件没有任何的明确说明, 只有:
Any successful read() from each byte position in the file that was modified by that write shall return the data specified by the write() for that position until such byte positions are again modified.
假设进程A准备了一个100MB的buffer调用一次write. 进程B在A调用write中, 尝试去write这个文件. 都是用APPEND. 那么如果进程B中的 write 能成功返回, 就需要FLOCK了. 假设B只写一个BYTE吧.
write系统调用写文件时候, 会调用内核算法取得一个内核管理的buffer. 老的UNIX这个叫getblk.
调用getblk函数貌似是不能被中断的. 也就是说, write超过一个buffer大小的数据, 这个进程就会调用多次getblk.
我想为了公平的原则, 那么write系统调用是允许被"抢占"的, 也就是被中断的. 但是如果write已经成功调用进入了getblk, 并且封锁了低于这个中断的所有中断, 那么这时候就没有进程调度了. 如果没有进入这类的函数, 那么write是可以被其他的进程所"抢占".
因此, 会出现写大于1个buffer的数据时候导致问题的出现.
如果write不能被抢占, 那么一个恶意用户可以申请一个大的buffer, 不断利用write来"阻止"其他用户的正常使用.
这个问题, 应该去看下write系统调用的实现...懒得找了, 版主不妨去看下. 手头没有代码...:em06:
Edengundam 回复于:2007-02-25 23:21:02
在windows下用msys做了一个试验
/* a.c */
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MAX 300*1024*1024
int main()
{
char *buf;
int i;
int fd;
buf = (char *) malloc(MAX);
for (i = 0; i < MAX; i++)
if (i % 80 != 0)
buf = 'a';
else
buf = '\n';
printf("ok\n");
getchar();
fd = open("./abc.log", O_WRONLY | O_APPEND);
if (write (fd, buf, MAX) == MAX)
printf("write ok!\n");
close(fd);
return 0;
}
/* b.c */
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
char buf[4096];
int i = 0;
int fd;
for (i = 0; i < 4096; i++)
buf = 'b';
buf[4095] = '\n';
i = 0;
fd = open("./abc.log", O_WRONLY | O_APPEND);
while (i++ < 100)
if (write (fd, &buf, 4096) == 4096)
printf("write: <%2d> ok!\n", i);
else
printf("error!\n");
close(fd);
return 0;
}
编译后, 先执行a, 现在会一次write一个300M的文件, 基本上是79个a一行. 在a执行时候(记得中间随便打个字符), 可以运行b看看... 我在msys环境下虽然b提示write成功了(我执行了3遍b).
但是最后我对这abc.log用grep -n 'b' abc.log 竟然看不到程序b添加进去的信息...这里不知道出什么问题, 手头没
有linux测试....
我下载了2.4.1&2.6.4的linux内核, 简单看了两眼. (很多函数没有任何注释, 只能通过函数名猜测了...这样的代码看着有点痛苦)
我没有直接去看system call的write. 我认为所有操作都是透过fs目录下的vfs来实现的(也许我认为错了...)
2.4.1&2.6.4只是锁的处理变了, 但是外面看上去差不多吧.
fs/read_write.c
asmlinkage ssize_t sys_write(unsigned int fd, const char * buf, size_t count)
{
ssize_t ret;
struct file * file;
ret = -EBADF;
file = fget(fd);
if (file) {
if (file->f_mode & FMODE_WRITE) {
struct inode *inode = file->f_dentry->d_inode;
/* locks_verify_area看起来是要把调用write操作的文件范围上锁 */
ret = locks_verify_area(FLOCK_VERIFY_WRITE, inode, file,
file->f_pos, count);
if (!ret) {
ssize_t (*write)(struct file *, const char *, size_t,
loff_t *);
ret = -EINVAL;
if (file->f_op && (write = file->f_op->write) != NULL)
ret = write(file, buf, count, &file->f_pos);
}
}
if (ret > 0)
inode_dir_notify(file->f_dentry->d_parent->d_inode,
DN_MODIFY);
fput(file);
}
return ret;
}
include\linux\fs.h 没有什么东西, 直接调用了locks_mandatory_area
static inline int locks_verify_area(int read_write, struct inode *inode,
struct file *filp, loff_t offset,
size_t count)
{
if (inode->i_flock && MANDATORY_LOCK(inode))
return locks_mandatory_area(read_write, inode, filp, offset, count);
return 0;
}
fs/locks.c
int locks_mandatory_area(int read_write, struct inode *inode,
struct file *filp, loff_t offset,
size_t count)
{
struct file_lock *fl;
struct file_lock *new_fl = locks_alloc_lock(0);
int error;
new_fl->fl_owner = current->files;
new_fl->fl_pid = current->pid;
new_fl->fl_file = filp;
new_fl->fl_flags = FL_POSIX | FL_ACCESS;
new_fl->fl_type = (read_write == FLOCK_VERIFY_WRITE) ? F_WRLCK : F_RDLCK;
new_fl->fl_start = offset;
new_fl->fl_end = offset + count - 1;
error = 0;
/* 这里直接锁住了内核, 往下面我只扫了一眼, 看起来很复杂, 注释说了一句多个CPU
竞争 */
lock_kernel();
repeat:
/* Search the lock list for this inode for locks that conflict with
* the proposed read/write.
*/
for (fl = inode->i_flock; fl != NULL; fl = fl->fl_next) {
if (!(fl->fl_flags & FL_POSIX))
continue;
if (fl->fl_start > new_fl->fl_end)
break;
if (posix_locks_conflict(new_fl, fl)) {
error = -EAGAIN;
if (filp && (filp->f_flags & O_NONBLOCK))
break;
error = -EDEADLK;
if (posix_locks_deadlock(new_fl, fl))
break;
/* 这里应该就是给要写的范围上锁了, 一次把要写的所有范围全部锁住, 这里又锁了内核
* 所以我觉得不会发生任何的竞争. 也就不会被其他进程干扰这些原子的write
*/
error = locks_block_on(fl, new_fl);
if (error != 0)
break;
/*
* If we've been sleeping someone might have
* changed the permissions behind our back.
*/
if ((inode->i_mode & (S_ISGID | S_IXGRP)) != S_ISGID)
break;
goto repeat;
}
}
locks_free_lock(new_fl);
unlock_kernel();
return error;
}
documents/filesystmes/vfs.txt中关于vfs定义...
What is it? <section>
===========
The Virtual File System (otherwise known as the Virtual Filesystem
Switch) is the software layer in the kernel that provides the
filesystem interface to userspace programs. It also provides an
abstraction within the kernel which allows different filesystem
implementations to co-exist.
这是2.4.1的代码, 和2.6.4只是锁住内核后代码有差距, 我没有看, 内核锁了, 肯定就已经保证原子了.
这里只是从vfs那里走的, 如果文件系统是通过vfs实现就应该没有问题了....谁能看下系统调用到sys_write这里是如果过来的??如果这里没有什么特殊处理, 我觉得write可以保证写的原子性...
大家可以试试我上面的小程序...
PS: 以前没有看过linux代码...有错还请指出
[ 本帖最后由 Edengundam 于 2007-2-25 23:46 编辑 ]
converse 回复于:2007-03-05 23:51:03
做个保留先~~
Edengundam 回复于:2007-03-06 08:55:40
引用:原帖由 思一克 于 2006-9-17 23:48 发表
原来的贴
2) 如果写文件, 每次写小于4096, 也是原子的, 数据不交叉, 也不需要LOCK. 结论用程序测试是对的,但没有得到可靠的理论根据. 数据大于4096, 不是原子的. 对于写LOG file, 一行一行的写, 因为每行<<4096,所以程序的结果是原子的.
刚才在linux上测试了, 不会发生任何问题, 保证是原子的, 前请求write的进程应该会通过lock阻止其他的进程访问被lock的地址. 后面的进程阻塞在write操作上, 当先请求的进程结束后, 后面的进程才完成write调用. :em02:
思一克 回复于:2007-03-06 09:05:06
写文件KENREL没有调用系统调用lock. 但KENREL会获得inode等资源的锁。具体看参看sys_write等函数。
Edengundam 回复于:2007-03-06 09:16:56
引用:原帖由 思一克 于 2007-3-6 09:05 发表
写文件KENREL没有调用系统调用lock. 但KENREL会获得inode等资源的锁。具体看参看sys_write等函数。
不是系统调用lock, 你看我贴的代码:em15:
sys_write之后的东西都是内核算法嘛..:)
这里有一个问题就是锁范围时候, 检查一个inode的flock并且有一个宏MANDATORY_LOCK(inode). 如果这个宏展开后没有显示使用锁or其他互斥手段, 那么应该考虑locks_mandatory_area这个里面的来申请锁吧.
思一克 回复于:2007-03-06 09:23:21
TO Edengundam,
我和你说的是一个意思。写4096以下字节应该是原子操作。只是我没有通过研究KERNEL代码找到100%的支持。
你给出的代码和结论应该是对的。
mingyanguo 回复于:2007-03-06 09:27:17
Linux的2.6.17.3里面,generic_file_write里面会用mutex_lock锁住inode。
所以,一个write应该可以认为是原子的。
没有测试过…….
Edengundam 回复于:2007-03-06 09:27:25
引用:原帖由 思一克 于 2007-3-6 09:23 发表
TO Edengundam,
我和你说的是一个意思。写4096以下字节应该是原子操作。只是我没有通过研究KERNEL代码找到100%的支持。
你给出的代码和结论应该是对的。
我意思是说>4096内核可以保证原子, 内核调用会去把你写的范围都上锁, 这个锁是内核资源, 不需要用户看到的. :em15:
思一克 回复于:2007-03-06 09:28:46
locks_verify_area
只有在文件被lock(用户调用了lockf等)才起作用。否则返回0
WRITE的原子性应该和函数
locks_verify_area
locks_mandatory_area
无关。因为这些函数只是在用户flock, lockf后才起作用的。而我们说的所有的write < 4096都是原子的。
思一克 回复于:2007-03-06 09:33:33
TO Edengundam,
》4096 不是。
你可以实验。
Edengundam 回复于:2007-03-06 09:36:40
引用:原帖由 思一克 于 2007-3-6 09:33 发表
TO Edengundam,
》4096 不是。
你可以实验。
因为我之前实验, 后面的进程会阻塞...第一个进程write完了第二个才OK的, 我是用grep搜索的. :em06:
我哪里可能弄错了...:em06:
如mingyanguo兄所说, 再往下看看其他的block操作去...:em02:...晚点研究下...:mrgreen:
[ 本帖最后由 Edengundam 于 2007-3-6 09:38 编辑 ]
|