本文内容整理自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. SpringBoot开发十八-显示评论

    需求介绍 显示评论,还是我们之前做的流程. 数据层:根据实体查询一页的评论数据,以及根据实体查询评论的数量 业务层:处理查询评论的业务,处理查询评论数量的业务 表现层:同时显示帖子详情数据时显示该帖子 ...

  2. netty系列之:netty中的懒人编码解码器

    目录 简介 netty中的内置编码器 使用codec要注意的问题 netty内置的基本codec base64 bytes compression json marshalling protobuf ...

  3. Nmap 常用命令及抓包分析

    1.主机发现:主机发现也称为ping扫描,但是Nmap中主机发现的技术已经不是简单的采用ping工具发送简单的ICMP回声请求报文.用户完全可以通过使用列表扫描(-sL)或者通过关闭ping(-P0) ...

  4. cobaltstrike 框架简述

    关于cobalt strike,火起来也有好几年了,首先感谢大佬们慷慨相助愿意在网上分享和翻译相关资料,让这么好的渗透测试框架工具被更多人知道 那就来整理一下在使用这个框架的过程中我认为需要了解的小知 ...

  5. VRRP的基本配置

    一.实验拓扑 二.实验编址 三.实验步骤: 1.设置PC的IP等信息 2.启动设备(全选) 3.设置路由器端口ip 4. 配置ospf网络 查看: 5.配置VRRP协议 配置完成后查看: 6.验证主备 ...

  6. .NET第三方补丁工具(Visual Patch)常用手册

    SetupFactory简介 这是Indigo Rose(蓝玫瑰)公司开发的一套打包-补丁解决方案的补丁工具,相比Setup Factory,他的知名度似乎不太高,网上也很少找到相关资料,但是真的很简 ...

  7. centos7.5 安装jdk环境

    最新下载地址 历史下载地址 [root@manage ~]# mkdir /application/ [root@manage ~]# tar -xf jdk-8u112-linux-x64.gz - ...

  8. IOC概念和原理:BeanFactory 接口与ApplicationContext

    IOC(概念和原理)1.什么是 IOC(1)控制反转,把对象创建和对象之间的调用过程,交给 Spring 进行管理(2)使用 IOC 目的:为了耦合度降低(3)做入门案例就是 IOC 实现2.IOC ...

  9. SpringCloud War 包部署导致服务未正常注册到 Nacos 问题

    转载地址:https://blog.csdn.net/qq_28379809/article/details/103773149  

  10. 微信小程序 errMsg: "navigateTo:fail webview count limit exceed"

    返回过多 用wx.redirectTo或者wx.reLaunch 解决