标题:【Swoole系列4.3】协程操作系统API

文章目录
    分类:PHP 标签:Swoole

    协程操作系统API

    学习完核心的协程相关操作 API 之后,我们再来看看协程可以操作的系统相关的 API 函数。其实也都是一些非常简单的功能,系统相关的调用无外乎就是操作文件、进程之类的功能,不过在协程中,它们的应用可能会略有不同。我们一个一个的来看一下。

    休息和调用方法

    首先就是我们非常熟悉的 sleep() 。

    \Swoole\Coroutine\run(function(){
       go(function(){
           co::sleep(2);
           echo "cid:" . Co::getCid() , PHP_EOL;
       });
    
       go(function(){
           \Swoole\Coroutine\System::sleep(2);
           echo "cid:" . Co::getCid() , PHP_EOL;
       });
    
       go(function(){
           echo "cid:" . Co::getCid() , PHP_EOL;
           $ret = \Swoole\Coroutine\System::exec("ls -l ./");
           var_dump($ret);
       });
    });

    我们可以使用 co::sleep() ,这也是之前测试中最常用的一咱写法。但其实更标准的写法是 \Swoole\Coroutine\System::sleep() 或者 Co\System::sleep() 。在新的版本中,为了规范命名空间,今天所讲的内容尽量去使用 System 相关的命名空间类来使用,但是,为了向下兼容,之前的写法也是可以的。


    关于 sleep() 之前我们也讲过了,它在内部其实就是实现了 yield() 和 resume() 的调度。在这里我们也不多说了,还不太了解的小伙伴可以仔细看一下上一篇文章中我们学习过的相关知识。


    另一个方法 exec() 则是执行一个外部程序。这个想必也不用过多地解释了。上面的测试代码输出的结果应该是下面这样的。

    //cid:4
    //array(3) {
    //    ["code"]=>
    //  int(0)
    //  ["signal"]=>
    //  int(0)
    //  ["output"]=>
    //  string(222) "total 16
    //-rw-r--r--. 1 root root 3509 Dec 20 21:50 4.1Swoole协程服务.php
    //-rw-r--r--. 1 root root 6704 Dec 27 22:57 4.2协程应用与容器.php
    //-rw-r--r--. 1 root root  416 Dec 28 20:05 4.3协程操作系统API.md.php
    //"
    //}
    //cid:2
    //cid:3

    进程回收等待

    进程回收等待还记得是啥嘛?在进程篇章中,我们几乎所有的测试代码都会用到,就是那个 Process::wait() 。它用于等待子进程完成并回收,避免产生僵尸进程。


    在协程中,也有类似的方法,可以在协程环境下进行进程的等待回收,效果也是一样的,为了避免出现僵尸进程浪费系统资源。

    $process = new \Swoole\Process(function(){
       echo "Process";
    });
    $process->start();
    echo "进程 pid: ". $process->pid, PHP_EOL;
    
    \Swoole\Coroutine\run(function(){
       $status = \Swoole\Coroutine\System::wait();
       echo "wait 进程 pid: ".$status['pid'], PHP_EOL;
       var_dump($status);
    });
    //进程 pid: 1489
    //Processwait 进程 pid: 1489
    //array(3) {
    //    ["pid"]=>
    //  int(1489)
    //  ["code"]=>
    //  int(0)
    //  ["signal"]=>
    //  int(0)
    //}
    
    $process1 = new \Swoole\Process(function(){
    });
    $process1->start();
    echo "进程1 pid: ". $process1->pid, PHP_EOL;
    
    $process2 = new \Swoole\Process(function(){
       sleep(5);
    });
    $process2->start();
    echo "进程2 pid: ". $process2->pid, PHP_EOL;
    
    \Swoole\Coroutine\run(function() use ($process1){
       $status = \Swoole\Coroutine\System::waitPid($process1->pid);
       echo "waitPid 进程 pid: ".$status['pid'], PHP_EOL;
       var_dump($status);
    
       $status = \Swoole\Coroutine\System::wait();
       echo "wait 进程 pid: ".$status['pid'], PHP_EOL;
       var_dump($status);
    
    });
    //进程1 pid: 1491
    //进程2 pid: 1492
    //waitPid 进程 pid: 1491
    //array(3) {
    //    ["pid"]=>
    //  int(1491)
    //  ["code"]=>
    //  int(0)
    //  ["signal"]=>
    //  int(0)
    //}
    //wait 进程 pid: 1492
    //array(3) {
    //    ["pid"]=>
    //  int(1492)
    //  ["code"]=>
    //  int(0)
    //  ["signal"]=>
    //  int(0)
    //}

    除了普通的 wait() 之外,还有一个 waitPid() 方法,可以指定只回收指定 pid 的进程。这个功能在进程模块相关的方法中好像是没有的。


    另外,我们在协程中也可以监听信号,也就是和 Process::signal() 一样的功能。

    $process = new \Swoole\Process(function () {
       \Swoole\Coroutine\run(function () {
           $bool = \Swoole\Coroutine\System::waitSignal(SIGUSR1);
           var_dump($bool);
       });
    });
    $process->start();
    sleep(1);
    $process::kill($process->pid, SIGUSR1);
    
    //[root@localhost source]# php 4.3协程操作系统API.md.php
    //[root@localhost source]# bool(true)

    域名操作

    域名操作主要就是返回对应的域名 ip 信息。

    \Swoole\Coroutine\run(function(){
       $ip = Swoole\Coroutine\System::gethostbyname("www.baidu.com", AF_INET, 0.5);
       echo $ip, PHP_EOL;
    
       $ip = Swoole\Coroutine\System::dnsLookup("www.baidu.com");
       echo $ip, PHP_EOL;
    
       $ips = Swoole\Coroutine\System::getaddrinfo("www.baidu.com");
       var_dump($ips);
    });
    //112.80.248.75
    //112.80.248.76
    //array(2) {
    //    [0]=>
    //  string(13) "112.80.248.75"
    //    [1]=>
    //  string(13) "112.80.248.76"
    //}

    gethostbyname() 基于 libc 的 gethostbyname() 实现,将指定域名解析为 IP 。dnsLookup() 是另一种域名对应 IP 的查询方式,它不是基于 gethostbyname() 的。getaddrinfo() 进行 DNS 解析,通过 DNS 信息查询对应域名的 IP 地址。


    关于这三个函数,可以查询一下 C 语言中相关的资料,也可以了解一下网络相关的知识。

    文件操作

    文件操作就是对文件进行读写操作,和我们普通的 PHP 开发没什么区别,只是说调用 System 的方法是实现了协程版本的。

    $file = __DIR__ . "/test.data";
    $fp = fopen($file, "a+");
    Swoole\Coroutine\run(function () use ($fp, $file)
    {
       $r = Swoole\Coroutine\System::fwrite($fp, "hello world\n" . PHP_EOL, 5);
       var_dump($r);
    
       var_dump(\Swoole\Coroutine\System::readFile($file));
    });
    //int(5)
    //string(5) "hello"
    
    Swoole\Coroutine\run(function () use ($fp, $file)
    {
       \Swoole\Coroutine\System::writeFile($file, "happy new year\n", FILE_APPEND);
    
       $r = Swoole\Coroutine\System::fread($fp);
       var_dump($r);
    
       while($r = Swoole\Coroutine\System::fgets($fp)){
           var_dump($r);
       }
    });
    //string(15) "happy new year
    //"
    //string(20) "hellohappy new year
    //"

    两段测试代码,但我们都用了不同的方式来读写。第一段代码中,使用的是 System::fwrite() 来写入文件,注意它可以指定写入大小,很明显,我们这一行代码只能写进去 5 个字符。然后,在这个协程容器中,通过 System::readFile() 来读取整个文件。


    第二段协程容器中,我们先使用 System::writeFile() 来向文件中追加内容,它就不是流式写入了,直接会把内容全部写进去,所以文件当前的内容是 "hellohappy new year\n" 。接着,我们使用 fread() 的方式读取文件,它也是可以指定读取长度的,不填的话就是全部读取。


    最后,我们还使用了 System::fgets() ,和普通的 File 操作函数中的 fgets() 一样,它也是按行读取数据的。


    其实这几个方法函数和普通模式下的文件操作相关的函数差不多,而且最主要的是,后面我们学习了一键协程化了之后,其实根本不用这些函数了,直接通过一键协程化就可以让普通的 PHP 函数以协程方式运行。感觉很嗨吧?我们在协程篇最后才会说这个东西,不急哦。

    进程交互通信

    由于在协程空间内 fork 进程会带着其他协程上下文,因此底层禁止了在 Coroutine 中使用 Process 模块。可以使用:

    • System::exec() 或 Runtime Hook+shell_exec 实现外面程序运行

    • Runtime Hook+proc_open 实现父子进程交互通信

    上面这段是官网的原文,意思嘛很明显,没事别让协程和进程通信。如果要通信,使用 Table 之类的方案呗。System::exec() 的例子上面已经演示过了,官网上的例子实现的就是第二个进程交互通信,我也不再敲了,直接粘过来。

    // main.php
    use Swoole\Runtime;
    use function Swoole\Coroutine\run;
    
    Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
    run(function () {
        $descriptorspec = array(
            0 => array("pipe", "r"),
            1 => array("pipe", "w"),
            2 => array("file", "/tmp/error-output.txt", "a")
        );
    
        $process = proc_open('php ' . __DIR__ . '/read_stdin.php', $descriptorspec, $pipes);
    
        $n = 10;
        while ($n--) {
            fwrite($pipes[0], "hello #$n \n");
            echo fread($pipes[1], 8192);
        }
    
        fclose($pipes[0]);
        proc_close($process);
    });
    
    // read_stdin.php
    while(true) {
        $line = fgets(STDIN);
        if ($line) {
            echo $line;
        } else {
            break;
        }
    }

    官网例子的意思就是通过Runtime Hook(一键协程化)的 proc_open() 函数打开另一个 PHP 脚本 read_stdin.php ,也就是一个新的进程,这个脚本持续挂起并获得 STDIN 输入流的内容,然后 main.php 这边就可以通过输入输出流进行数据写入将数据传给 read_stdin.php ,从而最终实现了两个进程的通信。

    总结

    今天的内容比较简单也比较好理解吧,没有什么特别难的地方,只是函数方法的使用而已。其实我们用得最多的就只是 System::sleep() ,注意,以后新项目就尽量这么写吧,万一哪天 co::sleep() 的写法就会被标记为过时了呢。


    下篇文章,我们将学习协程间的通信工具 Channel 模块的使用。


    测试代码:


    https://github.com/zhangyue0503/swoole/blob/main/4.Swoole%E5%8D%8F%E7%A8%8B/source/4.3%E5%8D%8F%E7%A8%8B%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9FAPI.php


    参考文档:


    https://wiki.swoole.com/#/coroutine/system


    https://wiki.swoole.com/#/coroutine/proc_open

    视频链接

    微信文章地址:https://mp.weixin.qq.com/s/HKbtpH4YfzPRTQoBZ2ue8Q

    B站视频地址:https://www.bilibili.com/video/BV1sV4y1V7wS

    微信视频地址:https://mp.weixin.qq.com/s/SZ4y0CQAjKC8Bc9zyYdxrQ

    搜索
    关注