linux 下 I/O 多路复用初探
本文内容整理自B站up主 free-coder 发布的视频:【并发】IO多路复用select/poll/epoll介绍
引入
一般来讲,服务器在处理IO请求(一般指的是socket编程)时,需要对socket的数据进行 accept, recv, send 等操作。
这些操作都是阻塞式的系统调用,线程会在调用处阻塞,等待OS返回。如果这时,目标socket还没做好准备,那么线程会一直处在waiting状态。这就是这种原始模式的致命缺点:线程阻塞被阻塞的时候,只能干等着,无法处理后续的其他客户端请求。
于是,为了高效的利用服务器的硬件资源,为了不让其他客户端干着急,大家想出了多进/线程IO模型——“每一个IO请求,交由一个执行体(进程/线程)处理”。
然而进/线程不能无限制地开辟,因为执行体创建,需要占用内存资源,执行体的切换也需要消耗CPU资源。过多的执行体会造成服务器整体吞吐量的下降,无法支撑大规模的IO请求。
为了避免上述的进/线程频繁切换问题,于是人们想到是否可以把所有的IO请求,都交由1个执行体操作?于是引入了IO多路复用的模型。(我对“多路复用”这个词的理解,就是“多路IO请求数据流,重复使用1个执行体收发”,类比于通信中的“多个信号复用同一个信道”)
多路复用(Multiplexing)的简单实现
设想一下,如果由我们自己实现单执行体操作所有IO的代码,我们可以怎么做呢?
见下面伪代码:
int* fds[n];
// 死循环,轮询各个fd,检查是否有数据
while (1) {
for (int i = 0; i < n; i++) {
if (fds[i] is ready) {
handle(fds[i]);
}
}
}
上述代码中,while 循环内部是不停地对 fds 列表进行遍历轮询,针对每一个fd,都会检查其状态(如:是否有网络数据到达等),这个操作是一般会是一个系统调用(因为fd资源的管理一般是由操作系统来维护的,用户无法避开操作系统内核去取得fd的一些状态)。
由于大多数时间,fd的状态都是空闲的,所以上边轮询代码会导致大量的无效查询,导致CPU空转,浪费了服务器算力。
为了解决上述问题,linux的开发者们想出了一种 “select” 模型。
Multiplexing 之 select
先来看一下 select man page 的描述:
select() allows a program to monitor multiple file descriptors,
waiting until one or more of the file descriptors become "ready"
for some class of I/O operation (e.g., input possible). A file
descriptor is considered ready if it is possible to perform a
corresponding I/O operation (e.g., read(2), or a sufficiently
small write(2)) without blocking.select() can monitor only file descriptors numbers that are less
than FD_SETSIZE;
select 让调用者可以监控多个文件描述符(即相应的网络socket)的状态。当 select 被调用时,调用线程会阻塞在此处,直到有 >= 1 个文件描述符就绪之后,select 系统调用才会返回。这里,“就绪”,意思是,该文件描述符可以被无阻塞地、顺畅地读取,或写入。select 能监控的文件描述符,其编号须小于 FD_SETSIZE (一般是1024),否则select的结果将是未定义的。
上边的描述中,有以下几个要点:
- select 可以监控多个文件描述符,这里,需要给select传递一个文件描述符集合,以告知OS去监控哪些描述符;
- select 是阻塞的,仅当有文件描述符就绪之后,才会返回;
- 只要 select 返回了,那么必有 >= 1 的文件描述符是可以无阻塞地读or写的;
- select 监听的描述符编号须小于 FD_SETSIZE (一般是1024);
下面,我们来看一组 man page 上的示例:
fd_set rfds; // <-- 声明一个要监听的文件描述符集合 file-descriptors set
struct timeval v;
int retval;
// 监听 stdin (fd:0)
FD_ZERO(&rfds); // <- 重置监听集合
FD_SET(0, &rfds); // 将 fd:0 置位,即 rfds 中,代表 fd:0 的那一位被设置为 1
// 等待5秒
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv); // select (nfds, readfds, writefds, exceptfds, timeout)
// nfds 的值为: readfds,writefds,exceptfds 3个集合中,最大的文件描述符编号,再加1
// select 会根据文件描述符的状态,改写 rfds 中的标志位,如果目标描述符未就绪,那么对应的 rfds 中的标志位会被置零
if (retval == -1)
perror("select()");
else if (retval)
printf(Data is available now.\n"); // FD_ISSET(0, &rfds) ,检测 fd:0 是否置位,会返回 1
else
printf("No data within five seconds.\n");
上边的代码中,做了这么几件事:
- 声明一个fd_set,和超时时间tv;
- 初始化 fd_set, 并对 fd_set 中表示 fd:0 的位置打上标记(置位,表明调用者要监听 fd:0 的IO事件)
- 调用 select,这里,OS 会监听 fd_set 中被标记的 fd,一旦有1或多个fd就绪,就对 fd_set 中重新置位,未就绪的fd,fd_set 中的对应标志位会被 OS 置零。(这里,OS对 fd_set 集合进行了覆盖性修改)
- 检查 select 返回值,判断目标fd是否有数据
接下来,我们看一个网络编程的例子:
... // 此处是绑定&监听socket的代码
for (i = 0; i < 5; i++) {
memset(&client, 0, sizeof(client));
addrlen = sizeof(client);
fds[i] = accept(sockfd, (struct sockaddr*) &client, &addrlen); // <-- 此处,fds 是描述符数组
if (fds[i] > maxfd) maxfd = fds[i]; // <-- maxfd 是最大fd编号
}
while (1) {
FD_ZERO(&rset); // <-- 此处,rset 是 readyset “读取”文件描述符集合
for (i = 0; i < 5; i++) {
FD_SET(fds[i], &rset); // <-- 对每个要监听的fd,都在 rset 相应标志位上置位一下
}
select(max+1, &rset, NULL, NULL, NULL);
for (i = 0; i < 5; i++) {
if (FD_ISSET(fds[i], &rset)) {
... // 此处,处理 fds[i] 上的数据
}
}
}
可以发现,代码主体的 while 循环中,主要做了3件事:
- 重置&重新初始化 fd_set;(因为每次 select 调用之后,OS 都会覆盖性修改 fd_set 的标志位);
- select;
- 循环遍历 fd_set,找出返回的集合中,就绪的 fd,并处理。
select 的编程相对来说,较好地实现了“单执行体同时处理多路IO”地目标。但是也有着如下的缺点:
- 监听的IO源(即 fd)数量有限(默认1024个)
- OS 会对 fd_set 进行覆盖性修改,所以:
- 每次 select 都需要先从用户内存空间,将 fd_set 完整得拷贝到内核空间。返回时,再从内核空间把OS修改之后的 fd_set 拷贝回用户内存空间;
- fd_set 被 OS 修改过,所以每次 select 之前,用户代码必须重新初始化 fd_set,把要监听的 fd 设置上。
- select 返回后,用户代码中,需要循环遍历整个 fd_set,才知道哪些 fd 可以被处理。
针对上述缺点的 1、2.2,人们提出了优化后的方案——poll
Multiplexing 之 poll (select 优化版)
先来看一下 poll man page 的描述:
poll() performs a similar task to select(2): it waits for one of
a set of file descriptors to become ready to perform I/O.
和 select 方式一样,poll 也是阻塞式的系统调用,仅当有 >= 1 个fd就绪后,poll才会返回。但是 poll 放弃了 fd_set 这一用位图表示监听fd集合的数据结构,而改用了 pollfd 数组(见下方代码):
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
结构体 pollfd 中包含三个字段: fd,存储对应的文件描述符编号;events 存储调用者感兴趣的 IO 事件标记;revents,由 OS 负责设置,存储 fd 当前就绪的 IO 事件标记.
poll 也破除了 select 的 fd_set 位图“fd编号必须小于 FD_SETSIZE ”的限制,理论上,只要计算机硬件和操作系统允许,可以有无限制数量的 pollfd。
同时,由于 pollfd 结构体设定了 revents 字段,因此 OS 可以在不“覆盖性修改”的情况下,把 fd 的状态传递给用户空间,因而免除了 select 方案中“每次都要重新初始化 fd_set 标志位”的麻烦。
以下是 poll 的网络编程示例:
for (i = 0; i < 5; i++) {
memset(&client, 0, sizeof(client));
addrlen = sizeof(client);
pollfds[i].fd = accept(sockfd, (struct sockaddr*) &client, &addrlen);
pollfds[i].events = POLLIN; // 设置该fd要监听的事件类型为读取(POLLIN)
}
while (1) {
poll(pollfds, 5, 500);
for (i = 0; i < 5; i++) {
if (pollfds[i].revents & POLLIN) {
... // 处理可读取数据
}
}
}
对比 select 的代码可以发现, 程序主体的 while 循环中,少了每次对要监听的fd集合进行置位的操作。但是,poll 还是会在用户内存空间和内核内存空间来回地复制 pollfds 数组,且 poll 返回之后,用户程序还是需要对全部 pollfd 数组进行遍历,才能找到IO请求就绪的 fd 。
对于上述两点不足,人们又提出了一个改进方案,这就是 epoll。
Multiplexing 之 epoll (poll 优化版)
epoll man page 上是这么描述的:
The epoll API performs a similar task to poll(2): monitoring
multiple file descriptors to see if I/O is possible on any of
them. The epoll API can be used either as an edge-triggered or a
level-triggered interface and scales well to large numbers of
watched file descriptors.
epoll 同 poll 一样,也是监听多个(数量可以很大)文件描述符,以检查它们是否有IO事件就绪。同时,epoll 还支持“边缘触发”和“水平触发”两种方式。
“边缘触发”的意思是:IO 的读写缓冲区状态变化时(如由不可读->可读),触发相应事件,用来监听“变化”;“水平触发”意思是:IO 的读写缓冲区处于可读(可写)状态时,持续触发可读(可写)事件,用来监听“当前状态”。
The central concept of the epoll API is the epoll instance, an
in-kernel data structure which, from a user-space perspective,
can be considered as a container for two lists:
The interest list (sometimes also called the epoll set): the
set of file descriptors that the process has registered an
interest in monitoring.The ready list: the set of file descriptors that are "ready"
for I/O. The ready list is a subset of (or, more precisely, a
set of references to) the file descriptors in the interest
list. The ready list is dynamically populated by the kernel as
a result of I/O activity on those file descriptors.
epoll 的核心概念:epoll 实例,是一种内核中的数据结构,从用户态角度看,可以把 epoll 实例视为两张列表:
- 兴趣表(也叫 epoll 集合):用户程序注册的,需要 epoll 去监听的文件描述符集合;
- 就绪表:IO 事件已就绪的fd集合。就绪表是兴趣表的子集(准确说,是兴趣表中fd的引用,的集合)
由于就绪表的存在,每次 epoll 返回的时候,就不用把所有注册的fd都复制一遍,相应的,用户程序也不用把所有的fd都遍历一遍。epoll 只返回 IO 事件就绪的fd,用户程序也只需处理这些fd。极大地方便了用户程序的编写和管理。
下边是 epoll 网络编程的示例:
struct epoll_event readyList[5]; // epoll 实例要返回的“就绪列表”
int epfd = epoll_create(10); // 参数 10,在内核版本2.6.8之后无意义。但是必须传,切须>0
for (i = 0; i < 5; i++) {
static sturct epoll_event ev;
memset(&client, 0, sizeof(client));
addrlen = sizeof(client);
ev.data.fd = accept(sockfd, (struct sockaddr*) &client, &addrlen);
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev); // 向 epoll 实例中的兴趣表注册该fd的 IO 事件
}
while(1) {
nfds = epoll_wait(epfd, readyList, 5, 10000); // epoll_wait返回当前就绪列表的fd数量
for (i = 0; i < nfds; i++) {
...// 挨个处理 readyList[i] 的 IO 事件
}
}
结束
select, poll, epoll, 都是同步阻塞的方式,对IO进行了多路复用,它们是不同历史时期逐个发展出来的。了解它们各自出现的背景,以及相应的不足,再去审视它们设计细节,会容易理解得多。
linux 下 I/O 多路复用初探的更多相关文章
- Linux下Power Management开发总结
本文作为一个提纲挈领的介绍性文档,后面会以此展开,逐渐丰富. 1. 前言 在 <开发流程>中介绍了PM开发的一般流程,重点是好的模型.简单有效的接口参数.可量化的测试环境以及可独性强的输出 ...
- linux下多路复用模型之Select模型
Linux关于并发网络分为Apache模型(Process per Connection (进程连接) ) 和TPC , 还有select模型,以及poll模型(一般是Epoll模型) Select模 ...
- Linux下MakeFile初探
make是linux下的编译命令,用于编译和生成Linux下的可执行文件.这个命令处理的对象是Makefile,makefile等.由于make的强大解析能力,makefile文件的编写也变得极为简单 ...
- (转)Linux下C++开发初探
1.开发工具 Windows下,开发工具多以集成开发环境IDE的形式展现给最终用户.例如,VS2008集成了编辑器,宏汇编ml,C /C++编译器cl,资源编译器rc,调试器,文档生成工具, nmak ...
- Linux下5种IO模型的小结
概述 接触网络编程,我们时常会与各种与IO相关的概念打交道:同步(Synchronous).异步(ASynchronous).阻塞(blocking)和非阻塞(non-blocking).关于概念的区 ...
- Linux下的IO模式
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间.所以说,当一个read操作发生时,它会经历两个阶段:1. 等待数据准 ...
- linux下epoll如何实现高效处理百万句柄的
linux下epoll如何实现高效处理百万句柄的 分类: linux 技术分享 2012-01-06 10:29 4447人阅读 评论(5) 收藏 举报 linuxsocketcachestructl ...
- 八、Linux下的网络服务器模型
服务器设计技术有很多,按使用的协议来分有TCP服务器和UDP服务器,按处理方式来分有循环服务器和并发服务器. 在网络程序里面,一般来说都是许多客户对应一个服务器,为了处理客户的请求,对服务端的程序就提 ...
- Linux 下的五种 IO 模型
概念说明 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方).操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的 ...
随机推荐
- 3、基于Python建立任意层数的深度神经网络
一.神经网络介绍: 神经网络算法参考人的神经元原理(轴突.树突.神经核),在很多神经元基础上构建神经网络模型,每个神经元可看作一个个学习单元.这些神经元采纳一定的特征作为输入,根据自身的模型得到输出. ...
- RHCE_DAY04
sed流式编辑器 sed是一个非交互的文本编辑器,实现的功能跟vim相同,主要是对文件内容进行输出.删除.替换.复制.剪切.导入.导出等功能 命令格式1:前置命令 | sed [选项] '[指令]' ...
- 为数不多的人知道的 Kotlin 技巧及解析
文章中没有奇淫技巧,都是一些在实际开发中常用,但很容易被我们忽略的一些常见问题,源于平时的总结,这篇文章主要对这些常见问题进行分析. 这篇文章主要分析一些常见问题的解决方案,如果使用不当会对 性能 和 ...
- String.trim的作用
Java的字符串处理方法trim是如何改变字符串的?下面是一个例子: @Test public void testTrim() { String test = "origin"; ...
- S3C2440—6.串口的printf实现
文章目录 一.框架 二.printf函数原理 2.1 printf的声明 2.2 参数解读 2.3 如何得到可变参数的值 2.4 解决变参的宏定义 2.5 完成printf函数的封装 三.结合UART ...
- noip29
T1 以下的LIS代指最长不降子序列. 考场看到取模,便想到了之前写过的Medain,取模操作让序列分布均匀,对应到本题上,既然是求LIS,那它应该是有循环节的,后来打表证实确实是有. 然后,我码了个 ...
- Java社区——个人项目开发笔记(一)
1.maven安装与测试 安装过程略,常用的maven命令行工具: mvn --version 查看maven版本 mvn compile 编译maven工程 mvn clean 删除编译文件 mvn ...
- 常用css样式(文字超出部分用省略号显示、鼠标经过图片放大、出现阴影)
文字超出部分用省略号显示: white-space: nowrap; /* 不换行 */ overflow: hidden; /* 超出部分不显示 */ text-overflow: ellipsis ...
- WPF---数据绑定(二)
一.绑定到非UI元素 上篇中,我们绑定的数据源均是派生自UIElement的WPF元素.本篇描述的绑定数据源是一个我们自定义的普通的类型. 注:尽管绑定的数据源可以是任意类型的对象,但Path必须总是 ...
- 从拟阵基础到 Shannon 开关游戏
从拟阵基础到 Shannon 开关游戏 本文中的定理名称翻译都有可能不准确!如果有找到错误的同学一定要联系我! 本文长期征集比较好的例题,如果有比较典型的题可以联系我 目录 从拟阵基础到 Shanno ...