多个进程对同一文件写入的问题

多个进程对同⼀⽂件写⼊的问题
转载。
讨论关于并发环境下,多个进程对同⼀⽂件写⼊的问题,我们会涉及到⽂件共享的知识。在开始之前,我们先讨论⼀些有关⽂件共享的知识。
1. ⽂件共享
Unix系统⽀持在不同进程间共享打开的⽂件。为此,我们先介绍⼀下内核⽤于所有I/O的数据结构。注意,下⾯的说明是概念性的,与特定的实现可能匹配,也可能不匹配。
内核使⽤三种数据结构表⽰打开的⽂件,它们之间的关系决定了在⽂件共享⽅⾯⼀个进程对另⼀个进程可能产⽣的影响。
(1) 每个进程在进程表中都有⼀个记录项,记录项中包含有⼀张打开⽂件描述符表,可将其视为⼀个⽮量,每个描述符占⽤⼀项。与每个⽂件描述符相关联的是:
(a) ⽂件描述符标识(close_on_exec)。
(b)指向⼀个⽂件表项的指针。
(2)内核为所有的打开⽂件维持⼀张⽂件表。每个⽂件表项包含:
(a)⽂件状态标志(读、写、添加、同步和⾮阻塞等)。
(b)当前⽂件偏移量。
(c)指向该⽂件v节点的指针。
(3)每个打开⽂件(或设备)都有⼀个v节点(v-node)结构。v节点包含了⽂件类型和对此⽂件进⾏各种操作的函数的指针。对于⼤多数⽂件,v节点还包含了该⽂件的i节点(i-node,索引节点)。这些信息是在打开⽂件时从磁盘上读⼊内存的,所以所有关于⽂件的信息都是快速可供使⽤的。例如,i节点包含了⽂件的所有者,⽂件长度,⽂件所在的设备,指向⽂件实际数据块在磁盘上所在位置的指针等等。
注意:Linux没有使⽤v节点,⽽是使⽤了通⽤i节点结构。虽然两种实现有所不同,但在概念上,v节点与i节点是⼀样的。两者都指向⽂件系统特有的i节点结构。
我们忽略了默写实现细节,但这并不影响我们的讨论。例如,打开⽂件描述符表可存放在⽤户控件,⽽⾮进程表中。这些表也可以⽤于多种⽅式的实现,不必⼀定是数组;例如,可将它们实现为结构的连接表。这些细节并不影响我们在⽂件共享⽅⾯的讨论。
图1显⽰了⼀个进程的三张表之间的关系。该进程有两个不同的打开⽂件:⼀个⽂件打开为标注输⼊(⽂件描述符为0),另⼀个打开为标准输出(⽂件描述符为1)。从Unix系统的早期版本中[Thompson 1978]以来,这三张表之间的基本关系⼀直保持⾄今。这种安排对于在不同进程之间共享⽂件的⽅式⾮常重要。
图1 打开⽂件的内核数据结构
1. 注意:创建v节点结构的⽬的是对在⼀个计算机系统上的多⽂件系统类型提供⽀持。这⼀⼯作是有Peter Weihberger(贝尔实验室)和
Bill Joy(Sun公司)分别独⽴完成的。Sun称此种⽂件系统为虚拟⽂件系统(Virtual File System),称与⽂件系统类型⽆关的i节点部分为v节点[Kleiman 1986].当哥哥制造商的实现增加了对Sun的⽹络⽂件系统(NFS)的⽀持时,它们都⼴泛采⽤了v节点结构。在BSD 系统中⾸先提供v节点的是4.3BSD Reno版本,其中加⼊了NFS。
2.
3. 在SVR4中,v节点代换了SVR3中与⽂件系统类型⽆关的i节点结构。Solaris是从SVR4发展⽽来的,他也是⽤了v节点。
4.
5. Linux没有将相关的数据结构分为i节点和v节点,⽽是采⽤了⼀个独⽴于⽂件系统的i节点和⼀个依赖于⽂件系统的i节点。
如果两个独⽴进程各⾃打开了同⼀个⽂件,则有图2中所⽰的安排。我们假设第⼀个进程在⽂件描述符3上打开该⽂件,⽽另⼀个进程则在⽂件描述4上打开该⽂件。打开该⽂件的每⼀个进程都得到⼀个⽂件表项,但对⼀个给定的⽂件只有⼀个v节点表项。每个进程都有⾃⼰的⽂件表项的⼀个理由是:这种安排使每⼀个进程都有它⾃⼰的对该⽂件的当前偏移量。
图2 两个独⽴进程各⾃打开同⼀个⽂件
给出了这种数据结构后,现在对前⾯所描述的操作做进⼀步说明。
在完成每个write后,在⽂件表项中的当前⽂件偏移量即增加所写的字节数。如果当前⽂件偏移量超过了当前⽂件长度,则在i节点表项中的当前⽂件长度被设置为当前⽂件的偏移量。
如果⽤O_APPEND标志打开了⼀个⽂件,则相应标志也被设置到⽂件表项的⽂件状态标志中。每次对这种具有添写标志的⽂件执⾏写操作时,在⽂件表项中的当前⽂件偏移量⾸先被设置为i节点表项中的⽂件长度。这就使得每次写的数据都添加到⽂件的当前尾端处。
若⼀个⽂件⽤lseek定位到⽂件当前的尾端,则⽂件表项中的当前⽂件偏移量被设置为i节点表项中的当前⽂件长度。注意,这与⽤O_APPEND标志打开⽂件是不同的。
sleek函数只修改⽂件表项中的当前⽂件偏移量,没有进⾏任何⽂件I/O操作。
可能有多个⽂件描述符指向同⼀个⽂件表项。在下⼀⼩节中讨论dup函数时,我们将能看见这⼀点。函数调⽤fork后产⽣的⽗⼦进程中,它们共享相同的i或v节点和同⼀个⽂件表项。(通过在现代Linux系统中进⾏测试得到的,测试程序和结构见下⽂(更正:测试结果分析有误!已更正!))。
注意,⽂件描述符标志和⽂件状态标志在作⽤域⽅⾯的区别,前者只⽤于⼀个进程的⼀个描述符,⽽后者适⽤于指向该给定⽂件表项的任何进程中的所有描述符。上⾯所述的⼀切对多个进程读同⼀个⽂件都能正确⼯作。每个进程都有它⾃⼰的⽂件表项,其中也有它⾃⼰的当前⽂件偏移量。但是,当多个进程写同⼀个⽂件时,则可能产⽣预期不到得结果。为了说明如何避免这种情况,我们需要理解原⼦操作的概念。
2. 原⼦操作
2.1 添写⾄⼀个⽂件
考虑⼀个进程,它要将数据添加到⼀个⽂件尾端。早期的UNIX系统版本并不⽀持open的O_APPEND
选项,所以程序被编写成下列形式:
1. if(lseek(fd, 0L, 2)< 0)/* position to EOF */蝽卵
2.    err_sys("lseek error");
3. if(write(fd, buf, 100)!= 100)/*and write */
4.    err_sys("write error");
对单个进程⽽⾔,这段程序能正常⼯作,但若对多个进程同时使⽤这种⽅法将数据添加到同⼀⽂件,则会产⽣问题。(例如,若此程序由多个进程同时执⾏,各⾃将消息添加到⼀个⽇志⽂件中,就会产⽣这种情况。)
假定有两个独⽴的进程A和B都对同⼀个⽂件进⾏操作,给个进程都已打开了该⽂件,但未使⽤O_APPEND标志。此时,各数据结构之间的关系如图2所⽰。每个进程都有⾃⼰的⽂件表项,但是共享⼀个v节点表项。假定进程A调⽤了lseek,它将进程A的该⽂件当前偏移量设置为1500字节(当前⽂件尾端处)。然后内核调度进程使进程B运⾏。进程B执⾏sleek,也将其对该⽂件的当前偏移量设置为1500字节(当前⽂件尾端处)。然后B调⽤write函数,它将B的该⽂件当前⽂件偏移量增值1600.引⽂该⽂件的长度已经增加了,所以内核对v节点中的当前⽂件长度更新为1600.然后,内核⼜进⾏进程
切换使进程A恢复运⾏。当A调⽤write时,就从其当前⽂件偏移量(1500字节)处将数据写到⽂件中去。这样就代换了进程B刚写到该⽂件中的数据。
问题出在逻辑操作“定位到⽂件尾端处,然后写”上,它使⽤了两个分开的函数调⽤。解决问题的⽅法是使这两个操作对于其他进程⽽⾔成为⼀个原⼦操作。任何⼀个需要多个函数调⽤的操作都不可能是原⼦操作,因为在两个函数调⽤之间,内核有可能会临时挂起该进程。
UNIX系统提供了⼀种⽅法是这种操作成为原⼦操作,该⽅法是在打开⽂件时设置O_APPEND标志。正如前⾯所述,这就是内核每次对这种⽂件进⾏写之前,都将进程的当前偏移量设置到⽂件的尾端处,于是在每次写之前就不在需要调⽤sleek了。
2.2 pread和pwrite函数
青龙膏Single UNIX Specification包括了XSI扩展,该扩展允许原⼦性地定位搜索(seek)和执⾏I(/O。pread和pwrite就是这种扩展。
1. #include <unistd.h>
2.
3. ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);
4.                                        返回值:读到的字节数,若已到⽂件结尾则返回0,若出现错误返回-1
5. ssize_t pwrite(int filedes,const void *buf, size_t nbytes, off_t offset);
6.                                        返回值:若成功则返回已写的字节数,若出错则返回-1
调⽤pread相当于顺序调⽤lseek和read,但是pread⼜与这种顺序调⽤有下列重要区别:
调⽤pread时,⽆法中断其定位和读操作。
不更新⽂件指针。
调⽤pwrite相当于顺序调⽤lseek和write,但也和它们有类似的区别。
2.3 创建⼀个⽂件
在对open函数的O_CREAT和O_EXCL选项进⾏说明时,我们已经见到另⼀个有关原⼦操作的例⼦。当同时指定这两个选项,⽽该⽂件⼜已经存在时,open将失败。我们曾提及检查该⽂件是否存在以及创建该⽂件这两个操作是作为⼀个原⼦操作执⾏的。如果没有这样⼀个原⼦操作,那么可能会编写下⾯的程序段:
安徽新长江投资集团
1. if((fd = open(pathname, O_WRONLY))< 0){
2. if(errno == ENOENT){
3. if((fd = creat(pathname, mode))< 0)
4.              err_sys("create error");
少先队章程
5. }else{
6.          err_sys("open error");
7. }
8. }
如果在open和creat之间,另⼀个进城创建了该⽂件,那么就会引起问题。例如,若在这两个函数调⽤之间。另⼀个进程创建了该⽂件,并且写进了⼀些数据,然后,原先的进程执⾏这段程序中的creat,这时,刚由另⼀个进程写上去的数据就会被擦除掉。如若将这两者合并在⼀个原⼦操作中,这种问题就不会存在了。
⼀般⽽⾔,原⼦操作(atomic operation)指的是由多步组成的操作,如果该操作原⼦地执⾏,则要么执⾏完所有步骤,要么⼀步也不执⾏,不可能只执⾏所有步骤的⼀个⼦集。
3. dup和dup2函数
下⾯两个函数都可⽤来复制⼀个现存的⽂件描述符:
1. #include <unistd.h>
2.
3. int dup(int filedes);
4. int dup2(int filedes,int filedes2);
5.                      两函数的返回值:若成功则返回新的⽂件描述符,若出错则返回-1
由dup返回的新⽂件符⼀定是当前可⽤⽂件描述符中的最⼩数值。⽤dup2则可以⽤filedes2参数指定新描述符的数值。如果filedes2已经打开,则先将其关闭。如若filedes等于filedes2,则dup2返回filedes2,⽽不关闭它。
这些函数返回的新⽂件描述符与参数参数filesdes共享同⼀个⽂件表项。如图3所⽰。
物质的量
图3 执⾏dup之后的内核数据结构
在图3中,我们假定进程执⾏了:
1. newfd = dup(1);
当此函数开始执⾏时,假定下⼀个可⽤的⽂件描述是3(这是⾮常有可能的,因为0、1、2是由shell打开的)。因为两个描述符指向同⼀⽂件表项,所以它们共享同⼀个⽂件状态标志(读、写、添加等)以及同⼀⽂件当前偏移量。
每个⽂件描述符都有它⾃⼰的⼀套⽂件描述符标志。新描述的执⾏时关闭(close-on-exec)标志总是由dup函数清除。
复制⼀个描述符的另⼀种⽅法是使⽤fcntl函数,实际上,调⽤
1. dup(filedes);
等效于
1. dup2(filedes, F_DUPFD, 0);
⽽调⽤
1. dup2(filedes, filedes2);
等效于潘海东
1. close(filedes2);
2. fcntl(filedes, F_DUPFD, filedes2);
在后⼀种情况下,dup2并不完全等同于close()加上fcntl.它们之间的区别是:
dup2是⼀个原⼦操作,⽽close及fcntl则包含两个函数调⽤。有可能在close和fcntl之间插⼊执⾏信号捕获函数,它可能修改⽂件描述符。
dup2和fcntl有某些不同的errno。
4. 测试结果
带有O_APPEND标志的测试代码和结果数据如下:
没有带O_APPEND标志的测试代码和结果数据如下:
可以看到,o_append_text.rar⾥⾯的测试结果数据⽂件⼤⼩是test.rar⾥⾯的2倍。分析其原因,是因为函数调⽤fork时,在⽗⼦进程中没有为他们采取必要的同步措施,因此在写⽂件时有竞争发⽣,导致结果混乱。另外,⽂件⼤⼩的不同是因为,⽂件表项中的⽂件偏移量的值类型并不是⼀个易失形变量类型,从⽽导致在写⽂件时读取的偏移值变量的值不是最新的值,从⽽导致⽂件⼤⼩会不同的结果。
另外,从结果数据中(可能数据没有充分表现出如下所说的情况,但是你可以通过调整测试程序⾥的参数,并多运⾏⼏次测试程序就可以得到如下所述的结果)可以得出:当以O_APPEND标志打开⽂件时,write将执⾏原⼦操作,read亦然。⽽没有使⽤O_APPEND标志打开⽂件时,⽗⼦进程的数据输出将出现乱序的情况。

本文发布于:2024-09-23 03:14:50,感谢您对本站的认可!

本文链接:https://www.17tex.com/xueshu/474176.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:进程   节点   操作   数据   描述符
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议