【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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章