【面試】徹底理解 IO 多路複用
目錄
1、什麼是 IO 多路複用?
2、爲什麼出現 IO 多路複用機制?
3、IO 多路複用的三種實現方式
4、select 函數接口
5、select 使用示例
6、select 缺點
7、poll 函數接口
8、poll 使用示例
9、poll 缺點
10、epoll 函數接口
11、epoll 使用示例
12、epoll 缺點
13、epoll LT 與 ET 模式的區別
14、epoll 應用
15、select/poll/epoll 之間的區別
16、IO 多路複用完整代碼實現
17、高頻面試題
1、什麼是 IO 多路複用
「定義」
- IO 多路複用是一種同步 IO 模型,實現一個線程可以監視多個文件句柄;一旦某個文件句柄就緒,就能夠通知應用程序進行相應的讀寫操作;沒有文件句柄就緒時會阻塞應用程序,交出 cpu。多路是指網絡連接,複用指的是同一個線程
2、爲什麼有 IO 多路複用機制?
沒有 IO 多路複用機制時,有 BIO、NIO 兩種實現方式,但有一些問題
同步阻塞(BIO)
- 服務端採用單線程,當 accept 一個請求後,在 recv 或 send 調用阻塞時,將無法 accept 其他請求(必須等上一個請求處 recv 或 send 完),
無法處理併發
// 僞代碼描述
while(1) {
// accept阻塞
client_fd = accept(listen_fd)
fds.append(client_fd)
for (fd in fds) {
// recv阻塞(會影響上面的accept)
if (recv(fd)) {
// logic
}
}
}
- 服務器端採用多線程,當 accept 一個請求後,開啓線程進行 recv,可以完成併發處理,但隨着請求數增加需要增加系統線程,
大量的線程佔用很大的內存空間,並且線程切換會帶來很大的開銷,10000個線程真正發生讀寫事件的線程數不會超過20%,每次accept都開一個線程也是一種資源浪費
// 僞代碼描述
while(1) {
// accept阻塞
client_fd = accept(listen_fd)
// 開啓線程read數據(fd增多導致線程數增多)
new Thread func() {
// recv阻塞(多線程不影響上面的accept)
if (recv(fd)) {
// logic
}
}
}
同步非阻塞(NIO)
- 服務器端當 accept 一個請求後,加入 fds 集合,每次輪詢一遍 fds 集合 recv(非阻塞) 數據,沒有數據則立即返回錯誤,
每次輪詢所有fd(包括沒有發生讀寫事件的fd)會很浪費cpu
setNonblocking(listen_fd)
// 僞代碼描述
while(1) {
// accept非阻塞(cpu一直忙輪詢)
client_fd = accept(listen_fd)
if (client_fd != null) {
// 有人連接
fds.append(client_fd)
} else {
// 無人連接
}
for (fd in fds) {
// recv非阻塞
setNonblocking(client_fd)
// recv 爲非阻塞命令
if (len = recv(fd) && len > 0) {
// 有讀寫數據
// logic
} else {
無讀寫數據
}
}
}
IO 多路複用(現在的做法)
- 服務器端採用單線程通過 select/epoll 等系統調用獲取 fd 列表,遍歷有事件的 fd 進行 accept/recv/send,使其能
支持更多的併發連接請求
fds = [listen_fd]
// 僞代碼描述
while(1) {
// 通過內核獲取有讀寫事件發生的fd,只要有一個則返回,無則阻塞
// 整個過程只在調用select、poll、epoll這些調用的時候纔會阻塞,accept/recv是不會阻塞
for (fd in select(fds)) {
if (fd == listen_fd) {
client_fd = accept(listen_fd)
fds.append(client_fd)
} elseif (len = recv(fd) && len != -1) {
// logic
}
}
}
3、IO 多路複用的三種實現方式
-
select
-
poll
-
epoll
4、select 函數接口
#include <sys/select.h>
#include <sys/time.h>
#define FD_SETSIZE 1024
#define NFDBITS (8 * sizeof(unsigned long))
#define __FDSET_LONGS (FD_SETSIZE/NFDBITS)
// 數據結構 (bitmap)
typedef struct {
unsigned long fds_bits[__FDSET_LONGS];
} fd_set;
// API
int select(
int max_fd,
fd_set *readset,
fd_set *writeset,
fd_set *exceptset,
struct timeval *timeout
) // 返回值就緒描述符的數目
FD_ZERO(int fd, fd_set* fds) // 清空集合
FD_SET(int fd, fd_set* fds) // 將給定的描述符加入集合
FD_ISSET(int fd, fd_set* fds) // 判斷指定描述符是否在集合中
FD_CLR(int fd, fd_set* fds) // 將給定的描述符從文件中刪除
5、select 使用示例
int main() {
/*
* 這裏進行一些初始化的設置,
* 包括socket建立,地址的設置等,
*/
fd_set read_fs, write_fs;
struct timeval timeout;
int max = 0; // 用於記錄最大的fd,在輪詢中時刻更新即可
// 初始化比特位
FD_ZERO(&read_fs);
FD_ZERO(&write_fs);
int nfds = 0; // 記錄就緒的事件,可以減少遍歷的次數
while (1) {
// 阻塞獲取
// 每次需要把fd從用戶態拷貝到內核態
nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout);
// 每次需要遍歷所有fd,判斷有無讀寫事件發生
for (int i = 0; i <= max && nfds; ++i) {
if (i == listenfd) {
--nfds;
// 這裏處理accept事件
FD_SET(i, &read_fd);//將客戶端socket加入到集合中
}
if (FD_ISSET(i, &read_fd)) {
--nfds;
// 這裏處理read事件
}
if (FD_ISSET(i, &write_fd)) {
--nfds;
// 這裏處理write事件
}
}
}
6、select 缺點
-
單個進程所打開的 FD 是有限制的,通過 FD_SETSIZE 設置,默認 1024
-
每次調用 select,都需要把 fd 集合從用戶態拷貝到內核態,這個開銷在 fd 很多時會很大
-
對 socket 掃描時是線性掃描,採用輪詢的方法,效率較低(高併發時)
7、poll 函數接口
poll 與 select 相比,只是沒有 fd 的限制,其它基本一樣
#include <poll.h>
// 數據結構
struct pollfd {
int fd; // 需要監視的文件描述符
short events; // 需要內核監視的事件
short revents; // 實際發生的事件
};
// API
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
8、poll 使用示例
// 先宏定義長度
#define MAX_POLLFD_LEN 4096
int main() {
/*
* 在這裏進行一些初始化的操作,
* 比如初始化數據和socket等。
*/
int nfds = 0;
pollfd fds[MAX_POLLFD_LEN];
memset(fds, 0, sizeof(fds));
fds[0].fd = listenfd;
fds[0].events = POLLRDNORM;
int max = 0; // 隊列的實際長度,是一個隨時更新的,也可以自定義其他的
int timeout = 0;
int current_size = max;
while (1) {
// 阻塞獲取
// 每次需要把fd從用戶態拷貝到內核態
nfds = poll(fds, max+1, timeout);
if (fds[0].revents & POLLRDNORM) {
// 這裏處理accept事件
connfd = accept(listenfd);
//將新的描述符添加到讀描述符集合中
}
// 每次需要遍歷所有fd,判斷有無讀寫事件發生
for (int i = 1; i < max; ++i) {
if (fds[i].revents & POLLRDNORM) {
sockfd = fds[i].fd
if ((n = read(sockfd, buf, MAXLINE)) <= 0) {
// 這裏處理read事件
if (n == 0) {
close(sockfd);
fds[i].fd = -1;
}
} else {
// 這裏處理write事件
}
if (--nfds <= 0) {
break;
}
}
}
}
9、poll 缺點
-
每次調用 poll,都需要把 fd 集合從用戶態拷貝到內核態,這個開銷在 fd 很多時會很大
-
對 socket 掃描時是線性掃描,採用輪詢的方法,效率較低(高併發時)
10、epoll 函數接口
#include <sys/epoll.h>
// 數據結構
// 每一個epoll對象都有一個獨立的eventpoll結構體
// 用於存放通過epoll_ctl方法向epoll對象中添加進來的事件
// epoll_wait檢查是否有事件發生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可
struct eventpoll {
/*紅黑樹的根節點,這顆樹中存儲着所有添加到epoll中的需要監控的事件*/
struct rb_root rbr;
/*雙鏈表中則存放着將要通過epoll_wait返回給用戶的滿足條件的事件*/
struct list_head rdlist;
};
// API
int epoll_create(int size); // 內核中間加一個 ep 對象,把所有需要監聽的 socket 都放到 ep 對象中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 負責把 socket 增加、刪除到內核紅黑樹
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 負責檢測可讀隊列,沒有可讀 socket 則阻塞進程
11、epoll 使用示例
int main(int argc, char* argv[])
{
/*
* 在這裏進行一些初始化的操作,
* 比如初始化數據和socket等。
*/
// 內核中創建ep對象
epfd=epoll_create(256);
// 需要監聽的socket放到ep中
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
while(1) {
// 阻塞獲取
nfds = epoll_wait(epfd,events,20,0);
for(i=0;i<nfds;++i) {
if(events[i].data.fd==listenfd) {
// 這裏處理accept事件
connfd = accept(listenfd);
// 接收新連接寫到內核對象中
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
} else if (events[i].events&EPOLLIN) {
// 這裏處理read事件
read(sockfd, BUF, MAXLINE);
//讀完後準備寫
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
} else if(events[i].events&EPOLLOUT) {
// 這裏處理write事件
write(sockfd, BUF, n);
//寫完後準備讀
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
}
}
return 0;
}
12、epoll 缺點
- epoll 只能工作在 linux 下
13、epoll LT 與 ET 模式的區別
-
epoll 有 EPOLLLT 和 EPOLLET 兩種觸發模式,LT 是默認的模式,ET 是 “高速” 模式。
-
LT 模式下,只要這個 fd 還有數據可讀,每次 epoll_wait 都會返回它的事件,提醒用戶程序去操作
-
ET 模式下,它只會提示一次,直到下次再有數據流入之前都不會再提示了,無論 fd 中是否還有數據可讀。所以在 ET 模式下,read 一個 fd 的時候一定要把它的 buffer 讀完,或者遇到 EAGAIN 錯誤
14、epoll 應用
-
redis
-
nginx
15、select/poll/epoll 之間的區別
16、完整代碼示例
https://github.com/caijinlin/learning-pratice/tree/master/linux/io
17、高頻面試題
-
什麼是 IO 多路複用?
-
nginx/redis 所使用的 IO 模型是什麼?
-
select、poll、epoll 之間的區別
-
epoll 水平觸發(LT)與 邊緣觸發(ET)的區別?
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/KO0ogu-gTBF2fQiYC4rcDw