1. 程式人生 > >Linux 檔案描述符(file descriptor, fd)以及檔案描述符操作dup(), dup2()

Linux 檔案描述符(file descriptor, fd)以及檔案描述符操作dup(), dup2()

1.概述

在Linux系統中,一切皆可以看做是“檔案”,這裡“檔案”包括普通檔案、目錄檔案、連結檔案和裝置檔案等。而檔案描述符(file descriptor, 簡稱fd)是Linux核心所建立的索引,其目的為了高效管理已被開啟的“檔案”。其實,檔案描述符就是一個非負整數(通常是小整數),用於指代被開啟的“檔案”,而所有對該“檔案”的I/O操作都是通過該檔案描述符來執行的。

通常,程式剛剛啟動時,系統就已經佔用了3個檔案描述符:0(標準輸入)、1(標準輸出)和2(標準錯誤)。如果程式此時開啟一個新檔案或建立一個socket連線,其對應的檔案描述符將會是3。

至於為什麼會是3?原因:POSIX標準要求每次開啟檔案(socket)時,其分配的檔案描述符必須是當前程序中最小可用

的檔案描述符。因此,在網路程式設計時,網路通訊過程中稍不注意就可能造成“串話”現象。

POSIX標準輸入輸出的檔案描述符,如下:

file descriptor  purpose POSIX STDIO
0 標準輸入 STDIN_FILENO stdin
1 標準輸出 STDOUT_FILENO stdout
2 標準錯誤 STDERR_FILENO stderr

2.檔案描述符、檔案指標、開啟檔案

在Linux系統中,每個檔案描述符對應一個開啟的檔案,同時不同的檔案描述符也可以指向同一個檔案。相同的檔案可以被不同的程序開啟,每個程序中有對應的檔案描述符(可以相同也可不相同);相同檔案也可以在同一個程序中多次開啟(對應不同的檔案描述符)。上面已簡單說明,為了高效管理開啟的檔案,Linux系統為每個程序維護了一個檔案描述表,該表的值都是從0開始的,所以在不同的進城中可以看到相同的檔案描述符。這種情況下,相同的檔案描述符有可能指向同一個檔案,也有可能指向不同的檔案,具體情況要依據具體場景分析。要想理解具體的概況,需要了解和檢視核心維護的3個數據結構:

  1. 程序級的檔案描述符表
  2. 系統級的開啟檔案描述符表
  3. 檔案系統的i-node表
程序級的檔案描述符表中的每一項記錄了該程序中單個檔案描述符的相關資訊。
  1. 控制檔案描述符的一組標誌(目前,此類標記只定義了一個,即close-on-exec)
  2. 對應開啟檔案控制代碼的應用(檔案指標)
檔案指標:C語言中使用檔案指標作為I/O控制代碼。檔案指標指向程序使用者區中的一個被稱為FILE結構的資料結構。其中,FILE結構包括一個緩衝區和一個檔案描述符,而改檔案描述符就是PCB中檔案描述符表中的一個索引。因此,從某種意義上來說,檔案指標就是檔案控制代碼的控制代碼。

Linux核心對所有開啟檔案的維護有一個系統級的描述符表(open file descriptor table),或稱之為開啟檔案表(open file table)。表中每條記錄稱為開啟檔案控制代碼(open file handler),一個開啟檔案控制代碼儲存了與一個開啟檔案相關的全部資訊,譬如:當前檔案偏移量、檔案訪問模式、該檔案i-node物件的引用、檔案型別和訪問許可權等。

下圖展示了檔案描述符、打卡檔案控制代碼(檔案指標)、開啟檔案以及i-node之間的關係,其中,三個程序擁有多個開啟的檔案描述符。


其中:

Process 1中,檔案描述符3和21都指向了同一個檔案a,這可能是通過dup()、dup2()、fcntl()或對同一個檔案多次呼叫open()函式形成的;

Process 1檔案描述符21和Process 2中檔案描述符35都指向檔案a,但是卻對應兩個不同開啟檔案控制代碼(檔案指標),二者開啟模式不同(O_RDONLY與O_WRDONLY)。這種情形可能是呼叫fork()後建立子程序,或某個程序通過UNIX套接字將一個開啟的檔案描述符傳遞給另一個程序,或兩個獨立的程序分別open()開啟同一個檔案。

Process 1和Process 3中檔案描述符3分別對應檔案a和檔案b。

3.檔案描述符限制

通常,在涉及檔案操作或網路通訊程式設計時,初學者一般可能會遇到“Too many open files”問題。其原因是檔案描述符是作業系統的一個重要資源,雖說系統記憶體有多少就可以開啟多少檔案描述符,但實際實現過程中核心一般都會做相應處理,一般開啟檔案數會是系統記憶體的10%(以KB來計算),稱之為系統級限制。檢視系統級的最大開啟檔案數可以使用sysctl -a | grep fs.file-max命令檢視。與此同時,核心為了不讓某一個程序消耗掉所有的檔案資源,也會對單個程序最大開啟檔案數做預設處理,稱之為使用者級限制,預設值一般是1024。使用者可以通過ulimit -a 和ulimit -n命令來檢視和修改使用者級限制。

使用者也可以修改下limits.conf檔案,永久更改系統檔案描述符的最大值

vi /etc/security/limits.conf

在最後新增如下兩行:

*       soft    nofile  65536
*       hard    nofile  65536

有關檔案描述符的限制和設定,具體參見參考資料[1]和[2]。

4.dup()、dup2()檔案描述符操作

dup()和dup2()都是對檔案描述符的操作,程式可以通過系統呼叫來複制檔案描述符。

函式原型:

#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
其中:

dup()用來複制oldfd所指的檔案描述符。複製成功時,返回最小的尚未被使用的檔案描述符;若出現錯誤則返回-1,且錯誤碼存於errno中。而返回的新檔案描述符和引數oldfd指向同一個檔案,共享所有的索性、讀寫指標、各項許可權或標誌位等。

dup2()將引數newfd指向引數oldfd所指向的檔案。若檔案描述符newfd已經被程式使用(對已某個開啟檔案),系統則會將其關閉釋放該檔案描述符;若newfd和oldfd相等,dup2()將返回newfd,但不關閉newfd。dup2()呼叫成功返回新的檔案描述符,出錯則返回-1。

針對dup()和dup2(),下分別給出對應程式碼展示其相應的簡單功能。

dup_test.c

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
  int fd_out = fileno(stdout);
  int fd_err = fileno(stderr);
  int fd_file = open("dup_test_file.log", O_RDWR | O_CREAT | O_APPEND | O_LARGEFILE, 0644);

  write(fd_out, "1: write message into fd_out.\n", strlen("1: write message into fd_out.\n"));
  write(fd_err, "1: write message into fd_err.\n", strlen("1: write message into fd_err.\n"));
  write(fd_file, "1: write message into fd_file.\n", strlen("1: write message into fd_file.\n"));

  printf("stdout's fd = %d.\n", fd_out);
  printf("stderr's fd = %d.\n", fd_err);
  printf("file's fd = %d.\n", fd_file);
  
  /**
   * int dup(int oldfd);
   * 用來複制oldfd所指的檔案描述符.
   * 複製成功時,返回最小的尚未被使用的檔案描述符;若出現錯誤則返回-1,且錯誤碼存於errno.
   * 返回的新的檔案描述符和引數oldfd指向同一個檔案,共享所有的鎖定、讀寫指標、各項許可權或標誌位等.
  **/

  int new_fd_err = dup(fd_err);
  if (-1 == new_fd_err)
  {
    printf("dup() failed.\n");
    return 1;
  }
  else
  {
    printf("new stderr's fd = %d.\n", new_fd_err);
    write(new_fd_err, "2: write message into new_fd_err(not close(fd_err)).\n", strlen("2: write message into new_fd_err(not close(fd_err)).\n"));
    close(fd_out);
    write(new_fd_err, "2: write message into new_fd_err(close(fd_err)).\n", strlen("2: write message into new_fd_err(close(fd_err)).\n"));
  }

  int new_fd_file = dup(fd_file);
  if (-1 == new_fd_file)
  {
    printf("dup() failed.\n");
    return 1;
  }
  else
  {
    printf("new file's fd = %d\n", new_fd_file);
    write(new_fd_file, "2: write message into new_fd_file(not close(fd_file)).\n", strlen("2: write message into new_fd_file(not close(fd_file)).\n"));
    close(fd_out);
    write(new_fd_file, "2: write message into new_fd_file(not close(fd_file)).\n", strlen("2: write message into new_fd_file(close(fd_file)).\n"));
  }

  return 0;
}

通過dup()對fd_err和fd_file進行復制,得到new_fd_err和new_fd_file,同時呼叫write()向新的檔案描述符寫入訊息時,均會對應寫到老的檔案描述符指向的檔案中,即使老的檔案描述符已經關閉。

dup2_test.c

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
  int fd_out = fileno(stdout);
  int fd_err = fileno(stderr);

  int fd_file1 = open("dup2_test_file1.log", O_RDWR | O_CREAT | O_APPEND | O_LARGEFILE, 0644);

  write(fd_out, "1: write message into fd_out.\n", strlen("1: write message into fd_out.\n"));
  write(fd_err, "1: write message into fd_err.\n", strlen("1: write message into fd_err.\n"));
  write(fd_file1, "1: write message into file1.\n", strlen("1: write message into file1.\n"));
  
  printf("stdout's fd = %d.\n", fd_out);
  printf("stderr's fd = %d.\n", fd_err);
  printf("file1's fd = %d.\n", fd_file1);
  
  /**
   * int dup(int oldfd);
   * 用來複制oldfd所指的檔案描述符.
   * 複製成功時,返回最小的尚未被使用的檔案描述符;若出現錯誤則返回-1,且錯誤碼存於errno.
   * 返回的新的檔案描述符和引數oldfd指向同一個檔案,共享所有的鎖定、讀寫指標、各項許可權或標誌位等.
  **/
  
  /**
   * newfd與oldfd不相等,且newfd對應的檔案只有一個檔案描述符(引用計數為1)
   * 呼叫dup2()時會將newfd對應的檔案關閉,同時newfd指向oldfd對應的檔案(引用計數+1)
   * 此時,向newfd寫入訊息時,就會寫到oldfd對應的檔案
  **/
  int fd_ret = dup2(fd_file1, fd_err);
  printf("dup2() return value: fd_ret = %d.\n", fd_ret);
  // 下面這兩種輸出都會寫到dup2_test_file1.log中
  write(fd_err, "2: write message into fd_err.\n", strlen("2: write message into fd_err.\n"));
  fprintf(stderr, "2: printf message into stderr.\n", strlen("2: printf message into stderr.\n"));

  /**
   * newfd與oldfd不相等,且newfd對應的檔案不只一個檔案描述符(引用計數大於1)
   * 呼叫dup2()時,newfd指向oldfd對應的檔案(引用計數+1),而newfd對應的原檔案並不關閉,僅將引用計數-1
   * 此時,向newfd寫入訊息時,就會寫到oldfd對應的檔案
  **/
  int fd_file2 = open("dup2_test_file2.log", O_RDWR | O_CREAT | O_APPEND | O_LARGEFILE, 0644);
  write(fd_file2, "3: write message into file2.\n", strlen("3: write message into file2.\n"));
  printf("file2's fd = %d.\n", fd_file2);

  int fd_file2_dup = dup(fd_file2);
  printf("file2's dup_fd = %d.\n", fd_file2_dup);

  fd_ret = dup2(fd_file1, fd_file2);
  printf("dup2() return value: fd_ret = %d.\n", fd_ret);

  // 寫入file2_fd的訊息將寫入檔案dup2_test_file1.log中
  write(fd_file2, "3: wriet message into file2_fd.\n", strlen("3: wriet message into file2_fd.\n"));
  // 寫入file2_fd_dup的訊息仍然寫入dup2_test_file2.log中
  write(fd_file2_dup, "3: wriet message into file2_fd_dup.\n", strlen("3: wriet message into file2_fd_dup.\n"));

  /**
   * newfd與oldfd相等, dup2()不做任何事情,並返回newfd
  **/
  int fd = fd_file1;
  fd_ret = dup2(fd_file1, fd);
  printf("dup2() return value: fd_ret = %d.\n", fd_ret);
 
  //close(fd_file1);
  //把上面的操作在執行一遍,也會是同樣的結果
  //同樣也可以對stdout類似的操作,進而可以控制所有的標準輸出都寫到檔案中

  return 1;
}
上述給出的例子裡並未涉及網路通訊(socket)所佔用檔案描述符時的操作使用,以及fork()子程序時,子程序和父程序間檔案描述符的共享等。

5.總結

在Linux系統中,檔案描述符是一個很重要的資源,尤其在設計檔案操作或網路通訊程式設計。而且,對於某些系統的調優,譬如mysql、java、squid等單程序處理大量併發請求的應用來說,如何設定和調優檔案描述符限制,對系統的吞吐量和效能有至關重要的作用。 同時,自己設計併發程式時,也需要考慮使用者級的檔案描述符限制,進而可以控制自己的程式的併發性。

6.參考資料