linux下的C开发11,多进程编程实验,多进程操作同一文件可能存在的问题

上一节提到在 linux 中进行 C语言程序开发时,多个进程打开同一个文件写入数据,如果不小心处理,会产生一些意想不到的错误。那么,会有哪些错误呢?该怎么解决呢?本节将给出几个 C语言实例,并在此基础上讨论这个问题。

多进程同时操作同一个文件,可能存在的问题

之前我们说过,C语言的 I/O 读写函数每次操作时,linux 内核都会记录当前文件偏移量指针。还记得下面这张图吗?

多个进程同时打开同一个文件时,linux 内核会为每一个进程都分配一个文件表,操作文件时的当前文件偏移了指针就记录在文件表里。如果程序员做了下面的操作:

  • 进程 A 和进程 B 同时打开了文件 file;
  • 进程 A 的文件表中记录的 file 的当前文件偏移量是 10;
  • 进程 B 的文件表中记录的 file 的当前文件偏移量也是 10;
  • 进程 A 往 file 里写入了 20 个字节的数据;
  • 进程 B 往 file 里写入了 30 个字节的数据。

想想看,会有什么情况发生?答案是显而易见的,有数据被覆盖了。这是因为进程 A 和进程 B 有各自的文件表,也就是说他们各自的文件偏移量,不会随着对方的 I/O 操作改变。

这样一来,如果进程 A 先从偏移量为 10 处写入 file 20 个字节,接着进程 B 也从偏移量为 10 处写入 file 30 个字节,那么进程 A 的写入的数据就被覆盖了。如果进程 B 先于进程 A 进行,那么进程 B 写入的前 20 个字节的数据也会被覆盖。

上面只说明了多进程操作同一文件,可能会出现的一种糟糕情况,事实上,还有别的更坏的情况可能发生,这点就留给读者自己思考了。

linux 中 C语言编程,产生一个子进程

现在将使用 C语言编程,来进一步说明上面的问题。首先,在 linux 中,C语言怎样编写多进程程序呢?还记得我之前写的《linux学习系列文章》吗?linux 中的进程有着比较明显的继承关系,几乎所有进程的源头都是 init 进程。

init 进程 fork 出若干子进程,这些子进程又 fork 出若干自己的子进程,linux 启动后,所有进程都是这么来的。

所以,这里也使用 C语言的 fork 函数创建新的进程。输入 man 命令即可看到 fork 函数的说明:

根据手册,写出创建子进程的 C语言程序是容易的,请看:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t pid = fork();

    if(0==pid){
        printf("this is child: %ld\n", (long)getpid());
    }else{
        printf("this is father: %ld\n", (long)getpid());
        int status;
        wait(&status);
    }
    return 0;
}

以上程序创建了一个子进程,子进程打印出自己的 pid 就退出。父进程则打印出自己的 pid 后,等待子进程结束后才退出。

在《linux学习系列文章》里,我们说过,子进程“死亡”后,需要父进程为它收尸,否则子进程可能会成为“僵尸进程”。

编译执行,得到如下结果:

# gcc t.c
# ./a.out
this is father: 10454
this is child: 10455

多进程同时写入数据到同一个文件

现在知道在 linux 中,如何使用 C语言编写多进程程序了。实现上面的操作是简单的,请看:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    char* filename = "test.bin";
    pid_t pid = fork();
    if(0==pid){
        printf("this is child: %ld\n", (long)getpid());
        char buf[20];
        memset(buf, 1, sizeof(buf));
        int fd = open(filename, O_WRONLY|O_CREAT);
        write(fd, buf, sizeof(buf));
        close(fd);
    }else{
        printf("this is father: %ld\n", (long)getpid());
        char buf[30] = {2};
        memset(buf, 2, sizeof(buf));
        int fd = open(filename, O_WRONLY|O_CREAT);
        write(fd, buf, sizeof(buf));
        close(fd);
        int status;
        wait(&status);
    }
    return 0;
}


程序的逻辑非常简单,就是父子进程都打开 test.bin 文件,子进程往其中写入 20 个字节的 0x01,父进程往其中写入 30 个字节的 0x02。现在编译执行,得到如下结果:

容易看出,因为父进程先执行了,所以父进程的数据被覆盖了一部分。现在在父进程里加入延时,目的是为了让子进程先执行:

...
usleep(500);
printf("this is father: %ld\n", (long)getpid());
...

再编译执行,会发现子进程的数据全部被父进程的数据覆盖了,这与我们的分析是一致的。

解决数据冲突

那么,怎样解决上面这种错误呢?看了之前文章的朋友可能想到了 lseek 函数,在每次写入之前都使用 lseek 把当前文件偏移量设置到文件数据之后,不就解决了问题吗?

这并不一定能解决问题,因为父子进程可能会同时运行(或者交叉运行)。想象一下这种情况:当前文件数据最大只到 100 字节处,父进程在写入数据前,使用 lseek 函数将自己的文件偏移量设置到 100 字节处。但是,这时由于系统调度,子进程在父进程 lseek 之后,写入数据之前这个空档,也执行了 lseek 函数,并且把数据写入文件了。待子进程写入数据完成后,父进程才开始写入数据,这又会把子进程才写入的数据覆盖。

上面这种情况就是因为 lseek 和 写入是两个动作,不是原子操作,可能会被 linux 内核分开执行。关于原子操作,在说到多线程时再讨论。

那就没办法解决问题了吗?也不是,只需要在调用 open 函数时,传入 O_APPEND 函数即可。上一节讨论过 O_APPEND 参数,linux 内核检查到这个标志位后,写入数据前,会首先把写入偏移量设置到文件数据最后。现在我们试一下,请看:

...
int fd = open(filename, O_WRONLY|O_CREAT|O_APPEND);
...
int fd = open(filename, O_WRONLY|O_CREAT|O_APPEND);
...


现在再编译执行,发现写入的数据终于正常了。

执行程序之前,记得先把刚才的 test.bin 文件删除,否则程序会接着上一次写入的数据接着写。
阅读更多:   Linux笔记
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif