Swoole TaskWorker

内容

  • Swoole进程
  • Task进程原理、使用和问题
  • Timer线程定时器的使用
  • Q/A

Swoole进程

进程

4933701-79751d6c6f4294e5.png
进程

进程 = 内存 + 上下文环境

什么是进程呢?

进程是指正在执行的程序,进程是具有独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的独立单位,是应用程序的载体。

进程是一种抽象的概念,并没有统一的标准定义。进程一般是由程序、数据集合、进程控制块三部分构成。程序用来描述进程要完成的功能,是控制进程执行的指令集。数据集合是程序在执行时所需的数据和工作区。程序控制快包含进程的描述信息和控制信息,是进程存在的唯一标志。

例如:在终端中使用PHP运行一个脚本,此时就相当于创建了一个进程,这个进程会在系统中贮存并申请自己的内存空间、系统资源,用来运行相应的程序。

对于一个进程最核心的两部分是内存和上下文环境,内存是创建初始时从系统中分配的,程序创建的所有变量都会存储在这片内存空间中。运行在操作系统中的进程是需要依赖系统资源的,操作系统的一些状态以及进程自身的状态就构成了上下文环境。

进程在内存上的布局是什么样的呢?

所有进程都必须占用一定数量的内存,它或是用来存放从磁盘上载入的代码,或是存取用户输入的数据等。不过进程对这些内存的管理方式因内存用途不一而不尽相同,有些内存是事先静态分配和统一回收的,有些却是按需动态分配和回收的,对任何一个进程而言,都会涉及到进程在内存空间上的布局。

每个进程所分配的内存是由多个部分组成,这些部分通常称之为段。对于LInux系统上的进程其内存空间可粗略的从高内存到地内存排列划分为以下几大段:

4933701-2f8b1550e4c1a6fc.png
进程在内存中的排列分布
  1. 内核态内存空间
    内核态内存空间大小一般比较固定,可以在编译时调整,但32位系统和64位系统的值不一样。
  2. 用户态的堆栈
    用户态的堆栈大小不固定,可以使用ulimit -s命令进行调整,默认为8MB,依次从高地址向低地址增长。
  3. MMAP区域
    MMAP是一种内存映射文件的方法,MMAP会将一个文件或其它对象映射进内存,文件被映射到多个页上,如果文件大小不是所有页大小之和,最后一个页不被使用的空间将会清零。
  4. BRK区域
    临近数据段从低位向高位延展,其大小取决于mmap如何增长。一般而言即使是32位的进程以传统方式延伸也有差不多1GB的空间,准确来说是TASK_SIZE/3 - 代码段数据段
  5. 数据段
    数据段是进程中初始化和未初始化的全局数据综合,还有编译器生成的辅助数据结构等,大小取决于具体进程,其位置紧贴着代码段。
  6. 代码段
    代码段主要是进程的指令,包括用户代码和编译器生成的辅助代码,其大小取决于具体程序,但起始位置根据32位或64位而定。

什么是进程的上下文环境?

进程上下文是指一个进程在执行时CPU寄存器中的值、进程状态、堆栈内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,也就是保存当前进程的上下文,以便再次执行该进程时,能够恢复切换的状态继续执行。

进程相关的命令包括pstophtoppstreekill

父子进程

  • 子进程会复制父进程的内存空间和上下文环境
  • 修改子进程内存空间,不会修改父进程或其他子进程中的内存空间。

操作系统中可以运行多个进程,对于一个进程而言可以通过系统调用fork创建自己的子进程,子进程和父进程一样,同样拥有自己的内存空间和上下文环境,需要注意的是,在创建出来的子进程会复制父进程的内存空间和上下文环境。这里的复制指的是子进程的内存空间和父进程的内存空间是相互独立的,它们之间并不会相互影响。如果修改子进程中某个变量并不会影响到父进程。在创建子进程之前,如果父进程已经拥有了若干个变量,那么创建出来的子进程中会拥有相同的变量,只不过它们的值并不一样。

在使用fork系统调用创建子进程后,子进程重新申请物理内存空间,复制父进程地址空间中所有信息,子进程复制父进程的代码段、数据段、BBS段、堆、栈用户空间信息,在内核中操作系统为其重新申请一个PCB,并且使用父进程的PCB来进行初始化,除了PID等特殊信息外,几乎所有信息都是一样的。

父进程在使用fork系统调用创建子进程后,父进程与子进程互不关联,子进程以独立身份抢占CPU资源,具体谁先执行由调度算法决定,用户空间是没有办法干预的,子进程执行代码的位置是forkvfork系统调用返回的位置。

共享内存

4933701-f367bd61598c5551.png
共享内存
  • 共享内存不属于任何一个进程
  • 在共享内存中分配的内存空间可以被任何进程访问
  • 即使进程关闭,共享内存仍然可以继续保留。

共享内存是进程间通讯的一种方式,Swoole中进程之间的通讯可以通过管道的方式是实现。

共享内存并不属于任何进程,它可以调用系统提供的函数来创建共享内存,并指定索引通过索引任何一个进程都可以在这片共享内存中申请内存空间并存储对应的值。在共享内存中分配的空间可以被任何进程访问,只要拥有这片共享内存的索引。另外,即使进程关闭共享内存仍然可以继续保留。

共享内存是指允许两个不相关的进程访问同一个逻辑内存,是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存,进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存中写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

在Linux系统中,每个进程都有属于自己的进程控制块PCB和地址空间Addr Space,并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元MMU进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。

4933701-7bfc196948304571.png
共享内存通信原理

共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对其进行读取,所以需要使用其他机制对同步共享内存进行访问如信号量。

查看共享内存的分片

$ ipcs -m
------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态      
0x00000000 131072     jc         777        16384      1          目标       
0x00000000 163841     jc         777        3063808    1          目标       
0x00000000 393218     jc         600        524288     2          目标       
0x00000000 425987     jc         777        3063808    1          目标       
0x00000000 557060     jc         777        8077312    2          目标       
0x00000000 491525     jc         600        67108864   2          目标       
0x00000000 524294     jc         777        8077312    2          目标       
0x00000000 655367     jc         600        524288     2          目标       
0x00000000 688136     jc         600        524288     2          目标       
0x00000000 720905     jc         777        2457600    1          目标       
0x00000000 753674     jc         777        7798784    2          目标       
0x00000000 851979     jc         600        524288     2          目标       
0x00000000 884748     jc         600        524288     2          目标       
0x00000000 917517     jc         600        524288     2          目标    

Swoole进程结构

4933701-9826b0b0920eb661.png
Swoole的结构

Swoole进程执行流程

4933701-8ce17dd38a59871e.png
Swoole进程执行流程
  1. 当有客户端请求进入到Master主进程中时首先会被Master主线程接收到
  2. Master主线程将读写操作的监听注册到对应的Reactor线程中,并通知Worker进程处理onConnect,即接收到连接时进行回调。
  3. 客户端的数据会通知对应的Reactor线程发送给Worker进程进行处理
  4. 如果Worker进程投递任务,也就是将数据通过管道发送给Task进程,Task进程处理完毕后会发送给Worker进程。
  5. Worker进程会通知Reactor线程发送数据给客户端
  6. 当Worker进程出现异常时会关闭,Manager进程会重新创建一个新的Worker进程以保证Worker进程的数量是固定的。

TaskWorker进程

  • Task进程必须是同步阻塞的
  • Task进程支持定时器

Swoole的业务逻辑部分是同步阻塞运行的,如果遇到耗时较长的操作,例如访问数据库、广播消息等,就会影响服务器的响应速度,因此Swoole提供了Task功能,将这些耗时操作放到另外的进程中去处理,当前进程则继续执行后续的逻辑。

Task进程是Swoole中独立于Worker进程的一个工作进程,用于处理耗时较长的逻辑,这些逻辑在处理过程中,并不会影响到Task进程处理来自客户端的请求,因此可以大大提高系统的并发能力。

Task进程和Worker进程之间是通过UnixSock管道进行通信的,也可以为其配置消息队列进行通信,Task进程的消息传递只能是字符串。

Task进程底层使用Unix Sock管道进行通信,是全内存的没有IO消耗,单进程读写性能可达到100万次每秒,不同的进程使用不同的管道通信,可以最大化利用多核。

4933701-acff2076502d8988.png
Task进程

Task进程和Worker进程之间会通过Unix Sock管道通信,也可以配置通过消息队列进行通信。

Task进程可以用来做一些异步的慢速任务,典型的应用场景包括异步支付处理、异步订单处理、异步日志处理、异步发送邮件或短信、即时通讯中发送广播等。

例如:Worker进程处理数据请求,分配给Task进程执行。

服务器

$ vim server.php
<?php
/**服务器 */
class Server
{
    private $server;
    private $host;
    private $port;
    /**构造函数 */
    public function __construct($host, $port, $config)
    {
        $this->host = $host;
        $this->port = $port;
        //构建异步服务器Server对象
        $this->server = new swoole_server($host, $port);
        //设置运行时参数
        $this->server->set($config);
        //注册事件回调函数
        $this->server->on("Start", [$this, "onStart"]);
        $this->server->on("Connect", [$this, "onConnect"]);
        $this->server->on("Receive", [$this, "onReceive"]);
        $this->server->on("Close", [$this, "onClose"]);
        $this->server->on("Task", [$this, "onTask"]);
        $this->server->on("Finish", [$this, "onFinish"]);
        //启动服务器
        $this->server->start();
    }
    /**启动后Master主进程回调*/
    public function onStart()
    {
        echo "server start".PHP_EOL;
    }
    /**有新连接进入时在Worker进程中回调*/
    public function onConnect($server, $fd, $from_id)
    {
        echo "client {$fd}: connect".PHP_EOL;
    }
    /**TCP客户端关闭连接后在Worker进程中回调 */
    public function onClose($server, $fd, $from_id)
    {
        echo "client {$fd}: close".PHP_EOL;
    }
    /**Worker进程接收客户端发送过来的数据*/
    public function onReceive(swoole_server $server, $fd, $from_id, $data)
    {
        echo "client {$fd}: {$data}".PHP_EOL;

        //接收客户端传递的数据,传递给Task进程,在onTask中获取传递的数据。
        $send = [];
        $send["task"] = "taskname";
        $send["params"] = $data;//客户端传递的数据
        $send["fd"] = $fd;//客户端描述符
        $server->task(json_encode($send));
    }
    /**TaskWorker进程获得投递过来的数据*/
    public function onTask($server, $task_id, $from_id, $data)
    {
        echo "task {$task_id}: from worker {$from_id}".PHP_EOL;
        echo $data.PHP_EOL;

        //解析投递过来的数据
        $recv = json_decode($data, true);

        //向客户端发送数据
        $fd = $recv["fd"];//客户端描述符
        $message = "success";
        $server->send($fd, $message);

        //返回给worker进程告诉已完成
        return "finished";
    }
    /**Worker进程获取onTask返回的数据 */
    public function onFinish($server, $task_id, $data)
    {
        echo "task {$task_id}: finish".PHP_EOL;
        echo "worker: {$data}".PHP_EOL;
    }
}

$config = [];
$config["worker_num"] = 8;//设置启动的Worker进程数量
$config["daemonize"] = false;//是否守护进程化
$config["max_request"] = 10000;//最大允许的连接数
$config["dispatch_mode"] = 2;//数据分发策略 2固定模式
$config["task_worker_num"] = 8;//配置Task进程数量
$server = new Server("0.0.0.0", 9000, $config);

客户端

$ vim client.php
<?php
/**客户端 */
class Client
{
    private $client;
    /**构造函数 */
    public function __construct($host, $port)
    {
        //构建异步非阻塞客户端
        $this->client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
        //回调函数
        $this->client->on("Connect", [$this, "onConnect"]);
        $this->client->on("Receive", [$this, "onReceive"]);
        $this->client->on("Close", [$this, "onClose"]);
        $this->client->on("Error", [$this, "onError"]);
        //连接到远程服务器
        if(!$this->client->connect($host, $port, 0.5)){
            echo $this->client->errCode.":".$this->client->errMsg.PHP_EOL;
            return;
        }
    }
    /**客户端连接服务器成功后回调 */
    public function onConnect($client)
    {
        fwrite(STDOUT, "send: ");
        //将socket加入到底层reactor事件监听中
        swoole_event_add(STDIN, function(){
            fwrite(STDOUT, "send: ");
            $message = trim(fgets(STDIN));
            $this->send($message);
        });
    }
    /**发送数据到远程服务器,必须在建立连接后才能向服务器发送数据。 */
    public function send($message){
        $this->client->send($message);
    }
    /**获取客户端连接状态 */
    public function isConnected()
    {
        return $this->client->isConnected();
    }
    /**客户端接收来自服务器的数据时回调 */
    public function onReceive($client, $data)
    {
        echo PHP_EOL."receive: {$data}".PHP_EOL;
    }
    /**连接被关闭时回调 */
    public function onClose($client)
    {
        echo "close".PHP_EOL;
    }
    /**连接服务器失败时回调 */
    public function onError($client)
    {
        echo "error";
    }
}
$client = new Client("127.0.0.1", 9000);

运行服务器

$ php server.php
server start

运行客户端

$ php client.php
send: 

客户端发送数据

$ php client.php
send: hello

查看服务器打印消息

$ php server.php
server start
client 1: connect
client 1: hello
task 0: from worker 7
{"task":"taskname","params":"hello","fd":1}
task 0: finish
worker: finished

查看客户端打印消息

$ php client.php
send: hello
send: 
receive: success

使用注意

  • Task进程可以在后台运行而不会影响后续代码的执行
  • Task进程可以设置多个,当一个Task进程被占用时会使用另外一个。
  • Task进程完成任务后会被闲置,可给下一个客户端使用。
  • 设置Task进程数量时注意防止开销过多的性能
  • 注意防止进程的阻塞,阻塞的进程十分小号资源且长期占用,无法被下一个客户端所使用。
  • exitdie是危险的将会导致Worker进程的退出

未完待续...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值