I/O多路复用:在网络I/O中,用 1个或1组线程 管理 多个连接描述符。
如果有至少一个描述符准备就绪,就处理对应的事件
如果没有,就会被阻塞,让出CPU给其他应用程序运行,直到有准备就绪的描述符 或 超时
(当超时时间设置为 -1 时,表示永不超时)
I/O多路复用的优势
I/O多路复用的优势如下所示:
-
高效利用资源: 通过在单一线程中管理多个 I/O 操作,减少了线程创建和上下文切换的开销。
-
减少阻塞: 允许程序在等待 I/O 操作时继续处理其他任务,提高了应用的响应能力和吞吐量。
-
降低系统负担: 减少了对系统调用的频繁使用,尤其在处理大量连接时,性能优于传统的多线程或多进程模型。
-
简化编程模型: 使得程序可以以事件驱动的方式处理多个 I/O 操作,从而简化了复杂的并发编程模型。
一、select模式
int select(int nfds, //最大描述符值
fd_set *readfds, //读
fd_set *writefds, //写
fd_set *exceptfds, //异常
struct timeval *timeout); //超时时间
select支持Window和linux系统。他的流程如下:
-
设置文件描述符集: 程序通过
FD_SET
宏将感兴趣的文件描述符添加到读、写或异常事件的集合中。(默认会创建三种内置描述符:
0描述符:输入流
1描述符:输出流
2描述符:错误流 )
-
调用
select
函数: 程序调用select
,传入最大文件描述符值、读、写和异常文件描述符集以及超时时间。 -
阻塞等待:
select
会被阻塞,直到至少有一个文件描述符准备好进行 I/O 操作,或直到超时。 -
检查文件描述符状态:
select
返回后,程序通过FD_ISSET
宏检查哪些文件描述符的状态发生了变化。 -
处理事件: 根据
FD_ISSET
返回的结果,程序可以处理那些已经准备好的文件描述符。
(这里用可读事件举例子:
<1> 如果,接收到的描述符 == 服务器描述符,说明 服务器存在可读事件
也就是说,此时有客户端发出了新的连接,需建立一个新的连接
然后将新的客户端的文件描述符添加到集合中,并更新最大文件描述符值
<2> 如果,接收到的描述符 == 客户端描述符,说明 客户端有新消息
此时读出消息,再根据内容向客户端回传信息。
如果读出错误则关闭这条连接。 )
-
重置文件描述符集: 如果
select
被重新调用,文件描述符集必须在每次调用前重新设置
1.1 优缺点
优点:select方式是兼容性最广的,支持Windows和Linux
缺点:每次调用select函数,会将整个FD_SET(维护所有文件描述符的数据结构)
从 用户态 的堆 复制到 内核态的堆,以供内核态检查。系统开销大。
二、poll模式
//poll函数返回的是 “发生事件的文件描述符数量”
int poll(struct pollfd *fds, //描述符数组的指针
nfds_t nfds, //数组中描述符数量
int timeout); //超时时间
struct pollfd {
int fd; // 文件描述符
short events; // 需要监视的事件类型
short revents; // 实际发生的事件
};
2.1 监视事件类型
对于每个文件描述符,设置所需监视的事件类型events(如读、写或异常事件)。这些事件类型可以是:
POLLIN
:表示可以读取数据。
POLLOUT
:表示可以写入数据。
POLLERR
:表示发生了错误。
POLLHUP
:表示挂起(比如管道关闭)
POLLNVAL
:表示无效的文件描述符
2.2 检查真实发生事件
上述五种类型,本质上是五种宏定义,其实是各自取一位为1,其余为0。(比如,POLLIN为0x1,POLLOUT为0x4)
因此,当我们需要判断一个真实发生事件的类型是否是读取类型,可以通过以下语句:
if(fd[i].revents & POLLIN) {
//处理读取事件
}
三、epoll模式
epoll模式又叫做event poll模式,它与select、poll不同的是:
select返回的是一系列文件描述符,至于哪些要做哪种事件,会将整个FD_SET复制到内核态,然后在内核态检测,系统开销大。
epoll只是传递了一个epoll描述符,需要处理的文件描述符都可以通过这个epoll描述符找到,开销非常小。
2.1 epoll模式流程
1. 第一步:通过epoll_create函数,创建epoll对象,返回epoll描述符
//第一步:通过epoll_create函数,创建epoll对象,返回epoll描述符
int epollfd = epoll_create();
2. 第二步:通过create_ctl函数,将感兴趣的文件描述符添加进epoll对象的管理中
//第二步:通过create_ctl函数,将感兴趣的文件描述符添加进epoll对象的管理中
struct epoll_event event;
event.events = EPOLLIN; //假设事件为读取
event.data.fd = fd; //fd为感兴趣的文件描述符
create_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
struct epoll_event {
__uint32_t events; // 事件类型
epoll_data_t data; // 用户数据
};
union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
3. 第三步:通过epoll_wait函数,阻塞到至少有一个事件发生
//第三步:通过epoll_wait函数,阻塞到至少有一个事件发生
struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
/*
epollfd表示epoll描述符
events表示文件数组
MAX_EVENTS表示数组数量
-1表示阻塞
*/
4. 第四步:遍历events数组,处理发生的事件
//第四步:遍历events数组,处理发生的事件
for (int i = 0; i < nfds; ++i) {
if (events[i].events & EPOLLIN) {
// 处理可读事件
}
if (events[i].events & EPOLLOUT) {
// 处理可写事件
}
}
5. 第五步:可以通过epoll_ctl函数修改描述符的事件类型,或删除描述符
//修改事件类型
event.events = EPOLLOUT; // 修改关注的事件类型
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &event);
//删除文件描述符
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
6. 第六步:当不需要去监听时,通过close函数关闭epoll对象
//第六步:当不需要去监听时,通过close函数关闭epoll对象
close(epfd);