文件句柄?文件描述符?傻傻分不清楚

概述

在實際工作中會經常遇到一些bug,有些就需要用到文件句柄,文件描述符等概念,比如報錯: too many open files, 如果你對相關知識一無所知,那麼debug起來將會異常痛苦。在linux操作系統中,文件句柄(包括Socket句柄)、打開文件、文件指針、文件描述符的概念比較繞,而且windows的文件句柄又與此有何關聯和區別?這一系列的問題是我們不得不面對的。

筆者通過翻閱相關資料,並採用了一些demo來驗證相關觀點。如果文中有理解偏差,歡迎指正。

這裏先籠統的將一下筆者對上面的問題的一些理解:

句柄,熟悉Windows編程的人知道,句柄是Windows用來標識被應用程序所建立或使用的對象的唯一整數,windows使用各種各樣的句柄標識諸如應用程序實例、窗口、控制、位圖等。Windows的句柄有點像C語言中的文件句柄。更通俗的理解,句柄是一種指向指針的指針。

在linux系統中文件句柄(file handles)和文件描述符(file descriptor)是一個一一對應的關係(如果錯誤,歡迎指正),按照C語言的理解文件句柄是FILE*(fopen()返回),而文件描述符是fd(int型,open()函數返回),FILE這個結構體中有一個字段是_fileno,其就是指fd(文章末尾通過程序驗證),且FILE*和fd可以通過C語言函數進行互相轉換,故此筆者認爲linux的文件句柄和文件描述符應該是一個一一對應的關係。文件指針即指FILE*,即指文件句柄。打開文件(open files)包括文件句柄但不僅限於文件句柄,由於linux所有的事物都以文件的形式存在,要使用諸如共享內存、信號量、消息隊列、內存映射等都會打開文件,但這些是不會佔用文件句柄。

ulimit

查看進程允許打開的最大文件句柄數:ulimit -n。設置進程能打開的最大文件句柄數:ulimit -n xxx。

ulimit在系統允許的情況下,提供對特定shell可利用的資源的控制(Provides control over the resources avaliable to the shell and to processes started by it, on systems that allow such control)。-H和-S選項設定指定資源的硬限制和軟限制。硬限制設定之後不能再添加,而軟限制則可以增加到硬限制規定的值。如果-H和-S選項都沒有指定,則軟限制和硬限制同時設定。限制值可以是指定資源的數值或者hard, soft, unlimited這些特殊值,其中hard代表當前硬限制, soft代表當前軟件限制, unlimited代表不限制. 如果不指定限制值, 則打印指定資源的軟限制值, 除非指定了-H選項.如果指定了不只一種資源, 則限制名和單位都會在限制值前顯示出來.

[root@hidden ~]# ulimit -Sn
1024
[root@hidden ~]# ulimit -Hn
4096

需要注意的是ulimit提供的是對特定shell可利用的資源的控制,而shell是與具體用戶相關的。因此ulimit提供的是對單個用戶的限制。包括以下項:

[root@hidden ~]# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 62799
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 10240
cpu time               (seconds, -t) unlimited
max user processes              (-u) 65536
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

其中就有個“open files”的限制,默認是1024,也就是這個用戶最大可以打開1024個文件。如果使用ulimit -n修改最大文件打開數,那麼只對當前shell用戶有用,同時也只對當前shell和這個shell fork出來的子shell生效,重啓之後會重新恢復爲默認值。

limits.conf

limits.conf這個文件是在/etc/security/目錄下,因此這個文件是出於安全考慮的。limits.conf文件是用於提供對系統中的用戶所使用的資源進行控制和限制,對所有用戶的資源設定限制是非常重要的,這可以防止用戶發起針對處理器和內存數量等的拒絕服務攻擊。這些限制必須在用戶登錄時限制。

[root@hidden ~]#  cat /etc/security/limits.conf
(省略若干....)
# End of file
apps soft nofile 65535
apps hard nofile 65535
apps soft nproc 10240
apps hard nproc 10240

其中含義如下:

  • 第一列表示域(domain),可以使用用戶名(root等),組名(以@開頭),通配置*和%,%可以用於%group參數。

  • 第二列表示類型(type),值可以是soft或者hard

  • 第三列表示項目(item),值可以是core, data, fsize, memlock, nofile, rss, stack, cpu, nproc, as, maxlogins, maxsyslogins, priority, locks, msgqueue, nie, rtprio。其中nofile(Number of Open File)就是文件打開數。

  • 第四列表示值.

關於第三列的詳細解釋如下:

#<item> can be one of the following:
#        - core - limits the core file size (KB)
#        - data - max data size (KB)
#        - fsize - maximum filesize (KB)
#        - memlock - max locked-in-memory address space (KB)
#        - nofile - max number of open file descriptors
#        - rss - max resident set size (KB)
#        - stack - max stack size (KB)
#        - cpu - max CPU time (MIN)
#        - nproc - max number of processes
#        - as - address space limit (KB)
#        - maxlogins - max number of logins for this user
#        - maxsyslogins - max number of logins on the system
#        - priority - the priority to run user process with
#        - locks - max number of file locks the user can hold
#        - sigpending - max number of pending signals
#        - msgqueue - max memory used by POSIX message queues (bytes)
#        - nice - max nice priority allowed to raise to values: [-20, 19]
#        - rtprio - max realtime priority

limits.conf與ulimit的區別在於前者是針對所有用戶的,而且在任何shell都是生效的,即與shell無關,而後者只是針對特定用戶的當前shell的設定。在修改最大文件打開數時,最好使用limits.conf文件來修改,通過這個文件,可以定義用戶,資源類型,軟硬限制等。也可修改/etc/profile文件加上ulimit的設置語句來是的全局生效。
當達到上限時,會報錯:too many open files或者遇上Socket/File: Cannot open so many files等。

file-max & file-nr

[root@hidden ~]# cat /proc/sys/fs/file-max 
798282
[root@hidden fd]# sysctl -a | grep fs.file-max
fs.file-max = 798282

該文件指定了可以分配的文件句柄的最大數目(系統全局的可用句柄數目. The value in file-max denotes the maximum number of file handles that the Linux kernel will allocate)。如果用戶得到的錯誤消息諸如“由於打開文件數已經達到了最大值”之類,那麼說明他們不能打開更多文件,則可能需要增加該值。可將這個值設置成任意多個文件,並且能通過將一個新數字值寫入該文件來更改該值。這個參數的默認值和內存大小有關係,可以使用公式:file-max ≈ 內存大小/ 10k.

[root@hidden ~]# cat /proc/sys/fs/file-nr
1440        0   798282

關於file-nr參數的解釋如下:
Historically, the three values in file-nr denoted the number of allocated file handles, the number of allocated but unused file handles, and the maximum number of file handles. Linux 2.6 always reports 0 as the number of free file handles – this is not an error, it just means that the number of allocated file handles exactly matches the number of used file handles.

這三個值分別指:系統已經分配出去的句柄數、已經分配但是還沒有使用的句柄數以及系統最大的句柄數(和file-max一樣)。

[root@hidden fd]# lsof | wc -l
2538

lsof是列出系統所佔用的資源(list open files),但是這些資源不一定會佔用句柄。比如共享內存、信號量、消息隊列、內存映射等,雖然佔用了這些資源,但不佔用句柄。
如果出了某些故障,使用lsof | wc -l的結果,這個時候可以通過file-nr粗略的估算一下。

查看硬盤信息:df -m
查看內存信息:free -m
查看CPU信息:cat /proc/cpuinfo
查看內核所能打開的線程數:cat /proc/sys/kernel/threads-max

爲什麼有限制?

爲什麼Linux內核對文件句柄數、線程和進程的最大打開數進行了限制?以及如果我們把它調的太大,會產生什麼樣的後果?

原因1 - 資源問題:the operating system needs memory to manage each open file, and memory is a limited resource - especially on embedded systems.
原因2 - 安全問題:if there were no limits, a userland software would be able to create files endlessly until the server goes down.

What’s more? If the file descriptors are tcp sockets, etc, then you risk using up a large amount for the socket buffers and other kernel objects, this memory is not going to be swappable.

最主要的是資源問題,爲防止某一單一進程打開過多文件描述符而耗盡系統資源,對進程打開文件數做了限制。

lsof

lsof(list open files)是一個列出當前系統打開文件的工具。在linux環境下,任何事物都以文件的形式存在,通過文件不僅僅可以訪問常規數據,還可以訪問網絡連接和硬件。所以如TCP和UDP等,系統在後臺都爲該應用程序分配了一個文件描述符,無論這個文件的本質如何,該文件描述符爲應用程序與基礎操作系統之間的交互提供了通用接口。因爲應用程序打開文件的描述符列表提供了大量關於這個應用程序本身的信息,因此通過lsof工具能夠查看這個列表對系統檢測以及拍錯將是很有幫助的。

在終端下輸入lsof即可顯示系統打開的文件,因爲lsof需要訪問核心內存和各種文件,所以必須以root身份運行它才能夠充分地發揮其功能。

[root@hidden linuxC]# lsof -p 14895
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF    NODE NAME
java    14895 root  cwd    DIR              252,1     4096 1310824 /root/util/kafka_2.10-0.8.2.1
java    14895 root  rtd    DIR              252,1     4096       2 /
java    14895 root  txt    REG              252,1     7734 1583642 /root/util/jdk1.8.0_112/bin/java
java    14895 root  mem    REG              252,1 10485760 1325066 /tmp/kafka-logs/default_channel_kafka_zzh_demo-4/00000000000003626728.index
...(省略若干)
java    14895 root   85u   REG              252,1        0 1311594 /tmp/kafka-logs/default_channel_kafka_zzh_demo-4/00000000000003626728.log
java    14895 root   87u   REG              252,1        0 1325038 /tmp/kafka-logs/default_channel_kafka_zzh_demo-3/00000000000003915669.log
java    14895 root   88u  IPv6           40855648      0t0     TCP zhuzhonghua2-fqawb:XmlIpcRegSvc->xx.xx.139.85:64708 (ESTABLISHED)
java    14895 root   89u   REG              252,1        0 1325037 /tmp/kafka-logs/default_channel_kafka_zzh_demo-2/00000000000005892533.log
java    14895 root   93u   REG              252,1        0 1325040 /tmp/kafka-logs/default_channel_kafka_zzh_demo-1/00000000000005494790.log
java    14895 root   94u   REG              252,1        0 1325043 /tmp/kafka-logs/default_channel_kafka_zzh_demo-0/00000000000003858999.log
[root@hidden linuxC]# ls /proc/14895/fd | wc -l
89
[root@hidden linuxC]# ls /proc/14895/fd 
0  10  12  14  16  18  2   21  23  25  27  29  30  32  34  36  38  4   41  43  45  47  49  50  52  54  56  58  6   61  63  65  67  69  70  72  75  77  79  80  82  85  88  9   94
1  11  13  15  17  19  20  22  24  26  28  3   31  33  35  37  39  40  42  44  46  48  5   51  53  55  57  59  60  62  64  66  68  7   71  74  76  78  8   81  83  87  89  93

lsof輸出各列信息的意義如下:

COMMAND:進程的名稱
PID: 進程標識符
USER:進程所有者
FD:文件描述符,應用程序通過文件描述符識別該文件。如cwd, rtd, txt, mem, DEL, 0u, 3w, 4r等
TYPE:文件類型,如DIR, REG, CHR, Ipv6, unix, FIFO等
DEVICE:指定磁盤的名稱
SIZE/OFF:文件的大小
NODE:索引節點
NAME:打開文件的確切名稱

FD列中的文件描述符cwd表示應用程序的當前工作目錄,這是該應用程序啓動的目錄,除非它本身對這個目錄進行更改;txt類型的文件是程序代碼,如應用程序二進制文件本身或共享庫,如上列表中顯示的、sbin/init程序;數值表示應用程序的文件描述符,這是打開該文件時返回的一個整數,如“lsof -p 14895”命令解析出來的最後一行的文件描述符爲94,u表示該文件被打開處於讀寫模式,而不是隻讀r或只寫w模式,同時還有大寫的W表示該應用程序具有對整個文件的寫鎖。該文件描述符用於確保每次只能打開一個應用程序實例。初始打開每個應用程序時,都有三個文件描述符:0、1、2,分別表示標準輸入、標準輸出、錯誤流。所以大多數應用程序所打開的文件的FD都是從3開始的。

TYPE列比較直觀。文件和目錄分別爲REG和DIR。而CHR和BLK分別表示字符和塊設備。或者unix, FIFO, Ipv6分表表示UNIX域套接字,FIFO隊列和IP套接字。

查看當前進程打開了多少文件:lsof -n|awk ‘{print $2}’|sort|uniq -c|sort -nr|more | grep [PID]

[root@hidden fd]# lsof -n|awk '{print $2}'|sort|uniq -c|sort -nr|more | grep 14895
    173 14895

第一列是句柄數,第二列是進程號PID.

[root@hidden proc]# lsof -p 14895 | wc -l
174

這裏多了一個是由於:

COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF    NODE NAME
java    14895 root  cwd    DIR              252,1     4096 1310824 /root/util/kafka_2.10-0.8.2.1
java    14895 root  rtd    DIR              252,1     4096       2 /
java    14895 root  txt    REG              252,1     7734 1583642 /root/util/jdk1.8.0_112/bin/java
java    14895 root  mem    REG              252,1 10485760 1325066 /tmp/kafka-logs/default_channel_kafka_zzh_demo-4/00000000000003626728.index
java    14895 root  mem    REG              252,1 10485760 1325044 /tmp/kafka-logs/default_channel_kafka_zzh_demo-0/00000000000003858999.index
java    14895 root  mem    REG              252,1 10485760 1325042 /tmp/kafka-logs/default_channel_kafka_zzh_demo-2/00000000000005892533.index
java    14895 root  mem    REG              252,1 10485760 1325041 /tmp/kafka-logs/default_channel_kafka_zzh_demo-1/00000000000005494790.index
....(省略若干)
java    14895 root   85u   REG              252,1        0 1311594 /tmp/kafka-logs/default_channel_kafka_zzh_demo-4/00000000000003626728.log
java    14895 root   87u   REG              252,1        0 1325038 /tmp/kafka-logs/default_channel_kafka_zzh_demo-3/00000000000003915669.log
java    14895 root   88u  IPv6           40855648      0t0     TCP zhuzhonghua2-fqawb:XmlIpcRegSvc->xx.xx.139.85:64708 (ESTABLISHED)
java    14895 root   89u   REG              252,1        0 1325037 /tmp/kafka-logs/default_channel_kafka_zzh_demo-2/00000000000005892533.log
java    14895 root   93u   REG              252,1        0 1325040 /tmp/kafka-logs/default_channel_kafka_zzh_demo-1/00000000000005494790.log
java    14895 root   94u   REG              252,1        0 1325043 /tmp/kafka-logs/default_channel_kafka_zzh_demo-0/00000000000003858999.log

多了“COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME”這一行。
而文件描述符的個數爲90:

[root@zhuzhonghua2-fqawb linuxC]# ls /proc/14895/fd |wc -l
90
[root@zhuzhonghua2-fqawb linuxC]# ls /proc/14895/fd
0  10  12  14  16  18  2   21  23  25  27  29  30  32  34  36  38  4   41  43  45  47  49  50  52  54  56  58  6   61  63  65  67  69  70  72  75  77  79  80  82  84  87  89  93
1  11  13  15  17  19  20  22  24  26  28  3   31  33  35  37  39  40  42  44  46  48  5   51  53  55  57  59  60  62  64  66  68  7   71  74  76  78  8   81  83  85  88  9   94

文件描述符(file descriptor)

對於linux而言,所有對設備和文件的操作都使用文件描述符來進行的。文件描述符是一個非負的整數,它是一個索引值,指向內核中每個進程打開文件的記錄表。當打開一個現存文件或創建一個新文件時,內核就向進程返回一個文件描述符;當需要讀寫文件時,也需要把文件描述符作爲參數傳遞給相應的函數。
通常,一個進程啓動時,都會打開3個文件:標準輸入、標準輸出和標準出錯處理。這3個文件分別對應文件描述符爲0、1和2(宏STD_FILENO、STDOUT_FILENO和STDERR_FILENO)。

每一個文件描述符會與一個打開文件相對應,同時,不同的文件描述符也會指向同一個文件。相同的文件可以被不同的進程打開也可以在同一個進程中被多次打開。系統爲每一個進程維護了一個文件描述符表,該表的值都是從0開始的,所以在不同的進程中你會看到相同的文件描述符,這種情況下相同文件描述符有可能指向同一個文件,也有可能指向不同的文件。具體情況要具體分析,要理解具體其概況如何,需要查看由內核維護的3個數據結構。

  1. 進程級的文件描述符表

  2. 系統級的打開文件描述符表

  3. 文件系統的i-node表

由於進程級文件描述符表的存在,不同的進程中會出現相同的文件描述符,它們可能指向同一個文件,也可能指向不同的文件。兩個不同的文件描述符,若指向同一個打開文件句柄,將共享同一文件偏移量。因此,如果通過其中一個文件描述符來修改文件偏移量,那麼從另一個文件描述符中也會觀察到變化,無論這兩個文件描述符是否屬於不同進程,還是同一個進程,情況都是如此。

文件句柄 vs 文件描述符

文件句柄也稱爲文件指針(FILE *):C語言中使用文件指針做爲I/O的句柄。文件指針指向進程用戶區中的一個被稱爲FILE結構的數據結構。FILE結構包括一個緩衝區和一個文件描述符。而文件描述符是文件描述符表的一個索引,因此從某種意義上說文件指針就是句柄的句柄(在Windows系統上,文件描述符被稱作文件句柄)。

C語言中FILE結構體的定義:

/* Define outside of namespace so the C++ is happy.  */
struct _IO_FILE;

__BEGIN_NAMESPACE_STD
/* The opaque type of streams.  This is the definition used elsewhere.  */
typedef struct _IO_FILE FILE;
__END_NAMESPACE_STD
#if defined __USE_LARGEFILE64 || defined __USE_SVID || defined __USE_POSIX \
    || defined __USE_BSD || defined __USE_ISOC99 || defined __USE_XOPEN \
    || defined __USE_POSIX2
__USING_NAMESPACE_STD(FILE)
#endif

# define __FILE_defined 1
#endif /* FILE not defined.  */
#undef  __need_FILE


#if !defined ____FILE_defined && defined __need___FILE

/* The opaque type of streams.  This is the definition used elsewhere.  */
typedef struct _IO_FILE __FILE;
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;

signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

這個_IO_FILE結構體中的“int _fileno”就是fd,即文件描述符。這個可以通過程序驗證:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>

int main(){
        char buf[50] = {"file descriptor demo"};
        FILE *myfile;

        myfile = fopen("test","w+");
        if(!myfile){
                printf("error: openfile failed!\n");
        }
        printf("The openfile's descriptor is %d\n", myfile->_fileno);
        if(write(myfile->_fileno,buf,50)<0){
                perror("error: write file failed!\n");
                exit(1);
        }else{
                printf("writefile successed!\n");
        }

        exit(0);
}

編譯:g++ fileno.cpp -o fileno.out
執行+輸出:

[root@hidden linuxC]# ./fileno.out 
The openfile's descriptor is 3
writefile successed!

查看test文件:

[root@hidden linuxC]# cat test
file descriptor demo
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章