協程
我們都知道線程是進程中的執行體,擁有一個執行入口,以及從進程虛擬地址空間中分配的棧(用戶棧和內核棧),操作系統會記錄線程控制信息,而線程獲得CPU時間片以后才可以執行,切換時這里棧指針,指令指針,棧基等寄存器都要切換到對應的線程。
如果線程自己又創建了幾個執行體(協程),給它們各自指定執行入口,申請一些內存分給它們用作執行棧,那么線程就可以按需調度這幾個執行體。
為了實現這些執行體的切換,線程也需要記錄它們的控制信息。包括ID,執行棧的位置,執行入口地址,執行現場等等。線程可以選擇一個執行體來執行,此時CPU中指令指針就會指向這個執行體的執行入口,棧基和棧指針寄存器也會指向線程給它分配的執行棧。
通過同樣的方式,可以恢復到之前的執行體, 這樣就可以從上次中斷的地方繼續執行。這些由線程創建的執行體就是所謂的協程(纖程等)
因為用戶程序不能操作內核空間,所以只能給協程分配用戶棧,而操作系統對協程一無所知,所以協程又被稱為“用戶態線程”。
所以協程思想的關鍵在于,控制流的“主動讓出”和“恢復”,每個協程都擁有自己的執行棧,可以保存自己的執行現場。
所以可以由用戶程序,按需創建協程,協程“主動讓出”執行權時,會保存執行現場,然后切換到其它協程,協程恢復執行時,會根據之前保存的執行現場,恢復到中斷前的狀態繼續執行,這樣就通過協程,實現了既輕量又靈活的,由用戶態進行調度的,多任務模型。可以參考goroutine的調度方式理解協程。
I/O多路復用
前言
我們知道通過操作系統記錄的進程控制信息,可以找到打開文件描述符表,進程打開的文件,創建的socket等等,都會記錄到這張表里。
socket的所有操作都由操作系統來提供,也就是要通過系統調用來完成,每創建一個socket,就會在打開文件描述符表中,對應增加一條記錄,而返回給應用程序的只有一個socket描述符,用于識別不同的socket。
而且每個TCP socket在創建時,操作系統都會為它分配一個讀緩沖區和一個寫緩沖區,要獲得響應數據,就要從讀緩沖區拷貝過來,同樣的要通過socket發送數據,也要先把數據拷貝到寫緩沖區才行。
所以,問題出現了,用戶程序想要讀數據的時候,讀緩沖區里未必有數據,想發送數據的時候,寫緩沖區里也未必有空間。
那怎么辦?第一種辦法,乖乖的讓出CPU,進到等待隊列里,等socket就緒后,再次獲得時間片就可以繼續執行了。這就是阻塞式IO。
使用阻塞式IO,要處理一個socket就要占用一個線程。等這個socket處理完才能接手下一個,這在高并發場景下會加劇調度開銷
第二中辦法是非阻塞式IO,也就是不讓出CPU,但是需要頻繁的檢查socket是否就緒了。這是一種“忙等待”的方式,很難把握輪詢的間隔時間,容易造成空耗CPU,加劇相應延遲。
第三種辦法就是“IO多路復用”,由操作系統提供支持,把需要等待的socket加入到監聽集合,這樣就可以通過一次系統調用,同時監聽多個socket。
有socket就緒了,就可以逐個處理了,既不用為了等待某個socket而阻塞,也不會陷入“忙等待”之中
select
第一種select,我們可以設置要監聽的描述符,也可以設置等待超時時間,如果有準備好的fd,或達到指定超時時間,select函數就會返回。從函數簽名來看,它支持監聽可讀,可寫,異常三類事件。因為這個fd_set是個unsigned long型的數組,共16個元素,每一位對應一個fd,16*64=1024,最多可以監聽1024個fd。這就有點少了,而且每次調用select都要傳遞所有監聽集合,這就需要頻繁的從用戶態到內核態拷貝數據。除此之外,即便有fd就緒了,也需要遍歷整個監聽集合,來判斷哪個fd是可操作的,這些都會影響性能。
poll
第二種IO多路復用實現方式:poll。在select的基礎上使用鏈表來記錄fd,雖然支持的fd數目等于最多可打開的文件描述符的個數,但是另外兩個問題依然存在。
epoll
epoll就沒有這些問題了,它提供三個接口,epoll_create1用于創建一個epoll,并獲取一個句柄,epoll_ctl用于添加,修改或刪除fd與對應的事件信息,除了指定fd和要監聽的事件類型,還可以傳入一個event data,通常會按需定義一個數據結構,用于處理對應的fd。可以看到每次都只需傳入要操作的一個fd,無需傳入所有監聽集合,而且只需要注冊這一次,通過epoll_wait得到的fd集合都是已經就緒的,逐個處理即可,無需遍歷所有監聽集合。
int epfd = epoll_create1(0);
struct epoll_event events[MAX_EVENTS], ev;
---
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for(int i = 0; i < nfds; ++i){
if(events[i].data.fd == sockfd){ //新客戶端連接
struct sockaddr_in clnt_addr;
bzero(&clnt_addr, sizeof(clnt_addr));
socklen_t clnt_addr_len = sizeof(clnt_addr);
int clnt_sockfd = accept(sockfd, (sockaddr*)&clnt_addr, &clnt_addr_len);
bzero(&ev, sizeof(ev));
ev.data.fd = clnt_sockfd;
ev.events = EPOLLIN | EPOLLET;
setnonblocking(clnt_sockfd);
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sockfd, &ev);
} else if(events[i].events & EPOLLIN){ //可讀事件
char buf[READ_BUFFER];
while(true){ //由于使用非阻塞IO,讀取客戶端buffer,一次讀取buf大小數據,直到全部讀取完畢
bzero(&buf, sizeof(buf));
ssize_t bytes_read = read(events[i].data.fd, buf, sizeof(buf));
if(bytes_read > 0){
write(events[i].data.fd, buf, sizeof(buf));
} else if(bytes_read == -1 && errno == EINTR){ //客戶端正常中斷、繼續讀取
printf("continue reading");
continue;
} else if(bytes_read == -1 && ((errno == EAGAIN) || (errno == EWOULDBLOCK))){//非阻塞IO,這個條件表示數據全部讀取完畢
printf("finish reading once, errno: %d\n", errno);
break;
} else if(bytes_read == 0){ //EOF,客戶端斷開連接
printf("EOF, client fd %d disconnected\n", events[i].data.fd);
close(events[i].data.fd); //關閉socket會自動將文件描述符從epoll樹上移除
break;
}
}
} else{ //其他事件
printf("something else happened\n");
}
通過IO多路復用,線程再也不用為了等待某一個socket,而阻塞或空耗CPU。并發處理能力因而大幅提升,但是也并非沒有問題,例如一個socket可讀了,但是這回只讀到了半條請求,也就是說需要再次等待這個socket可讀,在繼續處理下一個socket之前,需要記錄下這個socket的處理狀態,下一個這個socket可讀時,也需要恢復上次保存的現場,才好繼續處理。
也就是說,在IO多路復用中實現業務邏輯時,我們需要隨著事件的等待和就緒,而頻繁的保存和恢復現場,這并不符合常規開發習慣,如果業務邏輯比較簡單還好,若是較為復雜的業務場景,就是悲劇了。
既然業務處理過程中,要等待事件時,需要保存現場并切換到下一個就緒的fd,而事件就緒時又需要恢復現場繼續處理,那豈不是很適合協程?
在IO多路復用這里,事件循環依然存在,依然要在循環中逐個處理就緒的fd,但處理過程卻不是圍繞具體業務,而是面向協程調度。如果是用于監聽端口的fd就緒了,就建立連接創建一個新的fd,交給一個協程來負責,協程執行入口就指向業務處理函數入口,業務處理過程中,需要等待時就注冊IO事件,然后讓出。
這樣執行權就會回到切換到該協程的地方繼續執行。如果是其他等待IO事件的fd就緒了,只需要恢復關聯的協程即可。
協程擁有自己的棧,要保存和恢復現場都很容易實現。這樣IO多路復用這一層的事件循環,就和具體業務邏輯解耦了。可以把read,write,connect等可以回發生等待的函數包裝一下,在其中實現IO事件注冊與主動讓出,這樣在業務邏輯層面就可以使用這些包裝函數,按照常規的順序編程方式,來實現業務邏輯了。
這些包裝函數在需要等待時,就會注冊IO事件,然后讓出協程。這樣我們在實現業務邏輯時就完全不用關心保存和恢復現場的問題了。協程和IO多路復用之間的合作,不僅保留了IO多路復用的高并發性能,還解放了業務邏輯的實現。
參考:https://www.cnblogs.com/Me1onRind/p/10671741.html
https://zhuanlan.zhihu.com/p/400371315