【面試】徹底理解 IO多路復(fù)用
目錄
1、什么是IO多路復(fù)用?
2、為什么出現(xiàn)IO多路復(fù)用機(jī)制?
3、IO多路復(fù)用的三種實現(xiàn)方式
4、select函數(shù)接口
5、select使用示例
6、select缺點
7、poll函數(shù)接口
8、poll使用示例
9、poll缺點
10、epoll函數(shù)接口
11、epoll使用示例
12、epoll缺點
13、epoll LT 與 ET模式的區(qū)別
14、epoll應(yīng)用
15、select/poll/epoll之間的區(qū)別
16、IO多路復(fù)用完整代碼實現(xiàn)
17、高頻面試題
1、什么是IO多路復(fù)用
「定義」
IO多路復(fù)用是一種同步IO模型,實現(xiàn)一個線程可以監(jiān)視多個文件句柄;一旦某個文件句柄就緒,就能夠通知應(yīng)用程序進(jìn)行相應(yīng)的讀寫操作;沒有文件句柄就緒時會阻塞應(yīng)用程序,交出cpu。多路是指網(wǎng)絡(luò)連接,復(fù)用指的是同一個線程
2、為什么有IO多路復(fù)用機(jī)制?
沒有IO多路復(fù)用機(jī)制時,有BIO、NIO兩種實現(xiàn)方式,但有一些問題
同步阻塞(BIO)
服務(wù)端采用單線程,當(dāng)accept一個請求后,在recv或send調(diào)用阻塞時,將無法accept其他請求(必須等上一個請求處recv或send完),
無法處理并發(fā)
// 偽代碼描述
while(1) {
// accept阻塞
client_fd = accept(listen_fd)
fds.append(client_fd)
for (fd in fds) {
// recv阻塞(會影響上面的accept)
if (recv(fd)) {
// logic
}
}
}
服務(wù)器端采用多線程,當(dāng)accept一個請求后,開啟線程進(jìn)行recv,可以完成并發(fā)處理,但隨著請求數(shù)增加需要增加系統(tǒng)線程,
大量的線程占用很大的內(nèi)存空間,并且線程切換會帶來很大的開銷,10000個線程真正發(fā)生讀寫事件的線程數(shù)不會超過20%,每次accept都開一個線程也是一種資源浪費
// 偽代碼描述
while(1) {
// accept阻塞
client_fd = accept(listen_fd)
// 開啟線程read數(shù)據(jù)(fd增多導(dǎo)致線程數(shù)增多)
new Thread func() {
// recv阻塞(多線程不影響上面的accept)
if (recv(fd)) {
// logic
}
}
}
同步非阻塞(NIO)
服務(wù)器端當(dāng)accept一個請求后,加入fds集合,每次輪詢一遍fds集合recv(非阻塞)數(shù)據(jù),沒有數(shù)據(jù)則立即返回錯誤,
每次輪詢所有fd(包括沒有發(fā)生讀寫事件的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) {
// 有讀寫數(shù)據(jù)
// logic
} else {
無讀寫數(shù)據(jù)
}
}
}
IO多路復(fù)用(現(xiàn)在的做法)
服務(wù)器端采用單線程通過select/epoll等系統(tǒng)調(diào)用獲取fd列表,遍歷有事件的fd進(jìn)行accept/recv/send,使其能
支持更多的并發(fā)連接請求
fds = [listen_fd]
// 偽代碼描述
while(1) {
// 通過內(nèi)核獲取有讀寫事件發(fā)生的fd,只要有一個則返回,無則阻塞
// 整個過程只在調(diào)用select、poll、epoll這些調(diào)用的時候才會阻塞,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多路復(fù)用的三種實現(xiàn)方式
select
poll
epoll
4、select函數(shù)接口
#include <sys/select.h>
#include <sys/time.h>
#define FD_SETSIZE 1024
#define NFDBITS (8 * sizeof(unsigned long))
#define __FDSET_LONGS (FD_SETSIZE/NFDBITS)
// 數(shù)據(jù)結(jié)構(gòu) (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
) // 返回值就緒描述符的數(shù)目
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() {
/*
* 這里進(jìn)行一些初始化的設(shè)置,
* 包括socket建立,地址的設(shè)置等,
*/
fd_set read_fs, write_fs;
struct timeval timeout;
int max = 0; // 用于記錄最大的fd,在輪詢中時刻更新即可
// 初始化比特位
FD_ZERO(&read_fs);
FD_ZERO(&write_fs);
int nfds = 0; // 記錄就緒的事件,可以減少遍歷的次數(shù)
while (1) {
// 阻塞獲取
// 每次需要把fd從用戶態(tài)拷貝到內(nèi)核態(tài)
nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout);
// 每次需要遍歷所有fd,判斷有無讀寫事件發(fā)生
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缺點
單個進(jìn)程所打開的FD是有限制的,通過FD_SETSIZE設(shè)置,默認(rèn)1024
每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個開銷在fd很多時會很大
對socket掃描時是線性掃描,采用輪詢的方法,效率較低(高并發(fā)時)
7、poll函數(shù)接口
poll與select相比,只是沒有fd的限制,其它基本一樣
#include <poll.h>
// 數(shù)據(jù)結(jié)構(gòu)
struct pollfd {
int fd; // 需要監(jiān)視的文件描述符
short events; // 需要內(nèi)核監(jiān)視的事件
short revents; // 實際發(fā)生的事件
};
// API
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
8、poll使用示例
// 先宏定義長度
#define MAX_POLLFD_LEN 4096
int main() {
/*
* 在這里進(jìn)行一些初始化的操作,
* 比如初始化數(shù)據(jù)和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從用戶態(tài)拷貝到內(nèi)核態(tài)
nfds = poll(fds, max+1, timeout);
if (fds[0].revents & POLLRDNORM) {
// 這里處理accept事件
connfd = accept(listenfd);
//將新的描述符添加到讀描述符集合中
}
// 每次需要遍歷所有fd,判斷有無讀寫事件發(fā)生
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缺點
每次調(diào)用poll,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個開銷在fd很多時會很大
對socket掃描時是線性掃描,采用輪詢的方法,效率較低(高并發(fā)時)
10、epoll函數(shù)接口
#include <sys/epoll.h>
// 數(shù)據(jù)結(jié)構(gòu)
// 每一個epoll對象都有一個獨立的eventpoll結(jié)構(gòu)體
// 用于存放通過epoll_ctl方法向epoll對象中添加進(jìn)來的事件
// epoll_wait檢查是否有事件發(fā)生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可
struct eventpoll {
/*紅黑樹的根節(jié)點,這顆樹中存儲著所有添加到epoll中的需要監(jiān)控的事件*/
struct rb_root rbr;
/*雙鏈表中則存放著將要通過epoll_wait返回給用戶的滿足條件的事件*/
struct list_head rdlist;
};
// API
int epoll_create(int size); // 內(nèi)核中間加一個 ep 對象,把所有需要監(jiān)聽的 socket 都放到 ep 對象中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 負(fù)責(zé)把 socket 增加、刪除到內(nèi)核紅黑樹
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 負(fù)責(zé)檢測可讀隊列,沒有可讀 socket 則阻塞進(jìn)程
11、epoll使用示例
int main(int argc, char* argv[])
{
/*
* 在這里進(jìn)行一些初始化的操作,
* 比如初始化數(shù)據(jù)和socket等。
*/
// 內(nèi)核中創(chuàng)建ep對象
epfd=epoll_create(256);
// 需要監(jiān)聽的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);
// 接收新連接寫到內(nèi)核對象中
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
} else if (events[i].events&EPOLLIN) {
// 這里處理read事件
read(sockfd, BUF, MAXLINE);
//讀完后準(zhǔn)備寫
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
} else if(events[i].events&EPOLLOUT) {
// 這里處理write事件
write(sockfd, BUF, n);
//寫完后準(zhǔn)備讀
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
}
}
return 0;
}
12、epoll缺點
epoll只能工作在linux下
13、epoll LT 與 ET模式的區(qū)別
epoll有EPOLLLT和EPOLLET兩種觸發(fā)模式,LT是默認(rèn)的模式,ET是“高速”模式。
LT模式下,只要這個fd還有數(shù)據(jù)可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作
ET模式下,它只會提示一次,直到下次再有數(shù)據(jù)流入之前都不會再提示了,無論fd中是否還有數(shù)據(jù)可讀。所以在ET模式下,read一個fd的時候一定要把它的buffer讀完,或者遇到EAGAIN錯誤
14、epoll應(yīng)用
redis
nginx
15、select/poll/epoll之間的區(qū)別

16、完整代碼示例
https://github.com/caijinlin/learning-pratice/tree/master/linux/io
17、高頻面試題
什么是IO多路復(fù)用?
nginx/redis 所使用的IO模型是什么?
select、poll、epoll之間的區(qū)別
epoll 水平觸發(fā)(LT)與 邊緣觸發(fā)(ET)的區(qū)別?
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
