Prologue
处理了一起too many open files的报错,中途忽然感觉这三个概念很容易混淆,网上其他博客也是众说纷纭。于是做了一点考证,专门写一篇来尽量准确地记录下。
本文的内容有不少来自Linux领域的权威书籍,Michael Kerrisk所著《The Linux Programming Interface:A Linux and UNIX System Programming Handbook》的第4、5两章。
文件描述符 & 文件描述符表
文件描述符(file descriptor, fd)是Linux系统中对已打开文件的一个抽象标记,所有I/O系统调用对已打开文件的操作都要用到它。这里的“文件”仍然是广义的,即除了普通文件和目录外,还包括管道、FIFO(命名管道)、Socket、终端、设备等。
文件描述符是一个较小的非负整数,并且0、1、2三个描述符总是默认分配给标准输入、标准输出和标准错误。这就是常用的nohup ./my_script > my_script.log 2>&1 &命令里2和1的由来。
Linux系统中的每个进程会在其进程控制块(PCB)内维护属于自己的文件描述符表(file descriptor table)。表中每个条目包含两个域:一是控制该描述符的标记域(flags),二是指向系统级别的打开文件表中对应条目的指针。那么打开文件表又是什么呢?
打开文件表 & 文件句柄
内核会维护系统内所有打开的文件及其相关的元信息,该结构称为打开文件表(open file table)。表中每个条目包含以下域:
文件的偏移量。POSIX API中的read()/write()/lseek()函数都会修改该值;
打开文件时的状态和权限标记。通过open()函数的参数传入;
文件的访问模式(只读、只写、读+写等)。通过open()函数的参数传入;
指向其对应的inode对象的指针。内核也会维护系统级别的inode表,关于inode的细节请参考这篇文章。
文件描述符表、打开文件表、inode表之间的关系可以用书中的下图来表示。注意图中的fd 0、1、2...只是示意下标,不代表三个标准描述符。
可见,一个打开的文件可以对应多个文件描述符(不管是同进程还是不同进程),一个inode也可以对应多个打开的文件。打开文件表中的一行称为一条文件描述(file description),也经常称为文件句柄(file handle)。
多嘴一句,“句柄”这个词在UNIX世界中并不很正式,但在Windows里遍地都是。Windows NT内核会将内存中的所有对象(文件、窗口、菜单、图标等一切东西)的地址列表维护成整数索引,这个整数就叫做句柄,逻辑上讲类似于“指针的指针”,感觉上还是有一些相通的地方的。
文件I/O API & 文件指针
说了这么多,用最基础的POSIX库函数写个示例程序吧。它将一个文件中的内容读出来,并原封不动地写入另外一个文件。#include #include #define BUF_SIZE 1024int 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);}
严格来讲,POSIX提供的这些函数只是用户与内核之前的桥梁,实际仍位于系统调用层之上。但是现实应用中,我们一般也把它们叫做系统调用了(尽管不太正确)。
要使用open()/read()/write()/close()这些系统调用,必须引入fcntl.h头文件。open()返回的是文件描述符,其参数中传入的flags和mode值也会保存在打开文件表中。在整个读、写并最终关闭文件的过程中,操作的也都是文件描述符。
那么我们在大学C语言课程上学习的“文件指针”(file pointer)又是什么呢?这个就比较简单,继续看下面的栗子。#include #include #define BUF_SIZE 1024int 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结构体的指针,与前两个概念不属于同一层。当通过文件指针操作文件时,需要调用C语言stdio.h中提供的文件API(fopen()、fread()等),而C标准库最终调用了POSIX的库函数。并且“file pointer”这个词里的“file”指的是狭义的文件,不包括管道、设备等其他东西,所以单纯用C API只能操作普通文件。
FILE结构体中是包含了文件描述符的,所以C语言也提供了互相转换的方法:int inputFd;FILE *inputFp;inputFd = fileno(inputFp);inputFp = fdopen(inputFd, "r");
文件描述符和文件句柄的限制
文章开头提到了"too many open files"这条报错信息,它的实际含义是文件描述符数量超限。用ulimit -a命令打印出各限制值:~ ulimit -acore file size (blocks, -c) 0data seg size (kbytes, -d) unlimitedscheduling priority (-e) 0file size (blocks, -f) unlimitedpending signals (-i) 127961max locked memory (kbytes, -l) 64max memory size (kbytes, -m) unlimitedopen files (-n) 65535pipe size (512 bytes, -p) 8POSIX message queues (bytes, -q) 819200real-time priority (-r) 0stack size (kbytes, -s) 8192cpu time (seconds, -t) unlimitedmax user processes (-u) 127961virtual memory (kbytes, -v) unlimitedfile locks (-x) unlimited
其中open files一行就表示当前用户、当前终端、单个进程能拥有的文件描述符的数量阈值(很多文章都描述错了这一点),可以用ulimit -n [阈值]命令来临时修改,退出登录即失效。如果想要永久修改,可以将ulimit -n [阈值]写入用户的.bash_profile文件或/etc/profile中,也可以修改/etc/security/limits.conf:~ vim /etc/security/limits.conf# 用户名 软/硬限制 限制项 阈值root soft nofile 65535root hard nofile 65535
那么如何列出各个进程的文件描述符呢?可以利用lsof(list open files)命令。这个命令的用法很丰富,本文暂时不表。
既然有了进程级别的描述符数量限制,也就有系统级别的文件句柄数量限制。可以这样查看其阈值,以及当前已分配的句柄数:~ cat /proc/sys/fs/file-max3247469 # 阈值~ cat /proc/sys/fs/file-nr# 已分配且使用中 / 已分配但未使用 / 阈值2976 0 3247469
如果需要临时修改,可以直接向file-max写入新值。永久生效的方法是修改/etc/sysctl.conf:~ vim /etc/sysctl.conffs.file-max = 5242880# 立即生效~ sysctl -p
The End
最后总结一下吧。
文件描述符是进程级别的,文件句柄是系统级别的,不能混用。它们在不同级别表示已打开的文件。
文件描述符与文件句柄直接关联,文件句柄与inode直接关联。
文件描述符在POSIX系统调用中直接可见,文件指针是C语言在其基础上的包装。
文件句柄在UNIX里不是个正式概念,所以无论在系统还是C语言API中都不显式存在。
明天公司年会,民那晚安晚安。
联系客服