本文内容整理自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的结果将是未定义的。

上边的描述中,有以下几个要点:

  1. select 可以监控多个文件描述符,这里,需要给select传递一个文件描述符集合,以告知OS去监控哪些描述符;
  2. select 是阻塞的,仅当有文件描述符就绪之后,才会返回;
  3. 只要 select 返回了,那么必有 >= 1 的文件描述符是可以无阻塞地读or写的;
  4. 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");

上边的代码中,做了这么几件事:

  1. 声明一个fd_set,和超时时间tv;
  2. 初始化 fd_set, 并对 fd_set 中表示 fd:0 的位置打上标记(置位,表明调用者要监听 fd:0 的IO事件)
  3. 调用 select,这里,OS 会监听 fd_set 中被标记的 fd,一旦有1或多个fd就绪,就对 fd_set 中重新置位,未就绪的fd,fd_set 中的对应标志位会被 OS 置零。(这里,OS对 fd_set 集合进行了覆盖性修改
  4. 检查 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件事:

  1. 重置&重新初始化 fd_set;(因为每次 select 调用之后,OS 都会覆盖性修改 fd_set 的标志位);
  2. select;
  3. 循环遍历 fd_set,找出返回的集合中,就绪的 fd,并处理。

select 的编程相对来说,较好地实现了“单执行体同时处理多路IO”地目标。但是也有着如下的缺点:

  1. 监听的IO源(即 fd)数量有限(默认1024个)
  2. OS 会对 fd_set 进行覆盖性修改,所以:
    • 每次 select 都需要先从用户内存空间,将 fd_set 完整得拷贝到内核空间。返回时,再从内核空间把OS修改之后的 fd_set 拷贝回用户内存空间;
    • fd_set 被 OS 修改过,所以每次 select 之前,用户代码必须重新初始化 fd_set,把要监听的 fd 设置上。
  3. 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 多路复用初探的更多相关文章

  1. Linux下Power Management开发总结

    本文作为一个提纲挈领的介绍性文档,后面会以此展开,逐渐丰富. 1. 前言 在 <开发流程>中介绍了PM开发的一般流程,重点是好的模型.简单有效的接口参数.可量化的测试环境以及可独性强的输出 ...

  2. linux下多路复用模型之Select模型

    Linux关于并发网络分为Apache模型(Process per Connection (进程连接) ) 和TPC , 还有select模型,以及poll模型(一般是Epoll模型) Select模 ...

  3. Linux下MakeFile初探

    make是linux下的编译命令,用于编译和生成Linux下的可执行文件.这个命令处理的对象是Makefile,makefile等.由于make的强大解析能力,makefile文件的编写也变得极为简单 ...

  4. (转)Linux下C++开发初探

    1.开发工具 Windows下,开发工具多以集成开发环境IDE的形式展现给最终用户.例如,VS2008集成了编辑器,宏汇编ml,C /C++编译器cl,资源编译器rc,调试器,文档生成工具, nmak ...

  5. Linux下5种IO模型的小结

    概述 接触网络编程,我们时常会与各种与IO相关的概念打交道:同步(Synchronous).异步(ASynchronous).阻塞(blocking)和非阻塞(non-blocking).关于概念的区 ...

  6. Linux下的IO模式

    对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间.所以说,当一个read操作发生时,它会经历两个阶段:1. 等待数据准 ...

  7. linux下epoll如何实现高效处理百万句柄的

    linux下epoll如何实现高效处理百万句柄的 分类: linux 技术分享 2012-01-06 10:29 4447人阅读 评论(5) 收藏 举报 linuxsocketcachestructl ...

  8. 八、Linux下的网络服务器模型

    服务器设计技术有很多,按使用的协议来分有TCP服务器和UDP服务器,按处理方式来分有循环服务器和并发服务器. 在网络程序里面,一般来说都是许多客户对应一个服务器,为了处理客户的请求,对服务端的程序就提 ...

  9. Linux 下的五种 IO 模型

    概念说明 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方).操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的 ...

随机推荐

  1. template.js模板工具案例

    案例一 1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset=&qu ...

  2. TCP可靠传输

    1. TCP 可靠性如何保证? 信道可靠:用三次握手.四次挥手保证连接正确: 数据正确:分区编号.校验和.超时重传: 传输控制:流量控制.拥塞控制 2. 重传机制 TCP可靠传输方式是序列号与确认应答 ...

  3. NOIP 模拟 $36\; \rm Cicada 与排序$

    题解 \(by\;zj\varphi\) 设 \(rk_{i,j}\) 表示第 \(i\) 个数最后在相同的数里排第 \(j\) 位的概率. 转移时用一个 \(dp\),\(dp_{i,j,0/1}\ ...

  4. NOIP 模拟 $21\; \rm Park$

    题解 \(by\;zj\varphi\) 首先,分析一下这个答案:本质上是求在一条路径上,选择了一些点,这些点的贡献是它周围的点权和 - 它上一步的点权 对于一棵树,可以先确定一个根,然后每条路径就可 ...

  5. 从零开始实现简单 RPC 框架 5:网络通信之序列化

    我们在接下来会开始讲网络通信相关的内容了.既然是网络通信,那必然会涉及到序列化的相关技术. 下面是 ccx-rpc 序列化器的接口定义. /** * 序列化器 */ public interface ...

  6. springboot 和spring cloud 博客分享

    spring boot 知识点总结 天狼星 https://www.cnblogs.com/wjqhuaxia/p/9820902.html spring cloud 知识点总结 姿势帝 https: ...

  7. C#录音从声卡

    原文   http://stackoverflow.com/questions/18812224/c-sharp-recording-audio-from-soundcard 我想从我的声卡(输出)录 ...

  8. [C#]c#中数据的同步加锁机制 的几种方法

    一,锁定机制最简单的做法就是使用锁定关键字Lock.Lock关键字英文中就是锁的意思,顾名思义就是为操作加上一把锁.它的语法如下: lock(lockObj){//加锁的代码段,一般是操作共同资源的代 ...

  9. 网络视频m3u8解密及ts文件合并

    网络视频m3u8解密及ts文件合并 参考了两篇博客: https://blog.csdn.net/weixin_41624645/article/details/95939510 https://bl ...

  10. 检测一个页面所用的时间的js

    window.onload = function () { var loadTime = window.performance.timing.domContentLoadedEventEnd-wind ...