跟 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 對方關閉連接,或對方關閉寫端
- events:要監聽的事件。
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;
}