Linux下檔案描述符剖析
1、open一個檔案
一個Linux程序啟動後,會在核心空間建立一個PCB程序控制塊,PCB是一個程序的私有財產。
這個PCB中有一個已開啟檔案描述符表,記錄著所有該程序開啟的檔案描述符以及對應的file結構體地址。
預設情況下,啟動一個Linux程序後,會開啟三個檔案,分別是標準輸入、標準輸出、標準錯誤分別使用了0、1 、2號檔案描述符。
當該程序使用函式open開啟一個新的檔案時,一般會在核心空間申請一個file結構體,並且把3號檔案描述符對應的file指標指向file結構體。
程式碼如下:
testOpen.c
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
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指向這兩個結構體。
程式碼如下:
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
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
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
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掉所有的檔案描述符。