系列文章目录
第一章 高性能服务器技术栈 (select)
第二章 高性能服务器技术栈 (epool/poll)
文章目录
- 系列文章目录
- 前言
- 一、epoll 接口
- 二、epoll 原理
- 三、epoll 触发方式
- 四、设置阻塞方式
- 代码 实例
- 总结
- 参考
前言
在网络中实现IO多路复用的技术,最常用的就是(select, poll,epoll)三种模型,但是select 受限于底层的实现,随着管理fd数量的增多,造成轮询效率下降。进而出现了epoll模型,epoll 模型底层实现是采用红黑树,不会受限于检测句柄的数量。
一、epoll 接口
epoll 的实现共有三个接口。它们分别如下所示:
接口 epoll_create
epoll_create主要用来创建eventpoll 实例并对其进行初始化,同时返回一个指向epoll实例的文件描述符。
int epoll_create(int size)
参数 size: 现无意义,必须大于0,一般填1,告知内核事件表有多大。
返回值:成功返回对应的 epfd ( eventpoll 结构体) ,失败返回 -1。
接口 epoll_ctl:
将一个 fd 添加到 epoll 的红黑树中,并设置 ep_poll_callback,callback 触发时,把对应 fd 加入到rdlist 就绪列表中。 对fd 实现增删改的操作。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
/*
返回值:成功返回0,失败返回-1。
参数:
- 参数1 epfd: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对象进行操作:
EPOLL_CTL_ADD:先查看含fd的epitem对象是否在红黑树中,如果在则退出,不在则将fd和event加到epitem对象中并将该对象插入红黑树中。
EPOLL_CTL_DEL:先查看含fd的epitem对象是否在红黑树中,如果不在则退出,在则将该epitem对象从红黑树中删除。
EPOLL_CTL_MOD:先查看含fd的epitem对象是否在红黑树中,如果不在则退出,在则更新事件。
epoll_wait
检测 rdlist 列表是否为空,不为空时候则主要是把就绪链表中的事件信息拷贝到events数组中 返回就绪的 fd 的数量
// 收集 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(等待时间)。断开与服务器没有交互的客户端
其它接口 fcntl
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 原理
注:调用 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 触发方式
当事件就绪的时候,调用 epoll_wait(select、poll)可以得到通知。事件通知的模式有两种:水平触发(LT)和边缘触发(ET); 在处理非大量数据时候性能相差不大,但是代码逻辑处理有区别。
水平触发 LT: 当内核读缓冲区非空,写缓冲区不满,则一直触发,直至数据处理完成。
边缘触发 ET: 当 IO 状态发生改变,触发一次。每次触发需要一次性把事件处理完。
LT 和 ET 的特性,决定了对于数据的读操作不同
LT + 一次性读,阻塞
ET + 循环读, 非阻塞 循环recv
代码实现
// lt + 一次性读,小数据
ret = read(fd, buf, sizeof(buf));
// et + 循环读,大数据
while(true) {
ret = read(fd, buf, sizeof(buf);
// 此时,说明读缓冲区已经空了
if (ret == EAGAIN || ret == EWOULDBLOCK) break;
}
ET特点:
- ET 模式避免了 LT 模式可能出现的惊群现象(如:一个 listenfd 被多个 epoll 监听,一个调用accept接受连接,其他 accept 阻塞);
- ET 模式减少了 EPOLL 事件被重复触发的次数,效率高。
ET 的使用
ET 的使用: ET+ 非阻塞 IO + 循环读
循环读:若数据不能一次性处理完,只能等到下次数据到达网卡后才触发读事件。
四、设置阻塞方式
将 recv 函数设置为非阻塞的两种方式:
1.recv 函数的属性设置为MSG_DONWAIT;
ret = recv(newFd, buf, sizeof(buf)-1, MSG_DONTWAIT);
- fcntl 函数将文件描述符设置为非阻塞性的。
代码 实例
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等技术内容,点击立即学习: