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