第一章 多路复用I/O-select
第二章 多路复用I/O-epoll
在网络中实现IO多路复用的技术,最常用的就是(select, poll,epoll)三种模型,但是select 受限于底层的实现,随着管理fd数量的增多,造成轮询效率下降。进而出现了epoll模型,epoll 模型底层实现是采用红黑树,不会受限于检测句柄的数量。
epoll 的实现共有三个接口。它们分别如下所示:epoll_create()、 epoll_ctl()、epoll_wati()
;
头文件:
#include
struct epoll_event{ }是一个结构体,用于描述一个文件描述符上的事件。其定义如下:
struct epoll_event {
uint32_t events; // 表示监听的事件类型
epoll_data_t data; // 用户数据,可以是指针或者整数值
};
其中events字段表示需要监听的事件类型,常用事件类型如下:
EPOLLIN:表示该文件描述符上有可读数据。
EPOLLOUT:表示该文件描述符上可以写数据。
EPOLLRDHUP:表示对端关闭连接或者半关闭连接,即收到FIN包。
EPOLLHUP:表示该文件描述符被挂起,可能是对端进程崩溃或者其他错误情况导致的。
EPOLLERR:表示出错。
全部事件类型
EPOLLIN
The associated file is available for read(2) operations.
EPOLLOUT
The associated file is available for write(2) operations.
EPOLLRDHUP (since Linux 2.6.17)
Stream socket peer closed connection, or shut down writing half of connection. (This flag is especially useful for
EPOLLOUT
The associated file is available for write(2) operations.
EPOLLRDHUP (since Linux 2.6.17)
Stream socket peer closed connection, or shut down writing half of connection. (This flag is especially useful for
writing simple code to detect peer shutdown when using Edge Triggered monitoring.)
EPOLLPRI
There is urgent data available for read(2) operations.
EPOLLERR
Error condition happened on the associated file descriptor. epoll_wait(2) will always wait for this event; it is not
necessary to set it in events.
EPOLLHUP
Hang up happened on the associated file descriptor. epoll_wait(2) will always wait for this event; it is not neces‐
sary to set it in events. Note that when reading from a channel such as a pipe or a stream socket, this event merely
indicates that the peer closed its end of the channel. Subsequent reads from the channel will return 0 (end of file)
only after all outstanding data in the channel has been consumed.
EPOLLET
Sets the Edge Triggered behavior for the associated file descriptor. The default behavior for epoll is Level Trig‐
gered. See epoll(7) for more detailed information about Edge and Level Triggered event distribution architectures.
EPOLLONESHOT (since Linux 2.6.2)
Sets the one-shot behavior for the associated file descriptor. This means that after an event is pulled out with
epoll_wait(2) the associated file descriptor is internally disabled and no other events will be reported by the epoll
interface. The user must call epoll_ctl() with EPOLL_CTL_MOD to rearm the file descriptor with a new event mask.
EPOLLWAKEUP (since Linux 3.5)
If EPOLLONESHOT and EPOLLET are clear and the process has the CAP_BLOCK_SUSPEND capability, ensure that the system
does not enter "suspend" or "hibernate" while this event is pending or being processed. The event is considered as
being "processed" from the time when it is returned by a call to epoll_wait(2) until the next call to epoll_wait(2)
on the same epoll(7) file descriptor, the closure of that file descriptor, the removal of the event file descriptor
with EPOLL_CTL_DEL, or the clearing of EPOLLWAKEUP for the event file descriptor with EPOLL_CTL_MOD. See also BUGS.
epoll_create()主要用来创建epoll 实例并对其进行初始化,同时返回一个指向epoll实例的文件描述符。
int epoll_create(int size)
参数 int size: 现无意义,必须大于0,一般填1,告知内核事件表有多大。
返回值:成功返回对应的 epfd ( event poll 结构体) ,失败返回 -1。
将一个 fd 添加到 epoll 的红黑树中,并设置 ep_poll_callback,callback 触发时,把对应 fd 加入到rdlist 就绪列表中。 对fd 实现增删改的操作。
epoll_ctl()函数是Linux内核提供的用于控制epoll实例的系统调用函数之一,它可以用来添加、修改或删除需要监听的文件描述符以及相应事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
返回值:成功返回0,失败返回-1。
参数:
- 参数1 epfd:epoll对象/epoll实例标识符
- 参数2 op:指定操作类型。
EPOLL_CTL_ADD 增加 将指定文件描述符加入到监听队列中;
EPOLL_CTL_MOD 修改 修改已经加入监听队列中的文件描述符对应事件信息;
EPOLL_CTL_DEL 删除 将已经加入监听队列中的文件描述符移除。
- 参数3 fd:要监听的句柄(fd)
- 参数4 event: 指向一个结构体变量,用来设置需要监听的事件类型以及相关属性 要监听的事件类型,红黑树键值对kv:fd-event
event.events常用类型:
EPOLLIN 可读
EPOLLOUT 可写
EPOLLET 边缘触发,默认⽔平触发LT
具体详解:
epoll_ctl主要是对epitem对象进行操作:把fd 和struct epoll_event{}绑定
EPOLL_CTL_ADD:先查看含fd的epitem对象是否在红黑树中,如果在则退出,不在则将fd和event加到epitem对象中并将该对象插入红黑树中。
EPOLL_CTL_DEL:先查看含fd的epitem对象是否在红黑树中,如果不在则退出,在则将该epitem对象从红黑树中删除。
EPOLL_CTL_MOD:先查看含fd的epitem对象是否在红黑树中,如果不在则退出,在则更新事件。
检测 红黑树列表(rdlist 列表)是否为空,不为空时候则主要是把就绪链表中的事件信息拷贝到events数组中 返回就绪的 fd 的数量;epoll_wait函数是Linux内核提供的用于异步IO操作的系统调用函数之一,它可以用于等待一个或多个文件描述符上的事件发生,并在事件发生时通知用户进程。
// 收集 epoll 监控的事件中已经发生的事件,如果 epoll 中没有任何⼀个事件发⽣,则最多等待 timeout 毫秒后返回。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
返回值:成功返回实际就绪的 fd 数量,失败返回-1
参数:
- 参数1 epfd:epoll 对象。
- 参数2 events:指向存储返回事件的结构体数组。 用户态创建的evnet数组,内核拷贝就绪的 fd 到该数组中。
events[i].events:
EPOLLIN 触发读事件
EPOLLOUT 触发写事件
EPOLLERR 连接发生错误
EPOLLRDHUP 连接读端关闭
EPOLLHUP 连接双端关闭
- 参数3 maxevents:可以返回的最⼤事件数目,一般设置为event数组的⻓度
- 参数4 timeout:超时时间 ms。-1(一直等待),0(不等待),>0(等待时间)。断开与服务器没有交互的客户端
当有事件到达时,epoll_wait会将所有就绪的事件存放在传入的events数组中,并返回就绪事件数量。对于每个就绪的文件描述符,需要通过判断events[i].events字段中位设置情况来确定具体是哪种类型的事件(例如可读、可写等)。同时,在处理完就绪事件后,需要将相应文件描述符重新加入到epoll监听队列中。
fcntl 函数可以将 fd 设置为非阻塞。
//修改(获取)文件描述符属性
int fcntl(int fd, int cmd, ... /* arg */ );
返回值:失败返回-1
参数
- 参数1:需要修改的文件描述符,
- 参数2:修改(获取)文件描述符的操作
例子:
//1、获取原有套接字状态的信息
int status = fcntl(fd, F_GETFL);
//2、将非阻塞的标志与原有的标志信息做或操作
status |= O_NONBLOCK;
//3、将标志位信息写回到socket中
fcntl(fd, F_SETFL, status);
注:调用 epoll_create
会创建一个 epoll 对象。调用 epoll_ctl
添加到 epoll 中的事件都会与网卡驱动程序建立回调关系,相应事件触发时会调用回调函数(ep_poll_callback),将触发的事件拷贝到 rdlist 双向链表中。调用epoll_wait 将会把 rdlist 中就绪事件拷贝到用户态中;
调用 epoll_create 函数:返回一个epfd,同时内核会创建 eventpoll 结构体,该结构体的成员由红黑树和双向链表组成
。红黑树:保存需要监听的描述符。双向链表:保存就绪的文件描述符。
调用 epoll_ctl 函数: 对红黑树进行增添,修改、删除。
添加 fd 到红黑树上,从用户空间拷贝到内核空间。一次注册 fd,永久生效。内核会给每一个节点注册一个回调函数,当该 fd 就绪时,会主动调用自己的回调函数,将其加入到双向链表当中。
调用 epoll_wait 函数: 内核监控双向链表,如果双向链表里面有数据,从内核空间拷贝数据到用户空间;如果没有数据,就阻塞。
当事件就绪的时候,调用 epoll_wait(select、poll)可以得到通知。事件通知的模式有两种:水平触发(LT)和边缘触发(ET); 在处理非大量数据时候性能相差不大,但是代码逻辑处理有区别。
水平触发模式
在水平触发模式下,如果文件描述符上的事件没有被处理完毕,epoll 会持续通知应用程序该文件描述符上仍有事件待处理。在这种情况下,如果应用程序不及时响应并读取数据,则 epoll 会一直通知应用程序该文件描述符上有数据可读取。
边沿触发模式
在边沿触发模式下,只要文件描述符上出现新的事件(例如数据可读或连接建立),epoll 就会通知应用程序。但是,在通知之后,如果应用程序没有立即响应并读取所有数据,则 epoll 不会再次通知该文件描述符上有新的数据可读。
水平触发 LT: 当内核读缓冲区非空,写缓冲区不满,则一直触发,直至数据处理完成。
边缘触发 ET: 当 IO 状态发生改变,触发一次。每次触发需要一次性把事件处理完。
LT 和 ET 的特性,决定了对于数据的读操作不同
LT + 一次性读,阻塞
ET + 循环读, 非阻塞 循环recv
总体来说,边沿触发模式相比于水平触发模式更为高效,并且可以避免由于重复监听导致 CPU 占用率过高的问题,一般用于数据量很大,需要分批次接收的时候。但是,在使用边沿触发模式时需要注意及时读取所有数据,并确保每个事件都得到了正确处理。
需要注意的是,EPOLLET模式下,并不会丢失数据。即如果数据未全部接收,此时又发送新的数据,接收的时候将先接收上一次的数据。并且,epoll默认是EPOLLLT。
代码实现
// lt + 一次性读,小数据
ret = read(fd, buf, sizeof(buf));
// et + 循环读,大数据
while(true) {
ret = read(fd, buf, sizeof(buf);
// 此时,说明读缓冲区已经空了
if (ret == EAGAIN || ret == EWOULDBLOCK) break;
}
ET特点:
ET 的使用
ET 的使用: ET+ 非阻塞 IO + 循环读
循环读:若数据不能一次性处理完,只能等到下次数据到达网卡后才触发读事件。
将 recv 函数设置为非阻塞的两种方式:
1.recv 函数的属性设置为MSG_DONWAIT;
ret = recv(newFd, buf, sizeof(buf)-1, MSG_DONTWAIT);
int conn_fd = 0;
// fd --> epoll 在epoll 底层创建的时候也是通过fd 操作的。
int epfd = epoll_create(1);//参数无意义只要大于0 即可;老版本参数是用来确定一次行就绪数量
struct epoll_event ev, events[EVENTS_LENGTH];
ev.events = EPOLLIN|EPOLLET;
ev.data.fd = listen_fd
epoll_ctl(epfd,EPOLL_CTL_ADD,listen_fd,&ev) // 先把listen_fd 注册到事件中; 非阻塞,不可能在这个地方存在挂起
printf("fd : %d\n",epfd);
while(1){ //7*24 所有的服务器都有这个循环,做成永真的循环,while(!finshed) / for(;;)
int nready = epoll_wait(epfd,events, EVENTS_LENGTH,1000); // 是阻塞
printf("nready :%d",nready);
int i = 0;
for (i = 0; i < nready;i++){
int clientfd = events[i].data.fd ;
if (listen_fd == clientfd){ // accept处理
while( 1) {
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
fcntl(listen_fd, F_SETFL, fcntl(listen_fd, F_GETFL, 0)|O_NONBLOCK);
conn_fd = accept(listen_fd,(struct sockaddr *)&client_addr, &length);
if (0 > conn_fd ) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (conn_fd == -1){
break;
}
printf("accept : %d\n",conn_fd);
ev.events = EPOLLIN;
ev.data.fd = conn_fd;
epoll_ctl(epfd,EPOLL_CTL_ADD, conn_fd, &ev);
}
}else if(events[i].events & EPOLLIN) { // clientfd
unsigned char buff[BUFF_LENGTH] = {0};
int n = recv(clientfd, rbuff, BUFF_LENGTH,0);
if (n > 0) {
//rbuffer[n] = '\0';
printf("recv: %s, n: %d\n", rbuffer, n);
memcpy(wbuffer, rbuffer, BUFFER_LENGTH);
ev.events = EPOLLOUT;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev);
}
}else if (events[i].events & EPOLLOUT) {
int sent = send(clientfd, wbuffer, BUFFER_LENGTH, 0); //
printf("sent: %d\n", sent);
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev);
}
}
}
略
IO多路复用
epoll原理学习笔记
荐一个零声学院免费教程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习: