epoll操作函数
epoll_create:创建监听红黑树
#include <sys/epoll.h>
int epoll_create(int size);
size:指定创建的树所能挂载的fd个数。(仅供内核参考)
返回值:
成功:指向红黑树树根的 fd
失败:-1, errno
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd: epoll_create函数返回值。
op:操作:
EPOLL_CTL_ADD:添加 fd 到监听红黑树
EPOLL_CTL_MOD:修改监听红黑树一个 fd 属性。
EPOLL_CTL_DEL:将 fd 从监听红黑树上摘下
fd:待操作的 文件描述符
event:描述fd监听事件(结构体地址)。
struct epoll_event {
uint32_t events;
epoll_data_t data;
};
events成员:fd对应的监听事件:
EPOLLIN(读)
EPOLLOUT(写)
EPOLLERR(异常)
data成员:(联合体、共用体)
int fd;
void *ptr;
struct test {
int fd;
void *call_back(fd, void *arg);
....
} *ptr;
uint32_t u32;
uint64_t u64;
返回值:
成功:0
失败:-1, errno
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd: epoll_create函数返回值。
events: 传出【数组】。传出的数组中,包含所有满足监听条件的fd及对应结构体。
maxevents:数组容量。
timeout:超时。
-1:阻塞。
0:非阻塞
>0:超时时间(毫秒)
返回值:
>0: 满足对应监听条件的文件描述符个数。用作 for 循环上限。
0: 超时到达,没有满足条件的fd。
-1: 错误,errno.
EPOLL事件有两种模型: Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。 Level Triggered (LT) 水平触发只要有数据都会触发。
ET模式和LT模式的区别:
LT 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。
ET 模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。
ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
-
ET:边沿触发。 只支持 非阻塞模式。
- fd 对应的缓冲区中,剩余数据不会触发 epoll_wait() 返回。
- 设置方法:
- int flg = fcntl(cfd, F_GETFL)
- flg |= O_NONBLOCK;
- fcntl(cfd, F_SETFL, flg);
- event.events = EPOLLIN | EPOLLET
-
LT:水平触发(默认)
- fd 对应的缓冲区中,剩余数据会触发 epoll_wait() 返回。
-
结论:
- epoll 边沿触发模式固定用法:
- 非阻塞 + 忙轮询while() + EPOLLIN | EPOLLET
epoll优缺点
1)监视的描述符数量不受限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 2048,举个例子,在 1GB 内存的机器上大约是 10 万左右,具体数目可以 cat /proc/sys/fs/file-max 察看,一般来说这个数目和系统内存关系很大。select() 的最大缺点就是进程打开的 fd 是有数量限制的。这对于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache 就是这样实现的),不过虽然 Linux 上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。
2)I/O 的效率不会随着监视 fd 的数量的增长而下降。select(),poll() 实现需要自己不断轮询所有 fd 集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而 epoll 其实也需要调用 epoll_wait() 不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait() 中进入睡眠的进程。虽然都要睡眠和交替,但是 select() 和 poll() 在“醒着”的时候要遍历整个 fd 集合,而 epoll 在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的 CPU 时间。这就是回调机制带来的性能提升。
3)select(),poll() 每次调用都要把 fd 集合从用户态往内核态拷贝一次,而 epoll 只要一次拷贝,这也能节省不少的开销。
代码参考
#include <sys/epoll.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int ret;
int fd;
ret = mkfifo("test_fifo", 0666);
if(ret != 0){
perror("mkfifo:");
}
fd = open("test_fifo", O_RDWR);
if(fd < 0){
perror("open fifo");
return -1;
}
ret = 0;
struct epoll_event event;
struct epoll_event wait_event;
int epfd = epoll_create(10);
if(-1 == epfd ){
perror ("epoll_create");
return -1;
}
event.data.fd = 0;
event.events = EPOLLIN;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);
if(-1 == ret){
perror("epoll_ctl");
return -1;
}
event.data.fd = fd;
event.events = EPOLLIN;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
if(-1 == ret){
perror("epoll_ctl");
return -1;
}
ret = 0;
while(1){
ret = epoll_wait(epfd, &wait_event, 2, -1);
if(ret == -1){
close(epfd);
perror("epoll");
}else if(ret > 0){
char buf[100] = {0};
if( ( 0 == wait_event.data.fd )
&& ( EPOLLIN == wait_event.events & EPOLLIN ) ){
read(0, buf, sizeof(buf));
printf("stdin buf = %s\n", buf);
}else if( ( fd == wait_event.data.fd )
&& ( EPOLLIN == wait_event.events & EPOLLIN ) ){
read(fd, buf, sizeof(buf));
printf("fifo buf = %s\n", buf);
}
}else if(0 == ret){
printf("time out\n");
}
}
close(epfd);
return 0;
}
|