文件描述符瞭解一下

文章首發自 blog.cc1324.cchtml

前言

文件描述符在unix系統中幾乎無處不在linux

  • 網絡接口 select、poll、epoll 涉及到文件描述符
  • IO接口 read、write 也涉及到文件描述符

從形式上來看文件描述就是一個整數,那麼咱們可不能夠更進一步去了解一下呢?shell

本文打算經過一步一步實驗去了解文件描述符究竟是什麼, 並在最後經過Linux內核相關的源碼進行驗證。數組

一個獲取文件描述符的實例

咱們能夠經過 open 系統調用獲得一個指定文件的文件描述符。bash

open 函數須要傳入一個文件路徑和操做模式, 調用會返回一個整型的文件描述符, 具體方法簽名以下網絡

/** * path 表明文件路徑 * oflag 表明文件的打開模式,好比讀,寫等 */
int open(char *path, int oflag, ...) 複製代碼

咱們寫一段簡單的代碼來驗證一下函數

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
	// 以只讀模式打開 demo.txt 文件
	int fd = open("demo.txt", O_RDONLY);
	if (fd == -1) {
		perror("open demo.txt error\n");
		return EXIT_FAILURE;
	}
	// 打印獲取到的文件描述符
	printf("demo.txt fd = %d \n", fd);
	return EXIT_SUCCESS;
}

複製代碼

而後使用 GCC 編譯,執行編譯後的程序,咱們就能夠獲得 demo.txt 的文件描述符了。ui

不出意外你將獲得如下的執行結果:atom

$ echo hello>>demo.txt
$ gcc test.c -o -test
$ ./test
$ demo.txt fd = 3
複製代碼

和方法簽名一致,文件描述符是一個整型數。spa

你能夠嘗試屢次執行該程序, 你會發現打印的文件描述符始終都是 3

難道每一個文件的文件描述符都是固定的?

每一個文件的描述符是固定的嗎?

爲了驗證前面的猜測,我在程序裏面連續調用兩次 open 函數,並打印兩次返回的文件描述符, 代碼以下:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
	int fd_a = open("demo.txt", O_RDONLY);
	int fd_b = open("demo.txt", O_RDONLY);
	printf("fd_a = %d, fd_b = %d \n", fd_a, fd_b);
	return EXIT_SUCCESS;
}
複製代碼

下面是最終的執行結果:

$ gcc test.c -o test
$ ./test
$ fd_a = 3, fd_b = 4
複製代碼

儘管是同一個文件, 獲得的文件描述符卻並不同,說明每一個文件的描述符並非固定的。

但是文件描述符每次都是從 3 開始的,這是爲何呢 ?

熟悉UNIX系統的同窗應該知道,系統建立的每一個進程默認會打開3個文件:

  • 標準輸入(0)
  • 標準輸出(1)
  • 標準錯誤(2)

爲何是 3 ? 由於 0、一、2 被佔用了啊......

等等!文件描述符難道是遞增的?我也不知道啊, 要不寫個程序試試。

這裏應該還有一個疑問:爲何前一節屢次執行程序都是返回 3 ,而在代碼裏調用兩次 open 打開一樣的文件倒是 3 和 4 ?

這個問題在後面多進程時會再提到。

文件描述符是遞增的嗎?

爲了驗證文件描述符是遞增的, 我設計了這樣一個程序

  1. 調用兩次 open , 分別獲得兩個文件描述符 三、 4
  2. 調用 close 函數將文件描述符 3 關閉
  3. 再次調用 open 函數打開同一個文件

若是文件描述符的規則是遞增的,第 3 步返回的結果就應該是 5 。

Show me the code:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
  // 第一次打開
	int a = open("demo.txt", O_RDONLY);
  // 第二次打開
	int b = open("demo.txt", O_RDONLY);

	printf("a = %d, b = %d \n", a, b);
  // 關閉a文件描述符
	close(a);

  // 第三次打開
	int c = open("demo.txt", O_RDONLY);
	printf("b = %d, c = %d \n", b, c);
	return EXIT_SUCCESS;
}
複製代碼

編譯執行

$ gcc test.c -o test
$ ./test
$ a = 3, b = 4
  b = 4, c = 3
複製代碼

第三次打開的結果是 3 ,這說明文件描述符不是遞增的

並且從結果上來看,文件描述符被回收掉後是能夠再次分配的。

前面討論的上下文都是在單進程下,若是是多個進程同時打開同一個文件,文件描述符會同樣嗎?

文件描述符和多進程

fork 函數能夠建立多個進程, 該函數返回一個 int 值, 當返回值爲 0 時表明當前是子進程正在執行,非 0 就爲父進程在執行。(爲了簡化代碼,就不考慮進程建立失敗的狀況了)

程序很簡單,就是父子進程各自打開同一個文件, 並打印該文件的文件描述符:

PS: 下面的代碼並不規範,可能會產生殭屍進程和孤兒進程,但這並非本文的重點......

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {

	int npid = fork();
	
	if (npid == 0 ){
	// 子進程
		int child_fd = open("demo.txt", O_RDONLY);
		pid_t child_pid = getpid();
		printf("child_pid = %d, child_fd = %d \n", child_pid, child_fd);
	} else {
	// 父進程
		int parent_fd = open("demo.txt", O_RDONLY);
		pid_t parent_pid = getpid();
		printf("parent_pid = %d, parent_fd = %d \n", parent_pid, parent_fd);
	}
	return EXIT_SUCCESS;
}

複製代碼

編譯執行

$ gcc test_process.c -o test_process
$ ./test_process
$ child_pid = 28212, child_fd = 3
  parent_pid = 28210, child_fd = 3
複製代碼

每一個進程打開的都是同一個文件,並且返回的文件描述符也是同樣的。

前面咱們已經得知每一個文件的描述符並非固定的,這樣看來,每一個進程都單獨維護了一個文件描述符的集合啊。

還記得最開始實驗時,咱們對編譯好的程序屢次執行都是打印的 3,可是在代碼裏對同一個文件 open 兩次倒是返回的 3 和 4 嗎?

這是由於在 shell 每次執行程序,其實都是建立了一個新的進程在執行

而在代碼裏連續調用兩次,始終是在一個進程下執行的。

先總結一下

經過上面的實驗,咱們能夠得出文件描述的一些規律

  1. 文件描述符就是一個整形數字
  2. 每一個進程默認打開 0、一、2 三個文件描述符, 新的文件描述符都是從 3 開始分配
  3. 一個文件描述符被回收後能夠再次被分配 (文件描述符並非遞增的)
  4. 每一個進程單獨維護了一個文件描述符的集合

Show me the code

talk is cheap , show me the code

​ By: Linus Benedict Torvalds

下面就須要在 Linux內核 的源碼中去尋找真相了。

既然實驗代表每一個進程單獨維護了文件描述符集合, 那就從和進程相關的結構體 task_struct 入手,該結構體放在 /include/linux/sched.h 頭文件中。

我將這個結構體的代碼精簡了一下, 只保留了一些分析須要關注的屬性

struct task_struct {
    ...
  /* Filesystem information: */
	struct fs_struct *fs;

	/* Open file information: */
	struct files_struct *files;
	
	...
	/* -1 unrunnable, 0 runnable, >0 stopped: */
	volatile long			state;
	pid_t				pid;
	pid_t				tgid;
	...

};
複製代碼

注意 struct files_struct *files ,註釋說該屬性表明着打開的文件信息,那這就沒得跑了。

繼續看 files_struct 結構體,該結構體定義在 /include/linux/fdtable.h 頭文件中:

struct files_struct {
  // 打開的文件數
	atomic_t count;
	...
	// fdtable 維護着全部的文件描述符
	struct fdtable *fdt;
	struct fdtable fdtab;
    ...
  // 下一個可用文件描述符
	unsigned int next_fd;
	...
};
複製代碼

相信你也一眼就看見了 fdtable 這個結構體,見名知意,這不就是文件描述符表嗎? 那麼它是否是維護了全部的文件描述符呢?

struct fdtable {
  // 最大文件描述符
	unsigned int max_fds;
	// 全部打開的文件
	struct file **fd;      /* current fd array */
	...
};
複製代碼

fdtable 裏面有一個 file 結構體的數組,這個數組表明着該進程打開的全部文件。

先將上面的結構用一個圖畫下來:

這個源碼結構展現了每一個進程單獨維護了一個文件描述符的集合的信息。

可是文件描述符是什麼,以及它的生成規則仍是沒有找到。那隻能換個方向,從函數調用去尋找了。

openclose 都涉及到對文件描述符的操做,因爲 close 函數更加簡單,就從 close 爲入口進行分析。

下面是 close 函數的內部系統調用:

SYSCALL_DEFINE1(close, unsigned int, fd)
{
	int retval = __close_fd(current->files, fd);
	...
	return retval;
}
複製代碼

close 調用了 __close_fd 函數, 該函數定義在/fs/file.c文件中,下面是簡化後的代碼:

int __close_fd(struct files_struct *files, unsigned fd)
{
	struct file *file;
	struct fdtable *fdt;
  // 獲取fdtable
	fdt = files_fdtable(files);
	// *** 經過文件描述符獲取file結構體指針
	file = fdt->fd[fd];
	rcu_assign_pointer(fdt->fd[fd], NULL);

// 回收文件描述符
__put_unused_fd(files, fd);
	return filp_close(file, files);
}
複製代碼

這裏面又出現了咱們熟悉的結構體 files_struct,注意 file = fdt->fd[fd] 這一段代碼。

fdt 就是 fdtable結構體,它的 fd 屬性就是打開的全部文件數組,這樣一看也就恍然大悟了。

用戶傳進來的 fd 參數實際就是 fdtable 內的文件數組的索引。

因此, 文件描述符其實就是file結構體數組的索引

相信關於後面

  • 被回收後的文件描述符如何再次分配
  • 文件描述符爲何從0開始
  • 文件描述符爲何不能爲負數

這些問題你都能迎刃而解了。

參考

  1. Linux內核源碼在線查看
  2. 孤兒進程與殭屍進程總結
  3. 文件描述符
相關文章
相關標籤/搜索