linux epoll

来源:互联网 发布:拍照的姿势软件 编辑:程序博客网 时间:2024/05/20 23:29

epoll 是 poll 的一种变种,在监听大量的文件描述符(file descriptors)有很好的伸缩性。

epoll_create

  1. // 这里两个系统调用失败时都是返回 -1,成功返回 epoll 文件描述符
  2. #include <sys/epoll.h>
  3.  
  4. // 从 Linux 内核 2.6.8 以后,size 就已经不再使用了
  5. // size 并非指定后备存储器(backing store)的最大大小
  6. int epoll_create(int size);
  7. // flags 为 0 其含义同于 epoll_create
  8. // flags 可以被设置为 EPOLL_CLOEXEC 表示
  9. // 对于此 epoll 实例对应的文件描述符设置 close-on-exec 标识
  10. // close-on-exec 在 http://name5566.com/3201.html 中有详细的描述
  11. int epoll_create1(int flags);

epoll_create 系统调用用于创建 epoll 并返回一个表示创建的 epoll 实例的文件描述符。epoll_create1 则扩展了 epoll_create 的功能。如果某个 epoll 实例不再使用,那么应该调用close 进行关闭。

epoll_ctl

  1. #include <sys/epoll.h>
  2.  
  3. // epfd 表示 epoll 实例对应的文件描述符
  4. // op 表示控制操作
  5. // fd 表示目标文件描述符(op 操作的对象)
  6. // 函数调用成功返回 0,失败返回 -1
  7. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl 系统调用主要是完成一些控制操作,这些操作包括(op 参数可选值):

  1. EPOLL_CTL_ADD
    注册 fd 目标文件描述符到 epoll 实例上,并且关联事件 event 和目标文件描述符 fd(也就是指定文件描述符 fd 关心的事件)
  2. EPOLL_CTL_MOD
    改变和目标文件描述符 fd 相关联的事件 event
  3. EPOLL_CTL_DEL
    从 epoll 实例中移除目标文件描述符 fd。event 参数会被忽略,一般来说可以传递 NULL,但是在内核版本 2.6.9 之前 event 不能为 NULL,虽然 event 参数是被忽略的

epoll_event 结构体被定义如下:

  1. typedef union epoll_data {
  2. void *ptr;
  3. int fd;
  4. uint32_t u32;
  5. uint64_t u64;
  6. } epoll_data_t;
  7.  
  8. struct epoll_event {
  9. // events 表示一个事件集
  10. uint32_t events;
  11. epoll_data_t data;
  12. };

这里的 epoll_event 中的 events 成员是一个 bits 集合(事件集合),可以使用了以下事件类型:

  1. EPOLLIN
    相关文件对 read(以及 accept)操作有效
  2. EPOLLOUT
    相关文件对 write 操作有效
  3. EPOLLRDHUP(最低要求 2.6.17 内核)
    Stream socket(TCP)关闭连接或者关闭连接的写的一方
  4. EPOLLPRI
    对于 read 操作存在可用的 urgent 数据了
  5. EPOLLERR
    相关文件描述符出现错误情况。我们不需要设置此事件类型,epoll_wait 总是会等待此事件
  6. EPOLLHUP
    相关文件描述符出现挂起情况。我们不需要设置此事件类型,epoll_wait 总是会等待此事件
  7. EPOLLET
    对相关文件描述符设置边缘触发行为(Edge triggered behavior)。epoll 默认的行为是水平触发(Level triggered)
  8. EPOLLONESHOT(最低要求 2.6.2 内核)

epoll_wait

  1. #include <sys/epoll.h>
  2.  
  3. // epfd 参数表示 epoll 实例对应的文件描述符
  4. // events 输出参数用于在函数返回时获取到多个 epoll_event 对象,events 通常都是数组的首地址
  5. // maxevents 参数用于描述 events 数组的长度
  6. // maxevents 的详细讨论参考:http://stackoverflow.com/questions/2969425/epoll-wait-maxevents
  7. // timeout 参数表示此函数调用等待的最长时间,单位为毫秒,值为 -1 表示无限等待
  8. //
  9. // 出错时函数返回 -1,否则返回为 I/O 请求已做好准备的文件描述符的数量
  10. int epoll_wait(int epfd, struct epoll_event *events,
  11. int maxevents, int timeout);
  12. // 此函数特有一个参数 sigmask 用于设置信号掩码,使用 NULL 表示不进行设置
  13. int epoll_pwait(int epfd, struct epoll_event *events,
  14. int maxevents, int timeout,
  15. const sigset_t *sigmask);

epoll_wait 用来等待在某个 epoll 上发生的 I/O 事件。epoll_pwait 和 epoll_wait 的区别在于,epoll_pwait 可以设定一个信号掩码(http://name5566.com/3206.html 中谈到了信号掩码)。我们这样使用 epoll_pwait:

  1. ready = epoll_pwait(epfd, &events, maxevents, timeout, &sigmask);

其等价于:

  1. sigset_t origmask;
  2.  
  3. sigprocmask(SIG_SETMASK, &sigmask, &origmask);
  4. ready = epoll_wait(epfd, &events, maxevents, timeout);
  5. sigprocmask(SIG_SETMASK, &origmask, NULL);

水平触发和边缘触发

水平触发和边缘触发两个术语也被用于其他领域。抽象来说:

  1. 水平触发:在特定状态下触发
  2. 边缘触发:在状态变化时触发

我们假定:

  1. 存在一个用于从某个管道中读取数据的文件句柄 rfd 且 rfd 被注册到 epoll 中
  2. 上述管道的另一端被写入了 2kB 的数据
  3. 调用 epoll_wait 完成并且返回 rfd(也就是 rfd 为一个准备就绪的文件描述符)
  4. 上述管道读的一方从 rfd 中读取了 1kB 的数据
  5. 再次调用 epoll_wait

如果 rfd 文件描述符在加入 epoll 时使用了 EPOLLET 标志(也就是使用边缘触发),在第 5 步中调用 epoll_wait 将可能阻塞(虽然在此文件输入缓冲区中仍有可用数据)。对于边缘触发模式,只有被监听的文件描述符发生变化时(changes occur)才投递事件。使用了 EPOLLET 标志(也就是使用边缘触发)意味着我们需要使用非阻塞的文件描述符并且一直调用 accept、read、write 直到出现错误 EAGAIN。

如果我们使用水平触发模式,它就相当于一个快速的 poll,只要文件描述符上还有数据可以读,则每次调用 epoll_wait 都会返回(而非阻塞)。在水平触发模式下,我们编写会容易一些,也可以不需要使用(当然也可以选择使用)非阻塞的文件描述符。从效率上来说,边缘触发通常被认为效率更高。


————————————————————————————————————————————————————————————————————————

简述EPOLL

1,epoll为啥高效
TCP协议栈发现一个socket有事件发生时,它就会唤醒阻塞中的进程(调用select、poll、epoll_wait而睡眠的应用程序)。区别在于,select和poll的内核实现中,协议栈仅仅是唤醒应用程序,没有其他后续操作,所以select内部醒来后还得自己轮询所有的socket;当socket的数量很大时,这里的效率就有了瓶颈。
epoll不同,在内核支持epoll后,唤醒应用程序的操作中,可以挂载回调函数,所以epoll实现的回调函数就把发生事件的socket信息记录到一个就绪队列中了。当epoll_wait醒来的时候,它当然不需要轮询,直接就从就绪队列中就可以取出所有发生了事件的套接字。
这里面,显然有计算机中中断和轮询的思想在。
2,ET和LT
简单地说,LT的套接字从就绪队列中取出后,如果该套接字上确实是有事件,那么内核还会将它再入放入就绪队列,下次epoll_wait还会返回该套接字。ET就不一样,从就绪队列中取出来后就不会再放进去了,除非TCP协议栈再次因为有了新事件再次将其放入队列。
这样导致的差别是LT的套接字不会发生“饿死”的现象,比如说LT的套接字收到了10000个字节的数据,那么你每次read 100个字节后继续epoll_wait,该套接字的事件依然会被返回(因为每次epoll_wait时,内核发现数据没有读完,依然会让该套接字保持在就绪队列中),不管有没有新的数据到达。ET就不一样,如果你读了100字节后就继续epoll_wait,只要没有新数据(或事件)到达,那么就永远不会监听到该套接字的事件了。所以ET的套接字常常设置为非阻塞的,在epoll_wait返回后一次性读完,直到返回EAGAIN为止。LT的套接字当然最好也设置为非阻塞的。
3,结论
写技术文章是件很麻烦的事,不知道说明白没有,EPOLL的关键是在内核里设置了一个“就绪队列”以及回调函数机制。

范例

http://man7.org/linux/man-pages/man7/epoll.7.html 中给出了一个简单的范例,而https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/epoll-example.c 则有一个更加详细的范例,此详细范例也可以在这里查看。

0 0
原创粉丝点击