大話「I-O 多路複用」
我是一名來自帝都 985 高校的機械研究生
本號專注於分享工科專業學習考試攻略、程序員學習求職經驗、教師求職考試經驗等相關內容,同時還會兼顧生活向,歡迎大家的關注和轉發!
目錄
select
poll
epoll
I/O多路複用
使得程序能同時監聽多個文件描述符,能夠提高程序的性能,Linux 下實現 I/O 多路複用的系統調用主要有 select
、poll
和 epoll
。
select
函數調用過程:
-
首先要構造一個關於文件描述符的列表,將要監聽的文件描述符添加到該列表中。
-
調用一個系統函數,監聽該列表中的文件描述符,直到這些描述符中的一個或者多個進行 I/O 操作時,該函數才返回。
-
這個函數是阻塞
-
函數對文件描述符的檢測的操作是由內核完成的
- 在返回時,它會告訴進程有多少(哪些)描述符要進行 I/O 操作。
// sizeof(fd_set) = 128 1024
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- 參數:
- nfds : 委託內核檢測的最大文件描述符的值 + 1
- readfds : 要檢測的文件描述符的讀的集合,委託內核檢測哪些文件描述符讀的屬性
- 一般檢測讀操作
- 對應的是對方發送過來的數據,因爲讀是被動的接收數據,檢測的就是讀緩衝區
- 是一個傳入傳出參數
- writefds : 要檢測的文件描述符的寫的集合,委託內核檢測哪些文件描述符寫的屬性
- 委託內核檢測寫緩衝區是不是還可以寫數據(不滿的就可以寫)
- exceptfds : 檢測發生異常的文件描述符的集合
- timeout : 設置的超時時間
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
- NULL : 永久阻塞,直到檢測到了文件描述符有變化
- tv_sec = 0 tv_usec = 0, 不阻塞
- tv_sec > 0 tv_usec > 0, 阻塞對應的時間
- 返回值 :
- -1 : 失敗
- >0(n) : 檢測的集合中有n個文件描述符發生了變化
// 將參數文件描述符fd對應的標誌位設置爲0
void FD_CLR(int fd, fd_set *set);
// 將參數文件描述符fd對應的標誌位設置爲1
void FD_SET(int fd, fd_set *set);
// 判斷fd對應的標誌位是0還是1,
// 返回值:fd 對應標誌位的值
// 0,返回0, 1,返回1
int FD_ISSET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化爲0
void FD_ZERO(fd_set *set);
- 缺點:
-
每次調用 select,都需要把 fd 集合從用戶態拷貝到內核態,這個開銷在 fd 很多時會很大;
-
同時每次調用 select 都需要在內核遍歷傳遞進來的所有 fd,這個開銷在 fd 很多時也很大;
-
select 支持的文件描述符數量太小了,默認是 1024;
-
fds 集合不能重用,每次都需要重置。
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
int main() {
// 創建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 綁定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 監聽
listen(lfd, 8);
// 創建一個fd_set的集合,存放的是需要檢測的文件描述符
fd_set rdset, tmp;
FD_ZERO(&rdset);
FD_SET(lfd, &rdset);
int maxfd = lfd;
while(1) {
tmp = rdset;
// 調用select系統函數,讓內核幫檢測哪些文件描述符有數據
int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
if(ret == -1) {
perror("select");
exit(-1);
} else if(ret == 0) {
continue;
} else if(ret > 0) {
// 說明檢測到了有文件描述符的對應的緩衝區的數據發生了改變
if(FD_ISSET(lfd, &tmp)) {
// 表示有新的客戶端連接進來了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 將新的文件描述符加入到集合中
FD_SET(cfd, &rdset);
// 更新最大的文件描述符
maxfd = maxfd > cfd ? maxfd : cfd;
}
for(int i = lfd + 1; i <= maxfd; i++) {
if(FD_ISSET(i, &tmp)) {
// 說明這個文件描述符對應的客戶端發來了數據
char buf[1024] = {0};
int len = read(i, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
close(i);
FD_CLR(i, &rdset);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
poll
#include <poll.h>
struct pollfd {
int fd; /* 委託內核檢測的文件描述符 */
short events; /* 委託內核檢測文件描述符的什麼事件 */
short revents; /* 文件描述符實際發生的事件 */
};
//栗子
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 參數:
- fds : 是一個struct pollfd 結構體數組,這是一個需要檢測的文件描述符的集合
- nfds : 這個是第一個參數數組中最後一個有效元素的下標 + 1
- timeout : 阻塞時長
0 : 不阻塞
-1 : 阻塞,當檢測到需要檢測的文件描述符有變化,解除阻塞
>0 : 阻塞的時長
- 返回值:
-1 : 失敗
>0(n) : 成功,n表示檢測到集合中有n個文件描述符發生變化
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
int main() {
// 創建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 綁定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 監聽
listen(lfd, 8);
// 初始化檢測的文件描述符數組
struct pollfd fds[1024];
for(int i = 0; i < 1024; i++) {
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lfd;
int nfds = 0;
while(1) {
// 調用poll系統函數,讓內核幫檢測哪些文件描述符有數據
int ret = poll(fds, nfds + 1, -1);
if(ret == -1) {
perror("poll");
exit(-1);
} else if(ret == 0) {
continue;
} else if(ret > 0) {
// 說明檢測到了有文件描述符的對應的緩衝區的數據發生了改變
if(fds[0].revents & POLLIN) {
// 表示有新的客戶端連接進來了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 將新的文件描述符加入到集合中
for(int i = 1; i < 1024; i++) {
if(fds[i].fd == -1) {
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}
// 更新最大的文件描述符的索引
nfds = nfds > cfd ? nfds : cfd;
}
for(int i = 1; i <= nfds; i++) {
if(fds[i].revents & POLLIN) {
// 說明這個文件描述符對應的客戶端發來了數據
char buf[1024] = {0};
int len = read(fds[i].fd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
close(fds[i].fd);
fds[i].fd = -1;
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(fds[i].fd, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
epoll
#include <sys/epoll.h>
// 創建一個新的epoll實例。在內核中創建了一個數據,這個數據中有兩個比較重要的數據:
// 一個是需要檢測的文件描述符的信息(紅黑樹)
// 還有一個是就緒列表,存放檢測到數據發生改變的文件描述符信息(雙向鏈表)
int epoll_create(int size);
- 參數:
size : 目前沒有意義了。隨便寫一個數,必須大於0
- 返回值:
-1 : 失敗
> 0 : 文件描述符,操作epoll實例的
// 對epoll實例進行管理:添加文件描述符信息,刪除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 參數:
- epfd : epoll實例對應的文件描述符
- op : 要進行什麼操作
EPOLL_CTL_ADD: 添加
EPOLL_CTL_MOD: 修改
EPOLL_CTL_DEL: 刪除
- fd : 要檢測的文件描述符
- event : 檢測文件描述符對應的檢測事件
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
常見的 Epoll 檢測事件:
- EPOLLIN
- EPOLLOUT
- EPOLLERR
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
// 檢測函數
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 參數:
- epfd : epoll實例對應的文件描述符
- events : 傳出參數,保存發送變化的文件描述符的信息
- maxevents : 第二個參數結構體數組的大小
- timeout : 阻塞時間
- 0 : 不阻塞
- -1 : 阻塞,直到檢測到fd數據發生變化,解除阻塞
- > 0 : 阻塞的時長(毫秒)
- 返回值:
- 成功,返回發送變化的文件描述符的個數 > 0
- 失敗 -1
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
// 創建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 綁定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 監聽
listen(lfd, 8);
// 調用epoll_create()創建一個epoll實例
int epfd = epoll_create(100);
// 將監聽的文件描述符相關的檢測信息添加到epoll實例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 監聽的文件描述符有數據達到,有客戶端連接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有數據到達,需要通信
char buf[1024] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
- Epoll 的工作模式:
- LT 模式 (水平觸發)
假設委託內核檢測讀事件 -> 檢測 fd 的讀緩衝區
讀緩衝區有數據 - > epoll 檢測到了會給用戶通知
a. 用戶不讀數據,數據一直在緩衝區,epoll 會一直通知
b. 用戶只讀了一部分數據,epoll 會通知
c. 緩衝區的數據讀完了,不通知
LT(level - triggered)是缺省的工作方式,並且同時支持 block 和 no-block socket。在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的 fd 進行 IO 操作。如果你不作任何操作,內核還是會繼續通知你的。
- ET 模式(邊沿觸發)
假設委託內核檢測讀事件 -> 檢測 fd 的讀緩衝區
讀緩衝區有數據 - > epoll 檢測到了會給用戶通知
a. 用戶不讀數據,數據一直在緩衝區中,epoll 下次檢測的時候就不通知了
b. 用戶只讀了一部分數據,epoll 不通知
c. 緩衝區的數據讀完了,不通知
ET(edge - triggered)是高速工作方式,只支持 no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核通過 epoll 告訴你。然後它會假設你知道文件描述符已經就緒,並且不會再爲那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再爲就緒狀態了。但是請注意,如果一直不對這個 fd 作 IO 操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once)。
ET 模式在很大程度上減少了 epoll 事件被重複觸發的次數,因此效率要比 LT 模式高。epoll 工作在 ET 模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀 / 阻塞寫操作把處理多個文件描述符的任務餓死。
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
//常見的Epoll檢測事件:
- EPOLLIN
- EPOLLOUT
- EPOLLERR
- EPOLLET
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/CxymdtDLwE2G_J4KH3Leig