在阿里雲PHP技術沙龍專場中,阿里雲邀請到php-nsq做者,pecl、Swoole開發組成員吳振宇分享了Swoole進程模型的原理與Swoole協程實現的原理。並結合具體開發案例講解了Swoole在網絡編程中的應用。php
本次直播視頻精彩回顧,戳這裏!
直播回顧:https://yq.aliyun.com/live/965
PPT分享:https://yq.aliyun.com/download/3528編程
如下內容根據演講嘉賓視頻分享以及PPT整理而成。數組
網絡編程又可稱爲Socket編程。編程分爲基於Server端開發與基於Client端開發兩部分。基於Server端的編程由四大步驟組成,開發者首先建立Socket,利用bind與listen函數綁定監聽地址及相應的端口,最後使用accept函數接受來自監聽端的請求。Client端的操做較爲簡便,開發者在建立Socket後使用connect函數對服務器端進行鏈接便可實現。
下圖所示爲Client端與Server端的協做示意圖。Client端首先向Server端發起帶有SYN標識的握手請求,Server端接受到請求後,返回給Client端帶有SYN與ACK標識的請求並將Client端中的RCVD文件加載至隊列中,在三次握手完成以後,該文件描述符將被添加至accept隊列中等待下一步邏輯處理。緩存
下圖所示爲Socket編程的實現代碼服務器
在Socket編程中,Socket的讀寫狀態判斷十分重要。Socket可讀條件分爲如下四條:網絡
對應於Socket可讀條件的判斷,Socket可寫條件也分爲如下四條:數據結構
在一款應用開發初期,應用的用戶很少,服務器相對的要求一樣不高,此時開發者可使用多進程策略進行應用的開發,以此加快開發效率。下圖所示爲多進程同步阻塞開發的僞代碼。架構
當業務量擴大,系統須要進行優化時,開發者能夠對每一個子進程中的套接字進行監聽,其僞代碼以下圖所示。異步
當系統的用戶及業務量擴大到必定規模時,開發者可使用多路IO複用、Reactor及異步非阻塞等方法對系統進行改進。以下圖所示,在這些系統調用中,Select方法存在內存開銷大,支持文件描述符數量少的缺點。目前Epoll系統調用方式佔據開發的主流位置,Epoll方式採用了紅黑樹的數據結構模式,同時擁有就緒列表rdlist,當套接字中存在可讀或可寫的事件時,該事件將被直接添加到就緒列表當中,從而使系統省去了輪詢全部套接字屬性的過程,提升了系統的執行效率。函數
操做系統進程調用時分爲正在運行,阻塞運行及等待運行三個狀態。在處理進程的過程當中,內核會不斷髮生中斷,好比三次握手過程當中,當ACK發送時,內核會觸發中斷,系統此時須要放下正在執行的任務,去處理TCP的任務。處理完成後,系統結束中斷處理並恢復運行被打斷的進程。下圖所示爲操做系統進程調度的一些方法。
在三次握手中,系統執行如下三個步驟完成操做系統的調度:
1.網卡收到數據:網卡收到SYN消息,觸發內核中斷,系統將直接打斷當前執行的進程,同時CPU將會把套接字加入到Socket Queue隊列當中進行存儲。
2.中斷回調:若當前沒有新的鏈接,accept將阻塞到系統調用上,並將套接字註冊到Wait Queue上。
3.系統中斷回調:當新的鏈接產生時,Wait Queue隊列將觸發回調函數,將相應數據加載至rdlist列表中。
若網卡收到ACK消息,則繼續觸發內核中斷,內核完成標準的三次握手,將鏈接從半鏈接隊列移入鏈接隊列,因而 listen Socket有可讀事件,內核調用listen Socket的Wait Queue的喚醒回調函數,將以前阻塞的accept進程置爲 Ready調度狀態。
Epoll主要用來監聽Socket的可讀可寫過程,在Epoll建立時,開發者須要傳對應文件描述符EPOLLIN與EPOLLOUT做爲可讀與可寫的參數標誌,epoll_wait函數擁有accept的功能,會在事件發送後提醒開發者。下圖羅列了Epoll中的參數與主要方法。
將Socket建立與accept過程轉化爲Epoll的代碼示意圖以下所示。首先將fd做爲描述符加入建立好的Epoll中,同時把開發者想要監聽的可讀可寫事件也註冊入Epoll之中。當listen fd監聽到事件時,使用accept方法將該fd描述符設爲可讀事件,並再次將其加入到Epoll的監聽數組中,此時表明真正的客戶端鏈接已接入。
Reactor模型的建立與使用較爲簡單,其中含有如下四個方法:
在進程模式中,系統採用MainReactor線程監聽accept,線程將出現的問題拋給Worker進程進行處理,這樣即便單個Worker進程掛掉也不會對系統產生任何的影響。下圖所示爲進程模式的系統結構示意圖。
下圖展現了對Swoole模式的調用代碼示意。在用戶使用客戶端去鏈接服務器的過程當中,系統首先註冊可讀可寫與超時三個狀態回調函數。客戶端與服務器鏈接成功時,套接字變爲可寫狀態,系統調用可寫狀態的回調函數,在回調函數中處理相關的數據。
Swoole協程是由事件驅動與棧切換兩步共同實現完成的。
在C語言環境中,事件的調用每每使用堆棧進行處理。在堆棧中,指針EBP指向堆棧棧底,指針ESP指向堆棧棧頂,在函數調用以後,每一個EBP的返回值會返回上一個EBP的地址。以此來進行事件調用的檢索。下圖所示爲C語言中的事件調用示意圖。
在PHP中的函數調用步驟以下圖所示。PHP首先經過詞法分析與語法分析將代碼編譯成語法樹,語法樹中的每個語法會被編譯入opcode,語法中的每個函數會以oparray的形式存入結構體EG中,EG結構體使用函數表對這些函數進行存儲。
當函數調用時,結構體中的call對應指針ESP,prev對應於指針EBP。當用戶調取函數時,系統會向zend VM中爲每個方法申請一個堆棧的內存。當系統中一個函數調用其餘函數時,會調用code下方儲存的地址,調用方法的opcode從function存儲的成員中找到並進行編譯與執行。當觸發了opcode後,系統會申請一個新的內存來進行新的內存分配。下圖爲PHP調用示意圖。
下圖所示爲在PHP函數調用中壓棧的過程及函數中存在的opcode。FCALL與DO_FCALL負責函數的調用,當堆棧中第一個opcode執行時,將進行參數壓棧的操做。觸發函數調用時,將執行DO_FCALL操做,系統將會把下一個函數的調用地址壓入堆棧。當調用有結果後系統會將返回值返回入CALL FRAME中。
下圖所示爲Swoole協程代碼。協程代碼包括兩個執行網絡IO操做的go函數,當系統執行connect操做時觸發網絡IO操做,並將當前的PHP調用棧先保存起來。在當前調用棧保存好後,系統順次執行下面的函數調用。當connect遇到IO函數時,系統會跳出當前任務去執行堆棧中儲存的任務。
在Swoole2.0中使用C函數進行線程任務的協程。當開發者調用setjmp時,函數的返回值爲0並調起first函數。當調用longjmp時,setjmp也一樣被調起,此時返回值爲1。Swoole2.0利用該代碼實現了PHP執行的跳轉,代碼示意圖以下。
Swoole2.0協程時序圖與代碼展現以下圖所示。setjump方法設置當前函數堆棧,當有網絡事件產生時,系統將首先對產生的事件進行註冊,並在有事件通知時跳回執行中的代碼,以此完成代碼協程過程。
Swoole4.0經過實現C堆棧對Swoole2.0中的問題進行了改進。在Swoole4.0中用戶直接調用MySQL中的連接直接就能夠造成網絡協程。下圖所示爲Swoole4.0內核系統架構示意圖。
Swoole4.0的時序調度與Swoole2.0差異不大,不一樣的是Swoole4.0使用匯編指令對C棧與堆棧進行了存儲。在協程建立時,系統會產生C棧與PHP棧,兩個堆棧間會進行通訊,經過這種方法解決了C棧銷燬後的一些問題。下圖展示了Swoole4.0的時序圖。
當系統連接數量增多後會出現一些問題,開發者經過設置心跳參數與心跳收回能夠保證系統服務器的資源不會被浪費。下圖列舉了Swoole網絡編程對系統進行優化的方式。
下圖爲吳老師分享的內容的關鍵詞總結。
原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。