一. 談信號驅動IO (對比異步IO來看)
信號驅動IO 對比 異步 IO進行理解
信號驅動IO: 內核將數據準備好的時候, 使用SIGIO信號通知應用程序進行IO操作
通知應用程序處理IO, 是開始處理IO, 這個時候還是存在阻塞的,將數據從內核態拷貝進入到用戶態的過程至少是阻塞住的 (應用程序將數據從內核態拷貝到用戶態的過程是阻塞等待的, 和異步IO的區別) (此處是區分信號驅動IO和異步IO的關鍵所在)
信號驅動IO, 我們提前在信號集合中設置好IO信號等待, 注冊好對應的IO處理函數 handler,IO數據準備就緒后,會遞交SIGIO信號,通知應用程序中斷然后開始進行對應的IO處理邏輯. 但是通知處理IO的時候存在將數據從 內核空間拷貝到用戶空間的過程,(而異步IO是數據拷貝完成之后內核再通知應用程序直接開始處理, 應用程序直接處理,不需要拷貝數據阻塞等待)
異步IO: 由內核在數據拷貝完成時, 通知應用程序(而信號驅動是告訴應用程序何時可以開始拷貝數據)
真正的做到了完完全全的非阻塞,發起aio_read之后應用程序立即可以去做其他的事情了. 調用了aio_read之后會立即進行返回繼續向下執行應用程序,由kernel內核進行等待數據準備,只有當數據準備好了且拷貝到來用戶空間,一切完成后,kernel給應用程序發送一個signal,告知它read完成了, 沒有任何的阻塞,你直接處理就是
異步IO由于它不會對用戶進程,應用程序產生任何的阻塞,所以他對于高并發網絡服務器的實現至關緊要.
小結:
- 任何IO操作都是存在 等待數據準備完成 和 將 數據從內核態拷貝到用戶態兩個過程的
- 兩個過程中等待數據消耗的時間一般遠超于拷貝數據所花費的時間,所以一般我們進行IO的優化,都是想辦法盡量降低等待時間
- 所以信號驅動IO 因為是通知開始處理數據,應用程序需要將數據從內核拷貝進入到用戶態 (數據拷貝阻塞等待) 和異步IO的區別
- 異步IO 是完全不存在應用程序的阻塞等待,平時應用程序干自己的事情,當數據完全準備好了 (數據 完成了拷貝 ),直接通知應用程序回調處理數據
- 所以我們之前介紹的 blocking io non-blocking io io multiplexing (IO多路復用) 本質上都是屬于 synchronous IO (同步IO) 都是存在有阻塞的,有人說不對吧: 哪 non-blocking IO 呢? 非阻塞IO僅僅只是在數據準備階段上來說是非阻塞的,數據沒準備好立馬返回,可是數據拷貝階段還是阻塞住的,所以本質還是同步IO. (大大的狡猾,忘記了阻塞除了準備數據的時候存在,拷貝數據也是阻塞住的)
- 只有異步IO asynchronous 是完全做到了整個過程非阻塞的 , 當進程發起IO操作之后,就直接返回再也不必理睬,直kernel 發送一個信號,告訴進程說IO完成(涵蓋數據拷貝完成), 在這個過程中,是完全避免了阻塞進程了的
UDP + SIGIO信號注冊模擬實現一下信號驅動IO
流程:
1、注冊SIGIO的處理函數 (回調函數)
2、設置該套接口的屬主,通常使用fcntl的F_SETOWN命令設置
3、開啟該套接口的信號驅動I/O,通常使用fcntl的F_SETFL命令打開O_ASYNC標志完成
實現代碼 (簡單的信號驅動服務端)
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef struct sockaddr SA;
#define BUFFSIZE 512
int sockfd = 0; //定義全局的sockfd
//信號處理
void do_sigio(int signo) {
char buff[512] = {0};
struct sockaddr_in cli_addr;
socklen_t clilen = sizeof(cli_addr);
int rlen = recvfrom(sockfd, buff, 512, 0,
(SA*)&cli_addr, &clilen); //獲取cli_addr, 為后面send做準備
printf("Recvfrom message: %sn", buff);
int slen = sendto(sockfd, buff, rlen, 0, (SA*)&cli_addr, clilen);
}
int main() {
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd) {
perror("socket");
return 2;
}
signal(SIGIO, do_sigio);//注冊信號處理函數
//確定協議地址簇
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
serv_addr.sin_addr.s_addr = INADDR_ANY;
//設置套接口屬主
fcntl(sockfd, F_SETOWN, getpid());
//然后設置O_ASYNC 開啟信號驅動IO
int flags = fcntl(sockfd, F_GETFL);
if (-1 == fcntl(sockfd, F_SETFL, flags | O_NONBLOCK | O_ASYNC)) {
return 4;
}
if (-1 == bind(sockfd, (SA*)&serv_addr, sizeof(serv_addr))) {
return 3;
}
while (1) sleep(1);
close(sockfd);
return 0;
}
客戶端代碼:
#include
#include
#include
#include
#include
#include
#include
#include
typedef struct sockaddr SA;
#defien BUFFSIZE 512
int sockfd = 0; //定義全局的listenfd
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "usage: argv[0]n", argv[0]);
return 1;
}
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd) {
perror("socket");
return 2;
}
short port = atoi(argv[2]);
const char* ip = argv[1];
//獲取服務器協議地址簇
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
inet_pton(AF_INET, ip, &serv_addr.sin_addr);
//然后就是循環發送數據
char buffer[BUFFSIZE];
while (1) {
printf("請說>>: ");
scanf("%s", buffer);
sendto(sockfd, buffer, strlen(buffer), 0
(SA*)&serv_addr, sizeof(serv_addr));
}
return 0;
}
二. 生活的角度理解select poll epoll三種IO多路復用技術的工作模式
生活實例理解select 和 poll 工作原理
先抽象一個具體的場景出來:
假如說有這樣一家餐廳。 一桌餐對應著一個服務員 (生活化IO事件),服務員只是負責服務,這個時候老板需要安排一個 (跑堂伙計 管理收集服務員獲取的服務信息)
select 便是這個跑堂伙計了
由于服務事件的類型可能不盡相同:所以跑堂伙計 開始的時候帶著三個本子,分別記錄不同的事件類型
select(ionum, rfds, wfds, efds, timeout);
rfds: 讀事件集合 wfds寫事件集合 efds異常事件集合
ionum = maxfd + 1; fds {0, 1, 2, 3, 4 .....} fdsnum = maxfd + 1;
以上是一個生活中的一個小小栗子便于理解 select 工作模式,實際實現存在部分偏差
對應真實情景: select 之后是內核檢測IO事件的發生,內核輪詢所有的fd,內核重新設置底層的 fd_set :傳入內核的時候 (內核如果知曉fd是否需要監視???) FD_SET: 然后內核會對于傳入進去的fd_set 進行重新覆蓋,沒有IO事件發生的就像FD_CLR一樣 將對應集合位圖 位置上標記為0 有IO事件發生的就將對應位圖位置標記為1 這樣回到用戶態之后從新進行輪詢所有的 fd 就可以根據內核從新標記的發生IO事件的 位 來處理IO (select 內核 用戶態兩次輪詢) 定時輪詢,效率低下
poll的本質還是輪詢。只不過破除了位圖的限制,采取結構體存儲IO事件,將三個本子合成一個本子了,而且破除了位圖限制之后可以使用鏈式結構連接所有事件的結構體,沒有了最大監視IO事件的限制了 (位圖的fd_set的話大小是由內核開始確定的,如果修改大小比較麻煩,所以是存在fdnum上的限制的)
poll 雖然理論上是沒有了fdnum的限制了,但是隨著fd的數量上升到一定程度,性能會急劇下降
生活理解epoll工作原理
還是先抽象場景出來:
存在這樣一個小區的管理,小區里面很多的用戶都存在寄快遞的需求,每一次需要寄出快遞的時候大家都統一的放入門衛室里面
快遞員每一次來收取快遞的時候不再需要挨家挨戶的詢問,收取,而是直接去門衛室將所有的快遞放進自己的車子中帶走處理即可 (門衛室相當于是readylist,不再需要輪詢所有的IO事件是否發生,提高了效率)
epoll_wait就是這個快遞員:
epoll_wait(管理的小區, 快遞員存儲快遞包裹的容器, 容器可以容納的快遞數目,定時);
epoll_wait(epfd, events, eventscap, timeout);
epoll_create(size); //早期size標識最大居民數目,現在已經沒有限制了,只有0和1的區別了, 因為可以進行鏈式存儲,也就沒有容量限制這一說了
epoll_ctl(管理的小區, 小區居民搬入搬出修改的不同行為,新搬入居民的信息(標識), 描述需要寄出快遞的類型信息 );
epoll_ctl(epfd, op, fd, event); //epfd, epoll句柄,底層是紅黑樹 op:作何操作, fd : IO事件句柄, event:IO事件類型 (功能,向IO事件監視的紅黑樹上掛載新的監視IO事件,或者是刪除監視,或者是修改監視事件類型)
epoll對比poll select優勢出現小結:
將監視IO事件進行提前注冊,掛載在內核的監視IO事件紅黑樹上,每一次調用epoll_wait 獲取IO觸發事件的時候不再需要傳入待檢測IO的事件,接口分離,功能分離,而且內核中采取了使用就緒隊列存儲紅黑樹上發生的IO事件結點的方式,這樣每一次僅僅需要將就緒隊列從內核中拷貝至用戶空間拿取事件即可。。。
readylist放置觸發IO事件, 使其不需要輪詢獲知IO觸發的事件了, 提前注冊掛載監視IO事件結點到紅黑樹上,也使得不需要每一次都從新拷貝監視事件進入內核空間,降低了拷貝消耗, 正是由于epoll的這兩點優勢好處使其成為穩定高效的多路復用技術,在高并發服務器的設計中隨處可見epoll的身影
三. 細談一下epoll的ET和LT
ET : edge tigger邊沿觸發 LT : level tigger水平觸發
簡單理解一下兩種觸發模式:
LT : 指的是 內核recv_buffer緩沖區中存在數據就一直會進行觸發,處理數據, 讀事件一直觸發. 或者是 內核send_buffer緩沖區沒有滿,就一直觸發寫事件. 直到寫滿send_buffer
ET : 指的是緩沖區狀態發生變化之后引發觸發,而且核心關鍵,僅僅只會觸發一次 (邊沿觸發)
接收緩沖區recv_buffer 發生變化,數據從無到有,會觸發一次讀事件,核心,不論一次是否可以將數據完全處理,都只會觸發一次
或者發送緩沖區send_buffer狀態發生變化,也會觸發寫事件 (核心關鍵還在于觸發一次)
上述的觸發都指的是對于 epoll_wait 的觸發.
總結:針對便于理解的讀事件的觸發, recv_buffer來理解, 如果說recv_buffer中有數據,如果是LT 就會不斷地不停地觸發, 如果是ET, 不管數據能不能處理完,都僅僅只會觸發一次
光說不練是假把式,我們還是來一個實際地案例來解釋一下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef struct sockaddr SA;
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: %s ", argv[0]);
return 1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
return 2;
}
struct sockaddr_in serv_addr;
//確定協議地址簇
int port = atoi(argv[1]);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(port);
if (-1 == bind(sockfd, (SA*)&serv_addr, sizeof(serv_addr))) {
perror("bind");
return 3;
}
if (-1 == listen(sockfd, 5)) {
perror("listen");
return 4;
}
//至此可以開始IO多路復用監視IO了
//創建出來內核紅黑樹地根結點(epoll句柄)
int epfd = epoll_create(1);
struct epoll_event ev, evs[512];
//監視新的連接到來IO事件
ev.events = EPOLLIN;
ev.data.fd = sockfd;
//將其掛載到紅黑樹上
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
while (1) {
//epoll_wait(epfd, 存儲觸發事件地容器傳出參數, 容器大小size, timeout)
int nready = epoll_wait(epfd, evs, 512, -1);
if (nready < -1) {
break; //出錯
}
int i = 0;
for (i = 0; i < nready; ++i) {
//處理各種IO事件, 存在各種封裝形式
if (evs[i].events & EPOLLIN) {
if (evs[i].data.fd == sockfd) {
//新的連接到來
struct sockaddr_in cli_addr;
socklen_t clilen;
int clifd = accept(sockfd, (SA*)&cli_addr, &clilen);
if (clifd < 0) return 5;//出錯了嘛
char str[INET_ADDRSTRLEN] = {0};
//獲取一下信息
printf("recv from %s at %d connectionn", inet_ntop(AF_INET, &cli_addr.sin_addr, str, sizeof(str))
, ntohs(cli_addr.sin_port));
//從新設置一下ev, 將新的監視IO事件掛載到紅黑樹上
ev.events = EPOLLIN | EPOLLET;//關鍵哈, EPOLLET使用地是邊沿觸發
ev.data.fd = clifd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clifd , &ev);
continue;
}
//處理真正地讀事件, 將緩沖區給小一點,等下才好看見效果
char buff[5] = {0};
int ret = recv(evs[i].data.fd, buff, 5, 0);
if (ret < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
continue;
else {
//出錯了
}
//出錯了將其從內核紅黑樹上移除,避免僵尸結點
ev.events = EPOLLIN;
ev.data.fd = evs[i].data.fd;
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &ev);
close(evs[i].data.fd);
} else if (ret == 0) {
printf("%d disconnectionn", evs[i].data.fd);
//斷開連接,從內核紅黑樹中移除監視
ev.events = EPOLLIN;
ev.data.fd = evs[i].data.fd;
epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &ev);
close(evs[i].data.fd); //對端斷開連接
} else {
printf("recv %s, %d Bytesn", buff, ret);
//修改事件類型為寫事件
}
}
if (evs[i].events & EPOLLOUT) {
//此處暫時不寫,僅僅只是測試一下讀即可
}
}
}
return 0;
}
如上是使用ET地時候點一下地結果,沒有設置非阻塞哈,結果是啥,我發送了這么一段話,它僅僅只是觸發了一次,打印了一個Hello, why ? 我故意將緩沖區設置如此小,緩沖區狀態改變,但是最多緩沖區僅僅存儲5個數據,全發送了,后面再次循環過來,不觸發了我去
如果需要一直觸發直到recv_buffer內核緩沖區中沒有數據,咋辦。使用LT水平觸發,如何設置,easy默認就是呀
我僅僅只是做了如此一個小小改動,默認LT觸發,讓我們康康效果
點了一次發送,他就一直觸發,直到recv_buffer中沒了數據
四. 總結本文
- 本文主要還是進行了IO地理解實戰, 信號驅動IO 異步IO究竟區別在哪里?
- 異步IO 是完全不存在任何地應用程序掛起等待地, 其他哪些IO多多少少要么數據準備階段要么數據拷貝階段存在掛起等待阻塞
- 然后就是IO多路復用地生活化理解精進
- 最終介紹分析了epoll地ET 和 LT問題,這個超級重要好吧。大塊數據使用 LT一次讀,小塊數據使用ET + 循環讀(設置非阻塞) 出自大佬地結論
- LT: 水平觸發,recv_buffer內核緩沖區中存在數據,則讀事件一直不停地觸發
- ET : 邊沿觸發,recv_buffer中數據從無到有,狀態發生改變地時候進行觸發,且關鍵是僅僅只會觸發一次,不論你數據是不是一次可以讀完,都只是觸發一次
-
IO
+關注
關注
0文章
448瀏覽量
39132 -
數據
+關注
關注
8文章
7002瀏覽量
88943 -
服務器
+關注
關注
12文章
9123瀏覽量
85328 -
應用程序
+關注
關注
37文章
3265瀏覽量
57678
發布評論請先 登錄
相關推薦
評論