IO模型
1. 常见IO模型
- 同步阻塞I/O
- 同步非阻塞I/O
- I/O多路复用
- 信号驱动I/O
- 异步I/O
2 . 同步阻塞IO
每个客户端的连接从监听到数据读写都由一个线程完成,监听数据时,若无数据,会一直阻塞到接收到数据
3. 同步非阻塞IO
在同步阻塞IO的基础上,通过编程模型,让一个单独的线程负责全部的监听工作,监听到数据再另起线程将读写工作交给子线程
4. 多路复用的三个函数
单个线程同时监听多个fd,并在文件可写可读时得到通知,避免无效等待
4.1 select
4.1.1 函数定义
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数
- readfds:内核检测该集合中的文件描述符是否可读。如果想让内核检测某个IO是否可读,需要手动把文件描述符加入到该集合。
- writefds:内核检测该集合中的文件描述符是否可写。同readfds,需要手动把文件描述符加入到该集合。
- exceptfds:内核检测该集合中的文件描述符是否异常。同readfds,需要手动把文件描述符加入到该集合。
- nfds:以上三个集合中最大的文件描述符数值+1。例如集合是{0,1,5,10},那么maxfd就是11。
- timeout:用户线程调用select的超时时长。设置为null,表示如果线程会一直阻塞直到I/O事件发生;设置为非0的值,表示阻塞固定一段时间后返回;设置为0,表示检测完毕立即返回。
返回值
- 等于-1:表示调用失败
- 大于0:成功,返回集合中就绪的IO总个数
- 等于0:表示没有就绪的IO
fd_set的操作函数
// 将文件描述符fd从set集合中删除
void FD_CLR(int fd, fd_set *set);
// 判断文件描述符是否在set集合中
void FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到fd集合中
void FD_SET(int fd, fd_set *set);
// 将set集合中,所有文件描述符对应的标志位设置为0
void FD_ZERO(fd_set *set);
- select函数底层使用位图存储f存储文件描述符,以上函数均为对位图的操作
4.1.2 select执行逻辑
- 当用户线程调用select的时候,会将fd_set拷贝到内核中。(这里发生了一次复制)
- 然后内核会遍历文件描述符检查是否数据是否就绪。
- 若发现文件描述符数据就绪,返回就绪的文件描述符数量。
4.1.3 select的缺点
- select可以监听的文件描述符数量有上限。32位机默认上限为1024,64位机默认上限为2048。
- 每次调用select都需要将被监控的fds集合拷贝到内核中,高并发场景下这样的拷贝对于资源的消耗是很大的。
- select返回的时候,用户线程并不知道具体是哪些文件描述符就绪了,需要遍历被监听的文件描述符来检查。那么被监听的文件描述符越多,遍历检查的耗时越长。
4.1.4 文件描述符
- 一个进程最大的文件描述符数量
- 相关命令
ulimit -n命令查看当前一个进程最大文件描述符数量ulimit -n 2048修改当前会话的最大文件描述符数量cat /proc/sys/fs/nr_open获取最大文件描述符数量/etc/security/limits.conf永久修改最大文件描述符数量
- limits.conf文件配置详解
- 格式:
<domain> <type> <item> <value> - 详解
- domain: 代表限制的对象,对哪些用户生效。其可以是一个用户名,也可以是一个用户组的名字,可以使用通配符*和%。
- type: 仅仅有两种, soft和hard。 soft的限制的值不能超过hard限制的值。
- item: 代表可以限制的内容, 可以有下面的一些选项可以设置
- core - 显示coredump文件的大小 (单位KB)
- data - 最大数据大小 (单位KB)
- fsize - 最大文件大小 (单位KB)
- memlock - 最大锁定内存地址空间 (单位KB)
- nofile - 最大可以打开的文件描述符的数量
- rss - 最大驻留集大小 (单位KB)
- stack - 最大栈的大小 (KB)
- cpu - 最多CPU占用时间,单位为MIN分钟
- nproc - 最大可以打开的进程数量
- as - 地址空间限制 (KB)
- maxlogins - 用户可以同时登录的最大数量
- maxsyslogins - 系统最大允许的登录数量
- priority - 运行用户进程的优先级
- locks - 用户可以持有的文件锁的最大数量
- sigpending - 最大挂起信号数
- msgqueue - POSIX 消息队列使用的最大内存 (单位 bytes)
- nice - 允许的最大优先级提高到值 [-20,19]
- rtprio - 最大实时优先级
- value: 就是为item设置的具体的数值。
- nofile 即为文件描述符最大数量
- 配置:
tom hard nofile 10240 - 表示tom用户创建的每个进程的打开的最大的文件描述符数量不能超过硬限制10240
- 配置:
- type选项中hard和sort的差别
- 限制达到soft只记录警告日志,不影响功能;日志文件一般在
/var/log/messages - 限制达到hard会影响功能
- 限制达到soft只记录警告日志,不影响功能;日志文件一般在
- 格式:
- 相关命令
一个用户最多可以打开多少个文件描述符
- 一个用户最多可以打开多少个文件描述符 = 用户最大进程数 * 进程最大文件描述符数
- 用户最大进程数配置
ulimit -u 1024修改当前shell用户最大进程数为1024/etc/security/limits.conf添加hard nproc 131072永久修改- 修改
/etc/security/limits.d/90-nproc.conf文件 添加root soft nproc 131072 /etc/security/limits.d/目录下的配置会覆盖/etc/security/limits.conf中的配置cat /proc/sys/kernel/pid_max设置或查看操作系统总的进程数量
- 即用户打开的最大进程数 < hard limit < pid_max。
- 一个系统最多可以打开多少个文件描述符
cat /proc/sys/fs/file-max系统总限制,一般不会达到,可以检查修改
- 总结
- 一个进程可以打开的文件描述符的数量小于hard limit,而hard limit的值要小于nr_open。但是实际能打开的文件描述符的最大数量还和系统资源有关。
- 一个用户可以打开的文件描述符数量等于一个进程可以打开的文件描述符的数量* 一个用户最大可以打开的进程数量。
- 一个系统可以打开的文件描述符数量即所有用户的所有进程打开的文件描述符总数量受file-max限制。
4.2 poll
4.2.1 函数定义
int poll(struct pollfd *fds, unsigned nfds, int timeout);
函数参数
- fds:struct pollfd类型的数组,存储了待检测的文件描述符
- nfds:数组fds的大小
- timeout:指定poll函数的阻塞时长。等于-1表示阻塞直到IO就绪才返回;等于0表示不阻塞,不管是否有IO就绪,立即返回;大于0表示等待指定的毫秒数后返回。
函数返回值
- -1:失败
- 大于0:表示检测的集合中已就绪的文件描述符的总个数
4.2.2 和select的异同
- 文件描述符无上限
- 同样每次返回都需要遍历所有fd才能找到就绪的fd
4.2.3 poll函数的fd结构
poll的实现和select非常相似,只是描述fd集合的方式不同。poll使用pollfd结构来存储文件描述符的状态。
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 监控的事件 */
short revents; /* 监控事件中满足条件返回的事件 */
};
pollfd成员:
- fd:委托内核检测的文件描述符
- events:委托内核检测的fd事件(输入、输出、异常)
- revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果
4.3 epoll
4.3.1 epoll的接口的组成
三个函数
- epoll_create:创建一个epoll句柄
- epoll_ctl:向epoll对象中添加/修改/删除要管理的文件描述符
- epoll_wait:等待其管理的文件描述符上的IO事件
4.3.2 epoll_create函数
int epoll_create(int size);
- 功能:该函数生成一个epoll专用的文件描述符。
- 参数:用来告诉内核监听的数目有多大。参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
- 返回值:如果成功,返回epoll专用的文件描述符。否则失败返回-1。
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。注意,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
4.3.3 epoll_ctl函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl函数是epoll的事件注册函数。在调用epoll_wait之前,需要先调用该函数来注册需要监听的描述符及对应事件。
参数:
- epfd:epoll专用的文件描述符,epoll_create的返回值。
- op:表示动作用3个宏来表示。EPOLL_CTL_ADD,注册新的fd到epfd中;EPOLL_CTL_MOD,修改已经注册的fd的监听事件;EPOLL_CTL_DEL,从epfd中删除一个fd。
- fd:需要监听的文件描述符
- events:告诉内核需要监听的事件。
返回值:0表示成功,-1表示失败。
4.3.4 epoll_wait函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_wait函数用于等待监听事件的发生。
参数:
- epfd:epoll专用的文件描述符,epoll_create的返回值。
- events:分配好的epoll_event结构体数组,epoll会将发生的事件赋值到events数组中。
- maxevents:告诉events有多少个。
- timeout:超时时间,单位为毫秒。-1表示阻塞直到IO事件发生。
返回值:
- 等于0,表示已超时。
- 大于0,表示需要处理的事件数量。
- 等于-1,表示失败。
4.3.5 epoll高效的原因
- epoll_create在内核创建了一个事件监听表,epoll_ctl注册监听的fd和对应事件,调用epoll_wait时,节省了fd集合从用户空间到内核空间的拷贝
- epoll就绪的fd会被放进一个ready_list的双向链表中,不再需要遍历所有被监听的fd找到就绪fd
4.4 总结
- select支持最广,但性能也最差
- poll解决了select最大文件描述符的限制,但没解决需要轮询所有fd才能找到就绪fd的问题
- epoll解决select和poll的所有问题
4.5 判断系统是否支持poll和epoll
#include <stdio.h>
#include <sys/poll.h>
#if defined(__linux__)
#include <sys/epoll.h>
#endif
int main() {
// 检查是否支持 poll
#if defined(POLLIN)
printf("poll is supported\n");
#else
printf("poll is not supported\n");
#endif
// 检查是否支持 epoll
#if defined(__linux__) && defined(EPOLLIN)
printf("epoll is supported\n");
#else
printf("epoll is not supported\n");
#endif
return 0;
}
编译执行后输出结果
5. 信号驱动
信号驱动IO是与内核建立SIGIO得信号 关联并设置回调,当内核有FD就绪时,就会发出SIGIO信号通知用户进程,期间用户进程可以执行其他业务,无需阻塞等待。
参考文档
文档信息
- 本文作者:Ling He
- 本文链接:https://GoggleHe.github.io/2024/03/05/IO/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)