欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

epoll笔记

程序员文章站 2022-03-14 12:13:43
...

基础概念

epoll - I/O event notification facility,一种I/O事件通知机制

I/O事件

我们知道,内核有缓冲区。假设有两个进程A,B,进程B想读进程A写入的东西(即进程A做写操作,B做读操作)。进程A需要先写入到内核缓冲区中,然后B从内核缓冲区中读取,如图:
epoll笔记
进程B会监听内核缓冲区的变化。

I/O事件的阻塞与同步
  1. 当内核缓冲区为空的时候,进程B会阻塞住
  2. 当A往内核缓冲区写入时,内核缓冲区就不是空状态了,这时候就会唤醒进程B
  3. 如果缓冲区满了,但是进程B没有被唤醒,就会通知进程A,告诉A不要再写入数据了,也就是进程A被阻塞
  4. 当进程B被唤醒后,B就从缓冲区读取数据,由于B在读数据,缓冲区就不会是满的状态了,这时候就会通知A继续写数据,也就是进程A被唤醒
  5. 如果进程A还没有唤醒,而缓冲区被B读完了(缓冲区为空),这时候就会阻塞进程B
阻塞I/O的缺点

在阻塞I/O情况下,一个线程只能处理一个流的I/O事件。也就是说,如果想处理多个流的I/O事件,就必须使用多进程或者多线程——效率低

非阻塞I/O

非阻塞I/O涉及事件通知机制及轮询机制。

  • 通知机制——就是当事件发生的时候,去通知他
  • 轮询机制——通知机制的反面,工作过程类似枚举,效率很低

最开始能想到的就是用轮询的方法:依次询问每个流,如果缓冲区不为空,就进行操作;否则,询问下一个流
但是这种方法效率很低,会白白浪费掉CPU资源。于是便引入了代理——poll

poll

poll代理可以同时观察很多I/O流事件,在空闲的时候(即没有I/O事件的时候),会阻塞当前线程;当有I/O事件的时候,会被唤醒,然后把所有流轮询一遍。这样就能通过减少盲目的轮询来减少对CPU资源的浪费。但是,缺点也很明显:由于每次唤醒都需要把所有流都轮询一遍,当流很多的时候,轮询的时间会很长

poll进化版——epoll

epoll是基于事件的轮询,它会记录是哪个流产生了I/O事件,然后针对这个流来进行操作,大大降低了复杂度。

水平触发与边缘触发

水平触发(level-trggered)

  • 只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,
  • 当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知

边缘触发(edge-triggered)

  • 当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知,
  • 当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知

两者的区别?
水平触发是只要读缓冲区有数据,就会一直触发可读信号,而边缘触发仅仅在空变为非空的时候通知一次,举个例子:
1. 读缓冲区刚开始是空的
2. 读缓冲区写入2KB数据
3. 水平触发和边缘触发模式此时都会发出可读信号
4. 收到信号通知后,读取了1kb的数据,读缓冲区还剩余1KB数据
5. 水平触发会再次进行通知,而边缘触发不会再进行通知

所以边缘触发需要一次性的把缓冲区的数据读完为止,也就是一直读,直到读到EGAIN(EGAIN说明缓冲区已经空了)为止,因为这一点,边缘触发需要设置文件句柄为非阻塞。

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

An application that employs the EPOLLET flag should use nonblocking file descriptors to avoid having a blocking read or write starve a task that is handling multiple file descriptors. The suggested way to use epoll as an edge-triggered (EPOLLET) interface is as follows:

  • with nonblocking file descriptors; and
  • by waiting for an event only after read(2) or write(2) return EAGAIN.
//水平触发
ret = read(fd, buf, sizeof(buf));

//边缘触发
while(true) {
    ret = read(fd, buf, sizeof(buf);
    if (ret == EAGAIN) break;
}

Linux中的EAGAIN含义

Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。
从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。

例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。

epoll接口介绍

epoll_create

创建epoll实例,会创建所需要的红黑树,以及就绪链表,以及代表epoll实例的文件句柄。

//epoll_create, epoll_create1 - open an epoll file descriptor
#include <sys/epoll.h>

int epoll_create(int size);
int epoll_create1(int flags);

更多参考EPOLL_CREATE

epoll_ctl

添加,修改,或者删除注册到epoll实例中的文件描述符上的监控事件。

//epoll_ctl - control interface for an epoll file descriptor
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd: epoll实例file descriptor,epoll_create函数返回值
  • op:操作类型:
op description
EPOLL_CTL_ADD 添加事件
EPOLL_CTL_MOD 修改事件
EPOLL_CTL_DEL 移除事件
  • fd: 操作的目标文件描述符
  • event: 要操作的事件
// The event argument describes the object linked to the file descriptor fd. The struct epoll_event is defined as:
typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
};

更多参考EPOLL_CTL

epoll_wait

等待epoll实例中注册的事件触发。

//epoll_wait,  epoll_pwait  -  wait  for  an I/O event on an epoll file descriptor
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);
  • epfd: epoll实例文件描述符
  • events: 数组出参,用来记录被触发的events,其大小应该和maxevents一致
  • maxevents: 返回的events的最大个数,如果最大个数大于实际触发的个数,则下次epoll_wait的时候仍然可以返回
  • timeout: 等待事件,毫秒为单位 -1:无限等待 0:立即返回

更多参考EPOLL_WAIT

代码示例

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;

/* Code to set up listening socket, 'listen_sock',
(socket(), bind(), listen()) omitted */

epollfd = epoll_create1(0);
if (epollfd == -1) {
   perror("epoll_create1");
   exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
   perror("epoll_ctl: listen_sock");
   exit(EXIT_FAILURE);
}

for (;;) {
   nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
   if (nfds == -1) {
        perror("epoll_wait");
        exit(EXIT_FAILURE);
   }

   for (n = 0; n < nfds; ++n) {
       if (events[n].data.fd == listen_sock) {
           conn_sock = accept(listen_sock,(struct sockaddr *) &addr, &addrlen);
           if (conn_sock == -1) {
               perror("accept");
               exit(EXIT_FAILURE);
           }
           setnonblocking(conn_sock);
           ev.events = EPOLLIN | EPOLLET;
           ev.data.fd = conn_sock;
          if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,&ev) == -1) {
               perror("epoll_ctl: conn_sock");
               exit(EXIT_FAILURE);
          }
      } else {
              do_use_fd(events[n].data.fd);
             }
      }
}

参考文档:

[1]. EPOLL Linux Man官方文档
[2]. epoll事件通知机制详解,水平触发和边沿触发的区别
[4]. epoll内核源码分析
[5]. The C10K problem
[6]. IO多路复用之epoll总结

相关标签: epoll