IO多路复用——基础
本文主要介绍 select/poll/epoll 三大 IO 多路复用方法,主要介绍相关的函数使用
一、select
介绍:select 是最早的 I/O 多路复用技术之一,其历史可以追溯到 1980 年代,最早由 Unix 系统引入,作为时代的眼泪,在大量连接下的性能远不如 epoll,但 select 有个独特的优势:跨平台
1.1 相关函数
#include <sys/select.h>
struct timeval {
time_t tv_sec; // seconds
suseconds_t tv_usec; // microseconds
};
int select(int nfds, // 待测试的fd数量,值为最大fd + 1
fd_set *readfds, // 读描述符集合
fd_set *writefds, // 写描述符集合
fd_set *exceptfds, // 异常描述符集合
struct timeval *timeout // 超时时间
);
// 将fd从set中删除
void FD_CLR(int fd, fd_set *set);
// 判断fd是否在set中
int FD_ISSET(int fd, fd_set *set);
// 将fd添加到set中
void FD_SET(int fd, fd_set *set);
// 初始化set
void FD_ZERO(fd_set *set);
1.2 代码流程
fd_set rfds, rset;
FD_ZERO(&rfds); // 清空rfds
FD_SET(sockfd, &rfds);
int maxfd = sockfd;
while (1) {
rset = rfds; // 每次循环都重新设置rset
int nready = select(maxfd + 1, &rset, NULL, NULL, NULL); // 监测rset中所有fd的读事件
if (FD_ISSET(sockfd, &rset)) { // 新的连接请求
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
FD_SET(clientfd, &rfds);
if (clientfd > maxfd) maxfd = clientfd;
}
int i = 0;
for (i = sockfd + 1; i <= maxfd; ++i) {
if (FD_ISSET(i, &rset)) { // 有数据可读
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
if (count == 0) {
printf("client disconnect: %d\n", i);
close(i);
FD_CLR(i, &rfds);
continue;
}
printf("RECV: %s\n", buffer);
count = send(i, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
}
举个例子来理解上述过程,服务器启动,监听的 sockfd 假设为 3,有3个客户端来连接,假设其 fd 分别为 4,5,6;先初始化 fd_set rfds,并将 sockfd 添加进去。设置 maxfd 为 3(即 sockfd);第一个客户端来连接,文件描述符为 4,select 监测到 sockfd(3)上有连接请求,通过 accept 接受连接,返回新的文件描述符 4,将文件描述符 4 添加到 rfds 集合,更新 maxfd 为 4。第二、三个客户端同理,返回新的文件描述符 5,6。并将 5,6 添加到 rfds 集合,更新 maxfd 为 6。然后数据处理阶段,首先检测 fd 4,如果有数据可读,则读取并回发,然后 fd 5 和 fd 6 同理,然后持续监测…
1.3 select 流程图
二、poll
最早的 IO 多路复用机制是 select 系统调用,它出现在 4.2BSD UNIX中(大约在1983年),尽管 select 很有用,但它有一些限制和缺点。特别是 select 的文件描述符集大小受限于 FD_SETSIZE,并且在处理大量文件描述符时效率较低。为了解决这些问题,SVR3 引入了 poll 系统调用,大约在 1986 年左右。但由于后来大明星 epoll 的到来,poll 的存在感现在其实也不高
之所以这样,poll 可以被认为是 select 的小优化,poll 的机制与 select 类似,与 select 在本质上没有多大差别,使用方法也类似
2.1 相关函数
#include <poll.h>
// pollfd结构体
struct pollfd {
int fd; // 委托内核检测的fd
short events; // 委托内核检测fd的什么事件
short revents; // fd实际发生的事件 -> 传出
};
struct pollfd myfd[100];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
2.2 代码流程
struct pollfd fds[1024] = {0};
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;
int maxfd = sockfd;
while (1) {
int nready = poll(fds, maxfd + 1, -1);
if (fds[sockfd].revents & POLLIN) {
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
if (clientfd > maxfd) maxfd = clientfd;
}
int i = 0;
for (i = sockfd + 1; i <= maxfd; ++i) {
if (fds[i].revents & POLLIN) {
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
if (count == 0) {
printf("client disconnect: %d\n", i);
close(i);
fds[i].fd = -1;
fds[i].events = 0;
continue;
}
printf("RECV: %s\n", buffer);
count = send(i, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
}
代码结构与 select 很接近,应该非常好理解
三、epoll
最后来介绍我们的大明星,大网红,epoll 是使得 Linux 成为主流服务器的关键成员,在Linux操作系统中地位很高
3.1 相关函数
俗称“epoll三板斧”
#include <sys/epoll.h>
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
3.1.1 epoll_create() 函数
epoll_create() 函数的作用是创建一个红黑树模型的实例,用于管理待检测的 fd 集合。
函数参数 size:在Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数值就可以了。
函数返回值:失败则返回-1,成功则返回一个有效的 fd,通过这个 fd 就可以访问创建的epoll实例了
3.1.2 epoll_ctl() 函数
epoll_ctl() 函数的作用是管理红黑树实例上的节点,进行添加、删除、修改操作。
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events;
epoll_data_t data;
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数参数 epfd:epoll_create() 函数的返回值
函数参数 op:执行什么操作
①EPOLL_CTL_ADD:往epoll模型中添加新的节点
②EPOLL_CTL_MOD:修改epoll模型中已经存在的节点
③EPOLL_CTL_DEL:删除epoll模型中的指定的节点
函数参数 fd:要执行操作的 fd
函数参数event:epoll事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件
①events:委托epoll检测的事件,有EPOLLIN(读事件)、EPOLLOUT(写事件)和 EPOLLERR(异常事件)
②data:用户数据变量
函数返回值:失败则返回-1,成功则返回0
3.1.3 epoll_wait() 函数
epoll_wait() 函数的作用是检测创建的epoll实例中有没有就绪的 fd。
函数参数epfd:epoll_create() 函数的返回值
函数参数events:传出参数, 存储了已就绪的 fd 的信息
函数参数maxevents:修饰第二个参数
函数参数timeout:
①等于 0:函数不阻塞
②大于 0:函数阻塞对应的毫秒数再返回
③等于 -1:函数一直阻塞
函数返回值:成功有两种情况,等于 0 表示函数是阻塞被强制解除了,没有检测到满足条件的 fd;大于 0 表示检测到的已就绪 fd 的总个
数;失败则返回 -1
3.2 代码流程
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
while (1) {
struct epoll_event events[1024] = {0};
int nready = epoll_wait(epfd, events, 1024, -1);
int i = 0;
for (i = 0;i < nready; ++i) {
int connfd = events[i].data.fd;
if (connfd == sockfd) {
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
} else if (events[i].events & EPOLLIN) {
char buffer[1024] = {0};
int count = recv(connfd, buffer, 1024, 0);
if (count == 0) { // disconnect
printf("client disconnect: %d\n", connfd);
close(connfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
continue;
}
printf("RECV: %s\n", buffer);
count = send(connfd, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
}
后续会继续更新 epoll 的底层原理等