创作不易,本篇文章如果帮助到了你,还请点赞 关注支持一下♡>𖥦<)!!
主页专栏有更多知识,如有疑问欢迎大家指正讨论,共同进步!
🔥Linux系列专栏:Linux基础 🔥给大家跳段街舞感谢支持!ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ
在监听 socket 时,需要分配多个线程/进程维护多个 socket 连接,I/O 多路复用技术就是用来使用一个进程来监听维护多个 socket 连接
多路:
I/O 状态:可读、可写
复用:
使用一个线程/进程监听处理 I/O 事件,复用多个 socket 请求
一、select 模型
select 原理:
-
1.创建一个文件描述符集合
fd_set set
0 不监听 1 监听 -
2.设置文件描述符的状态
- FD_ZERO(&set); 初始化监听集合,将位码初始化为 0
- FD_SET(int sockfd,&set); 将 sockfd 在集合中对应的位设置成 1
- FD_CLR(int sockfd,&set); 将 sockfd 在集合中对应的位设置成 0
- int code = FD_ISSET(int sockfd,&set); 返回 sockfd 在文件描述符集合中的位码
-
3.调用 select 函数,传入文件描述符集合 set
select 函数:
#include <sys/select.h>
#include <unistd.h>
int ready = select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* errorfds,struct timeval* timeout);
参数:
- nfds: 需要监视的最大文件描述符值加 1
- readfds:监听读事件,设为 NULL 不监听
- writefds:监听写事件,设为 NULL 不监听
- errorfds:异常事件
- timeout:阻塞时间,设为 NULL 为阻塞监听,定义 timeval 结构体将成员设置为 0 为非阻塞。定时阻塞支持微妙级别定时
返回值:返回就绪的 socket 数量
select 函数会将整个文件描述符集合 set 拷贝到内核中,让内核检查是否有读/写事件,内核遍历文件描述符集合,
如果有事件发生,内核将对应的 socket 标记位可读/可写,然后将整个文件描述符集合 set 拷贝回用户空间,用户再遍历文件描述符集合找到就绪的 socket 进行后续处理
select 的缺点:
- 1.Linux 平台受最大文件描述符数量限制,select 的最大监听数为 1024
- 2.select 监听就绪后只返回就绪的数量,需要用户遍历找到就绪的 socket
- 3.监听集合会在监听到就绪后被修改,需要用户自行分离传入传出设置
- 4.轮询监听,轮询数量大, CPU 处理性能下降
- 5.拷贝和挂载开销大
- 6.设置监听不灵活,无法对不同的 socket 设置不同的监听类型
二、poll 模型
poll 监听事件种类更丰富,对监听和就绪数组进行了传入传出分离
poll 不受最大文件描述符数量限制,支持用户自定义长度结构体数组作为集合
#include <poll.h>
int ready = poll(struct pollfd* fds, int nfds, int timeout);
参数:
- fds:监听数组
- struct pollfd listen_array[4096]; //监听数组
- listen_array[0].fd = sockfd; //监听 sockfd,
-1
取消监听 - listen_array[0].events = POLLIN|POLLOUT|POLLERR; //设置监听事件
- listen_array[0].revents; //如果监听的 socket 就绪,系统将就绪事件传到 revents 中
- nfds:监听数组大小
- timeout:阻塞时间,
-1
为阻塞监听,0
为非阻塞。>0
定时阻塞 只支持毫妙级定时
poll 的缺点:
- 1.轮询监听,轮询数量大, CPU 处理性能下降
- 2.拷贝和挂载开销大
- 3.只有特定的 Linux 版本才能使用
三、epoll 模型
结合了 select 和 poll 的优势
创建监听树(创建于内核中,使用红黑树):
#include <sys/epoll.h>
int epfd = epoll_create(int size);
参数:size 为监听数量。返回值:指向监听树的描述符。
监听树节点
struct epolevent node;
node.data.fd = sockfd;
node.events = EPOLLIN|EPOLLOUT|EPOLLERR;
epoll 监听到就绪直接返回就绪节点,用户遍历处理这些就绪 socket 即可
向监听树中添加节点:
epoll_ctl(int epfd,EPOLL_CTL_ADD,int sockfd,struct epollevent* node);
在监听树中删除节点:
epoll_ctl(int epfd,EPOLL_CTL_DEL,int sockfd,NULL);
修改监听树中的节点(修改监听的事件):
epoll_ctl(int epfd,EPOLL_CTL_MOD,int sockfd,&node);
监听事件发生:
int ready = epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
参数:
- epfd:监听树
- events:监听事件数组
- maxevents:最大就绪数大小
- timeout:阻塞时间,
-1
为阻塞监听,0
为非阻塞。>0
定时阻塞
epoll 的优点:
- 没有监听数量限制,不用担心轮询问题
- epoll 使用了事件驱动的方式,监听集合在内核层,避免了每次调用时的重复设置和遍历操作。内核会将就绪的文件描述符直接返回
- 监听事件种类更丰富,可以对不同的 socket 设置不同的事件监听,对监听和就绪数组进行了传入传出分离
水平触发模式 EPOLLLT
- 当可读或可写事件发生时,epoll 会持续通知用户处理直到用户处理。
- 以在任何时候处理这些通知,不必立即响应
- 水平触发适用于处理数据量较大或需要缓冲数据的情况,因为可以确保用户不会错过任何数据
多个事件同时发生可能导致重复处理,可以使用 EPOLLONESHOT
设置为只触发一次通知
边缘触发模式 EPOLLET
- 当可读或可写事件发生时,epoll 会立即通知(只会通知 1 次)
- 应用程序需要在收到通知后立即处理相应的 I/O 事件,否则可能会丢失事件
- 边缘触发适用于处理大量并发连接且数据量较小的情况,可以减少不必要的系统调用和上下文切换。
边缘触发模式一般和非阻塞 I/O 搭配使用,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK
示例
//创建监听树并将serversock设置监听读事件
if((epfd = epoll_create(EPOLL_MAX)) == -1)
{
perror("epoll_initializer: create epoll tree failed");
}
struct epoll_event node;
node.data.fd = sockfd;
node.events = EPOLLIN;
//添加节点
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&node);
int readycode;
struct epoll_event readyarray[EPOLL_MAX];
int i = 0;
int flag = 1;
while(flag)
{
if((readycode = epoll_wait(epfd,readyarray,EPOLL_MAX,-1)) == -1)
{
perror("epoll_wait failed");
}
else
{
i = 0;
//根据readycode就绪数量处理就绪
while(readycode)
{
//判断就绪
if (readyarray[i].data.fd == server_sockfd)
{
//server sock ready
//添加tcp连接任务
}
else
{
//client sock ready
//添加响应处理任务
}
--(readycode);
++i;
}
}
}
大家的点赞、收藏、关注将是我更新的最大动力! 欢迎留言或私信建议或问题。 |
大家的支持和反馈对我来说意义重大,我会继续不断努力提供有价值的内容!如果本文哪里有错误的地方还请大家多多指出(●'◡'●) |