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掉所有的文件描述符。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章