理解操作系统的文件句柄和描述符
2021-02-17 15:56:03 阿炯

文件句柄是什么

参考百科:在文件I/O中,要从一个文件读取数据,应用程序首先要调用操作系统函数并传送文件名,并选一个到该文件的路径来打开文件。该函数取回一个顺序号,即文件句柄file handle,该文件句柄对于打开的文件是唯一的识别依据。要从文件中读取一块数据,应用程序需要调用函数ReadFile,并将文件句柄在内存中的地址和要拷贝的字节数传送给操作系统。当完成任务后,再通过调用系统函数来关闭该文件。

Linux下文件句柄

linux下文件句柄是有限制的,默认并不会太高,一般都是1024,使用指令ulimit -n可以查看文件句柄限制。

相关命令

ulimit -a:用来显示当前的各种用户进程限制
ulimit -n:查看当前用户默认的最大文件句柄数
lsof|wc -l:查看所有进程的文件打开数

lsof -n|awk '{print $2}'|sort|uniq -c|sort -nr|more 查看当前进程打开了多少句柄数,注意第一列是句柄数,第二列是进程id

linux硬、软限制

硬限制是实际的限制,而软限制,是warnning级别的限制,只会做出warning,可以通过ulimit来设定这两个参数,用root用户执行ulimit -HSn 4096H指定了硬性大小,S指定了软性大小,n表示设定单个进程最大的打开文件句柄数量。linux在整个系统层面和单个进程两个层面对打开的文件句柄进行限制。

配置文件/proc/sys/fs/file-max是对整个系统层面对打开的文件句柄最大数进行控制,单个用户或进程能够打开的文件句柄数受此限制。通过ulimit -a查看当前用户或进程能够打开的最大文件数。

上述只是默认值,在实际生产环境是不够用的,如果配置过小或当达到上限时,会在终端或日志中报错:too many open files或者Socket/File: Cannot open so many files等。通过ulimit -n可以对该值进行临时修改,如果想永久生效,需要修改配置文件/etc/security/limits.conf,如:
soft nofile 10000
hard nofile 10000

追加到上述的配置文件中。单个用户最大进程数(max user processes)就是单个用户最大进程数的限制,通过ulimit -u可以临时修改。永久修改需要做如上的追加,如:
soft nproc 10000
hard nproc 10000

不过这里的files不单是文件的意思,也包括打开的通讯链接(比如socket),正在监听的端口等等,所以有时候也可以叫做句柄(handle),这个错误通常也可以叫做句柄数超出系统限制。

产生的这些报错的原因:大多数情况是由于程序没有正常关闭一些资源引起的,所以出现这种情况,请检查io读写,socket通讯等是否正常关闭。在Linux平台上,无论编写客户端程序还是服务端程序,在进行高并发TCP连接处理时,最高的并发数量都要受到系统对用户单一进程同时可打开文件数量的限制(这是因为系统为每个TCP连接都要创建一个socket句柄,每个socket句柄同时也是一个文件句柄)。

file-max & file-nr

该文件指定了可以分配的文件句柄的最大数目(系统全局的可用句柄数目. The value in file-max denotes the maximum number of file handles that the Linux kernel will allocate)。如果用户得到的错误消息审批由于打开文件数已经达到了最大值,从而不能打开更多文件,则可能需要增加改之。可将这个值设置成任意多个文件,并且能通过将一个新数字值写入该文件来更改该值。这个参数的默认值和内存大小有关系,可以使用公式:file-max ≈ 内存大小/ 10k

关于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一样)。

lsof是列出系统所占用的资源(list open files),但是这些资源不一定会占用句柄,上文有提及。如果出了某些故障,使用lsof | wc -l的结果,这个时候可以通过file-nr粗略的估算一下,比如查看内核所能打开的线程数: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.

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.

最主要的是资源问题,为防止某一单一进程打开过多文件描述符而耗尽系统资源,进而对进程打开文件数做了限制。初始打开每个应用程序时,都有三个文件描述符:0,1,2,分别表示标准输入、标准输出、错误流。所以大多数应用程序所打开的文件的FD都是从3开始的。

lsof输出的文件描述符列
FD:文件描述符,应用程序通过文件描述符识别该文件。如cwd, rtd, txt, mem, DEL, 0u, 3w, 4r等
TYPE:文件类型,如DIR, REG, CHR, Ipv6, unix, FIFO等

FD列中的文件描述符cwd表示应用程序的当前工作目录,这是该应用程序启动的目录,除非它本身对这个目录进行更改;txt类型的文件是程序代码,如应用程序二进制文件本身或共享库,数值表示应用程序的文件描述符,这是打开该文件时返回的一个整数,u表示该文件被打开处于读写模式,而不是只读r或只写w模式,同时还有大写的W表示该应用程序具有对整个文件的写锁。该文件描述符用于确保每次只能打开一个应用程序实例。

TYPE列比较直观。文件和目录分别为REG和DIR,而CHR和BLK分别表示字符和块设备。或者unix, FIFO, Ipv6分表表示UNIX域套接字,FIFO队列和IP套接字。

句柄和文件描述符(FD)

查看系统中文件句柄的数量:
# cat /proc/sys/fs/file-nr
960    0    392546

通过lsof计算文件描述符数量
# lsof | wc -l
1776

发现文件描述符和句柄的数量差距很大,这里要解释一下上面的命令:

/proc/sys/fs/file-nr这里所展示的数据,第一列->已分配文件句柄的数目,第二列->已使用文件句柄的数目,第三列->文件句柄的最大数目,通过下面这个文件也可以看到文件句柄的最大数目:
# cat /proc/sys/fs/file-max
392546

lsof其实含义为:list open file。在万物皆文件的linux中,lsof是能够查看各类资源socket等等的很好用命令。

问题来了:
什么是文件描述符,什么是句柄?
为什么文件描述符和句柄的大小要一样和不一样?


句柄和文件描述符

句柄

原Windows下的概念,用以标记各种操作系统级的对象标识符,比如文件、资源、菜单、光标等等。文件句柄和文件描述符类似,它也是一个非负整数,也用于定位文件数据在内存中的位置。由于Linux下所有的东西都被看成文件,所以Linux下的文件描述符其实就相当于Windows下的句柄。文件句柄只是Windows下众多句柄中的一种类型而已。

文件描述符(file descriptor)

本质上是一个索引号非负整数,系统用户层可以根据它找到系统内核层的文件数据。这是一个POSIX标准下的概念,常见于Linux系统。内核kernel利用文件描述符来访问文件;打开现存文件或新建文件时,内核会返回一个文件描述符,读写文件也需要文件描述符来指定待读写的文件。

对于Linux而言,所有对设备和文件的操作都使用文件描述符来进行的。文件描述符是一个非负的整数,它是一个索引值,指向内核中每个进程打开文件的记录表。当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;当需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。通常在一个进程启动时,都会打开3个文件:标准输入、标准输出和标准出错处理。这3个文件分别对应文件描述符为0、1和2(宏STD_FILENO、STDOUT_FILENO和STDERR_FILENO)。

每一个文件描述符会与一个打开文件相对应,不同的文件描述符也会指向同一个文件;相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。具体情况要具体分析,需要查看由内核维护的3个数据结构:
1).进程级的文件描述符表
2).系统级的打开文件描述符表
3).文件系统的i-node表

由于进程级文件描述符表的存在,不同的进程中会出现相同的文件描述符,它们可能指向同一个文件,也可能指向不同的文件。两个不同的文件描述符,若指向同一个打开文件句柄,将共享同一文件偏移量。因此,如果通过其中一个文件描述符来修改文件偏移量,那么从另一个文件描述符中也会观察到变化,无论这两个文件描述符是否属于不同进程,还是同一个进程,情况都是如此。

句柄是一种指向指针的指针。在Linux系统中文件句柄(file handles)和文件描述符(file descriptor)是一一对应的关系,按照C语言的理解文件句柄是FILE(fopen()返回)而文件描述符是fd(int型,open()函数返回),FILE这个结构体中有一个字段是_fileno其就是指fd,且FILE和fd可以通过C语言函数进行互相转换。文件指针即指FILE*,即指文件句柄。打开文件(open files)并不仅限于文件句柄,由于linux所有的事物都以文件的形式存在,要使用诸如共享内存、信号量、消息队列、内存映射等都会打开文件,但这些是不会占用文件句柄。文件句柄也称为文件指针(FILE *):C语言中使用文件指针做为I/O的句柄,文件指针指向进程用户区中的一个被称为FILE结构的数据结构,FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。

两者比较

一个描述符上可以监控很多的事件,如果监控的是read,然而write事件到来的时候也会将该描述符唤醒。lsof在用户空间,主要还是从文件描述符的角度来看文件句柄,简单来说,每个进程都有一个打开的文件表fdtable)。表中的每一项是struct file类型,包含了打开文件的一些属性比如偏移量,读写访问模式等,这是真正意义上的文件句柄。而这其中,文件描述符可以理解为一个fdtable的下标,文件句柄是下标指向的数据这块可以类比于数据下标和数据中的元素。

每个文件描述符都与一个文件所对应的,不同的文件描述符也可能会指向同一个文件,相同的文件描述符也可能会指向不同的文件。这个主要是因为同一个文件可以被不同的进程打开,也可以被同一个进程的多次打开。系统为每一个进程维护了一个文件描述符表,所以在不同的进程中会看到相同的fd。那么要理解具体的内部结构,需要理解下面这三个数据结构:
    进程级的文件描述符表
    系统级的打开文件描述符表
    文件系统i-node表

进程级别的文件描述符表每一条都只是记录了单个文件描述符的信息,主要包含下面几个:
    控制文件描述符操作的一组标志
    对打开文件句柄的引用

内核对所有打开的文件都有一个系统级的文件描述符表,各条目称为打开文件句柄(open file handle)一个打开文件句柄存储了一个与一个打开文件相关的所有信息
    当前文件偏移量调用read()和write()时更新,或使用lseek()直接修改
    打开文件时的标识open()的flags参数
    文件访问模式读写
    与信号驱动相关的设置
    对该文件i-node对象的引用
    文件类型常规文件、socket等和访问权限
    一个指针,指向该文件所持有的锁列表
    文件的各种属性,时间戳啥的,等等。。。
    
顺着函数调用链路简单梳理下,就能知道有哪些地方会分配文件句柄了:
    open系统调用打开文件path_openat内核函数)
    打开一个目录dentry_open函数)
    共享内存attach do_shmat函数
    socket套接字sock_alloc_file函数
    管道create_pipe_files函数
    epoll/inotify/signalfd等功能用到的匿名inode文件系统anon_inode_getfile函数)

实际上,lsof的手册页也有部分描述:An open file may be a regular file, a directory, a block special file, a character special file, an executing text reference, a library, a stream or a network file (Internet socket, NFS file or UNIX domain socket.) A specific file or all the files in a file system may be selected by path.


文件描述符、打开的文件句柄、i-node关系


在进程A中,文件描述符1和30都指向了系统文件描述符表中的21句柄,这可能是通过dup() dup2() fcnt()或者对同一个文件调用了多次open()导致的

进程A的文件描述符2和进程B的文件描述符2都指向了系统文件描述符的30句柄,这可能是fork()的作用,出现了父子进程

进程A的文件描述符0和进程B的文件描述符3分别指向了不同的句柄,但是最后这两个句柄都指向了i-node表中的66,也就是指向了同一个文件。这是因为进程A和B各自同一个文件发起了open()的操作


简单小结一下上述的两个概念:文件描述符和文件句柄
每个进程都有一个打开的文件表fdtable)。表中的每一项是struct file类型,包含了打开文件的一些属性比如偏移量,读写访问模式等,这是真正意义上的文件句柄。
文件描述符是一个整数。代表fdtable中的索引位置下标,指向具体的struct file文件句柄。

file-nr文件里面的第一个字段代表的是内核分配的struct file的个数,也就是文件句柄个数,而不是文件描述符。本质上是因为文件描述符和文件句柄是两个不同的东西:lsof在用户空间,主要还是从文件描述符的角度来看文件句柄。linux内核中很多对象都是有引用计数的。虽然文件句柄是由open先打开的,但mmap之后,引用计数被加1,尽管我们接着把文件描述符close掉了,但是底层指向的struct file由于引用数大于0,不会被回收。


相关命令

ulimit

ulimit主要是用来限制进程对资源的使用情况的,它支持各种类型的限制:
内核文件的大小限制
进程数据块的大小限制
Shell进程创建文件大小限制
可加锁内存大小限制
常驻内存集的大小限制
打开文件句柄数限制
分配堆栈的最大大小限制
CPU占用时间限制用户最大可用的进程数限制
Shell进程所能使用的最大虚拟内存限制

先看下查询所有限制的数据:ulimit -a

查看进程允许打开的最大文件句柄数:ulimit -n
设置进程能打开的最大文件句柄数:ulimit -n xxx

limits.conf

limits.conf这个文件实在/etc/security/目录下,因此这个文件是处于安全考虑的。limits.conf文件是用于提供对系统中的用户所使用的资源进行控制和限制,对所有用户的资源设定限制是非常重要的,这可以防止用户发起针对处理器和内存数量等的拒绝服务攻击。这些限制必须在用户登录时限制。它与ulimit的区别在于前者是针对所有用户的,而且在任何shell都是生效的,即与shell无关,而后者只是针对特定用户的当前shell的设定。在修改最大文件打开数时,最好使用limits.conf文件来修改,通过这个文件,可以定义用户,资源类型,软硬限制等。也可修改/etc/profile文件加上ulimit的设置语句来是的全局生效。

*    soft    nofile    1000000
*    hard    nofile    1000000
*    soft    core    1048576
*    hard    core    1048576


说明:* 代表针对所有用户,noproc 是代表最大进程数,nofile 是代表最大文件打开数。

file-max & file-nr

这个在上面已经说过具体的含义和用法了。那么解释一下,这块file-max和ulimit的区别是什么?简单的说,ulimit -n控制进程级别能够打开的文件句柄的数量,而max-file表示系统级别的能够打开的文件句柄的数量。ulimit -n的设置在重启机器后会丢失,因此需要修改limits.conf的限制,limits.conf中有两个值soft和hard,soft代表只警告,hard代表真正的限制。

lsof

lsof是列出系统所占用的资源list open files,但是这些资源不一定会占用句柄。比如共享内存、信号量、消息队列、内存映射等,虽然占用了这些资源,但不占用句柄。lsof是通过查看进程的内存映射和文件描述符表来枚举打开文件的, 如果是一个多线程的服务。主线程先退出了,子线程还活着, 那么进程的fd表看起来就是空的。

常用命令

确认系统设置的最大文件句柄数:ulimit -a

统计系统中当前打开的总文件句柄数:lsof -w | awk '{print $2}' | wc -l

查看当前进程打开了多少文件:lsof -w -n|awk '{print $2}'|sort|uniq -c|sort -nr|more | grep pidId

统计各进程打开句柄数:lsof -n|awk '{print $2}'|sort|uniq -c|sort -nr

统计各用户打开句柄数:lsof -n|awk '{print $3}'|sort|uniq -c|sort -nr

统计各命令打开句柄数:lsof -n|awk '{print $1}'|sort|uniq -c|sort -nr


小结

由于进程级的文件描述符表的存在,不同的进程中会出现相同的文件描述符,而相同的文件描述符会指向不同句柄。

两个不同的文件描述符,如果指向同一个句柄,就会共享同一个文件偏移量,因此,如果其中一个文件描述符修改了偏移量调用了read、write、lseek,另一个描述符也会感受到变化。

文件描述符标志为进程和文件描述符私有,这一标志的修改不会影响同一进程或不同进程的其他文件描述符。

句柄其实是指针的指针。操作系统分为用户态和内核态,用户必然会使用内核的数据,但是用户进程又不能直接获取内核数据结构,所以才出现了指针的指针——句柄,用户态通过句柄调用系统服务,进入内核之后,内核根据句柄找到真正的数据结构。

回顾问题

什么是文件描述符,什么是句柄?——这个问题已经得到了解答:

为什么文件描述符和句柄的大小要一样?——刚开始将文件描述符fd和句柄理解成一个概念,以为这两个是一个东西的不同说法。文件描述符主要还是用户态的东西,而句柄主要是内核态的东西,从上面的图就可以理解出来。

为什么文件描述符和句柄的大小要不一样?——多个文件描述符可以指向同一个句柄,每个进程都会拥有一个文件描述符表。而lsof命令会漏掉很多数据:共享内存等。lsof在用户空间,主要还是从文件描述符的角度来看文件句柄。

实际上文件句柄和文件描述符是有区别的。文件描述符是一个数值,代表操作系统所使用的裸数据流,文件描述符是文件句柄的核心,文件句柄可以看作是文件描述符的更高一层次的封装,比如提供了和描述符有关数据流的输入、输出的buffer缓冲。也就是说文件句柄比文件描述符多一些额外的功能。


参考来源:

句柄和文件描述符
Linux文件句柄的这些技术内幕
设置Linux系统最大使用的资源限制