Linux檔案IO open、dup、fork核心原理圖解

嚇人的猿發表於2018-02-27

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

相關文章