故事背景
看在分布式架构的通信过程中,接触到了netty,又通过netty接触到了句柄和文件描述符,其实之前就有过一点点了解,但是概念和实际用法都很模糊,所以决定一探究竟
问题开始
查看系统中文件句柄的数量:
$ cat /proc/sys/fs/file-nr
203136 0 19593935
通过lsof计算文件描述符数量
$lsof | wc -l
253
发现文件描述符和句柄的数量差距很大。这里先解释一下上面这两个命令
/proc/sys/fs/file-nr这里所展示的数据,第一列->已分配文件句柄的数目,第二列->已使用文件句柄的数目,第三列->文件句柄的最大数目,通过下面这个文件也可以看到文件句柄的最大数目
$ cat /proc/sys/fs/file-max
19593935
lsof其实含义为:list open file。在万物皆文件的linux中,lsof是能够查看各类资源(socket等等)的很好用命令
那问题来了:
- 什么是文件描述符,什么是句柄?
- 为什么文件描述符和句柄的大小要一样?
- 为什么文件描述符和句柄的大小要不一样?
- 上面的问题不一定都对,但是可以带着思考往下看
句柄和文件描述符
句柄
Windows下的概念。句柄是Windows下各种对象的标识符,比如文件、资源、菜单、光标等等。文件句柄和文件描述符类似,它也是一个非负整数,也用于定位文件数据在内存中的位置。
由于Linux下所有的东西都被看成文件,所以Linux下的文件描述符其实就相当于Windows下的句柄。文件句柄只是Windows下众多句柄中的一种类型而已
文件描述符
本质上是一个索引号(非负整数),系统用户层可以根据它找到系统内核层的文件数据。这是一个POSIX标准下的概念,常见于Linux系统。内核(kernel)利用文件描述符来访问文件。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要文件描述符来指定待读写的文件。
两者比较
- 一个描述符上可以监控很多的事件,如果监控的是read,然而write事件到来的时候也会将该描述符唤醒
- lsof在用户空间,主要还是从文件描述符的角度来看文件句柄
简单来说,每个进程都有一个打开的文件表(fdtable)。表中的每一项是struct file类型,包含了打开文件的一些属性比如偏移量,读写访问模式等,这是真正意义上的文件句柄。
而这其中,文件描述符可以理解为一个fdtable的下标,文件句柄是下标指向的数据(这块可以类比于数据下标和数据中的元素)
-
每个文件描述符都与一个文件所对应的,不同的文件描述符也可能会指向同一个文件,相同的文件描述符也可能会指向不同的文件。这个主要是因为同一个文件可以被不同的进程打开,也可以被同一个进程的多次打开。系统为每一个进程维护了一个文件描述符表,所以在不同的进程中会看到相同的fd。那么要理解具体的内部结构,需要理解下面这三个数据结构:
- 进程级的文件描述符表
- 系统级的打开文件描述符表
- 文件系统i-node表
-
进程级别的文件描述符表每一条都只是记录了单个文件描述符的信息,主要包含下面几个:
- 控制文件描述符操作的一组标志
- 对打开文件句柄的引用
-
内核对所有打开的文件都有一个系统级的文件描述符表,各条目称为打开文件句柄(open file handle)一个打开文件句柄存储了一个与一个打开文件相关的所有信息
- 当前文件偏移量(调用read()和write()时更新,或使用lseek()直接修改)
- 打开文件时的标识(open()的flags参数)
- 文件访问模式(读写)
- 与信号驱动相关的设置
- 对该文件i-node对象的引用
- 文件类型(常规文件、socket等)和访问权限
- 一个指针,指向该文件所持有的锁列表
- 文件的各种属性,时间戳啥的,巴拉巴拉
-
文件描述符、打开的文件句柄、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()的操作
相关命令
ulimit
ulimit主要是用来限制进程对资源的使用情况的,它支持各种类型的限制
- 内核文件的大小限制
- 进程数据块的大小限制
- Shell进程创建文件大小限制
- 可加锁内存大小限制
- 常驻内存集的大小限制
- 打开文件句柄数限制
- 分配堆栈的最大大小限制
- CPU占用时间限制用户最大可用的进程数限制
- Shell进程所能使用的最大虚拟内存限制
先看下查询所有限制的数据
$ ulimit -a
core file size (blocks, -c) 4194304
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 772586
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1000000
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) 102400
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
查看进程允许打开的最大文件句柄数:ulimit -n
设置进程能打开的最大文件句柄数:ulimit -n xxx
limits.conf
limits.conf这个文件实在/etc/security/目录下,因此这个文件是处于安全考虑的。limits.conf文件是用于提供对系统中的用户所使用的资源进行控制和限制,对所有用户的资源设定限制是非常重要的,这可以防止用户发起针对处理器和内存数量等的拒绝服务攻击。这些限制必须在用户登录时限制。
limits.conf与ulimit的区别在于前者是针对所有用户的,而且在任何shell都是生效的,即与shell无关,而后者只是针对特定用户的当前shell的设定。在修改最大文件打开数时,最好使用limits.conf文件来修改,通过这个文件,可以定义用户,资源类型,软硬限制等。也可修改/etc/profile文件加上ulimit的设置语句来是的全局生效。
当达到上限时,会报错:too many open files或者遇上Socket/File: Cannot open so many files等。所以当出现这个问题的时候,知道要怎么解决了吧
$ cat /etc/security/limits.conf
# /etc/security/limits.conf
#
#Each line describes a limit for a user in the form:
#
#<domain> <type> <item> <value>
#
#Where:
#<domain> can be:
# - an user name
# - a group name, with @group syntax
# - the wildcard *, for default entry
# - the wildcard %, can be also used with %group syntax,
# for maxlogin limit
#
#<type> can have the two values:
# - "soft" for enforcing the soft limits
# - "hard" for enforcing hard limits
#
#<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 files
# - 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
#
#<domain> <type> <item> <value>
#
#* soft core 0
#* hard rss 10000
#@student hard nproc 20
#@faculty soft nproc 20
#@faculty hard nproc 50
#ftp hard nproc 0
#@student - maxlogins 4
* soft nofile 1000000
* hard nofile 1000000
* soft core 1048576
* hard core 1048576
# End of file
说明:* 代表针对所有用户,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),但是这些资源不一定会占用句柄。比如共享内存、信号量、消息队列、内存映射等,虽然占用了这些资源,但不占用句柄。
常用命令
确认系统设置的最大文件句柄数
ulimit -a
统计系统中当前打开的总文件句柄数
lsof -w | awk '{print $2}' | wc -l
查看当前进程打开了多少文件
lsof -w -n|awk '{print $2}'|sort|uniq -c|sort -nr|more | grep pidId
总结
- 由于进程级的文件描述符表的存在,不同的进程中会出现相同的文件描述符,而相同的文件描述符会指向不同句柄
- 两个不同的文件描述符,如果指向同一个句柄,就会共享同一个文件偏移量,因此,如果其中一个文件描述符修改了偏移量(调用了read、write、lseek),另一个描述符也会感受到变化
- 文件描述符标志为进程和文件描述符私有,这一标志的修改不会影响同一进程或不同进程的其他文件描述符
- 句柄其实是指针的指针。操作系统分为用户态和内核态,用户必然会使用内核的数据,但是用户进程又不能直接获取内核数据结构,所以才出现了指针的指针——句柄,用户态通过句柄调用系统服务,进入内核之后,内核根据句柄找到真正的数据结构
回顾问题
- 什么是文件描述符,什么是句柄?——这个问题已经得到了解答
- 为什么文件描述符和句柄的大小要一样?——刚开始我将文件描述符(fd)和句柄理解成一个概念,以为这两个是一个东西的不同说法。文件描述符主要还是用户态的东西,而句柄主要是内核态的东西,从上面的图就可以理解出来
- 为什么文件描述符和句柄的大小要不一样?——多个文件描述符可以指向同一个句柄,每个进程都会拥有一个文件描述符表。而lsof命令会漏掉很多数据:共享内存等。lsof在用户空间,主要还是从文件描述符的角度来看文件句柄。