前言
? ? ? ?
? ? ? ? 以<深入理解計算機系統>(以下稱“本書”)內容為基礎,對程序的整個過程進行梳理。本書內容對整個計算機系統做了系統性導引,每部分內容都是單獨的一門課.學習深度根據自己需要來定
引入?
? ? ? ? 接續上一帖理解計算機系統_并發編程(2)_基于I/O復用的并發(一):select淺解-CSDN博客,實現一個基于I/O多路復用的并發事件驅動服務器.研讀代碼,總結編程的一些模式.
狀態機
? ? ? ? 本書P686和P687介紹了狀態機的概念并配圖
? ? ? ? 在事件驅動程序中,某些事件會導致流向前推進.一般思路是將邏輯流模型化為狀態機.一個狀態機就是一組狀態,輸入事件和轉移,其中轉移是將狀態和輸入事件映射到狀態.---黑體字是原話
? ? ? ? ---解讀:邏輯是什么?是一種因果關系.邏輯流解釋為從因到果的流程.狀態機和邏輯流相互對應
? ? ? ? 狀態對應邏輯(因果),表述為:當條件A滿足時,用事件B響應.
? ? ? ? 輸入事件對應起因已滿足,表述為:條件A已滿足
? ? ? ? 轉移對應用結果響應,表述為:事件B
? ? ? ? /*書面敘述概念時,常用較大的篇幅來寫.而讀者理解時可以采用簡單的方式*/.
? ? ? ? 所有的邏輯都可以轉化成狀態機?
????????狀態機的推導:"狀態"是邏輯是想法,不需要代碼;"輸入事件"和"轉移"需要代碼表達.所以狀態機可以看作是一種理想的邏輯表達方式,從思路到實現都包含了進去.
服務器I/O多路復用的狀態機
? ? ? ? 本書P687第3段:服務器使用I/O多路復用,借助select函數檢測輸入事件的發生.當每個已連接描述符準備好可讀時,服務器就為相應的狀態機執行轉移,在這里就是從描述符讀和寫回一個文本行.
? ? ? ? ---解讀:select:檢測輸入事件;? 轉移:描述符讀和寫回一個文本行.
? ? ? ? 任何一個狀態機必然有檢測輸入事件,這里用的是select函數. 這里的轉移事件---"描述符讀和寫回一個文本行"非必須,演示用.
基于I/O多路復用的并發事件驅動服務器
=============================內容分割線↓===================================
讀代碼的說明
? ? ? ? ?/*代碼是最有說服力的,表現編程思想和程序模型的存在*/
? ? ? ? ?學習別人的源碼,除了理解別人的思路,還要一點也很重要:從源碼中整理和提取寫代碼的模式.
? ? ? ? ?注意:讀代碼的順序是從主程序開始讀,先理解大概意思,再去理解他調用的函數.同樣道理,遇到變量,也不要一個一個挨著去讀,要結合使用去理解.
=============================內容分割線↑===================================
代碼整體說明
? ? ? ? 本書P687最后一段是基于I/O多路復用的并發服務器示例代碼的整體說明.
????????/*如果自己寫了一段代碼,技術文檔也可以借鑒這種方式,讓別人容易理解*/
? ? ? ? 一個pool結構里維護著活動客戶端的集合(第3~11行)? ---黑體字是原話
? ? ? ? ---解讀:這里有個詞叫"維護",經常在其他技術文獻中看到什么維護著什么,感覺很專業.從內容上看,維護代表"描述"或者"控制".
????????活動客戶端的集合在服務器端用一個結構來描述,結構取名叫pool(池),理解為客戶端連接池.
? ? ? ? 然后是初始化init_pool,進入while循環,檢測輸入事件,連接請求到達,調用add_client函數,送回文本.這些內容要簡單了解后,邊讀源碼邊理解.
代碼研讀
? ? ? ? ?注意:經典書的經典代碼,很難得(好好看好好學)
? ? ? ? ?主程序在本書P687和P688,名為echoservers.c
函數開始
? ? ? ? ? 1>第3~11行是"連接池"結構聲明
typedef struct{int maxfd;fd_set read_set;fd_set ready_set;int nready;int maxi;int clientfd[FD_SETSIZE];rio_t clientrio[FD_SETSIZE];
}pool;
? ? ? ? ?如前所述,不用一下全看完,邊讀代碼邊看連接池結構是怎樣定義的.
? ? ? ? ?2>進入main函數,第17~20行是變量定義,也是結合后面代碼讀.注意pool定義為靜態變量,即表示不想被別的模塊使用,如果要用到,必須提供接口.---這是C語言基礎.這里加上了static,聲明成main函數中的局部靜態變量,為了保持更新.
????????注意這里的命名方式不太合理,類型名和變量名取得一樣.規則寫法應該把類型名寫成首字母大寫Pool.
? ? ? ? 3>第26行調用Open_listenfd函數
? ? ? ? ?這里再梳理一遍監聽描述符的建立:Open_listenfd函數是由服務器調用,表示等待客戶端連接請求.一旦客戶端發來連接請求,代碼才執行,生成listenfd監聽描述符,代表服務器已經知道你的請求了.后續將視情況調用accept接受請求,生成connfd描述符.
init_pool函數
? ? ? ? 在本書P688圖12-9.從名字看來表示:初始化連接池.
? ? ? ? 第4~7行代碼如下
int i;
p->maxi=-1;
for(i=0;i<FD_SETSIZE;i++)p->clentfd[i]=-1;
? ? ? ? ---解讀:clientfd[FD_SETSIZE]是第一個理解的數據,他是什么意思?書上說了clientfd數組表示已連接描述符的集合.
? ? ? ? /*已連接描述符在服務器端通常用connfd,書上用了clientfd來表示連接(客戶端連接),connfd用到了其他地方*/
????????開始時沒有連接進來,所以數組中所有整數值設置為-1.-1表示槽位(用其他值也可以,不一定是-1,但既然其值用來表示描述符,所以推薦小于-1的值和其他文件描述符做區分)
? ? ? ??FD_SETSIZE,本書沒有顯式說明.從全大寫的表示來看,他是#define宏定義的一個常數.字面意思代表了允許連接數.
? ? ? ? maxi:已連接集合里的最大索引.初始時沒有連接,也被設置為-1.
? ? ? ? 筆者開始時在這里也饒了半天,這個數組的來源是這樣的,演示如下:
????????服務器每當建立一個連接,就會生成一個描述符connfd來傳輸數據,描述符是個整型數據(從4開始).那么當前有多少個描述符已建立呢?這些描述符該怎樣控制其數量呢?設計了一個整型數組
typedef struct{...int maxi; //當前已連接描述符的個數-1,同時也是下面數組中不等于-1的索引值int clientfd[FD_SETSIZE]; //已連接描述符數組,最大連接數限制為FD_SETSIZE...
}pool;
? ? ? ? 初始化時,clientfd[FD_SETSIZE]={-1,-1.....-1},此時索引maxi設置-1.
????????當有一個描述符(比如4)被產生,寫個算法讓他進去數組中,此時clientfd[FD_SETSIZE]={4,-1.....-1},maxi加1,等于0.當遍歷這個數組查詢時,就知道有1個元素(索引等于0)
? ? ? ? 這樣是不是把意思表達清晰了?而這個把數組索引maxi單獨表達的寫法,也值得一學.
? ? ? ? 第10~12行代碼如下????????
p->maxfd=listenfd;
FD_ZERO(&p->read_set);
FD_SET(listenfd,&p->read_set);
? ? ? ---解讀:maxfd是讀集合的基數,"讀集合"和"基數"意思在理解計算機系統_并發編程(2)_基于I/O復用的并發(一):select淺解-CSDN博客有,此處不多說.
? ? ? ? FD_ZERO清空讀集合,FD_SET把listenfd添加到讀集合中. ---初始化配置讀集合?
回到主函數
? ? ? ? 第29行進入while循環,第30行設置"準備好集合",第31行調用Select函數
? ? ? ? ?再看pool中對應數據的使用:
? ? ? ? ?read_set:讀集合;
? ? ? ? ?ready_set:準備好集合.
? ? ? ? ?nready:準備好集合中元素的個數
? ? ? ? ?第35行調用FD_ISSET傳入listenfd,表示連接判定---如果有客戶端請求連接,則執行.
? ? ? ? ?第36行的意思在本書前面一章(筆者也沒有細讀,照著用)
? ? ? ? ?第37行調用Accept函數,表示服務器端同意連接,生成connfd描述符(寫法也參照前面一章)
add_client()函數
? ? ? ? 添加一個新的客戶端到活動客戶端池中,更新pool結構中的數據.
=============================內容分割線↓===================================
? ? ? ? 注意select函數的解讀,他的第一次調用和多次調用的返回值不同.
? ? ? ? 當第一次被調用,也就是maxfd+1=4的情況下,此時讀集合只有一種情況響應---第一個連接請求進來,listenfd描述符被激活.這種情況下p->nready等于1.而如果select函數非第一次調用,表示他可能會有新的連接,或者已有連接準備好讀,這時的p->nready不等于1.---代碼要考慮滿足多種情況.
=============================內容分割線↑===================================
? ? ? ? 第4行更新p->nready,添加描述符到客戶端池的時候,準備好集合的元素個數-1.
? ? ? ? 第5~8行查找槽位,把生成的connfd描述符寫入clientfd數組.
????????注意第6行的if(p->clientfd[i]<0)不是唯一寫法,因為在init_pool的定義中clientfd數組中的值都等于-1.而生成的connfd的值都是≥4的,所以寫成if(p->clientfd[i]==-1)也可以.
? ? ? ? 第9行初始化讀緩沖區,這部分內容沒有細說,可以"抄".這里有個clientrio數組,是分配給每個連接的緩沖區.
? ? ? ? ?第12行把connfd添加進讀集合,意思是下次調用select可以作為條件進入準備好集合.
? ? ? ? ?第15,16行更新maxfd,當讀集合添加了一個描述符后,其基數應該加1.這里用了一個比較
? ? ? ? ?第17,18行更新maxi,"順勢"使新進入的連接作為準備好集合的一員,參加后面的代碼,更新準備好集合的索引
? ? ? ? ?第21行可不寫,寫了更明確:如果i==FD_SETSIZE,超出最大連接數,add_client函數執行無效
check_clients()函數
? ? ? ? ? /*主函數中已沒有其他內容,check_clients()函數緊跟add_client()函數.*/
? ? ? ? ? ?本書P690中間有注釋:check_clients服務準備好的客戶端連接.遍歷clientfd數組,取出文件描述符并讀寫,不詳述.
? ? ? ? ? ?注意:
? ? ? ? ? ? 第7行的for條件中p->nready>0,表示第一次添加進準備好集合(上面17,18行的描述)不能進入這個環節.
????????????第12行的connfd>0筆者沒看懂,以為每個connfd都要≥4(可能有些東西沒完全搞懂),
????????????第12行的FD_ISSET(connfd,&p->ready_set)判斷遍歷出來的connfd是否是本次select執行的結果,以此作為后面代碼執行的前提.從這里可以反推出select函數執行的結果是類似于{1,3,5}這樣的整型數組.
? ? ? ? ? ? /*語法上使用if或者for,while做條件判斷時,如果有多重判斷,值得注意他的意思*/
I/O多路復用技術的優劣
? ? ? ? 以下黑體字是本書原話
? ? ? ? 1.基于事件驅動,聽起來很好,為客戶端提供他們需要的服務,對于基于進程的并發服務器來說,是很困難的.
? ? ? ? ?2.編碼復雜.我們的事件驅動的并發echo服務器需要的代碼比基于進程的服務器多三倍,并且很不行,隨著并發粒度的減小,復雜性還會上升.
? ? ? ? ?3.不能充分利用多核處理器.
代碼層面的小結
? ? ? ? 1.當想要對程序數據進行控制和描述時,維護一個結構.結構的數據類型設計,和讀代碼時一樣,不用一次到位,一邊寫代碼一邊根據需要增加(筆者在前面數據類設計中提到過,即使有冗余屬性問題也不大---也就是代碼不夠優雅).結構的好處是他是全局變量,可以動態修改,實時更新.
? ? ? ? 2.select函數的第一個參數,從前面的硬編碼listenfd+1變成了maxfd+1,實時更新.
? ? ? ? 3.遺憾本書并沒有把select講得很透徹,需要找資料補充.select的使用限于"復制粘貼".比如select調用到什么情況下結束?和時間長短是否有關系(像進程一樣由時間片停止)
小結
????????基于I/O多路復用的并發事件驅動服務器的一點理解
? ? ? ? ? ??
????????