epoll笔记

基础概念

epoll

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

I/O事件

我们知道,内核有缓冲区。假设有两个进程A,B,进程B想读进程A写入的东西(即进程A做写操作,B做读操作)。进程A需要先写入到内核缓冲区中,然后B从内核缓冲区中读取,如图:
image
进程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事件通知机制详解,水平触发和边沿触发的区别
[3]. I/O事件
[4]. epoll内核源码分析
[5]. The C10K problem
[6]. IO多路复用之epoll总结

猜你喜欢

转载自blog.csdn.net/s_lisheng/article/details/80363308