epoll的实现与应用
- epoll的原理
- 基本函数
- epoll_create()
- epoll_ctl()
- epoll_wait
- event事件
- 用epoll搭建一个简单的服务器
- 总结
epoll的原理
epoll底层使用了红黑树和链表的结合解决了select的最高限制,并且更高效的对多个IO同时进行操作。epoll中的红黑树和链表在内存中统一一份,就绪链表只是在红黑树的基础上,对节点进行连接指向。
课前小黑板
- 事件触发的两种模型
- ET: 边沿触发;buf中无数据到有数据才触发
- LT:水平触发;只要buf有数据就一直触发
- io三种状态
可读、可写、是否出错
可读可写的判断依据为recvbuf是否有值以及sendbuf是否为空
epoll默认为水平触发。如何选择使用哪种触发?一般来讲
- 当实际的大小大于buf大小的时候,选择LT模式
- listen监听的时候选择LT模式,因为如果选择ET模式的话,发生3个以上客户端同时连接时,有可能漏掉个把
我们都知道了select是遍历所有的IO口,再在fd_set 中判断该io口是否就绪。而epoll则在epoll_wait时获取的event,就是准备就绪的链表
基本函数
- 头文件
sys/epoll.h
epoll_create()
- 函数作用
用于创建一个epoll实例,返回实例的fd; - 函数原型
int epoll_create(int size);
- 参数介绍
参数名 | 说明 |
---|---|
size | 最大监听的数量。目前该参数无意义,只有是否大于零的区别,如果需要有监听,则填写一个大于零的数即可 |
epoll_ctl()
-
函数作用
用于添加或删除所要监听的socket。成功返回0,失败返回-1.错误值可在errno中获取。 -
函数原型
int epoll_ctl(int epfd,int op,int fd,struct epoll_event * event);
- 参数介绍
参数名 | 说明 |
---|---|
eptd | 需要操作的文件描述符,即一开始create的那个epollfd |
op | 操作类型 EPOLL_CTL_ADD:注册目标文件描述符fd EPOLL_CTL_MOD:更改与fd相关联的事件 EPOLL_CTL_DEL:删除epfd中的fd |
fd | 所要操作的socketfd |
event | event事件,设置一些事件类型,比如可读触发,边沿触发这些,具体可参看event事件 |
epoll_wait
-
函数作用
等待通过epoll句柄fd找到的就绪事件。函数返回准备好的事件数量。 -
函数原型
int epoll_wait(int _epfd, struct epoll_event *_events, int _maxevents, int _timeout)
- 参数介绍
参数名 | 说明 |
---|---|
_epfd | 总的句柄 |
_events | 准备好的事件集合 |
_maxevents | 希望返回的最大事件数量,一般为events的数组大小 |
_timeout | 最大等待时长,单位为毫秒。-1==infinite |
event事件
以下列出部分event事件,全部的话可在epoll.h中查看
事件名 | 事件说明 |
---|---|
EPOLLIN | 可读 |
EPOLLPRI | 有紧急的事件可以读 |
EPOLLOUT | 可写 |
EPOLLET | 边沿触发,默认为水平触发 |
用epoll搭建一个简单的服务器
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>#include <sys/socket.h>
#include <sys/epoll.h>
#include <errno.h>
#include <arpa/inet.h>#define EPOLL_SIZE 1024
#define BUFFER_LENGTH 1024int main()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);printf("sockfd is %d\n", sockfd);struct sockaddr_in servAddr;memset(&servAddr, 0,sizeof(servAddr));servAddr.sin_family = AF_INET;servAddr.sin_port = htons(4022);servAddr.sin_addr.s_addr = INADDR_ANY;if(bind(sockfd, (struct sockaddr*)&servAddr,sizeof(struct sockaddr_in)) < 0){printf("bind sockfd error!\n");return -1;}if(listen(sockfd, 5) < 0){printf("listen error!\n");return -1;}int epollFd = epoll_create(EPOLL_SIZE);struct epoll_event ev, events[EPOLL_SIZE] = {0};ev.events = EPOLLIN;ev.data.fd = sockfd;epoll_ctl(epollFd, EPOLL_CTL_ADD, sockfd, &ev);while(1){int nReady = epoll_wait(epollFd, events, EPOLL_SIZE, -1);if (nReady == -1){printf("nReady error!\n");break;}for (int i = 0; i < nReady; i++){printf("2 \n");if (events[i].data.fd == sockfd){struct sockaddr_in cliAddr;memset(&cliAddr, 0, sizeof(cliAddr));int cliLen = sizeof(cliAddr);printf("1 :%d\n");int clifd = accept(sockfd, (struct sockaddr*)&cliAddr, &cliLen);printf("clifd :%d\n", clifd);if (clifd <= 0){continue;}char str[INET_ADDRSTRLEN] = {0};printf("recvfrom %s at port %d, sockfd:%d, clifd:%d\n", inet_ntop(AF_INET, &cliAddr.sin_addr, str, sizeof(str)),ntohs(cliAddr.sin_port), sockfd, clifd);//将接收到的io口也放入观察集中ev.events = EPOLLIN | EPOLLET;ev.data.fd = clifd;epoll_ctl(epollFd, EPOLL_CTL_ADD, clifd, &ev);}else{int clifd = events[i].data.fd;char buffer[BUFFER_LENGTH] = {0};int ret = recv(clifd, buffer, BUFFER_LENGTH, 0);if (ret < 0){if (errno == EAGAIN || errno == EWOULDBLOCK){printf("read all data ready!\n");}close(clifd);ev.events = EPOLLIN | EPOLLET;ev.data.fd = clifd;epoll_ctl(epollFd, EPOLL_CTL_DEL, clifd, &ev);}else if(ret == 0){printf("disconnect %d \n", clifd);close(clifd);ev.events = EPOLLIN | EPOLLET;ev.data.fd = clifd;epoll_ctl(epollFd, EPOLL_CTL_DEL, clifd, &ev);continue;}else{printf("recv: %s, %dBytes", buffer, ret);}}}}return 0;}
总结
epoll 相较于 select来说在处理更多的io时,更为高效。这两种多路复用的原理都是将数据等待和读取数据分离开来。select用到了位图,而epoll使用的红黑树和链表。
遗憾的是不同的操作系统特供的 epoll 接口有很大差异,所以使用类似于 epoll 的接口实现具有较好跨平台能力的服务器会比较困难。比如mac电脑系统就没有epoll,只有kqueue,与epoll应用类似。