【Swoole源碼研究】淺析swoole中server的實現

做者:施洪寶php

一. 基礎知識

1.1 swoole

swoole是面向生產環境的php異步網絡通訊引擎, php開發人員能夠利用swoole開發出高性能的server服務。swoole的server部分, 內容不少, 也涉及不少的知識點, 本文僅對其server進行簡單的概述, 具體的實現細節在後續的文章中再進行詳細介紹。html

1.2 網絡編程

  1. 網絡通訊是指在一臺(或者多臺)機器上啓動一個(或者多個)進程, 監聽一個(或者多個)端口, 按照某種協議(能夠是標準協議http, dns; 也能夠是自行定義的協議)與客戶端交換信息。
  2. 目前的網絡編程可能是在tcp, udp或者更上層的協議之上進行編程。swoole的server部分是基於tcp以及udp協議的。
  3. 利用udp進行編程較爲簡單, 本文主要介紹tcp協議之上的網絡編程
  4. TCP網絡編程主要涉及4種事件,
  • 鏈接創建: 主要是指客戶端發起鏈接(connect)以及服務端接受鏈接(accept)
  • 消息到達: 服務端接受到客戶端發送的數據, 該事件是TCP網絡編程最重要的事件, 服務端對於該類事件進行處理時, 能夠採用阻塞式或者非阻塞式, 除此以外, 服務端還須要考慮分包, 應用層緩衝區等問題
  • 消息發送成功: 發送成功是指應用層將數據成功發送到內核的套接字發送緩衝區中, 並非指客戶端成功接受數據。對於低流量的服務而言, 數據一般一次性便可發送完, 並不須要關心此類事件。若是一次性不能將所有數據發送到內核緩衝區, 則須要關心消息是否成功發送(阻塞式編程在系統調用(write, writev, send等)返回後便是發送成功, 非阻塞式編程則須要考慮實際寫入的數據是否與預期一致)
  • 鏈接斷開: 須要考慮客戶端斷開鏈接(read返回0)以及服務端斷開鏈接(close, shutdown)

1.3 進程間通訊

  1. 進程之間的通訊有無名管道(pipe), 有名管道(fifo), 信號, 信號量, 套接字, 共享內存等方式
  2. swoole中採用unix域套接字用於多進程之間的通訊(指swoole內部進程之間)

1.4 socketpair

  1. socketpair用於建立一個套接字對, 相似於pipe, 不一樣的是pipe是單向通訊, 雙向通訊須要建立兩次, socketpair調用一次便可實現雙向通訊, 除此以外, 因爲使用的是套接字, 還能夠定義數據交換的方式
  2. 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)

  1. 守護進程是一種特殊的後臺進程, 它脫離於終端, 用於週期性的執行某種任務
  2. 進程組
  • 每一個進程都屬於一個進程組
  • 每一個進程組都有一個進程組號, 也就是該組組長的進程號(PID)
  • 一個進程只能爲本身或者其子進程設置進程組號
  1. 會話
  • 一個會話能夠包含多個進程組, 這些進程組中最多隻能有一個前臺進程組(也能夠沒有)
  • setsid能夠建立一個新的會話, 該進程不能是進程組的組長。setsid調用完成後, 該進程成爲這個會話的首進程(領頭進程), 同時變成一個新的進程組的組長, 若是該進程以前有控制終端, 則該進程與終端的聯繫被斷開
  • 用戶經過終端登陸或者網絡登陸, 會建立一個新的會話
  • 一個會話最多隻能有一個控制終端
  1. 建立守護進程的方式
  • 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模式

  1. 說明
  • base模式採用多進程模型, 這種模型與nginx一致, 每一個進程只有一個線程, 主進程負責管理工做進程, 工做進程負責監聽端口, 接受鏈接, 處理請求以及關閉鏈接
  • 多個進程同時監聽端口, 會有驚羣問題, 目前swoole並無解決
  • linux 內核3.9及其後續版本提供了新的套接字參數SO_REUSEPORT, 該參數容許多個進程綁定到同一個端口, 內核在接受到新的鏈接請求時, 會喚醒其中一個進行處理, 內核層面也會作負載均衡, 能夠解決上述的驚羣問題
  • base模式下, reactor_number參數並無做用, 由於每一個進程只有一個線程
  • 若是worker進程數設置爲1, 則不會fork出worker進程, 主進程直接處理請求
  1. 啓動過程
  • php代碼執行到$serv->start()時, 主進程進入int swServer_start(swServer *serv)函數, 該函數負責啓動server
  • 在函數swServer_start中會調用swReactorProcess_start, 這個函數會fork出多個worker進程
  • 主進程和worker進程各自進入本身的事件循環, 處理各種事件

2.2 process模式

  1. 說明
  • 這種模式爲多進程多線程, 有主進程, manager進程, worker進程, task_worker進程
  • 主進程下有多個線程, 主線程負責接受鏈接, 以後交給react線程處理請求。 react線程負責接收數據包, 並將數據轉發給worker進程進行處理, 以後處理worker進程返回的數據
  • manager進程, 該進程爲單線程, 主要負責管理worker進程, 相似於nginx中的主進程, 當worker進程異常退出時, manager進程負責從新fork出一個worker進程
  • worker進程, 該進程爲單線程, 負責具體處理請求
  • task_worker進程, 用於處理比較耗時的任務, 默認不開啓
  • worker進程與主進程中的react線程使用域套接字進行通訊, worker進程之間不進行通訊
  1. 啓動過程
  • 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);
  1. 結構圖

swoole process模式結構以下圖所示,
swoole_serverreact

  • 上圖並無考慮task_worker進程, 在默認狀況下, task_worker進程數爲0

三. 請求處理流程(process模式)

3.1 reactor線程與worker進程之間的通訊

  1. swoole master進程與worker進程之間的通訊以下圖所示,

image

  • swoole使用SOCK_DGRAM, 而不是SOCK_STREAM, 這裏是由於每一個reactor線程負責處理多個請求, reactor接收到請求後會將信息轉發給worker進程, 由worker進程負責處理,若是使用SOCK_STREAM, worker進程沒法對tcp進行分包, 進而處理請求
  • swFactoryProcess_start函數中會根據worker進程數建立對應個數的套接字對, 用於reactor線程與worker進程通訊(swPipeUnsock_create函數)
  1. 假設reactor線程有2個, worker進程有3個, 則reactor與worker之間的通訊以下圖所示,

image

  • 每一個reactor線程負責監聽幾個worker進程, 每一個worker進程只有一個reactor線程監聽(reactor_num<=worker_num)。swoole默認使用worker_process_id % reactor_num對worker進程進行分配, 交給對應的reactor線程進行監聽
  • reactor線程收到某個worker進程的數據後會進行處理, 值得注意的是, 這個reactor線程可能並非發送請求的那個reactor線程。
  1. 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 請求處理

  1. 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, 主線程則不會被阻塞, 執行成功後直接返回
  1. reactor線程中fd的寫事件被觸發, reactor線程負責處理, 發現是首次加入, 沒有數據可寫, 則開啓讀事件監聽
  2. reactor線程讀取到用戶的請求數據, 一個請求的數據接收完後, 將數據轉發給worker進程, 默認是經過fd % worker_num進行分配
  • reactor發送給worker進程的數據包, 會包含一個頭部, 頭部中記錄了reactor的信息
  • 若是發送的數據過大, 則須要將數據進行分片, 限於篇幅, reactor的分片, 後續再進行詳細講述
  • 可能存在多個reactor線程同時向同一個worker進程發送數據的狀況, 故而swoole採用SOCK_DGRAM模式與worker進程進行通訊, 經過每一個數據包的包頭, worker進程能夠區分出是由哪一個reactor線程發送的數據
  1. worker進程收到reactor發送的數據包後, 進行處理, 處理完成後, 將數據發送給主進程
  • worker進程發送給主進程的數據包, 也會包含一個頭部, 當reactor線程收到數據包後, 可以知道對應的reactor線程, 請求的fd等信息
  1. 主進程收到worker進程發送的數據包, 這個會觸發某個reactor線程進行處理
  • 這個reactor線程並不必定是以前發送請求給worker進程的那個reactor線程
  • 主進程的每一個reactor線程都負責監聽worker進程發送的數據包, 每一個worker發送的數據包只會由一個reactor線程進行監聽, 故而只會觸發一個reactor線程
  1. 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

五. 參考

相關文章
相關標籤/搜索