C/C++:文件描述符与进程之间的关系
在Unix中,文件在进程中通常抽象化为文件描述符(File Descriptor)。
文件描述符是一个非负整数,可以理解为一个句柄。
我们可以通过open一个磁盘文件,获取一个文件描述符,后续我们对这个文件的所有操作,都要通过这个描述符进行操作。
我们思考:
1.文件描述符如果脱离了进程,是否有意义?
2.文件描述符在内核中是如何表示的?
3.同一进程两次打开同一磁盘文件,获取的是同一个句柄—文件描述符吗?
在操作系统的内核中有专门的数据结构来描述一个进程相关的信息。
即,任何进程的任何信息,例如申请了多少内存、起始地址等都被内核所记录。
上面说的“任何信息”,也包含了一张关于文件描述符的“表”(可以简单理解为一张表)。
文件描述符不可脱离进程而存在,文件描述符的作用范围是进程,脱离某进程讨论FD是无意义的。
如上图所示,每个进程都有一张表,这个表中记录了每个文件描述符的标识信息以及文件指针。
文件描述符不能脱离进程而存在,作用域是进程,但“文件表”却不是,其是脱离进程存在,被内核所记录,作用域是内核全局级别。
当你在执行open系统调用时,内核将新创建一个文件表对象,并在当前进程中创建一个新的FD,并将FD与文件表对象进行关联。
我们可以看到,文件描述符在进程中,不仅仅有文件指针,指向某文件表对象,还有FD标识信息。
每一个FD都有自己的一套文件描述符标识信息。例如,FD_CLOEXEC。
文件表包含哪些我们关心的信息呢?
文件偏移量表示,如果对这个文件进行写、读操作,将会从距离文件起始位置的多少字节后进行操作。
V节点指针指明了文件在物理磁盘上的位置信息。
请看两个示例。
Case 1:
在同一进程中两次调用open,打开同一磁盘文件,并写入不同数据。
Code:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
char buf1[] = "abcdefg\n";
char buf2[] = "ABCDEFG\n";
int main()
{
int fd = -1;
if ( (fd = open("./newfile", O_WRONLY | O_CREAT)) < 0 )
{
printf("ERROR: open(%d)=%s\n", errno, strerror(errno));
return 1;
}
int fd2 = -1;
if ( (fd2 = open("./newfile", O_WRONLY | O_CREAT)) < 0 )
{
printf("ERROR: open(%d)=%s\n", errno, strerror(errno));
return 1;
}
if ( write(fd, buf1, strlen(buf1)) != strlen(buf1) )
{
printf("ERROR: write(%d)=%s\n", errno, strerror(errno));
return 1;
}
if ( write(fd2, buf2, strlen(buf2)) != strlen(buf2) )
{
printf("ERROR: write(%d)=%s\n", errno, strerror(errno));
return 1;
}
close(fd);
close(fd2);
return 0;
}
编译 & 运行:
[jiang@localhost 01]$ ./main
[jiang@localhost 01]$ ll
total 16
-rwxrwxr-x. 1 jiang jiang 7716 May 12 09:49 main
-rw-rw-r--. 1 jiang jiang 832 May 12 08:55 main.c
-rwxrwx--T. 1 jiang jiang 8 May 12 09:49 newfile
查看newfile内容:
[jiang@localhost 01]$ cat newfile
ABCDEFG
在进程执行中发生如下过程:
1)
在执行第一次open之后:
文件描述符3指向在内核中新创建的文件表1对象,此时偏移量为0.
2)
在执行第二次open之后:
内核创建了新的文件表2对象,并将进程中的文件描述符4与之关联。此时,文件表2对象的偏移量为0.
我们应当注意,文件表1和文件表2都是指向同一个V节点表,因为物理文件只有一份。
3)
在执行第一次write之后:
文件表1的偏移量更新为8.
也就是说,下次在write并且通过FD=3时,将会从文件起始位置偏移8字节处继续写入文件。
此时,物理磁盘上的文件内容为“abcdefg\n”.
但是,由于文件表1和文件表2并不是同一对象,所以文件表2对象的文件偏移量还是0.
4)
在执行第二次write之后:
文件表2的偏移量更新为8.
此时,物理磁盘上的文件内容为“ABCDEFG\n”.
这是因为,在write时,传入的FD=4,write系统调用一看文件表2的偏移量为0,直接从0起始位置开始写入数据,将上次写入的,旧有的数据“覆盖”咯!
最后结果自然是被覆盖之后的文本信息啦!
Case 2:
在同一进程中先open一个文件,然后通过dup系统调用“复制一份”,并通过write对两个不同的FD写入不同数据。
Code:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
char buf1[] = "abcdefg\n";
char buf2[] = "ABCDEFG\n";
int main()
{
int fd = open("./newfile", O_WRONLY | O_CREAT);
if (fd < 0)
{
printf("ERROR: open(%d)=%s\n", errno, strerror(errno));
return 1;
}
int fd2 = dup(fd);
if (fd2 < 0)
{
printf("ERROR: dup(%d)=%s\n", errno, strerror(errno));
return 1;
}
int wbytes = write(fd, buf1, strlen(buf1));
if (wbytes != strlen(buf1))
{
printf("ERROR: write(%d)=%s\n", errno, strerror(errno));
return 1;
}
int wbytes2 = write(fd2, buf2, strlen(buf2));
if (wbytes2 != strlen(buf2))
{
printf("ERROR: write(%d)=%s\n", errno, strerror(errno));
return 1;
}
close(fd);
close(fd2);
return 0;
}
编译 & 运行:
[jiang@localhost 02]$ gcc -o main main.c
[jiang@localhost 02]$ ./main
[jiang@localhost 02]$ ll
total 16
-rwxrwxr-x. 1 jiang jiang 7853 May 12 10:11 main
-rw-rw-r--. 1 jiang jiang 825 May 12 09:04 main.c
-rwxr-x--T. 1 jiang jiang 16 May 12 10:11 newfile
查看newfile内容:
[jiang@localhost 02]$ cat newfile
abcdefg
ABCDEFG
在进程执行中发生如下过程:
1)在执行第一次open之后:
文件描述符3指向在内核中新创建的文件表1对象,此时偏移量为0.
2)在执行dup复制操作之后:
dup仅仅是“duplicate a file descriptor”,内核不会创建新的文件表对象出来,而是将新的FD和旧有的文件表对象关联。
此时,在同一进程中,有两个(多个)FD指向同一文件表对象。
3)在执行第一个write之后:
文件表1的偏移量更新为8.
由于FD=3以及FD=4关联的是同一个文件表对象,所以,对FD=3的write的偏移量更新,对于FD=4来说是“可见的”。
此时物理文件内容为:”abcdefg\n”.
4)在执行第二个write之后:
在write FD=4操作执行时,内核发现文件表偏移量为8,所以不会从0位置开始写,而是先定位到8字节偏移处,然后写数据。
此时的文件表1的偏移量更新为16,这一更新同样对FD=3是可见的。
Case 2中第二次write不会将第一次write的内容覆盖,因为他们的偏移量对于不同FD来说是共享可见的。
总结思考:
1.文件描述符脱离进程是无意义的。文件描述符一定是在某个进程空间内才具有特定意义,指代某个物理文件。
2.文件描述符在内核中,或者说在内核维护的某进程数据结构中,可以理解为一条表记录。这条记录包含了这个文件描述符的标志信息以及文件指针信息(等)。
3.同一进程打开两次执行open打开同一文件,拿到的当然是不同的FD啦!而且都是不同的文件表对象。如果是通过dup的方式,拿到的也是不同的FD,但是指向的是同一文件表对象。
另:
当进程执行fork时,子进程会复制父进程的所有信息,并共享一些数据。
例如子进程会复制父进程的所有FD的表记录,包括FD标志信息、文件指针。这也就意味着,由父进程通过open创建出来的文件表,对于子进程来说是可见的。子进程、父进程共享同一文件表对象!