1. 一个进程open两次同一文件
1.1 笔试题目
//abc.txt
abcdefg
//main.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(){
int fd1, fd2;
char c;
fd1 = open("abc.txt", O_RDONLY,0);
fd2 = open("abc.txt", O_RDONLY,0);
read(fd1, &c,1);
printf("c=%c\n",c);
read(fd2, &c, 1);
printf("c=%c\n",c);
return 0;
}
在秋招笔试中遇到这样一个题目,问输出是多少,答案如下所示。
open函数、read函数都是C语言用于打开文件、读取文件的库函数。这道题目考察的是,fd1和fd2都是指向同一个文件的文件描述符,它们之间的文件偏移是否是共用,还是各自存在一个文件偏移。
1.2 open函数和read函数
通过命令man 2 open
可以看到函数功能描述,包含函数参数、参数说明等。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
使用open函数需要包含以上的头文件,其中函数的声明在fcntl.h文件,前两个文件包含的是flags的宏定义。
int open(const char *pathname, int flags);
pathname是文件名的字符串数组,flags必须包含以下的访问模式标志位之一:O_RDONLY, O_WRONLY, O_RDWR。这三个标志位的宏定义就在前两个头文件里面,分别代表只读、只写、读写。这三个标志位是互斥的,只能三选一。
open函数会返回一个文件描述符,一个较小的、非负数整数,用于定位到该文件。它总是返回系统中未被使用的最小数值的文件描述符。如果打开文件存在错误,就会返回-1。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
函数说明:
read函数试图从文件描述符fd所指定的文件读取内容到buf所指定的内存当中。在支持读取的文件中,读操作从当前文件偏移开始,随着读取字节的增加,文件偏移也会增加。如果文件偏移达到了文件末尾,无字节可读,read函数会返回0 。
参数说明:
fd:通过open函数返回的文件描述符;
buf:读出数据存放的地方
count:指定数组的大小
返回值:
如果读取成功,会返回读到内容的字节大小。如果出现错误,函数返回-1,并且会将errno这个全局变量设置成合适的值。
1.3 文件描述符
所有执行I/O操作的系统调用都以文件描述符,一个非负整数(通常是小整数),来指代打开的文件。每个进程都具备自己的文件描述符表。通过输出可以看到两次用open打开返回的文件描述符fd是不一样的。当一个程序以标准方式运行时,文件描述符0、1、2已经分别用于标准输入、标准输出和标准错误。所以才会从3开始技术。
文件描述与文件是如何对应起来的?要理解具体情况如何,需要查看由内核维护的3个数据结构。
- 进程级的文件描述符表
- 系统级的打开文件表
- 文件系统的i-node表
针对每个进程,内核都会维护一个文件描述符表(open file descriptor)。该表的每一条目都记录了单个文件描述符的相关信息。而内核对所有打开的文件维护有一个系统级的打开文件表(open file table),并将表中各条目称为打开文件句柄(open file handel)。一个打开文件句柄存储了与一个打开文件相关的全部信息,如下所示
- 当前文件偏移量(调用read函数和write函数时更新,或使用lseek函数直接修改)。
- 文件访问模式
- 打开文件时所使用的的状态标志
- 对该文件i-node对象的引用
上图源自《Liunx/UNIX系统编程手册》,在进程A中,文件描述符1和20都指向同一个打开的文件句柄。进程A的文件描述符2和进程B的文件描述符2都指向同一个打开的文件句柄。这种情形可能是调用fork()后出现。
此外,进程A的描述符0和进程B的描述符3分别指向不同的打开文件句柄,但这些句柄均指向i-node表中的相同条目(1976),即同一个文件。发生这种情况是因为,每个进程各自对同一文件发起了open调用或者同一进程两次打开同一文件。
上述证明,两个不同的文件描述符,若指向同一打开文件句柄,将共享同一文件偏移量。因此,如果通过其中一个文件描述符来修改文件偏移量(open函数、write函数、lseek函数),那么另外一个文件描述符也会观察到这一变化。无论这两个文件描述属于不同进程,还是同一个进程。
1.4 题目分析
fd1 = open("abc.txt", O_RDONLY,0);
fd2 = open("abc.txt", O_RDONLY,0);
回到最初的题目,在同一个进程中调用了两次open,其实和两个进程各自open一次是一样,只要调用open函数,就会创建一个打开文件表条目,即打开文件句柄。
如上图所示,fd1和fd2分别对应着两个打开文件句柄,都有各自的文件偏移量,最终都对应“abc.txt”这个文件。这也是为什么对他们read函数,都是读出了第一个字符的原因,因为他们不共用文件句柄。
2. 父子进程open同一个文件
2.1 笔试题目
//abc.txt
abcdefg
//main.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
int fd;
char c;
fd = open("abc.txt", O_RDONLY,0);
if (fork()==0){
read(fd,&c,1);
exit(0);
}
wait(NULL);
read(fd, &c, 1);
printf("c=%c\n",c);
return 0;
}
~
该笔试题目问中断输出什么?
2.2 fork函数、exit函数、wait函数
#include <unistd.h>
pid_t fork(void);
系统调用fork允许进程(父进程)创建新进程(子进程),子进程几乎是父进程的翻版,子进程获得父进程的栈、数据段、堆和执行文本段的拷贝。
库函数exit(status)终止一进程,把进程占用的所有资源(内存、文件描述符等)归还内核。父进程可使用wait()来获取该状态。
系统调用wait(&status)的目的有二:其一,如果子进程尚未调用exit终止,那么wait会挂起父进程直至子进程终止;其二,子进程的终止状态通过wait()的status参数返回。
理解fork()的诀窍是,要意识到,完成对其调用后将存在两个进程,且每个进程都会从fork()的返回处继续执行。
这两个进程将执行相同的程序文本段,开始时子进程的栈、数据以及栈段是对父进程对应部分的完全复制。执行fork()后,每个进程均可修改各自的栈数据、堆中的变量,两个进程互不影响。
在父进程中,fork()将返回新创建子进程的进程ID,而fork()在子进程中则返回0.
2.3 父、子进程间的文件共享
执行fork()时,子进程会获得父进程所有文件描述符的副本。这些副本的创建方式类似于dup(),这意味着父、子进程中对应的文件描述符指向打开文件表中相同条目,具备相同的打开文件句柄。打开文件句柄中包含文件偏移量。如果子进程更新了文件偏移量,那么这种改变也会反应到父进程当中去。
2.4 题目解析
这题目其实就是考察子进程使用read函数会不会影响到父进程的文件偏移量。这里为什么会存在一个wait函数。
这是因为调用fork函数之后,父进程和子进程到底哪个抢到了CPU执行权是不确定的。如果没有wait函数,父进程抢到了执行权,直接执行printf输出,那就压根不用考虑子进程的影响了。
当执行fork函数之后,子进程会满足if判断条件,进入函数体执行read函数,将文件偏移量变为1,然后执行exit(0)结束进程。父进程和子进程共享打开文件手柄,虽然父进程第一次使用read函数,但此时文件偏移量是1.
父进程执行到wait函数一直等待子进程的结束,当子进程结束后,执行read函数,此时读到的是"abc.txt"的第二个字符,所以最终输出的字符是b。
可以试试删除了wait函数,看谁抢到CPU。看来还是父进程比较厉害,抢到了CPU,首先执行完了printf。
通过lseek
函数,可以返回当前文件偏移位置。
《Liunx/UNIX编程手册》