背景
最近调试一款网关设备,它部署在客户端和服务端之间。在工作时,它同时接收来自客户端的连接,同时又向服务端建立连接。
网关在完全接收来自客户端的数据后,会校验数据合法性,只有数据合法,网关才会向服务器建立连接并转发数据。
这样,当存在一个客户端和服务端的通信时,网关有可能需要同时建立两个连接,占用两个fd。这对网关的数据处理能力提出了较高的要求。
在调试中出现的现象是,当客户端以较多的并发的速率向网关建立连接时,网关会因为已经打开的fd过多而拒绝连接,导致来自客户端的连接建立失败。
本文就对此现象进行了简略分析,同时复习一下linux下的文件描述符。
系统中fd的限制
linux系统中通常会对每个进程所能打开的文件数据有一个限制,当进程中已打开的文件描述符超过这个限制时,open()等获取文件描述符的系统调用都会返回失败。
linux下最大文件描述符的限制有两个方面,一个是用户级的限制,另外一个则是系统级限制。
- 用户级限制:ulimit命令看到的是用户级的最大文件描述符限制,也就是说每一个用户登录后执行的程序占用文件描述符的总数不能超过这个限制
- 系统级限制:sysctl命令和proc文件系统中查看到的数值是一样的,这属于系统级限制,它是限制所有用户打开文件描述符的总和
查看限制数量
- 查看用户级限制:
ulimit -n
-> % ulimit -n
1024
- 系统级限制:
- sysctl -a
- cat /proc/sys/fs/file-max
-> % sysctl -a | grep file-max
sysctl: fs.file-max = 100262
-> % cat /proc/sys/fs/file-max
100262
修改限制数量
- 修改用户级限制
- 临时修改,只对当前shell有效:
ulimit -HSn 65536
- 永久修改:编辑
/etc/security/limits.conf
- 临时修改,只对当前shell有效:
-> % ulimit -SHn 2048
yao@yao-virtual-machine [10时49分18秒] [~/work/util]
-> % ulimit -n
2048
vi /etc/security/limits.conf
* hard nofile 65536
* soft nofile 65536
- 修改系统级限制
通过sysctl命令修改/etc/sysctl.conf文件:sysctl -w fs.file-max=2048
,完成后执行sysctl -p
即可
文件描述符简述
简述
在Linux通用I/O模型中,I/O操作系列函数(系统调用)都是围绕一个叫做文件描述符的整数展开。
I/O操作系统调用都以文件描述符(一个非负整数),指代打开的文件。每个进程都有一个打开文件表,可以理解成一个数组,文件描述符可以理解成数组的下标。
相关I/O操作系统调用以文件描述符为参数,便可以通过数组访问定位到指定的文件对象,进而进行I/O操作。
当某个程序打开文件时,操作系统返回相应的文件描述符,程序为了处理该文件必须引用此描述符。所谓的文件描述符是一个低级的正整数。最前面的三个文件描述符(0,1,2)分别与标准输入(stdin),标准输出(stdout)和标准错误(stderr)对应,如下表。
文件描述符 | 用途 | POSIX名称 | stdio流 |
---|---|---|---|
0 | 标准输入 | STDIN_FILENO | stdin |
1 | 标准输出 | STDOUT_FILENO | stdout |
2 | 标准出错 | STDERR_FILENO | stderr |
正常情况下,程序在开始运行之前,由shell准备好这3个文件描述符。更准确的说法是,程序继承了shell文件描述符的副本,一般是指向shell所在的终端。当然了,可以通过在shell中对输入/输出进行重定向或者在程序启动后关闭并重新打开文件描述符,修改文件描述符指向。
在linux系统中,内核维护了三个数据结构,分别是进程级文件描述符表、系统级打开文件表和文件系统i-node表。
文件描述符表
内核为每个进程维护一个文件描述符表,该表每一条目都记录了单个文件描述符的相关信息,包括:
- 控制标志(flags),目前内核仅定义了一个,即close-on-exec
- 打开文件描述体指针
打开文件表
内核对所有打开的文件维护一个系统级别的打开文件描述表(open file description table),简称打开文件表。表中条目称为打开文件描述体(open file description),存储了与一个打开文件相关的全部信息,包括:
- 文件偏移量(file offset),调用read()和write()更新,调用lseek()直接修改
- 访问模式,由open()调用设置,例如:只读、只写或读写等
- i-node对象指针
i-node表
每个文件系统会为存储于其上的所有文件(包括目录)维护一个i-node表,单个i-node包含以下信息:
- 文件类型(file type),可以是常规文件、目录、套接字或FIFO
- 访问权限
- 文件锁列表(file locks)
- 文件大小
i-node存储在磁盘设备上,内核在内存中维护了一个副本,这里的i-node表为后者。副本除了原有信息,还包括:引用计数(从打开文件描述体)、所在设备号以及一些临时属性,例如文件锁。
复制与关闭
- 文件描述符的复制和重定向非常简单,使用dup()系统调用即可完成。在shell中,使用>即可进行重定向。
流程如下:
- 打开目标文件,返回文件描述符n;
- 关闭文件描述符1;
- 调用dup将文件描述符n复制到1;
- 关闭文件描述符n;
- 在程序中使用fork()创建子进程时,父进程中已经打开的fd也会自动在子进程中打开。子进程可以直接对这些文件进行操作。
此时,需要分别在子进程和父进程中关闭fd。一般父进程创建子进程后,父进程会直接关闭掉fd,子进程处理完成后再关闭fd。
- 使用unix域套接字也可以进行文件描述符的传递,但从一个进程传递到另一个进程后,fd可能会发生变化。
注意使用完毕后,分别关闭fd。
网友评论