【Linux网络编程】多路转接I/O(一)select,poll
目录
一,I/O多路转接之select
1,初识select
2,select函数原型
3,理解select执行过程
4,socket特点
5,select缺点
6,代码示例
二, I/O多路转接之poll
1,poll的作用和定位
2,认识poll接口
3,poll的优点
4,poll的缺点
5,代码示例
总结
核心概念:多路转接I/O允许单线程同时监控多个文件描述符(fd),当某个fd就绪(可读/可写/异常)时通知程序处理。相比于多线程/多进程方案,能显著降低资源消耗。
一,I/O多路转接之select
1,初识select
首先IO=等+拷贝,其中等待是等底层数据准备好了,然后通过拷贝拿到上层使用。IO工作慢的原因就在于等待时间太长,为了提高效率,我们需要减少等待所占的时间。
- 我们借助select系统调用来完成等待的工作,一次等待(select)传入多个文件描述符,也就是让select去帮我们监视这些文件描述符的状态变化。
- 程序会停在 select这里等待,知道被监视的文件描述符有一个或多个 发生了状态变化。状态变化可以理解为某个事件就绪了(比如可写/可读/异常等)。
2,select函数原型
select函数原型如下:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数解释:
(1)参数nfds是需要监视的文件描述符中,最大文件描述符值+1;
(1)readfds,writefds,exceptfds:分别对应需要检测的可读文件文件描述符的集合,可写文件描述符的集合,异常文件描述符的集合。它们的类型是fd_set。它的结构如下:
其实这个结构就是一个 整数数组,更严格的说,是一个 "位图",使用位图中的位来表示要监视的文件描述符。
eg:0000 0000,将该位图设置为0001 1111,表示select要关心0,1,2,3,4这些文件描述符的某个事件。如果将该位图放到select的第二个参数,表示关心读事件是否就绪,放到第三个参数表示关心写事件是否就绪,放到第四个参数表示异常是否就绪。
操作系统不允许我们直接对这个位图进行修改,使用提供了一组fd_set的接口,用来比较方便的操作位图:
void FD_CLR(int fd, fd_set *set);// 用来清除描述词组 set 中相关fd 的位
int FD_ISSET(int fd, fd_set *set);// 用来测试描述词组 set 中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set);// 用来设置描述词组 set 中相关fd 的位
void FD_ZERO(fd_set *set);// 用来清除描述词组 set 的全部位
(3)参数timeout为timeval结构体类型,用来设置select的等待时间。
timeval结构:
timeval 结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为 0。
参数timeout取值:
NULL:则表示select将一直阻塞,直到某个文件描述符上发生了事件才会返回。
特定的时间值:如果在指定的时间段里没有事件发生,select 将超时返回。
select函数返回值:
执行成功则返回文件描述符状态已改变的个数(也就是那些文件描述符对应的事件就绪了)。
如果返回0代表已超过timeout时间,没有就绪的文件描述符。
当有错误发生时则返回-1,然后errno被设置。
错误值可能为:
- EBADF 文件描述符为无效的或该文件已关闭
- EINTR 此调用被信号所中断
- EINVAL 参数 n 为负值
- ENOMEM 核心内存不足
3,理解select执行过程
理解 select 模型的关键在于理解 fd_set,为说明方便,取 fd_set 长度为 1 字节,fd_set中的每一 bit 可以对应一个文件描述符 fd。则 1 字节长的 fd_set 最大可以对应 8 个 fd。
(1)执行 fd_set set; FD_ZERO(&set);则 set 用位表示是 0000,0000。
(2)若 fd=5,执行 FD_SET(fd,&set);后 set 变为 0001,0000(第 5 位置为 1)
(3)若再加入 fd=2,fd=1,则 set 变为 0001,0011。
(4)执行 select(6,&set,0,0,0)阻塞等待。
(5)若 fd=1,fd=2 上都发生可读事件,则 select 返回,此时 set 变为0000,0011。注意:没有事件发生的 fd=5 被清空。
以关心读事件为例:
可以看出:用户定义的一个 fd_set对象,内核是会进行修改的。用户通过修改后的fd_set对象,来判断哪些文件描述符就绪了。
4,socket特点
可监控的文件描述符个数取决于 sizeof(fd_set)的值。
-
使用fd_set位图管理 FD
-
支持跨平台(Linux/Windows)
-
最大 FD 数受限(通常 1024)
-
每次调用需重新设置 FD 集合
5,select缺点
- 每次调用 select, 都需要手动设置 fd 集合, 从接口使用角度来说也非常不便。
- 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大。
- 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大,select 支持的文件描述符数量太小。
6,代码示例
#include <sys/select.h>
#include <iostream>
#include <unistd.h>
#include <vector>int main()
{// 关心读事件fd_set read_fds;int max_fd = STDIN_FILENO; // 标准输入 (fd=0),将来这里可以进行更改,比如换成网络套接字(监听套接字)// 使用辅助数组来保存要关心的fdstd::vector<int> active_fds = {STDIN_FILENO};while (true){// 清空集合FD_ZERO(&read_fds);// 将要关心的fd设置到集合中,同时求出最大的fdfor (int fd : active_fds){FD_SET(fd, &read_fds);if (fd > max_fd)max_fd = fd;}// 设置1秒超时timeval timeout{.tv_sec = 1, .tv_usec = 0};int ready = select(max_fd + 1, &read_fds, nullptr, nullptr, &timeout);if (ready < 0){//select出错perror("select error");break;}else if (ready == 0){//select超时std::cout << "Timeout occurred!\n";continue;}// 检查就绪的FDfor (int fd : active_fds){if (FD_ISSET(fd, &read_fds)){//如果文件描述符是标准输入(将来这里更改成网络套接字)if (fd == STDIN_FILENO){std::string input;std::getline(std::cin, input);std::cout << "STDIN: " << input << "\n";}// 可扩展其他FD处理//......}}}return 0;
}
上述例子是检查标准输入的可读事件是否就绪,可以扩展到网络套接字,这部分代码不在这里展示,可以在下面的仓库链接中查看,该部分代码是使用select多路转接实现的以一个echo server:
Select_Server · 小鬼/linux学习 - 码云 - 开源中国
通过select多路转接,就可以在单线程下,处理多个网络请求。
二, I/O多路转接之poll
1,poll的作用和定位
poll只负责等,一次可以等多个fd,事件就绪,就可以对上层进行事件通知。和select一样。
2,认识poll接口
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
第三个参数表示超时时间。
第一个参数是一个数组,类型为struct pollfd,第二个参数表示元素的个数。结构如下:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
这是一张事件位图,第一个成员填写要关心哪个文件描述符,第二个和第三个是事件,表示关心哪些事件。
events和revents的取值:
上面的值都是通过宏定义出来的,其中只有一个比特位为1,events|=POLLIN,表示关心读事件,events|=POLLIN|POLLOUT表示 关心读事件和写事件。通过按位或运算,告诉内核要关心哪些事件。
上层用户在调用的时候,fd和events有效:用户告诉内核,你要帮我关心fd上的events事件。
poll成功返回时,fd和revents有效:内核告诉用户,你要我关心的fd上的events事件,已经就绪了,存储在revents中。
3,poll的优点
不同于select使用三个位图来表示三个fd_set的方式,poll使用一个pollfd的指针实现。
pollfd结构包含了要监视的events和已经就绪的events,不再使用select"参数-值"的传递方式,将二者分离了,使用起来更方便。
poll并没有最大的数量限制(但是数量过大后,性能也会下降)
Linux 专有(Windows 不支持)
4,poll的缺点
和select一样,poll返回后,需要轮询pollfd来获取就绪的文件描述符。
每次调用poll,都需要将大量的pollfd结构从用户拷贝到内核中。
同时连接的大量客户端在某一时刻,可能有很少的处于就绪状态,因此随着监视的文件描述符的增多,其效率也会下降。
5,代码示例
#include <poll.h>
#include <iostream>
#include <unistd.h>
#include <vector>int main()
{std::vector<pollfd> poll_fds;poll_fds.push_back({STDIN_FILENO, POLLIN, 0}); // 监控标准输入while (true){int ready = poll(poll_fds.data(), poll_fds.size(), 1000); // 1秒超时if (ready < 0){perror("poll error");break;}else if (ready == 0){std::cout << "Timeout occurred!\n";continue;}for (auto &pfd : poll_fds){//检查并处理标准输入事件if (pfd.revents & POLLIN){if (pfd.fd == STDIN_FILENO){std::string input;std::getline(std::cin, input);std::cout << "STDIN: " << input << "\n";}// 可扩展其他FD处理}//检查并处理标准输出事件//......//检查并处理其他事件//.......//检查并处理错误事件if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)){std::cerr << "Error on fd: " << pfd.fd << "\n";// 通常移除失效FD}}}return 0;
}
同理,可以扩展到网络部分 ,使用poll实现多路转接,仓库连接:
Poll_Server · 小鬼/linux学习 - 码云 - 开源中国
总结
二者对比
特性 | Select | Poll |
---|---|---|
最大 FD 数 | 受限(FD_SETSIZE) | 无限制 |
性能(FD 量大时) | O(n) 扫描 | O(n) 扫描 |
事件类型 | 仅读/写/异常 | 更丰富(POLLRDNORM 等) |
平台支持 | 跨平台 | Linux 专用 |
FD 重用 | 每次需重置 fd_set | 可复用 pollfd 数组 |
使用建议:
-
Select:FD 数量少(<1024),需跨平台
-
Poll:FD 数量多,仅需支持 Linux
注意:两种模型都采用 水平触发(LT) 模式,即只要 FD 处于就绪状态,每次调用都会返回该 FD