【Linux 应用编程】IO 多路转接 - epoll

跟 select、poll 的对比

epoll 性能更高,Nginx、redis 等流行的软件,都是基于 epoll 实现的。epoll 优点有:

  • 监听描述符数量大于 1024
  • 只返回准备好的描述符,不需要浪费时间遍历
  • 描述符集合基于红黑树实现,高效

使用步骤

epoll 有 3 个函数:

  • epoll_create 指定 epoll 对应的红黑树的大概节点数,并返回 epoll 描述符
  • epoll_ctl 控制 epoll 描述符,增加、删除、修改节点
  • epoll_wait 开始监听

epoll_create

#include <sys/epoll.h>

int epoll_create(int size);

epoll_ctl

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数:

  • epfd:epoll 描述符
  • op:要进行的操作操作
    • EPOLL_CTL_ADD:增加要监听的描述符
    • EPOLL_CTL_MOD:修改描述符的 event
    • EPOLL_CTL_DEL:取消监听指定的描述符
  • fd:要操作的描述符
  • event:struct epoll_event 结构体类型,是跟 fd 描述符相关联的信息。其中 data 字段是 union epoll_data 联合体类型,可以存放 fd 用于回调,或存放回调函数的结构体指针
    • events:要监听的事件。
      • EPOLLIN:是否满足 read 操作
      • EPOLLOUT:是否满足 write 操作
      • EPOLLERR:是否出错
      • EPOLLRDHUP:socket 对方关闭连接,或对方关闭写端
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_wait

#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);

demo

struct epoll_event evt;
struct epoll_event cbevt[10];

epfd = epoll_create(10);
evt.events = EPOLLIN | EPOLLET;

evt.data.fd = lfd; /* 假设 lfd 是要监听的描述符 */
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &evt);

while(1) {
	ret = epoll_wait(epfd, cbevt, 10, -1);
	if (ret < 0) {
		perror("epoll_wait");
		return -1;
	} else if (ret == 0) {
		perror("peer closed");
		return 0;
	} else {
		for (i = 0; i < ret; i++) {
			if (cbevt[i].data.fd == lfd) {
				/* 业务代码 */
			}
		}
	}
}

水平触发、边沿触发

epoll 提供了两种触发模式,目的是减少对 epoll_wait 的调用次数,从而减少阻塞,减少在内核态和用户态之间切换的频率,提高效率。

  • 边沿触发:EPOLL_ET(Edge Trigger),只有用户端发送数据过来才会触发
  • 水平触发:EPOLL_LT(Level Trigger),缓冲区只要还是数据,就不停触发

假设客户端发送 1000B 数据,服务器首次取出 500B,此时,对于这两种触发模式有不同的表现:

  • 边沿触发:不再从 epoll_wait 返回,直到用户下一次数据到达
  • 水平触发:进入 epoll_wait 后立刻返回,直到服务器把所有数据都取出,才会阻塞

非阻塞 IO

Linux 文件 IO 操作时,除了普通文件和块设备文件外,都可以设置非阻塞 IO。有两种方式可以设置:

  • 通过 open 系统调用打开文件时,指定 O_NONBLOCK 参数
  • 对于已经打开的文件,通过 fcntl 设置 O_NONBLOCK 参数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

open(path, O_NONBLOCK);

flag = fcntl(fd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, O_NONBLOCK);

代码示例

下面代码创建了十个套接字,并在父进程中监听可读状态,在子进程中随机写入。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

#define MAX_FILE 10

void err_exit(const char* str) {
	perror(str);
	exit(EXIT_FAILURE);
}

int main() {
	int i, ret;
	int epfd;
	int pipefds[10][2];
	struct epoll_event cbevt[100];
	struct epoll_event evt;
	for (i = 0; i < 10; i++) {
		ret = pipe(pipefds[i]);
		if (ret < 0)
			err_exit("pipe");
	}
	ret = fork();
	if (ret < 0) {
		err_exit("fork");
	} else if (ret == 0) {
		char buf[] = "hello world\n";
		for (i = 0; i < 10; i++) {
			close(pipefds[i][0]);
		}
		while(1) {
			ret = rand() % 10;
			printf("child write pipe %d \n", ret);
			write(pipefds[ret][1], buf, sizeof(buf));
			sleep(1);
		}
	} else {
		char rdbuf[BUFSIZ];
		for (i = 0; i < 10; i++) {
			close(pipefds[i][1]);
		}
		epfd = epoll_create(MAX_FILE);
		if (epfd < 0)
			err_exit("epoll_create");
		for (i = 0; i < 10; i++) {
			evt.events = EPOLLIN;
			evt.data.fd = pipefds[i][0];
			epoll_ctl(epfd, EPOLL_CTL_ADD, pipefds[i][0], &evt);
		}
		while(1) {
			ret = epoll_wait(epfd, cbevt, 100, -1);
			printf("ret of epoll_wait is %d\n", ret);
			if (ret < 0) { 
				err_exit("epoll_wait");
			} else if (ret == 0) {
				puts("time out");
			} else {
				for (i = 0; i < ret; i++) {
				printf("ret is:%d\n", ret);
					read(cbevt[i].data.fd, rdbuf, sizeof(rdbuf));
					printf("read res is:%s\n", rdbuf);
				}
			}
		}
	}
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章