IO模型分析
何為同步與非同步
在LNMP的生態當中我們基本上用到的是同步操作.例如PHP的
file_get_contents
函式就是一個典型的同步任務.而Node.js當中的回撥模式是一個典型的非同步模式如Promise
.
同步
同步可以理解為:傳送一個系統呼叫,並等待系統呼叫的返回.
非同步
非同步可以理解為:傳送一個系統呼叫,不等待系統返回可以繼續處理當前的任務,等待系統處理完畢的回撥.
不同的IO模型
阻塞與非阻塞強調的是當前程序或者是執行緒的狀態.
阻塞IO
從圖中可知,資料經歷兩種變化.
1.資料從沒有準備狀態到準備完成
2.從核心態拷貝資料到使用者態.
非阻塞IO
發起recvfrom呼叫後核心在準備資料.
同步不斷的輪訓,發現數據準備完成後,將資料從核心態拷貝到使用者態.
多路複用IO
IO多路複用:無需使用polling或者多執行緒就可以"同時"(指時間段)處理多個檔案描述符.但是需要注意從核心讀取資料還是同步的.
經典的select與epoll都是IO多路複用.圖中描述的是select模型.epoll詢問方式和select有很大不同.
epoll與select的區別
- epoll要比select高效
- select的支援最大檔案描述符有限,epoll沒有限制
select:每次返回準備好的檔案描述數量.呼叫方自己遍歷所有的FD,判斷是否準備好,準備好然後再讀資料.
select的最大的問題,我們不知道那些FD是否準備好,只能去遍歷.
假設有100個FD,只有1個準備好,可想而知,相當於一個順序查詢,效率低下.
因此高效的epoll就出現了,只告訴呼叫方準備好的FD
fd_set readfds, writefds;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
for (int i=0; i < read_fd_count; i++)
FD_SET(my_read_fds[i], &readfds);
for (int i=0; i < write_fd_count; i++)
FD_SET(my_write_fds[i], &writefds);
struct timeval timeout;
timeout. tv_sec = 3;
timeout.tv_usec = 0;
int num_ready = select(FD_SETSIZE, &readfds, &writefds, NULL, &timeout);
if (num_ready < 0) {
perror("error in select()");
} else if (num_ready == 0) {
printf("timeout\n");
} else {
for (int i=0; i < read_fd_count; i++)
if (FD_ISSET(my_read_fds[i], &readfds))
printf("fd %d is ready for reading\n", my_read_fds[i]);
for (int i=0; i < write_fd_count; i++)
if (FD_ISSET(my_write_fds[i], &writefds))
printf("fd %d is ready for writing\n", my_write_fds[i]);
}
epoll不是POSIX標準.但卻在Linux當中被廣泛使用.
epoll:只返會準備好的FD.
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Code to set up listening socket, 'listen_sock',
(socket(), bind(), listen()) omitted */
epollfd = epoll_create1(0);//建立epoll
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {//監聽指定的listen_sock
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);//迴圈取事件,放入event事件
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock,
(struct sockaddr *) &addr, &addrlen);//接收客戶端連線
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_sock);// ET模式下需要設定為非阻塞模式
ev.events = EPOLLIN | EPOLLET;// ET模式下
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {//新增客戶端的事件到當前的epoll
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd); //使用新的準備好的檔案描述符不停接收資料,並在這裡記錄當前讀取,寫入的資料
}
}
}
訊號驅動IO
訊號驅動IO無需等待資料準備好,系統會發送訊號SIGIO回撥handler,然後讀取資料.
這時候IO是同步讀取,從核心態到使用者態.
非同步IO
真正的非同步IO,從圖中很明顯看出資料已經被拷貝到了使用者態,這是與訊號IO模型最大的區別.
IO模型比較
從圖中我可以得出:
- 非同步IO:不會導致執行緒阻塞.
- 同步IO:執行緒一直阻塞到資料IO操作完成.