作者:施洪寶
一. 基礎知識
1.1 swoole
swoole是面向生產環境的php異步網絡通信引擎, php開發人員可以利用swoole開發出高性能的server服務。swoole的server部分, 內容很多, 也涉及很多的知識點, 本文僅對其server進行簡單的概述, 具體的實現細節在後續的文章中再進行詳細介紹。
1.2 網絡編程
- 網絡通信是指在一臺(或者多臺)機器上啓動一個(或者多個)進程, 監聽一個(或者多個)端口, 按照某種協議(可以是標準協議http, dns; 也可以是自行定義的協議)與客戶端交換信息。
- 目前的網絡編程多是在tcp, udp或者更上層的協議之上進行編程。swoole的server部分是基於tcp以及udp協議的。
- 利用udp進行編程較爲簡單, 本文主要介紹tcp協議之上的網絡編程
- TCP網絡編程主要涉及4種事件,
- 連接建立: 主要是指客戶端發起連接(connect)以及服務端接受連接(accept)
- 消息到達: 服務端接受到客戶端發送的數據, 該事件是TCP網絡編程最重要的事件, 服務端對於該類事件進行處理時, 可以採用阻塞式或者非阻塞式, 除此之外, 服務端還需要考慮分包, 應用層緩衝區等問題
- 消息發送成功: 發送成功是指應用層將數據成功發送到內核的套接字發送緩衝區中, 並不是指客戶端成功接受數據。對於低流量的服務而言, 數據通常一次性即可發送完, 並不需要關心此類事件。如果一次性不能將全部數據發送到內核緩衝區, 則需要關心消息是否成功發送(阻塞式編程在系統調用(write, writev, send等)返回後即是發送成功, 非阻塞式編程則需要考慮實際寫入的數據是否與預期一致)
- 連接斷開: 需要考慮客戶端斷開連接(read返回0)以及服務端斷開連接(close, shutdown)
1.3 進程間通信
- 進程之間的通信有無名管道(pipe), 有名管道(fifo), 信號, 信號量, 套接字, 共享內存等方式
- swoole中採用unix域套接字用於多進程之間的通信(指swoole內部進程之間)
1.4 socketpair
- socketpair用於創建一個套接字對, 類似於pipe, 不同的是pipe是單向通信, 雙向通信需要創建兩次, socketpair調用一次即可實現雙向通信, 除此之外, 由於使用的是套接字, 還可以定義數據交換的方式
- socketpair系統調用
int socketpair(int domain, int type, int protocol, int sv[2]);
//domain表示協議簇
//type表示類型
//protocol表示協議, SOCK_STREAM表示流協議(類似tcp), SOCK_DGRAM表示數據報協議(類似udp)
//sv用於存儲建立的套接字對, 也就是兩個套接字文件描述符
//成功返回0, 否則返回-1, 可以從errno獲取錯誤信息
- 調用成功後sv[0], sv[1]分別存儲一個文件描述符
- 向sv[0]中寫入, 可以從sv[1]中讀取
- 向sv[1]中寫入, 可以從sv[0]中讀取
- 進程調用socketpair後, fork子進程, 子進程會默認繼承sv[0], sv[1]這兩個文件描述符, 進而可以實現父子進程間通信。例如, 父進程向sv[0]中寫入, 子進程從sv[1]中讀取; 子進程向sv[1]中寫入, 父進程從sv[0]中讀取。
1.5 守護進程(daemon)
- 守護進程是一種特殊的後臺進程, 它脫離於終端, 用於週期性的執行某種任務
- 進程組
- 每個進程都屬於一個進程組
- 每個進程組都有一個進程組號, 也就是該組組長的進程號(PID)
- 一個進程只能爲自己或者其子進程設置進程組號
- 會話
- 一個會話可以包含多個進程組, 這些進程組中最多只能有一個前臺進程組(也可以沒有)
- setsid可以創建一個新的會話, 該進程不能是進程組的組長。setsid調用完成後, 該進程成爲這個會話的首進程(領頭進程), 同時變成一個新的進程組的組長, 如果該進程之前有控制終端, 則該進程與終端的聯繫被斷開
- 用戶通過終端登錄或者網絡登錄, 會創建一個新的會話
- 一個會話最多只能有一個控制終端
- 創建守護進程的方式
- fork子進程後, 父進程退出, 子進程執行setsid即可成爲守護進程。這種方式下, 子進程是會話的領頭進程, 可以重新打開終端, 此時可以再次fork, fork產生的子進程無法再打開終端。第二次fork並不是必須的, 只是爲了防止子進程再次打開終端
- linux提供了daemon函數用於創建守護進程
1.6 swoole tcp server示例
<?php
//創建server
$serv = new Swoole\Server('0.0.0.0', 9501, SWOOLE_PROCESS, SWOOLE_SOCK_TCP);
//設置server的參數
$serv->set(array(
'reactor_num' => 2, //reactor thread num
'worker_num' => 3, //worker process num
));
//設置事件回調
$serv->on('connect', function ($serv, $fd){
echo "Client:Connect.\n";
});
$serv->on('receive', function ($serv, $fd, $reactor_id, $data) {
$serv->send($fd, 'Swoole: '.$data);
$serv->close($fd);
});
$serv->on('close', function ($serv, $fd) {
echo "Client: Close.\n";
});
//啓動server
$serv->start();
- 上述代碼在cli模式下執行時, 經過詞法分析, 語法分析生成opcode, 進而交由zend虛擬機執行
- zend虛擬機在執行到$serv->start()時, 啓動swoole server
- 上述代碼中設置的事件回調是在worker進程中執行, 後文會詳細介紹swoole server模型
二. swoole server
2.1 base模式
- 說明
- base模式採用多進程模型, 這種模型與nginx一致, 每個進程只有一個線程, 主進程負責管理工作進程, 工作進程負責監聽端口, 接受連接, 處理請求以及關閉連接
- 多個進程同時監聽端口, 會有驚羣問題, 目前swoole並沒有解決
- linux 內核3.9及其後續版本提供了新的套接字參數SO_REUSEPORT, 該參數允許多個進程綁定到同一個端口, 內核在接受到新的連接請求時, 會喚醒其中一個進行處理, 內核層面也會做負載均衡, 可以解決上述的驚羣問題
- base模式下, reactor_number參數並沒有作用, 因爲每個進程只有一個線程
- 如果worker進程數設置爲1, 則不會fork出worker進程, 主進程直接處理請求
- 啓動過程
- php代碼執行到$serv->start()時, 主進程進入int swServer_start(swServer *serv)函數, 該函數負責啓動server
- 在函數swServer_start中會調用swReactorProcess_start, 這個函數會fork出多個worker進程
- 主進程和worker進程各自進入自己的事件循環, 處理各類事件
2.2 process模式
- 說明
- 這種模式爲多進程多線程, 有主進程, manager進程, worker進程, task_worker進程
- 主進程下有多個線程, 主線程負責接受連接, 之後交給react線程處理請求。 react線程負責接收數據包, 並將數據轉發給worker進程進行處理, 之後處理worker進程返回的數據
- manager進程, 該進程爲單線程, 主要負責管理worker進程, 類似於nginx中的主進程, 當worker進程異常退出時, manager進程負責重新fork出一個worker進程
- worker進程, 該進程爲單線程, 負責具體處理請求
- task_worker進程, 用於處理比較耗時的任務, 默認不開啓
- worker進程與主進程中的react線程使用域套接字進行通信, worker進程之間不進行通信
- 啓動過程
- swoole server啓動入口: swServer_start函數,
//php 代碼中$serv->start(); 會調用函數, 進行server start
int swServer_start(swServer *serv);
// 該函數首先進行必要的參數檢查
static int swServer_start_check(swServer *serv);
// 其中有,
if (serv->worker_num < serv->reactor_num)
{
serv->reactor_num = serv->worker_num;
}//也就是說reactor_num <= worker_num
//之後執行factory start, 也就是swFactoryProcess_start函數, 該函數會fork出manager進程, manager進程進而fork出worker進程以及task_worker進程
if (factory->start(factory) < 0)
{
return SW_ERR;
}
//然後主進程的主線程生成reactor線程
if (serv->factory_mode == SW_MODE_BASE)
{
ret = swReactorProcess_start(serv);
}
else
{
ret = swReactorThread_start(serv);
}
- 如果設置了daemon模式, 在必要的參數檢查完後, 先將自己變爲守護進程再fork manager進程, 進而創建reactor線程
- 主進程先fork出manager進程, manager進程負責fork出worker進程以及task_worker進程。worker進程之後進入int swWorker_loop(swServer *serv, int worker_id), 也就是進入自己的事件循環, task_worker也是一樣, 進入自己的事件循環。
static int swFactoryProcess_start(swFactory *factory);
//swFactoryProcess_start會調用swManager_start生成manager進程
int swManager_start(swServer *serv);
// manager進程會fork出worker進程以及task_worker進程
- 主進程pthread_create出react線程, 主線程和react線程各自進入自己的事件循環, reactor線程執行static int swReactorThread_loop(swThreadParam *param), 等待處理事件
//主線程執行swReactorThread_start, 創建出reactor線程
int swReactorThread_start(swServer *serv);
- 結構圖
swoole process模式結構如下圖所示,
- 上圖並沒有考慮task_worker進程, 在默認情況下, task_worker進程數爲0
三. 請求處理流程(process模式)
3.1 reactor線程與worker進程之間的通信
- swoole master進程與worker進程之間的通信如下圖所示,
- swoole使用SOCK_DGRAM, 而不是SOCK_STREAM, 這裏是因爲每個reactor線程負責處理多個請求, reactor接收到請求後會將信息轉發給worker進程, 由worker進程負責處理,如果使用SOCK_STREAM, worker進程無法對tcp進行分包, 進而處理請求
- swFactoryProcess_start函數中會根據worker進程數創建對應個數的套接字對, 用於reactor線程與worker進程通信(swPipeUnsock_create函數)
- 假設reactor線程有2個, worker進程有3個, 則reactor與worker之間的通信如下圖所示,
- 每個reactor線程負責監聽幾個worker進程, 每個worker進程只有一個reactor線程監聽(reactor_num<=worker_num)。swoole默認使用worker_process_id % reactor_num對worker進程進行分配, 交給對應的reactor線程進行監聽
- reactor線程收到某個worker進程的數據後會進行處理, 值得注意的是, 這個reactor線程可能並不是發送請求的那個reactor線程。
- reactor線程與worker進程通信的數據包
//包頭
typedef struct _swDataHead
{
int fd;
uint32_t len;
int16_t from_id;
uint8_t type;
uint8_t flags;
uint16_t from_fd;
#ifdef SW_BUFFER_RECV_TIME
double time;
#endif
} swDataHead;
//reactor線程向worker進程發送的數據, 也就是worker進程收到的數據包
typedef struct
{
swDataHead info;
char data[SW_IPC_BUFFER_SIZE];
} swEventData;
//worker進程向reactor線程發送的數據, 也就是reactor線程收到的數據包
typedef struct
{
swDataHead info;
char data[0];
} swPipeBuffer;
3.2 請求處理
- master進程中的主線程負責監聽端口(listen), 接受連接(accept, 產生一個fd), 接受連接後將請求分配給reactor線程, 默認通過fd % reactor_num進行分配, 之後通過epoll_ctl將fd加入到對應reactor線程中(如果對應的reactor線程正在執行epoll_wait, 主線程會阻塞), 剛加入時監聽寫事件, 如果直接監聽讀事件, 可能會立刻被觸發, 而監聽寫事件可以允許reactor線程進行一些初始化操作
//主線程執行epoll_ctl將fd(新接受的連接)加入到reactor線程的監聽隊列中
epoll_ctl(epfd, fd, ...);
//對應的reactor線程如果正在執行
epoll_wait(epfd, ...);
- 這種情況主線程會被阻塞(兩個線程同時操作epfd)
- 如果reactor線程沒有正在執行epoll_wait, 主線程則不會被阻塞, 執行成功後直接返回
- reactor線程中fd的寫事件被觸發, reactor線程負責處理, 發現是首次加入, 沒有數據可寫, 則開啓讀事件監聽
- reactor線程讀取到用戶的請求數據, 一個請求的數據接收完後, 將數據轉發給worker進程, 默認是通過fd % worker_num進行分配
- reactor發送給worker進程的數據包, 會包含一個頭部, 頭部中記錄了reactor的信息
- 如果發送的數據過大, 則需要將數據進行分片, 限於篇幅, reactor的分片, 後續再進行詳細講述
- 可能存在多個reactor線程同時向同一個worker進程發送數據的情況, 故而swoole採用SOCK_DGRAM模式與worker進程進行通信, 通過每個數據包的包頭, worker進程可以區分出是由哪個reactor線程發送的數據
- worker進程收到reactor發送的數據包後, 進行處理, 處理完成後, 將數據發送給主進程
- worker進程發送給主進程的數據包, 也會包含一個頭部, 當reactor線程收到數據包後, 能夠知道對應的reactor線程, 請求的fd等信息
- 主進程收到worker進程發送的數據包, 這個會觸發某個reactor線程進行處理
- 這個reactor線程並不一定是之前發送請求給worker進程的那個reactor線程
- 主進程的每個reactor線程都負責監聽worker進程發送的數據包, 每個worker發送的數據包只會由一個reactor線程進行監聽, 故而只會觸發一個reactor線程
- reactor線程處理worker進程發送的數據包, 如果是直接發送數據給客戶端, 則可以直接發送, 如果需要改變這個這個連接的監聽狀態(例如close), 則需要先找到監聽這個連接的reactor, 進而改變這個連接的監聽狀態
- reactor處理線程與reactor監聽線程可能並不是同一個線程
- reactor監聽線程負責監聽客戶端發送的數據, 進而轉發給worker進程
- reactor處理線程負責監聽worker進程發送給主進程的數據, 進而將數據發送給客戶端
四. gdb調試
4.1 process模式啓動
//fork manager進程
#0 0x00007ffff67dae64 in fork () from /lib64/libc.so.6
#1 0x00007ffff553888a in swoole_fork () at /root/code/swoole-src/src/core/base.c:186
#2 0x00007ffff556afb8 in swManager_start (serv=serv@entry=0x1353f60) at /root/code/swoole-src/src/server/manager.cc:164
#3 0x00007ffff5571dde in swFactoryProcess_start (factory=0x1353ff8) at /root/code/swoole-src/src/server/process.c:198
#4 0x00007ffff556ef8b in swServer_start (serv=0x1353f60) at /root/code/swoole-src/src/server/master.cc:651
#5 0x00007ffff55dc808 in zim_swoole_server_start (execute_data=<optimized out>, return_value=0x7fffffffac50)
at /root/code/swoole-src/swoole_server.cc:2946
#6 0x00000000007bb068 in ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER () at /root/php-7.3.3/Zend/zend_vm_execute.h:980
#7 execute_ex (ex=0x7ffff7f850a8) at /root/php-7.3.3/Zend/zend_vm_execute.h:55485
#8 0x00000000007bbf58 in zend_execute (op_array=op_array@entry=0x7ffff5e7b340, return_value=return_value@entry=0x7ffff5e1d030)
at /root/php-7.3.3/Zend/zend_vm_execute.h:60881
#9 0x0000000000737554 in zend_execute_scripts (type=type@entry=8, retval=0x7ffff5e1d030, retval@entry=0x0,
file_count=file_count@entry=3) at /root/php-7.3.3/Zend/zend.c:1568
#10 0x00000000006db4d0 in php_execute_script (primary_file=primary_file@entry=0x7fffffffd050) at /root/php-7.3.3/main/main.c:2630
#11 0x00000000007be2f5 in do_cli (argc=2, argv=0x1165cd0) at /root/php-7.3.3/sapi/cli/php_cli.c:997
#12 0x000000000043fc1f in main (argc=2, argv=0x1165cd0) at /root/php-7.3.3/sapi/cli/php_cli.c:1389
// pthread_create reactor線程
#0 0x00007ffff552e960 in pthread_create@plt () from /usr/local/lib/php/extensions/no-debug-non-zts-20180731/swoole.so
#1 0x00007ffff5576959 in swReactorThread_start (serv=0x1353f60) at /root/code/swoole-src/src/server/reactor_thread.c:883
#2 0x00007ffff556f006 in swServer_start (serv=0x1353f60) at /root/code/swoole-src/src/server/master.cc:670
#3 0x00007ffff55dc808 in zim_swoole_server_start (execute_data=<optimized out>, return_value=0x7fffffffac50)
at /root/code/swoole-src/swoole_server.cc:2946
#4 0x00000000007bb068 in ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER () at /root/php-7.3.3/Zend/zend_vm_execute.h:980
#5 execute_ex (ex=0x7fffffffab10) at /root/php-7.3.3/Zend/zend_vm_execute.h:55485
#6 0x00000000007bbf58 in zend_execute (op_array=op_array@entry=0x7ffff5e7b340, return_value=return_value@entry=0x7ffff5e1d030)
at /root/php-7.3.3/Zend/zend_vm_execute.h:60881
#7 0x0000000000737554 in zend_execute_scripts (type=type@entry=8, retval=0x7ffff5e1d030, retval@entry=0x0,
file_count=file_count@entry=3) at /root/php-7.3.3/Zend/zend.c:1568
#8 0x00000000006db4d0 in php_execute_script (primary_file=primary_file@entry=0x7fffffffd050) at /root/php-7.3.3/main/main.c:2630
#9 0x00000000007be2f5 in do_cli (argc=2, argv=0x1165cd0) at /root/php-7.3.3/sapi/cli/php_cli.c:997
#10 0x000000000043fc1f in main (argc=2, argv=0x1165cd0) at /root/php-7.3.3/sapi/cli/php_cli.c:1389
4.2 base模式啓動
//base 模式下的啓動
#0 0x00007ffff67dae64 in fork () from /lib64/libc.so.6
#1 0x00007ffff553888a in swoole_fork () at /root/code/swoole-src/src/core/base.c:186
#2 0x00007ffff5558557 in swProcessPool_spawn (pool=pool@entry=0x7ffff2d2a308, worker=0x7ffff2d2a778)
at /root/code/swoole-src/src/network/process_pool.c:392
#3 0x00007ffff5558710 in swProcessPool_start (pool=0x7ffff2d2a308) at /root/code/swoole-src/src/network/process_pool.c:227
#4 0x00007ffff55741cf in swReactorProcess_start (serv=0x1353f60) at /root/code/swoole-src/src/server/reactor_process.cc:176
#5 0x00007ffff556f21d in swServer_start (serv=0x1353f60) at /root/code/swoole-src/src/server/master.cc:666
#6 0x00007ffff55dc808 in zim_swoole_server_start (execute_data=<optimized out>, return_value=0x7fffffffac50)
at /root/code/swoole-src/swoole_server.cc:2946
#7 0x00000000007bb068 in ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER () at /root/php-7.3.3/Zend/zend_vm_execute.h:980
#8 execute_ex (ex=0x7ffff2d2a308) at /root/php-7.3.3/Zend/zend_vm_execute.h:55485
#9 0x00000000007bbf58 in zend_execute (op_array=op_array@entry=0x7ffff5e7b340, return_value=return_value@entry=0x7ffff5e1d030)
at /root/php-7.3.3/Zend/zend_vm_execute.h:60881
#10 0x0000000000737554 in zend_execute_scripts (type=type@entry=8, retval=0x7ffff5e1d030, retval@entry=0x0,
file_count=file_count@entry=3) at /root/php-7.3.3/Zend/zend.c:1568
#11 0x00000000006db4d0 in php_execute_script (primary_file=primary_file@entry=0x7fffffffd050) at /root/php-7.3.3/main/main.c:2630
#12 0x00000000007be2f5 in do_cli (argc=2, argv=0x1165cd0) at /root/php-7.3.3/sapi/cli/php_cli.c:997
#13 0x000000000043fc1f in main (argc=2, argv=0x1165cd0) at /root/php-7.3.3/sapi/cli/php_cli.c:1389
五. 參考
- UNIX網絡編程
- UNIX環境高級編程
- https://wiki.swoole.com/
- https://www.cnblogs.com/welhz...
- https://www.cnblogs.com/JohnA...