1. 程式人生 > >socket模型之select與epoll

socket模型之select與epoll

1  描述
本文針對解決呼叫大方數需求中遇到的socket模型問題進行分析,對之前使用的select模型及為解決問題使用的epoll模型,簡單介紹,並進行對比和分析。

2  分析
2.1Select模型
2.1.1簡介

select模型的核心就是select函式,它用於確定一個或多個套介面的狀態,對每一個套介面,呼叫者可查詢它的可讀性、可寫性及錯誤狀態資訊。用fd_set結構來表示一組等待檢查的套介面,在呼叫返回時,這個結構存有滿足一定條件的套介面組的子集,並且select()返回滿足條件的套介面的數目。

標頭檔案#include <sys/select.h>
int select (int maxfd ,                    // 指集合中所有檔案描述符的範圍,[0,maxfd)
fd_set *readset,                           // 可選,指向一組等待可讀性檢查的套介面
fd_set *writeset,                          // 可選,指向一組等待可寫性檢查的套介面
fd_set *exceptset,                         // 可選,指向一組等待錯誤檢查的套介面
const struct timeval * timeout);           // 最多等待時間,對阻塞操作則為NULL
// fd_set是一組檔案描述字(fd)的集合,它用一位來表示一個fd。
typedef int32_t __fd_mask;
#define _NFDBITS (sizeof(__fd_mask) * 8)      /* 8 bits per byte */
#define __howmany(x,y)   (((x)+((y)-1))/(y))
typedef struct __fd_set
{
    long fds_bits[__howmany(FD_SETSIZE, (sizeof(long) * 8))];
}fd_set;
readset、writeset、exceptset指定我們要讓核心測試讀、寫和異常條件的描述字。如果對某一個的條件不感興趣,就可以把它設為NULL。
timeout是一個超時時間值,型別是一個struct timeval結構的變數的指標,微軟msdn有對應的說明:
The parameter time-out controls how long the select can take to complete. If time-out is 
a null pointer, select will block indefinitely until at least one descriptor meets the 
specified criteria. Otherwise, time-out points to a TIMEVAL structure that specifies 
the maximum time that select should wait before returning. When select returns, the 
contents of the TIMEVAL structure are not altered. If TIMEVAL is initialized to {0, 0}, 
select will return immediately; this is used to poll the state of the selected sockets.
If select returns immediately, then the select call is considered nonblocking and the 
standard assumptions for nonblocking calls apply. For example, the blocking hook will 
not be called, and Windows Sockets will not yield.

即有三種情況:
1、timeout=NULL(阻塞:直到有一個fd位被置為1函式才返回);
2、timeout所指向的結構設為非零時間(等待固定時間:有一個fd位被置為1或者時間耗盡,函式均返回);
3、 timeout所指向的結構,時間設為0(非阻塞:函式檢查完每個fd後立即返回)。
返回值:>0:某些檔案可讀寫或出錯的數目;-1:出錯;0 :等待超時,沒有可讀寫或錯誤的檔案。

2.1.2使用
以下四個巨集用來對fd_set結構體進行操作。

void FD_ZERO (fd_set *fdset); // clear all bits in fdset
#define FD_ZERO(p)    memset((void *)(p), (int) 0, sizeof(*(p)))

void FD_SET (int fd,fd_set *fdset); // turn on the bit for fd in fdset
#define FD_SET(n,p)   (((__fd_mask *)((p)->fds_bits))[(n)/_NFDBITS] |= (1 <<((n) % _NFDBITS)))

void FD_CLR (int fd,fd_set *fdset); // turn off the bit for fd in fdset
#define FD_CLR(n,p)     (((__fd_mask *)((p)->fds_bits))[(n)/_NFDBITS] &= ~(1 <<((n) % _NFDBITS)))

int FD_ISSET(int fd,fd_set *fdset); // is the bit for fd on in fdset
#define FD_ISSET(n,p) (((__fd_mask *)((p)->fds_bits))[(n)/_NFDBITS] & (1 <<((n) % _NFDBITS)))
簡單例程:
fd_set readfds;     // 讀集合,定義讀、寫或者異常的集合,不需要的就不用定義
FD_ZERO (&readfds); // 對集合清空;
int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 分配tcp型別套接字
FD_SET(fd, &readfds ); // 使fd對應可讀集合裡的bit可用,即對此fd進行檢測
While(ret = select (fd + 1, &readfds, NULL, NULL, NULL)) // 對可讀集合進行檢測,阻塞等待;
{if(ret < 0) printf (“select error\n”);
else if (ret = = 0) printf(“time out\n”);
else {
    if(FD_ISSET(fd, &rdfds)) // 檢查fd是否可讀
    recv( ); }               // fd可讀,讀取內容
FD_SET(fd, &readfds );}      // 重新把fd在set到集合中去

2.1.3注意事項
1)select的第一個引數為所有監視的檔案描述符的最大值+1,而不是監視的檔案描述符個數+1。在linux下是值而不是個數,fd_set集合儲存描述符是以位形式儲存,所以只能對最大FD_SETSIZE=1024個,即描述符值是0~1023的控制代碼進行檢測,可以在標頭檔案中修改這個值來改變select使用的檔案描述符集的大小,但是必須重新編譯核心才能使修改後的值有效;而在windows下其意義是個數,描述符值的大小不受限制,每次存入fd_set中的陣列時,計數器count++。2.1.1以及2.1.2中分別列出了fd_set以及相關的四個巨集在linux下的定義,可以看出是以位儲存,可以和在windows下的定義做下對比;
2)select返回後,在fd_set集合中的檔案描述符都會被清0,因此在select的迴圈中,每次進入都要重新設定我們所關注的檔案描述符。
3)如果select使用了超時操作,每次返回select都會修改計時器,將計時器設為餘下的時間,因此如果使用了計時器,每次進入迴圈都要重置計時器。

2.2Epoll模型
2.2.1簡介

直到Linux2.6才出現了由核心直接支援的實現方法,那就是epoll,是被公認為Linux2.6下效能最好的多路I/O就緒通知方法。epoll是linux核心為處理大批量檔案描述符而作了改進的poll,是Linux下多路複用I/O介面select/poll的增強版本,它能顯著提高程式在大量併發連線中只有少量活躍的情況下的系統CPU利用率。另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被核心IO事件非同步喚醒而加入Ready佇列的描述符集合就行了。
標頭檔案#include <sys/epoll.h>
epoll只有epoll_create,epoll_ctl,epoll_wait 3個系統呼叫:
1、int epoll_create(int size); //建立一個epoll的控制代碼,size是epoll所支援的最大控制代碼數。自從linux2.6.8之後,size引數只要大於0的數值就行,核心自己動態分配。
2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //epoll的事件註冊函式。
第一個引數是epoll_create()的返回值。
第二個引數表示動作,用三個巨集來表示:EPOLL_CTL_ADD:註冊新的fd到epfd中;EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個引數是需要監聽的fd。
第四個引數是告訴核心需要監聽什麼事,struct epoll_event結構如下:

typedef union epoll_data {   
•      void *ptr;   
•      int fd;   
•      __uint32_t u32;   
•      __uint64_t u64;   
•  } epoll_data_t;   
struct epoll_event {   
•      __uint32_t events; /* Epoll events */   
•      epoll_data_t data; };/* User data variable */   
events可以是以下幾個巨集的集合:
EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的檔案描述符可以寫;
EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);
EPOLLERR:表示對應的檔案描述符發生錯誤;
EPOLLHUP:表示對應的檔案描述符被結束通話;
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列裡;

3、int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); //收集在epoll監控的事件中已經發送的事件。

引數events是分配好的epoll_event結構體陣列,epoll將會把發生的事件賦值到events陣列中(events不可以是空指標,核心只負責把資料複製到這個events陣列中,不會去幫助我們在使用者態中分配記憶體)。maxevents告之核心這個events有多大,這個 maxevents的值不能大於建立epoll_create()時的size;引數timeout是超時時間(毫秒,0會立即返回,-1永久阻塞)。如果函式呼叫成功,返回對應I/O上已準備好的檔案描述符數目,返回0表示已超時。

2.2.2使用
簡單例程:

int epfd= epoll_create();//建立一個epoll的控制代碼,返回一個新的epoll控制代碼;
int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //分配tcp型別套接字
int nfds = 0;//臨時變數,有多少個socket有事件
struct epoll_event ev;    //事件臨時變數
struct epoll_event events[FD_SETSIZE];    //監聽事件陣列

ev.data.fd = fd;     //設定與要處理的事件相關的檔案描述符
ev.events = EPOLLIN|EPOLLET;   //設定要處理的事件型別
if ( 0 != epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) ) { //註冊epoll事件
printf("epoll_ctl  failed!\n");
    close(fd);
    close(epfd); }  
while(條件){
nfds = epoll_wait(epfd, events, FD_SETSIZE, -1); //最大事件是FD_SETSIZE個,-1表示沒有訊息就一直等待
for (s32 i = 0; i < nfds; i++){ //迴圈讀event裡的內容
    if (fd== events[i].data.fd && events[i].events&EPOLLIN ) {//fd上有讀事件
        recv();
//重新設定fd
ev.data.fd = fd;         
        ev.events = EPOLLIN|EPOLLET;  
        epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) }}}
close(fd);
close(epfd);

2.2.3注意事項
1、當建立好epoll控制代碼後,它會佔用一個fd值,在linux下如果檢視/proc/程序id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須呼叫close()關閉,否則會導致控制代碼洩漏。
2、呼叫epoll_wait後,如果fd上有對應的訊息,處理後應再次把fd重新設定到epoll中去。因為epoll_wait執行的原理是:等侍註冊在epfd上的socket fd的事件的發生,如果發生則將發生的sokct fd和事件型別放入到events陣列中。並且將註冊在epfd上的socket fd的事件型別給清空,所以如果下一個迴圈你還要關注這個socket fd的話,則需要用epoll_ctl(epfd,EPOLL_CTL_MOD, fd,&ev)來重新設定socket fd的事件型別。這時不用EPOLL_CTL_ADD,因為socket fd並未清空,只是事件型別被清空。
3、儘量少使用epoll_ctl,過多的使用epoll_ctl系統呼叫,有一定的效能開銷,可能成為這個系統的瓶頸。

2.2.4Select與epoll比較
1、select與epoll的執行原理
1)select:
select會迴圈遍歷它所監測的fd_set內的所有檔案描述符對應的驅動程式的poll函式。驅動程式提供的poll函式首先會將呼叫select的使用者程序插入到該裝置驅動對應資源的等待佇列(如讀/寫等待佇列),然後返回一個bitmask告訴select 當前資源哪些可用。當select迴圈遍歷完所有fd_set內指定的檔案描述符對應的poll函式後,如果沒有一個資源可用,則select讓該程序睡眠,一直等到有資源可用為止,程序被喚醒(或者timeout)繼續往下執行。
2)epoll:
對於epoll,一棵紅黑樹,一張準備就緒控制代碼連結串列,少量的核心cache,就幫我們解決了大併發下的socket處理問題。執行epoll_create時,建立了紅黑樹和就緒連結串列,執行epoll_ctl時,如果增加socket控制代碼,則檢查在紅黑樹中是否存在,存在立即返回,不存在則新增到樹幹上,然後向核心註冊回撥函式,用於當中斷事件來臨時向準備就緒連結串列中插入資料。執行epoll_wait時立刻返回準備就緒連結串列裡的資料即可。

2、select與epoll相對的優缺點
1)Select優缺點:
優點:
a) 在連線個數較小,連線的活躍度較高情況下,效率高於epoll;
b)select的跨平臺做的很好,幾乎每個平臺都支援;
缺點:
a)單個程序能夠監視的檔案描述符的數量存在最大限制;
b)select()所維護的儲存大量檔案描述符的資料結構,隨著檔案描述符數量的增長,其在使用者態和核心的地址空間的複製所引發的開銷也會線性增長;
c)由於網路響應時間的延遲使得大量TCP連線處於非活躍狀態,但呼叫select()還是會對所有的socket進行一次線性掃描 ,會造成一定的開銷;
d)每次在呼叫開始時,要把當前程序放入各個檔案描述符的等待佇列。在呼叫結束後,又把程序從各個等待佇列中刪除,影響效率;

2)Epoll優缺點:
優點:
a)支援的FD上限為系統定義的程序開啟的最大FD個數,與系統記憶體相關;
b)IO效率不隨FD數目增加而線性下降(FD中有大量的空閒連線),它只會對"活躍"的socket進行操作(根據每個fd上面的callback函式實現)
c)mmap加速核心與使用者空間的訊息傳遞: 核心和使用者空間mmap同一塊記憶體實現的。
d)epoll返回時已經明確的知道哪個sokcet fd發生了事件,不用再一個個比對,提高了效率。
缺點:
a) 在連線數量較小,或者連線活躍度都比較高的情況下,epoll的效率可能比不上select。
b)epoll只有Linux2.6以上才有實現 ,而其他平臺都沒有,跨平臺性存在缺陷。
    總結:從select和epoll的執行原理以及優缺點分析得出,epoll確實在高併發連線的情況下比select更有效率、更值得使用,但是對於連線數量較小的,使用selcet也是相當不錯的。

2.3實際問題分析
1、問題描述
平臺的需求是讓最大呼叫數由原來的256個擴大為512個,並且遠遙等功能也要正常運作。在解決這個問題的時候遇到如下問題:
問題1:呼叫512個終端失敗,只能呼叫最多330左右;
問題2:呼叫中容易出現堵塞現象;
背景:每個呼叫需要建立h225和h245兩條連線,則需要佔用至少兩個socket描述符,512個呼叫就要佔用至少1024個+2個監聽描述符(ras的1719和h225的1720)=1026個;
         遠遙模組(遠遙—即遠端遙控,遠端控制對方的攝像頭的功能模組,與下邊說的協議棧是另外一個獨立的模組)中會建立遠遙收、發通道,每個通道佔用一個socket描述符用來收發,所以收、發分別用掉512個控制代碼。

2、問題具體分析
對問題1:
首先把協議棧的呼叫數限制由之前的256改為512個後,使得協議棧可以為512個呼叫分配佔用的資源。由背景知識知道512個呼叫所用的描述符個數會大於1024,所以把協議棧的FD_SETSIZE(系統標頭檔案中定義的巨集,可被其他地方重新定義)由之前定義的1024改為2048(並且把系統對每個程序可以開啟的最大檔案控制代碼數限制也擴大到5120,臨時修改方法ulimit –n 5120,永久性修改請查閱相關資料),但是也只能呼叫330多個,並且有時候會出現堵塞(堵塞問題看問題2的分析)。
由於當時只是模糊瞭解到select對超過1024個描述符的支援有問題,遂決定把協議棧的socket模型由selcet改為epoll。改之後測試,可以建立1024個連線了,即可以呼叫512個gkmt(模擬的mt)了。但是有時候堵塞問題還是會出現。

對問題2:
通過gdb檢視棧呼叫,發現是堵塞在XXX_net遠遙模組的PV操作的P(S)(S為建立socket的訊號量)操作上。此操作是為遠遙接收通道建立socket控制代碼,基本過程是:分配一個watchsocket的socket用來收訊息(訊息為建立一個新的socket描述符),起一個執行緒用來對此watchsocket進行select,有訊息就處理。如果有需要建立,就給watchsocket傳送udp訊息,同時進行P(S)操作;watchsocket收到訊息後則建立控制代碼並進行V(S)操作。

新增打印發現,建立訊息成功傳送,進行了P(S)操作,但是在select那裡並沒有建立的訊息被檢測到。此模組對FD_SETSIZE之前的定義是256,被改為512以及改成後來的1024(每個呼叫都有可能開啟一個遠遙接收通道,512個呼叫就最多開512個遠遙接收通道,就要佔用至少512個socket描述符,但是加上watchsocket的佔用總體不會超過1024)個後都還是有時候會堵塞,所以懷疑堵塞問題與FD_SETSIZE的設定關係不大。

最後經過多次測試,發現堵塞規律:遠遙接收socket控制代碼的建立在512個呼叫的中間任何時候都有可能才開始建立(並不是每個呼叫成功後就立馬建立),列印建立的socket描述符的值,發現只要此值超過1024後進行遠遙接收socket控制代碼的建立就會出現堵塞,而有的時候不堵塞是因為512個都呼叫完了還沒有開始建立遠遙接收socket。

遂仔細蒐集以及查看了selcet相關的資料,原來這個問題就是由於2.1.3的注意事項1中說的那樣,FD_SETSIZE在linux下是值而不是簡單的個數。之前理解不深刻,而且只看了windows下的實現,以為select的FD_SETSIZE都指的是個數,找到linux下的定義看過後就明瞭了。於是也將此模組的socket模型有select改為epoll後堵塞問題解決。

3解決過程
見2.3描述。

4解決結果
把協議棧以及XXX_net模組的socket模型改為epoll後問題解決。

5總結
對select不熟悉,對其中的maxfd的意義理解不深刻導致問題一直未被徹底解決。解決問題1的時候,可能也看到了有資料說FD_SETSIZE在linux下是值而不是簡單的個數,但是看的實現又是windows下的,導致記憶不深、混淆概念,亂打亂撞用了epoll。多虧問題2的一直出現,讓對select以及epoll重新做了梳理,重新瞭解了它們在linux下的實現原理、過程以及注意事項。案例中只是對select和epoll的簡單介紹,部分內容是根據網上資料總結得出而並沒有親自實踐,如有不對請指正,更多資訊請搜尋其他資料。
最後總結幾句:有時候問題不是阻礙,而是推動你前進的引導力。通過問題,可以進一步加深並拓展對技術的理解認識,可以去發掘學習更多知識的機會。

相關推薦

socket模型selectepoll

1  描述 本文針對解決呼叫大方數需求中遇到的socket模型問題進行分析,對之前使用的select模型及為解決問題使用的epoll模型,簡單介紹,並進行對比和分析。 2  分析 2.1Select模型 2.1.1簡介 select模型的核心就是select函式,它用於確定

pythonselectepoll

select的用法寫網路socket的服務端 詳解在註釋裡 import select import socket import queue server = socket.socket() server.bind(('localhost',9000)) server.listen(100

網路程式設計-多路轉接pollepoll模型

首先,還是需要理解io過程:io過程總體來看分兩步,第一步就是等,第二步才是資料搬遷。而如果要想提高io的效能與效率,就要減少等的比重。 可以假想一個場景: 你去釣魚,但是你只有一個魚竿。你的同伴也和你一起去釣魚,但是他帶了100個魚竿。假設每條魚上

selectepoll

logs xevent 任務 文件 hash span int 註冊事件 檢查 select的api: #include <sys/select.h> #include <sys/time.h> int select(int maxfdp1,fd

I/O多路復用select,poll,epoll簡介

重新 才會 增長 文件描述 brush 重新編譯 () 情況 包含 一、select 1.起源 select最早於1983年出現在4.2BSD中(BSD是早期的UNIX版本的分支)。 它通過一個select()系統調用來監視多個文件描述符的數組,當select()返回後,該

Python網絡編程篇selectepoll

unix cat 必須 inpu 結束 新的 eno {} 提升 1. select 原理 在多路復?的模型中, ?較常?的有select模型和epoll模型。 這兩個都是系統接?, 由操作系統提供。 當然, Python的select模塊進?了更?級的封裝。 ?絡通信被U

網路IO模型select基礎

思考:為什麼執行緒開銷會大 一、IO 有兩種操作,同步 IO 和非同步 IO 。      同步 IO 指的是,必須等待 IO 操作完成後,控制權 才返回給使用者程序 。      非同步 IO 指的是,無須等待 I

Linux多路複用select/poll/epoll實現原理及優缺點對比

一、select的實現原理 支援阻塞操作的裝置驅動通常會實現一組自身的等待佇列如讀/寫等待佇列用於支援上層(使用者層)所需的BLOCK或NONBLOCK操作。當應用程式通過裝置驅動訪問該裝置時(預設為

socket程式設計以及selectepoll、poll示例詳解

socket程式設計 socket這個詞可以表示很多概念,在TCP/IP協議中“IP地址 + TCP或UDP埠號”唯一標識網路通訊中的一個程序,“IP + 埠號”就稱為socket。在TCP協議中,建立連線的兩個程序各自有一個socket來標識,那麼兩個soc

Socket程式設計Select()監聽

Select在Socket程式設計中還是比較重要的,它能夠監視我們需要監視的檔案描述符的變化情況——讀寫或是異常。    Select的函式格式(Unix系統下的伯克利socket程式設計,和windows下的略有區別,體現兩個方面:一是select函式的第一個引數,在w

手把手教你玩轉SOCKET模型重疊I/O篇(上)

手把手教你玩轉SOCKET模型之重疊I/O篇 “身為一個初學者,時常能體味到初學者入門的艱辛,所以總是想抽空作點什麼來盡我所能的幫助那些需要幫助的人。我也希望大家能把自己的所學和他人一起分享,不要去鄙視別人索取時的貪婪,因為最應該被鄙視的是不肯付出時的吝嗇。” -----

I/O複用模型select函式用法——伺服器開發

現在我們介紹另外一種常用併發伺服器開發的技術——select函式I/O複用模型。 先來介紹select及相關的函式: select函式的作用是監聽指定的多個I/O的檔案描述符,在設定的時間內阻塞,當有一個或者多個I/O埠滿足某個“讀”或者“寫”的條件,則在fd_set型別

linux非同步socket程式設計select()用法

轉載自http://blog.csdn.net/wypblog/article/details/6826286 Select在Socket程式設計中還是比較重要的,可是對於初學Socket的人來說都不太愛用Select寫程式,他們只是習慣寫諸如 connect、accep

多路複用I/O模型select

1 .所謂I/O多路複用是指核心一旦發現程序指定的一個或者多個I/O條件準備讀取,它就通知該程序。I/O多路複用適用如下場合:   (1)當客戶處理多個描述字時(一般是互動式輸入和網路套介面),必須使用I/O複用。   (2)當一個客戶同時處理多個套介面時,

Apache selectNginx epoll模型區別

Linux服務1.select 和epoll模型區別1.1.網絡IO模型概述通常來說,網絡IO可以抽象成用戶態和內核態之間的數據交換。一次網絡數據讀取操作(read),可以拆分成兩個步驟:1)網卡驅動等待數據準備好(內核態)2)將數據從內核空間拷貝到進程空間(用戶態)。根據這兩個步驟處理方式不一樣,我們通常把

Windows socketSelect模型開發

                                       Windows socket select模型開發。       套接字select模型是一種比較常用的IO模型。利用該模

socket通訊五:select多路複用的客戶/伺服器模型

前面一篇介紹了伺服器端使用多執行緒的方式來處理多個客戶端的請求的,但是當客戶端數量增多時執行緒數量會急劇增加,導致消耗大量的資源。 於是就引出了伺服器端的一種新的模型。 1. 阻塞與非阻塞 首先介紹幾個基本的概念。 阻塞方式( block ),顧名思義,就是程序或是

python實現selectepoll模型socket網路程式設計

select目前幾乎在所有的平臺上支援,其良好跨平臺支援也是它的一個優點,事實 上從現在看來,這也是它所剩不多的優點之一,現在其實更多的人用epoll,在 python下epoll文件有點少,就先講究搞搞select ~ select的一個缺點在於單個程序能夠監視

多路I/O轉接select模型

struct truct rose sleep 輸出 問題 pre strerror 結構 I/O復用使得程序可以同一時候監聽多個文件描寫敘述符。這對提高程序的性能至關重要。通常,網絡程序同一時候處理或者監聽多個socket文件描寫敘述符的時候可以考慮使用I/O復用模型

linux下select,poll,epoll的使用重點分析

end 復用 cps typedef lis callback 指向 hub 機制 好久沒用I/O復用了,感覺差點兒相同都快忘完了。記得當初剛學I/O復用的時候花了好多時間。可是因為那會不太愛寫博客,導致花非常多時間搞明確的東西,依舊非常easy忘記。俗