经过上一节的介绍,我们知道在 linux 中进行 C语言开发时,多进程同时写数据到同一个文件,如果不小心处理,写入的数据可能会混乱。这主要是因为每个进程打开文件时,都有独立的文件表记录当前文件偏移量的原因。
那么,在一个进程中多次打开同一个文件同时写入数据,也可能出现数据混乱吗?答案是肯定的,还记得第10节的这张图吗?
即使在一个进程中多次调用 open 函数打开同一个文件,系统也会为每次调用分配一个文件表记录当前文件偏移量。这时,情况就和上一节介绍的多进程同时写数据到同一个文件的情况类似了,还是非常可能出现数据混乱的。
请看下面的代码:
#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";
int fd, fd2;
fd = open(filename, O_WRONLY|O_CREAT);
fd2 = open(filename, O_WRONLY|O_CREAT);
char buf1[20] ;
char buf2[30] ;
memset(buf1, 1, sizeof(buf1));
memset(buf2, 2, sizeof(buf2));
write(fd, buf1, sizeof(buf1));
write(fd2, buf2, sizeof(buf2));
close(fd);
close(fd2);
return 0;
}
代码的逻辑很简单,就是在 main 函数里打开 test.bin 文件两次,并分别往里面写入 20 个字节的 1,和 30 个字节的 2。编译执行之,得到如下结果:
容易看出,fd 写入的数据被 fd2 写入的数据覆盖了,这与我们前面的分析是一致的。
实际开发中,还是非常有可能需要打开同一个文件多次的,比如多线程项目中,每个线程都需要操作 test.bin 文件,这时使用多个 fd 更加方便。
C语言的 dup 和 dup2 函数
好在,C语言提供的 dup 和 dup2 函数,就非常适合解决多个 fd 写数据到同一个文件的需求。dup 和 dup2 都可用来复制一个现存的文件描述符,使两个文件描述符指向同一个文件表。
在 linux 中输入 man 命令即可查询 dup 函数的描述:
现在使用 dup 函数修改上面的代码,复制一份 fd 传递给 fd2:
...
fd = open(filename, O_WRONLY|O_CREAT);
fd2 = dup(fd);
char buf1[20] ;
...
修改以后,编译执行,发现两次写入的数据都保留了。
这是因为 fd2 是由 dup 函数复制 fd 而来,它俩共享同一个文件表,也即共享同一个当前文件偏移量。前面两节介绍过,fd 调用 write 写入数据后,会将当前文件偏移量更新,这时 fd2 也就接着 fd 写入的数据尾部写入数据了。
再看看上面的代码,虽然 fd2 是复制 fd 而来的,但是仍需调用 close 函数关闭之。所以如果只执行第 26 行代码,linux 内核只会将 test.bin 文件的打开计数减一,并没有真正关闭文件。因此,第 27 行的 close(fd2); 是必需的。
这也能看出多线程操作同一个文件,使用 dup 的好处了。某个线程使用完文件后,直接 close 即可(这能使代码有更好的逻辑完整性,可阅读性更强),而无需担心其他线程。
dup 函数复制 fd 时,总是返回尽可能小的未使用 fd 号。dup2 函数与 dup 函数的功能时类似的,唯一的区别是 dup2 函数有两个参数,执行成功后,会返回第二个参数传递的 fd 值。
多进程打开同一个文件时,能利用 dup 函数避免数据紊乱吗?
既然 dup 函数能够复制 fd,那么,上一节出现的问题也能用 dup 函数的特性解决吗?这个问题就留给读者思考了。经过这两节的讨论,相信读者也有能力编写相应的 C语言代码验证自己的想法。
这里有一点小提示:进程间通常并不共享内存,而进程打开文件时,文件表信息保留在自己的内存空间里的。