IO多路复用
IO多路复用
select系统调用
维护的是一个文件描述符(fd)集合(set),监测这些fd集合。
#include <sys/select.h> // 头文件
运行机制
将fedset复制到内核空间,然后对其进行遍历,查看可读,可写,错误事件,返回就绪事件总数。
select函数
select函数调用时需要五个参数,包括文件描述符集合总数nfds(最大是1024个),底层fdset是数组实现的,fdset是比特位的集合。可读集合read_fds,可写集合write_fds,异常集合excep_fds,超时时间timeout。返回值是所有就绪事件的数量和。
fd_set fds, read_fds, write_fds, excep_fds; // fds是应用层,而其它三个都是内核层要进行操作的。
struct timeval timeout;
FD_ZERO(fds); // 首先所有标志位置0
FD_SET(listenfd, fds); // 将代表listenfd的进行置1
int maxfd = listenfd;
struct time_val timeout; // 如果为NULL,设定一直阻塞等待
timeout.tv_sec = 5; // 等待5s, 5s事件就继续执行
timeout.tv_usec = 0;
struct sockaddr_in client;
bezero(&client, sizeof(client));
socklen_t len = sizeof(client);
while(1){
read_fds = fds;
int nready = select(nfds, &read_fds, &write_fds, &excep_fds, &timeout); // 首先拿到这个,接下来就是处理这些集合,select会修改可读可写异常标志位
if(FD_ISSET(listenfd, &read_fds)) { // 考虑是否listenfds可读
int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
FD_SET(clientfd, fds); // 设置标志位,fds
if(clientfd > maxfd) maxfd = clientfd; // 这一步问题是解决回收连接之后的问题。
}
// 循环对fd集合进行读
for(int i = listenfd + 1; i < maxfd+1; ++i){
char buffer[1024] = {0};
int n = recv(i , buffer, 1024, 0);
if(n == 0){
close(i);
FD_CLR(i, &fds);
break;
}
}
}
注意
fd阻塞和select阻塞不同,select只是分辨fd会不会阻塞于读和写就行。select一般限制最多fd大小为1024,数组实现,由一个宏定义进行限制
poll系统调用
结构体
struct pollfd{
int fd;
short int events; // 期望得事件
short int revents; // 返回的就绪事件
}
运行机制
和select调用的机制类似
poll函数
poll系统调用维护三个参数,pollfd集合,nfds,fd集合数量(已有的fd数量),timeout。
// 一个重要的参数是数组,每个数组中保存一个pollfd元素
struct pollfd pfds[1024] = {0};
memset(&pfds, -1, sizeof(pfds)); // 0是存在的fd
pfds[sockfd].fd = sockfd;
pfds[sockfd].events = POLLIN; // 期望可读
int maxfd = sockfd;
while(1){
int nready = poll(pfds, maxfd+1, -1); // timeout =-1 一直等待事件发生
if(pfds[sockfd].revents & POLLIN){
int clientfd = accept(sockfd, (struct sockaddr*) &client, &len);
printf("accept finished: %d\n", clientfd);
pfds[clientfd].fd = clientfd;
pfds[clientfd].events = POLLIN;
if(clientfd > maxfd) maxfd = clientfd;
}
for(int i = sockfd+1; i < maxfd+1; ++i){
char buffer[1024] = {0};
if(pfds[i].revents & POLLIN){
int n = recv(i, buffer, sizeof(buffer), 0);
if(n == 0){
close(i);
pfds[i].fd = -1;
pfds[i].events = 0;
continue;
}
printf("recv: %s\n", buffer);
n = send(i, buffer, strlen(buffer), 0);
printf("send: %d bytes!\n", n);
}
}
}
注意
-
poll的限制是fd的最大数量,linux一般为1024ulimits -a # 查看特定值 ulimits -n # 查看fd的最大数量 ulimits -n 2048 # 修改 /etc/security/limits.conf -
特点:
IO与事件是绑定的,读写不分离
epoll系统调用
运行机制
epoll通过epoll_create通知内核可能会有多大的内核事件表(以前事件表的数据结构是数组,后面事件表的数据结构是链表,所有size对于现在的epoll_create是无效的)。- 使用
epoll_ctl将事件表复制到内核,控制管理添加、删除内核事件表上的事件。 - 使用
epoll_wait监控内核事件表上的事件,如果有就绪事件就将其复制到返回集合中。epoll内核事件表每个结点都是一个不同的fd, 这个结点中维护了每个fd的事件数。存疑是怎么组织的fd集合事件表
结构事件
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
epoll_create函数
一个参数size,这里的size对于现版本是没有意义的,现版本是链表实现的。返回值是一个epollfd,生成一个epollfd管理者
epoll_ctl函数
四个参数,epollfd管理者,操作模式添加、删除等,客户fd,客户带的事件。
epoll_wait函数
四个参数,epollfd管理者, 返回就绪事件表,返回就绪事件表的大小,超时时间(控制阻塞也是控制等待就绪事件的时间)
示例
struct epoll_event epl_ent;
epl_ent.events = EPOLLIN;
epl_ent.data.fd = sockfd; // 先将listenfd传上来
// epoll_create的作用是创建一个内核事件表,用专门的一个fd去管理
int epollfd = epoll_create(1); // 此时MAX_EVENTS不起作用,只是告诉内核有这么大
printf("epollfd: %d\n", epollfd);
// 将listenfd添加上去, 这里只是将事件表复制到内核
ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd,&epl_ent);
if(ret == -1){
printf("epoll_ctl failure!\n");
}
while(1){ // accept
struct epoll_event epl_rent[MAX_EVENTS]={0};
int nready = epoll_wait(epollfd, epl_rent, MAX_EVENTS, -1);
for(int i = 0; i < nready; ++i){
int connfd = epl_rent[i].data.fd;
// 如果是监听fd上有事件发生
if(connfd == sockfd){
printf("listenfd: %d\n", sockfd); // 输出3
int clientfd = accept(connfd, (struct sockaddr*)&client, &len);
printf("accept finished: %d\n", clientfd);
epl_ent.data.fd = clientfd;
epl_ent.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &epl_ent);
}
// 事件也是标志位的集合
else if(epl_rent[i].events & EPOLLIN){
char buffer[1024] = {0};
int n = recv(connfd, buffer, 1024, 0);
if(n == 0){
// 删除这里可写可不写因为其,直接按照fd进行删除
ret = epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, &epl_ent);
if(ret == -1){
printf("epoll_ctl_del failure!\n");
}
close(connfd);
continue;
}
printf("recv: %s\n", buffer);
}
}
}
LT和ET
LT事件就绪就一直通知epoll_wait,即使应用程序不处理事件,那么应用程序下次调用epoll_wait仍然会处理,直到全部处理完缓冲区,ET只会触发一次,如果应用程序当次没有处理事件,那么之后应用程序调用epoll_wait(),也不会再通知。做一次recv,所以如果发生缓冲区小于数据块大小的情况,所以要完全接收完缓冲区,就需要应用程序处理时调用多次recv ()函数,就需要使用while循环去一直进行接收。所以ET模式的优点是降低了,事件被触发的次数。
边沿触发适合包大小不确定的情况,水平触发适合包大小确定的情况。
ET因为需要多次recv,所以可能会阻塞在recv上,所以应用场景经常在非阻塞IO情况。
注意
epoll维护的内核事件表是积累添加的,不需要像select和poll一样循环进行复制到内核事件表。poll通过标志位标记可读等事件模式,select直接将可读、可写、异常事件分开(其中都是以数组实现,每一位代表一个fd)。select、poll每次都需要将fd集合复制到内核select、poll、epoll都是事件通知、事件触发机制linux2.4版本以前用select,没有poll和epoll,poll是通过pollfd进行组织的。结构体可以用链表去进行组织也可以顺序存储组织。

浙公网安备 33010602011771号