Prologue
處理了一起too many open files的報錯菇夸,中途忽然感覺文件描述符琼富、文件句柄、文件指針這三個概念很容易混淆庄新,網(wǎng)上其他博客也是眾說紛紜鞠眉。于是做了一點考證,專門寫一篇來盡量準(zhǔn)確地記錄下择诈。
本文的內(nèi)容有不少來自Linux領(lǐng)域的權(quán)威書籍械蹋,Michael Kerrisk所著《The Linux Programming Interface:A Linux and UNIX System Programming Handbook》的第4、5兩章羞芍。
文件描述符 & 文件描述符表
文件描述符(file descriptor, fd)是Linux系統(tǒng)中對已打開文件的一個抽象標(biāo)記哗戈,所有I/O系統(tǒng)調(diào)用對已打開文件的操作都要用到它。這里的“文件”仍然是廣義的荷科,即除了普通文件和目錄外唯咬,還包括管道、FIFO(命名管道)畏浆、Socket胆胰、終端、設(shè)備等全度。
文件描述符是一個較小的非負整數(shù)煮剧,并且0、1、2三個描述符總是默認(rèn)分配給標(biāo)準(zhǔn)輸入勉盅、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯誤佑颇。這就是常用的nohup ./my_script > my_script.log 2>&1 &
命令里2和1的由來。
Linux系統(tǒng)中的每個進程會在其進程控制塊(PCB)內(nèi)維護屬于自己的文件描述符表(file descriptor table)草娜。表中每個條目包含兩個域:一是控制該描述符的標(biāo)記域(flags)挑胸,二是指向系統(tǒng)級別的打開文件表中對應(yīng)條目的指針。那么打開文件表又是什么呢宰闰?
打開文件表 & 文件句柄
內(nèi)核會維護系統(tǒng)內(nèi)所有打開的文件及其相關(guān)的元信息茬贵,該結(jié)構(gòu)稱為打開文件表(open file table)。表中每個條目包含以下域:
- 文件的偏移量移袍。POSIX API中的read()/write()/lseek()函數(shù)都會修改該值解藻;
- 打開文件時的狀態(tài)和權(quán)限標(biāo)記。通過open()函數(shù)的參數(shù)傳入葡盗;
- 文件的訪問模式(只讀螟左、只寫、讀+寫等)觅够。通過open()函數(shù)的參數(shù)傳入胶背;
- 指向其對應(yīng)的inode對象的指針。內(nèi)核也會維護系統(tǒng)級別的inode表喘先,關(guān)于inode的細節(jié)請參考這篇文章钳吟。
文件描述符表、打開文件表窘拯、inode表之間的關(guān)系可以用書中的下圖來表示红且。注意圖中的fd 0、1树枫、2...只是示意下標(biāo)直焙,不代表三個標(biāo)準(zhǔn)描述符。
可見砂轻,一個打開的文件可以對應(yīng)多個文件描述符(不管是同進程還是不同進程)奔誓,一個inode也可以對應(yīng)多個打開的文件。打開文件表中的一行稱為一條文件描述(file description)搔涝,也經(jīng)常稱為文件句柄(file handle)厨喂。
多嘴一句,“句柄”這個詞在UNIX世界中并不很正式庄呈,但在Windows里遍地都是蜕煌。Windows NT內(nèi)核會將內(nèi)存中的所有對象(文件、窗口诬留、菜單斜纪、圖標(biāo)等一切東西)的地址列表維護成整數(shù)索引贫母,這個整數(shù)就叫做句柄,邏輯上講類似于“指針的指針”盒刚,感覺上還是有一些相通的地方的腺劣。
文件I/O API & 文件指針
說了這么多,用最基礎(chǔ)的POSIX庫函數(shù)寫個示例程序吧因块。它將一個文件中的內(nèi)容讀出來橘原,并原封不動地寫入另外一個文件。
#include <fcntl.h>
#include <sys/stat.h>
#define BUF_SIZE 1024
int main(int argc,char *argv[]) {
int inputFd, outputFd;
char buf[BUF_SIZE];
ssize_t numRead;
inputFd = open("data.txt", O_RDONLY);
if (inputFd == -1) {
exit(EXIT_FAILURE);
}
outputFd = open(
"data_copy.txt",
O_CREAT | O_WRONLY | O_TRUNC,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
);
if (outputFd == -1) {
exit(EXIT_FAILURE);
}
while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0) {
if (write(outputFd, buf, numRead) != numRead) {
exit(EXIT_FAILURE);
}
}
close(inputFd);
close(outputFd);
exit(EXIT_SUCCESS);
}
嚴(yán)格來講涡上,POSIX提供的這些函數(shù)只是用戶與內(nèi)核之前的橋梁趾断,實際仍位于系統(tǒng)調(diào)用層之上。但是現(xiàn)實應(yīng)用中吩愧,我們一般也把它們叫做系統(tǒng)調(diào)用了(盡管不太正確)芋酌。
要使用open()/read()/write()/close()這些系統(tǒng)調(diào)用,必須引入fcntl.h頭文件耻警。open()返回的是文件描述符隔嫡,其參數(shù)中傳入的flags和mode值也會保存在打開文件表中甸怕。在整個讀甘穿、寫并最終關(guān)閉文件的過程中,操作的也都是文件描述符梢杭。
那么我們在大學(xué)C語言課程上學(xué)習(xí)的“文件指針”(file pointer)又是什么呢温兼?這個就比較簡單,繼續(xù)看下面的栗子武契。
#include <stdio.h>
#include <stdlib.h>
#define BUF_SIZE 1024
int main(int argc,char *argv[]) {
char buf[BUF_SIZE];
FILE *inputFp;
size_t numRead;
inputFp = fopen("data.txt", "r");
if (inputFp == NULL) {
exit(EXIT_FAILURE);
}
while (!feof(inputFp)) {
numRead = fread(buf, sizeof(char), sizeof(buf), inputFp);
printf("%ld\t%s", numRead, buf);
}
fclose(inputFp);
exit(EXIT_SUCCESS);
}
可見募判,文件指針就是FILE結(jié)構(gòu)體的指針,與前兩個概念不屬于同一層咒唆。當(dāng)通過文件指針操作文件時届垫,需要調(diào)用C語言stdio.h中提供的文件API(fopen()、fread()等)全释,而C標(biāo)準(zhǔn)庫最終調(diào)用了POSIX的庫函數(shù)装处。并且“file pointer”這個詞里的“file”指的是狹義的文件,不包括管道浸船、設(shè)備等其他東西妄迁,所以單純用C API只能操作普通文件。
FILE結(jié)構(gòu)體中是包含了文件描述符的李命,所以C語言也提供了互相轉(zhuǎn)換的方法:
int inputFd;
FILE *inputFp;
inputFd = fileno(inputFp);
inputFp = fdopen(inputFd, "r");
文件描述符和文件句柄的限制
文章開頭提到了"too many open files"這條報錯信息登淘,它的實際含義是文件描述符數(shù)量超限。用ulimit -a
命令打印出各限制值:
~ 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) 127961
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 65535
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 127961
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
其中open files一行就表示當(dāng)前用戶封字、當(dāng)前終端黔州、單個進程能擁有的文件描述符的數(shù)量閾值(很多文章都描述錯了這一點)耍鬓,可以用ulimit -n [閾值]
命令來臨時修改,退出登錄即失效流妻。如果想要永久修改界斜,可以將ulimit -n [閾值]
寫入用戶的.bash_profile文件或/etc/profile中,也可以修改/etc/security/limits.conf:
~ vim /etc/security/limits.conf
# 用戶名 軟/硬限制 限制項 閾值
root soft nofile 65535
root hard nofile 65535
那么如何列出各個進程的文件描述符呢合冀?可以利用lsof
(list open files)命令各薇。這個命令的用法很豐富,本文暫時不表君躺。
既然有了進程級別的描述符數(shù)量限制峭判,也就有系統(tǒng)級別的文件句柄數(shù)量限制∽亟校可以這樣查看其閾值林螃,以及當(dāng)前已分配的句柄數(shù):
~ cat /proc/sys/fs/file-max
3247469 # 閾值
~ cat /proc/sys/fs/file-nr
# 已分配且使用中 / 已分配但未使用 / 閾值
2976 0 3247469
如果需要臨時修改,可以直接向file-max寫入新值俺泣。永久生效的方法是修改/etc/sysctl.conf:
~ vim /etc/sysctl.conf
fs.file-max = 5242880
# 立即生效
~ sysctl -p
The End
最后總結(jié)一下吧疗认。
- 文件描述符是進程級別的,文件句柄是系統(tǒng)級別的伏钠,不能混用横漏。它們在不同級別表示已打開的文件。
- 文件描述符與文件句柄直接關(guān)聯(lián)熟掂,文件句柄與inode直接關(guān)聯(lián)缎浇。
- 文件描述符在POSIX系統(tǒng)調(diào)用中直接可見,文件指針是C語言在其基礎(chǔ)上的包裝赴肚。
- 文件句柄在UNIX里不是個正式概念素跺,所以無論在系統(tǒng)還是C語言API中都不顯式存在。
明天公司年會誉券,民那晚安晚安指厌。