Linux下文件描述符剖析

Linux文件IO open、dup、fork内核原理分析

1、open一个文件

一个Linux进程启动后,会在内核空间创建一个PCB进程控制块,PCB是一个进程的私有财产。

这个PCB中有一个已打开文件描述符表,记录着所有该进程打开的文件描述符以及对应的file结构体地址。

默认情况下,启动一个Linux进程后,会打开三个文件,分别是标准输入、标准输出、标准错误分别使用了0、1 、2号文件描述符。

当该进程使用函数open打开一个新的文件时,一般会在内核空间申请一个file结构体,并且把3号文件描述符对应的file指针指向file结构体。

代码如下:

testOpen.c

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
    int fd = open("./log.txt", O_RDWR);
    printf("new fd = %d\n", fd);
}

原理图如下:

process table entry就是进程的文件描述符表,file table entry用来记录文件的读写打开模式,当前文件偏移量,以及v-node指针。

v-node table entry是虚拟文件系统对应的文件节点,i-node是磁盘文件系统对应的文件节点。通过这两个节点就能找到最终的磁盘文件。

每一个进程只有一个process table entry,一般情况下默认使用 fd 0、fd1、fd2,新打开的文件log.txt将使用

fd 3。

2、两个进程同时open一个文件

两个进程同时open一个文件,这个时候的原理图如下:


因为现在是两个进程,所以process table entry进程控制块也是两个,每个进程控制块中各自维护一个张文件描述符表,同时打开一个文件的时候,都各自申请了一个file table entry。

        由于打开的是同一一个文件,所以file table entry都指向了同一个v-node。

两个file table entry,怎么去证明呢?

test2open.c

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
    int fd = open("./log.txt", O_RDWR);
    printf("new fd = %d\n", fd);
    printf("%ld\n", lseek(fd, 0, SEEK_CUR));
    write(fd, "123", 3);
    sleep(5);
    printf("%ld\n", lseek(fd, 0, SEEK_CUR));
    close(fd);
}

file table entry中都保存了一个文件读写偏移量,如果是两个file table entry,那么两个进程读写位置是独立的,不受影响的。

上面的代码运行结果是:

#先启动进程0
$ ./a.out 
new fd = 3
0
3

#在5秒时间内,启动进程1
$ ./a.out 
new fd = 3
0
3

两个进程都分配了fd 3 给新打开个文件,并且读写位置不受其他进程的影响 。如果受影响了话,进程1的读写位置要变成3和6.

3 一个进程open两次同一个文件

一个进程open两次同一个文件,其实跟两个进程open一次的原理相同,都是调用了两次open,反正只要记住,调用一次open函数,就会创建一个file table entry。

原理图如下:

由于只有一个进程,所以只有一个process table entry,open了两次,所以是两个file table entry 分别分配了fd 3与fd 4指向这两个结构体。

代码如下:

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
    int fd0 = open("./log.txt", O_RDWR);
    int fd1 = open("./log.txt", O_RDWR);
    printf("new fd0 = %d\n", fd0);
    printf("new fd1 = %d\n", fd1);

    write(fd0, "123", 3);

    printf("fd0 lseek %ld\n", lseek(fd0, 0, SEEK_CUR));
    printf("fd1 lseek %ld\n", lseek(fd1, 0, SEEK_CUR));
    close(fd0);
    close(fd1);
}

上面代码open了两次log.txt,创建了两个file结构体,验证方法还是通过判断读写位置是否是独立的。

运行结果:

new fd0 = 3
new fd1 = 3
fd0 lseek 3
fd1 lseek 0

结果已经说明一切了,修改fd0的读写位置不会影响fd1的读写位置。

4、使用dup复制文件描述符

dup函数与open函数不同,open函数会创建一个file table,但是dup只是申请一个fd来指向一个已经存在的file table。原理图如下:

    

代码 testdup.c

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
 
int main(int argc, char *argv[])
{
    int fd, copyfd;
 
    fd = open("./log.txt", O_RDWR);
    /*复制fd*/
    copyfd = dup(fd);
 
    write(fd, "123", 3)
 
    /*打印出fd和copyfd的偏移量,经过上面的写操作,都变成3了*/
    printf("fd lseek %ld\n", lseek(fd, 0, SEEK_CUR));
    printf("copyfd lseek %ld\n", lseek(copyfd, 0, SEEK_CUR));
 
    close(fd);
    close(copyfd);
    return 0;
}

运行结果:

$ ./a.out 
fd lseek 3
copyfd lseek 3

结果证明只要操作了fd 或copyfd这两个文件描述符中一个的读写位置,就会影响到另一个文件描述符的读写位置。说明这两个文件描述符指向的是同一个file table。

需要注意的是,一旦dup了一次,就会file table引用计数加一,如果想要释放file table的内存,必须要把open以及所有dup出来的文件描述符都关闭掉。

5、fork之后open

如果在调用fork之后调用一次open函数,由于fork之后会返回两次,一次父进程返回,一次子进程返回,那么这个时候其实是相当与两个进程分别调用了一次open函数打开同一个文件,与第二节中的原理相同。


代码如下:testforkopen.c

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
    int pid = fork();
    int fd = open("./log.txt", O_RDWR);
    printf("pid %d %ld\n", pid, lseek(fd, 0, SEEK_CUR));
    write(fd, "123", 3);
    sleep(5);
    printf("pid %d %ld\n", pid, lseek(fd, 0, SEEK_CUR));
    close(fd);
}

运行结果:

$ ./a.out 
pid 6112 lseek 0  #父进程
pid 0 lseek 0     #子进程
pid 6112 lseek 3  #父进程
pid 0 lseek 3     #子进程

可以看到父子进程的读写位置都是3,并不受影响。

6 fork之前open

fork之前调用open函数,也就是只调用了一次,产生了一个fd以及file table,fork之后子进程的process table entry会从父亲进程中复制过来,文件描述表也复制过来了,那么子进程的fd指向的是同一个file table。

原理图如下:

代码如下:testopenfork.c

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
    int fd = open("./log.txt", O_RDWR);
    int pid = fork();
    printf("pid %d %ld\n", pid, lseek(fd, 0, SEEK_CUR));
    write(fd, "123", 3);
    sleep(5);
    printf("pid %d %ld\n", pid, lseek(fd, 0, SEEK_CUR));
    close(fd);
}

运行结果:

$ ./a.out 
pid 6388 lseek 0
pid 0 lseek 3
pid 6388 lseek 6
pid 0 lseek 6

父子进程都各自写入3字节,如果是两个file table,那么最终都应该打印的是3,而不是6,请与第5节进行对比。

需要注意的是:如果想要释放这个file table,也必须父子进程都close一次fd才会释放,如果不close,进程退出的时候会自动close掉所有的文件描述符。


  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值