乐趣区

Swoole源码研究浅析swoole中server的实现

作者:施洪宝

一. 基础知识

1.1 swoole

swoole 是面向生产环境的 php 异步网络通信引擎, php 开发人员可以利用 swoole 开发出高性能的 server 服务。swoole 的 server 部分, 内容很多, 也涉及很多的知识点, 本文仅对其 server 进行简单的概述, 具体的实现细节在后续的文章中再进行详细介绍。

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 模式结构如下图所示,

  • 上图并没有考虑 task_worker 进程, 在默认情况下, task_worker 进程数为 0

三. 请求处理流程(process 模式)

3.1 reactor 线程与 worker 进程之间的通信

  1. swoole master 进程与 worker 进程之间的通信如下图所示,

  • 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 之间的通信如下图所示,

  • 每个 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

五. 参考

  • UNIX 网络编程
  • UNIX 环境高级编程
  • https://wiki.swoole.com/
  • https://www.cnblogs.com/welhz…
  • https://www.cnblogs.com/JohnA…
退出移动版