文件句柄?文件描述符?傻傻分不清楚
概述
在實際工作中會經常遇到一些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?prioritylimits.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 2538lsof是列出系統所占用的資源(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??93lsof輸出各列信息的意義如下:
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?14895173?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:
文件描述符(file descriptor)
對于linux而言,所有對設備和文件的操作都使用文件描述符來進行的。文件描述符是一個非負的整數,它是一個索引值,指向內核中每個進程打開文件的記錄表。當打開一個現存文件或創建一個新文件時,內核就向進程返回一個文件描述符;當需要讀寫文件時,也需要把文件描述符作為參數傳遞給相應的函數。
通常,一個進程啟動時,都會打開3個文件:標準輸入、標準輸出和標準出錯處理。這3個文件分別對應文件描述符為0、1和2(宏STD_FILENO、STDOUT_FILENO和STDERR_FILENO)。
每一個文件描述符會與一個打開文件相對應,同時,不同的文件描述符也會指向同一個文件。相同的文件可以被不同的進程打開也可以在同一個進程中被多次打開。系統為每一個進程維護了一個文件描述符表,該表的值都是從0開始的,所以在不同的進程中你會看到相同的文件描述符,這種情況下相同文件描述符有可能指向同一個文件,也有可能指向不同的文件。具體情況要具體分析,要理解具體其概況如何,需要查看由內核維護的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
執行+輸出:
查看test文件:
[root@hidden?linuxC]#?cat?test file?descriptor?demo總結
以上是生活随笔為你收集整理的文件句柄?文件描述符?傻傻分不清楚的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入Lock锁底层原理实现,手写一个可重
- 下一篇: Spring Boot 发邮件和附件,超