本文是这《Linux C/C++多进程同时写一个文件》系列文章的第二篇,上一篇文章演示了两个非亲缘关系的进程同时写一个文件的情形,并得出了数据只会错乱但不会覆盖
的结论。这篇文章主要是讨论两个亲缘进程(fork)同时写一个文件的情况。
使用如下命令可以查看要写入的文件LINUX_MUTIL_PROCESS_WRITE
对应的inode情况,可以看到它的值为67530179
。
[vfhky@typecodes fork2]$ stat /home/vfhky/src/linux/process/fork2/LINUX_MUTIL_PROCESS_WRITE_1
File: ‘/home/vfhky/src/linux/process/fork2/LINUX_MUTIL_PROCESS_WRITE_1’
Size: 671744 Blocks: 1312 IO Block: 4096 regular file
Device: 803h/2051d Inode: 67530179 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ vfhky) Gid: ( 1000/ vfhky)
Access: 2017-10-29 15:48:33.404406925 +0800
Modify: 2017-10-28 20:36:34.185587055 +0800
Change: 2017-10-28 20:36:34.185587055 +0800
Birth: -
[vfhky@typecodes fork2]$
下面这个linux_process_fork2_1.c
程序很简单,主要是fork后的父子进程把对应p_buf指向的静态区数据写入到文件LINUX_MUTIL_PROCESS_WRITE_1
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | /** * @FileName linux_process_fork2_1.c * @Describe Linux C/C++多进程同时写一个文件(二) * @Author vfhky 2017-10-29 21:23 https://typecodes.com/cseries/linuxmutilprocesswrite2.html * @Compile gcc linux_process_fork2_1.c -o linux_process_fork2_1 */ #include <unistd.h> #include <string.h> #include <errno.h> #include "printnolog.h" #define FILE_NAME "LINUX_MUTIL_PROCESS_WRITE_1" static const char *p_buf_1 = "123456789"; static const char *p_buf_2 = "abcdefghi"; int main( const int argc, const char * const *argv ) { /** 上方中fopen函数的第2个参数使用ab,其中a表示追加,它能原子性地保证进程对应的文件表项中的当前文件偏移量每一次都等于v节点表中当前文件长度。 但是这里由于父子进程共享一个文件表项,也就是文件偏移量是相同的,所以可以直接使用w+(若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则建立该文件)。 */ FILE *fp = fopen( FILE_NAME, "w+" ); if( fp == NULL ) { printf( "Can not open %s, errno=%d errmsg=%s.\n", FILE_NAME, errno, strerror(errno) ); return -1; } PRINTNOLOG( "Begin to fork.\n" ); int pid; if( 0 == ( pid = fork() ) ) { PRINTNOLOG( "Child pid=%d ppid=%d.\n", getpid(), getppid() ); static size_t i_write_len = 0; for( int i=0; i<10000; i++ ) { usleep( 1000 ); i_write_len += fwrite( p_buf_1, 1, strlen(p_buf_1), fp ); } PRINTNOLOG( "i_write_len=%ld.\n", i_write_len ); } else if( 0 < pid ) { PRINTNOLOG( "Parent pid=%d, child pid=%d.\n", getpid(), pid ); static size_t i_write_len = 0; for( int i=0; i<10000; i++ ) { usleep( 1000 ); i_write_len += fwrite( p_buf_2, 1, strlen(p_buf_2), fp ); } PRINTNOLOG( "i_write_len=%ld.\n", i_write_len ); } else { PRINTNOLOG( "Fork errno=%d, errmsg=%s.\n", errno, strerror(errno) ); } return 0; } |
---|
打开一个linux ssh终端,使用ps ux
命令可以看到父进程(pid为6524)和子进程(pid为6525)。
再打开一个ssh终端,使用lsof -p 6524
命令查看两个进程打开的文件情况,从下图中可以看出这两个进程开打的都是同一个文件(对应的inode值为67530179)。
使用ll /proc/6524/fd
命令查看两个进程表项中的所有文件描述符,都是标准输入0
、标准输出1
、标准错误输出2
和打开的文件对应的描述符3
。也就是说对于文件描述符3
来说,这两个进程表项指向的文件表项中的v节点指针指向的是同一个v节点(包含i节点、文件长度等信息,且唯一)。
使用strace -p 6524
命令查看两个进程的内核调用情况,可以看到这两个进程由于for循环的缘故,一直在调用usleep
和fwrite
函数,而这两个函数最终会调用内核的nanosleep
和write
函数。
需要思考一点,为什么会函数write(3, "34567891234567891234567891234567"..., 4096) = 4096
第2个参数不是程序中指定的abcdefghi
而是4096个字节的字符。从这里可以延伸到write
和fwrite
的区别,就不多说了。
最后通过tail -f LINUX_MUTIL_PROCESS_WRITE
命令查看文件写入情况,从下图两个红色标注可以看出父进程写入4096字节(123456789
)后在字符3
后子进程开始4096字节(abcdefghi
)。
从上面小节的测试过程可以发现,和上文中两个非亲缘关系的进程同时写一个文件一样,两个亲缘关系的父子进程同时写一个文件时会出现数据混乱的情况,但是两个进程写入的数据没有覆盖。
由于父子进程表项中fd=3指向同一个文件表项,因此两个进程对应文件表项中当前文件偏移量是唯一的,所以尽管在程序没有fopen函数没有使用append
模式保证每次写入时的文件偏移量是正确的,但是依然能做到数据无覆盖。
这点从程序输出结果可以验证,父子进程分别写入了90000字节,所以文件总共大小为180000字节。