和老李一起手撕山寨Workerman(二)
- 2019 年 11 月 27 日
- 筆記
各位巨佬、大佬、腿子們,大家好,我是老李。
我感到一陣陣地無力眩暈,確切說我的腦子裡空蕩蕩的完全不知道要寫什麼,這第二篇到底該這麼開張,難道我連江郎還沒到就已然要才盡了嗎,當我腦海里飄過這個想法後突然感到胸口一陣壓抑呼吸不過來,自己奮力地想站起來卻又站不起來,似乎有一雙無形的大手將我狠狠地按在床上動彈不得,第二篇到底怎麼開篇咋寫,難道我是真的不行了嗎?
就在這個時候,突然一雙強有力的胳膊突然從身後抱住了我,毛茸茸的胸脯噌的我後背直痒痒,然後TA用力晃了我一下用急躁的口音對我說:
哎?老趙什麼時候回來了,這狗日的昨天還在濟南填坑呢,怎麼今天突然冒在我床上了?…
我努力地想揮動胳膊試圖保護住下邊,但是就是像鬼壓床那樣,胳膊似乎就是不聽使喚,我急的感覺都要爆炸了,突然電話鈴聲一頓聒噪:公司的一級報警電話就從來沒有如此讓我覺得親切過…
然後我就醒了。
辣么既然夢已醒來,那麼該要步入正規話題了。今天我們就正式動工破土山寨Workerman了,按照上一章拆解的模組順序,我複製粘貼一下:
- 進程模型
- IO模組
- Event-Loop模組
- 協議模組
從這行開始,你要暫時離開一下PHP-FPM SAPI,你得準備好PHP7和PHP-CLI,然後你還需要一個真正的Linux環境(WSL和WSL2不算,真有問題的,不是我黑TA不公正對待TA),發個圖你們感受下:
今天我們就從進程這裡開始,然後我馬上用一小段話簡單概括一下進程的一些小要點:進程是作業系統進行CPU調度的最小單元,在世界上最好的語言中實現進程相關操作的是一大坨以pcntl_*為前綴的函數族(pcntl應該是process control的簡稱),一般說來多個進程可以加速任務完成速度,但是CPU在同一個時刻只能執行一個進程,作業系統通過調度演算法在多個進程之間快速輪轉CPU佔用時間,弄的同一個CPU核心看起來好像同一時間真的可以支援多個進程似的然而實際上卻並不是都是假象PS唬人的幻覺;其次是多個進程之間的數據是隔離的,子進程會繼承走父進程的數據空間、堆、棧等資訊,總之就是父子之間的正文段是共享的,但是存儲空間是隔離的;雖然上一句我說了子進程會繼承父進程的堆、棧等數據副本,但實際上剛fork的時候也並不是這樣的,這裡用到了一種叫做COW(Copy On Write)寫時複製的「 高端技術 」,簡單說如果子進程不需要修改這些資訊,那麼就直接與父進程共享,如果一旦子進程或父進程修改了某些資訊,那麼才會真正的COPY一份這個修改區域的記憶體數據;父進程在fork完畢後是先接著執行父進程,還是先執行子進程,這個先後順序靠的是愛、靠的是信仰和三根香…
我不能再多說了,因為我自己剛才差點兒就吐了…再說下去估計你們可能就要點左上角的關閉了。我覺得還是需要可供CV的demo是王道…
讓我們盪起雙…先從pcntl_fork()開始說起,毫無疑問pcntl_fork()一定是fork()的包裝,TA的作用就是搞出來一個子進程。這個函數有點兒意思,TA會返回兩次,你乍一聽有點困惑[ 函數怎麼能返回兩次呢? ]後來你靈機一動[ 嗷,是不是父進程里返回一次,子進程里返回一次 ],我在這裡透過螢幕欣慰地微笑著點頭…
pcntl_fork()的返回是進程號PID,PID都是大於0的,如果返回結果小於0,那就表示是出錯了。在子進程里,返回的PID為0;在父進程里,返回的PID則為子進程的PID。為什麼子進程里返回的PID為0?這裡有個原因就是在子進程里可以通過posix_getpid()獲取自己的進程號並且通過posix_getppid()來獲取父進程的PID,但是在父進程可能會fork出很多個子進程,所以父進程沒有辦法獲取某一個確切子進程的PID~
<?php $i_pid = pcntl_fork(); // 子進程... if ( 0 == $i_pid ) { echo "I am in child process".PHP_EOL; } // 父進程 else if ( $i_pid > 0 ) { echo "I am in father process".PHP_EOL; } else { throw new Exception( "Exception:pcntl_fork err" ); }
上面程式碼反正能運行,只不過上面程式碼比較沙雕,因為說明不了什麼問題,xue微修改後你們再感受下:
<?php $s_slogan = "Hello, I'm from "; $i_pid = pcntl_fork(); // 子進程... if ( 0 == $i_pid ) { $s_slogan .= "child process"; echo $s_slogan." | 子進程PID:".posix_getpid()." | 父進程PID:".posix_getppid().PHP_EOL; } // 父進程 else if ( $i_pid > 0 ) { $s_slogan .= "father process"; echo $s_slogan." | 子進程PID:".$i_pid." | 當前進程PID:".posix_getpid().PHP_EOL; } else { throw new Exception( "Exception:pcntl_fork err" ); }
這坨程式碼運行結果如下圖,這坨程式碼可以說明兩個問題:
- 兩個進程中的數據是彼此隔離不相同的
- pcntl_fork()的返回值和父進程子進程是怎麼回事
前面我說了,fork之後的程式碼將會分別由父進程和子進程繼續執行。這句話乍一聽好理解,但是我寫一坨demo你們感受下:
<?php for ( $i = 1; $i <= 3; $i++ ) { $i_pid = pcntl_fork(); if ( 0 == $i_pid ) { echo "@子進程".PHP_EOL; } }
你們猜一下上面這個程式碼會顯示幾次【@子進程】?賣個關子,你們自己去運行一下去。
如果運行結果和你們預想的結果不一樣,那一定是你們想錯了
我先不說為什麼,咱先把上面程式碼xue微修改一下:
<?php for ( $i = 1; $i <= 3; $i++ ) { $i_pid = pcntl_fork(); if ( 0 == $i_pid ) { echo "@子進程".PHP_EOL; exit; } }
你們複製粘貼走跑一下,感受一下結果…然後再仔細思考下,好吧?
… …
… …
幾分鐘過去了,上面那個for循環里加pcntl_fork()的問題想明白了嗎?沒想明白我替你下個結論:如果你想得到for循環中計數一樣的子進程數,記得要在子進程里使用exit()函數,如果沒有exit()函數其實這整個過程就和細胞的有絲分裂極為相似了~高中生物中的有絲分裂你們自己腦補一下吧…
好了,我假設你們整明白上面的問題了,現在我們來玩個好玩的遊戲:我在pcntl_fork()執行之前先與Redis建立一個連接,然後再開3個子進程,用netstat -ant查看Redis連接數,你們猜有多少個Redis連接?
<?php $o_redis = new Redis(); $o_redis->connect( '127.0.0.1', 6379 ); // 使用for循環搞出3個子進程來 for ( $i = 1; $i <= 3; $i++ ) { $i_pid = pcntl_fork(); if ( 0 == $i_pid ) { // 使用while保證三個子進程不會退出... while( true ) { sleep( 1 ); } } } // 使用while保證主進程不會退出... while( true ) { sleep( 1 ); }
我貼下截圖你們感受一下,我相信你們應該能看明白這個圖是啥意思…
也就是說父進程和三個子進程一共四個進程,實際上共享了一個Redis連接,而且這個Redis連接是一個實打實的長鏈接,這個和我們平時在PHP-FPM里用的Redis connect方法還是xue微不一樣的,這種長鏈接避免了與Redis伺服器的頻繁連接(說百了就是沒有三次握手和四次揮手),這種看起來似乎並不起眼的零星性能差距,將會在並發越高的時候越能甩開PHP-FPM短連接一條街…
再回到這個問題當前是四個進程共享了同一個Redis連接,這種用法會有問題么?考慮到Redis是一個單進程單執行緒的伺服器,所有飛過去的命令本質上都是按照順序一個一個執行的,所以似乎聽起來好像沒問題…我們把上面程式碼改一小下下,你們用心去感受一下:
<?php $o_redis = new Redis(); $o_redis->connect( '127.0.0.1', 6379 ); // 使用for循環搞出3個子進程來 for ( $i = 1; $i <= 4; $i++ ) { $i_pid = pcntl_fork(); if ( 0 == $i_pid ) { $b_ret = $o_redis->sismember( "uid", $i ); echo $i.':'.json_encode( $b_ret ).PHP_EOL; // 使用while保證三個子進程不會退出... while( true ) { sleep( 1 ); } } } // 使用while保證主進程不會退出... while( true ) { sleep( 1 ); }
同時,我在Redis里整了一個set集合,名稱叫做uid,值為[ 1, 2, 3, 5, 6 ],注意沒有4!運行結果真的是… …
來來來,我連續運行了好多次,你們感受下:
很明顯這種用法暴露了兩個問題:
- 數字4是確實不會存在uid集合中的,但是有兩次結果都是true
- 理論上應該有四條bool結果,結果有時候卻只返回三條bool結果
多個進程復用同一個Redis連接,Redis的返回結果永遠無法判斷會被哪個進程給處理掉了,所以正確的用法應該是給每一個進程分別創建一個Redis連接,程式碼修改成下面這樣就可以了:
<?php $o_redis = new Redis(); // 使用for循環搞出3個子進程來 for ( $i = 1; $i <= 4; $i++ ) { $i_pid = pcntl_fork(); if ( 0 == $i_pid ) { $o_redis->connect( '127.0.0.1', 6379 ); $b_ret = $o_redis->sismember( "uid", $i ); echo $i.':'.json_encode( $b_ret ).PHP_EOL; // 使用while保證三個子進程不會退出... while( true ) { sleep( 1 ); } } } // 使用while保證主進程不會退出... while( true ) { sleep( 1 ); }
這坨程式碼你們自己去測試執行吧,我就不貼運行結果了,不過我貼一張Redis連接數量的圖:
四個子進程,四個與Redis的連接。
所以,如果你要用MySQL,道理也是一樣的,同樣也會是長鏈接,同樣也是多進程不要共享同一個MySQL連接。
你們以為到這裡就算完了嗎?不,並沒有,我還有一個問題,那就是file_put_contents()…我寫了下面一坨程式碼,你們感受一下並猜測一下使用多個進程利用file_put_contents()函數向同一個文件里寫數據,會出問題嗎?
<?php // 使用for循環搞出3個子進程來 for ( $i = 1; $i <= 100; $i++ ) { $i_pid = pcntl_fork(); if ( 0 == $i_pid ) { file_put_contents( "./Core.log", $i.PHP_EOL, FILE_APPEND ); // 使用while保證三個子進程不會退出... while( true ) { sleep( 1 ); } } } // 使用while保證主進程不會退出... while( true ) { sleep( 1 ); }
100個子進程向同一個文件里沒有先後順序地寫數字,那麼最終結果會寫入100個數字嗎?會產生進程數據覆蓋的情況嗎?
這個有興趣的同學自己研究一下,沒有興趣的同學直接看推送的另外一篇文章…