前面介绍了select
函数和poll
函数, select
我们知道能够支持的套接字个数太少了, 但是poll
函数已经很好了也没有什么缺点啊为啥还要介绍epoll
呢? 接下来我们就来谈谈poll
和select
函数的其他问题.
poll和select的问题
-
poll
函数一般使用都是将它设置为轮询的方式, 这样是很占用CPU的; 当然可以将其设置为阻塞也就能避免这个问题. -
select
能够监听的描述符太少. -
poll
和select
的底层实现. poll函数其实和select底层实现类似.当有监听的事件到来时, select会将该事件的描述符和事件的状态从内核复制到用户空间中. poll会将到来事件的描述符状态复制到用户空间.
最后用户都要重新遍历所有的描述符判断是哪一个描述符状态改变.
epoll的优点
poll和select最大的问题都是实现上需要占用CPU大量的时间, 那么epoll又是怎样避免这样的问题呢.
- epoll函数也要设置监听描述符并将其复制到内核中为每一个监听描述符注册一个回调函数.
- 当监听描述符的时候到来时直接返回该描述符的回调函数不需要在进行复制.
可以看出来epoll只有在设置监听时才复制一次描述符, 事件到来时不需要复制并且也不需要遍历判断. 大大的减少了占用CPU的时间.
如果对于底层实现感兴趣可以看一下以前总结的epoll源码分析和select源码分析.
函数原型
epoll可是有三个函数哦. 我们一个个来介绍并探讨他们的底层实现的功能.
1. epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
成功 : 返回一个文件描述符.
失败 : 返回-1.
功能 : 在cache中申请创建的红黑树大小.
参数
size
: 该参数 原本是指定在cache中分配的内存大小, 在linux2.6以上已经没有用了.
2. epoll_ctl
int epoll_ctl(int epfd, int opt, int fd, struct epoll_event *event);
成功 : 返回0
失败 : 返回-1
功能 : 注册要监听的事件类型.
参数 :
-
epfd
: epoll_create函数返回的文件描述符. -
opt
: 功能选项, 用三个宏定义表示不同的功能opt值 描述 EPOLL_CTL_ADD 注册fd的回调函数到epfd中 EPOLL_CTL_DEL 删除fd注册的回调函数 EPOLL_CTL_MOD 修改已注册的fd的监听事件 -
fd
: 需要监听的文件描述符 -
event
: 内核需要监听的事件, 与poll的事件类似.event值 描述 EPOLLIN 监听是描述符是否可读 EPOLLOUT 监听是描述符是否可写 EPOLLERR 发生错误 EPOLLHUP 对端挂断, 或其中一端关闭了 EPOLLET 设置为边沿触发模式
3. epoll_wait
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
成功 : 返回0
失败 : 返回-1
功能 : 从就绪事件的链表中返回监听的回调函数.
参数
epfd
: epoll_create函数返回的文件描述符.events
: 内核返回的监听事件maxevents
: events数组的大小timeout
: 超时时间- timeout == INFTIM(负数) : select函数永远阻塞等待监视文件描述符集合中某个文件描述符发生变化为止.
- timeout == 0 : 函数为非阻塞函数, 不管有无等待的文件描述符发生变化都会返回
- timeout > 0 : 等待的超时时间, 即函数在timeout时间内阻塞, 超时时间之内有事件到来就返回了, 否则在超时后不管怎样一定返回.
函数调用
看了前面的三个函数估计第一次接触可能会产生抵触, 太麻烦了. 但是麻烦归麻烦, 好用高效率才是王道. 比如在百万连接时, epoll_wait都可以只需要常量时间就能精确返回触发事件的套接字.
说了那么多, 还是练习能最快掌握, 完整代码epoll_service.c
void doService(int servicefd)
{
int clientfd, epfd;
char buf[1024];
// 设置监听套接字
struct epoll_event event, evts[EPOLL_MAX];
event.events = EPOLLIN;
event.data.fd = servicefd; // 这里需要设置监听的套接字
epfd = epoll_create(1);
epoll_ctl(epfd, EPOLL_CTL_ADD, servicefd, &event); // 这里也需要设置监听的套接字
int n, eventNum;
while(1)
{
// 设置为永久阻塞
eventNum = epoll_wait(epfd, evts, EPOLL_MAX, -1);
if(eventNum == 0)
continue;
else if(eventNum < 0)
EXIT("epoll_wait");
// 遍历状态改变的套接字
for(int i = 0; i < eventNum; i++)
{
if(evts[i].data.fd == servicefd && (evts[i].events &EPOLLIN))
{
clientfd = Accept(servicefd, NULL, NULL);
event.events = EPOLLIN;
event.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &event);
}
else if(evts[i].events & EPOLLIN)
{
n = recv(evts[i].data.fd, buf, sizeof(buf), 0);
if(n <= 0)
{
// 删除关闭的套接字
epoll_ctl(epfd, EPOLL_CTL_DEL, evts[i].data.fd, NULL);
close(evts[i].data.fd);
fprintf(stderr, "peer close\n");
}
send(evts[i].data.fd, buf, n, 0);
}
}
}
}
总结
本节介绍了epoll的三个函数, 希望能够掌握并且自己修改服务端的函数. 接下来我们还要继续介绍epoll的功能.
- epoll函数
- epoll的优点
- poll和select的缺点