php socket网络编程基础知识(四):多进程

php socket网络编程基础知识(四):多进程

说明

  • php在web编程时是不需要考虑多进程的,但整个php流程是涉及到多进程的,只不过nginxphp-fpm帮我们处理好了,我们配置他们参数时就需要设置进程个数相关参数
  • php在多进程涉及到的是PCNTL扩展和POSIX扩展,这两个扩展交叉涉及到进程和信号相关,他们只支持Unix平台,windows平台不支持,而且只支持cli模式下运行。
  • php也提供了其它函数来处理多进程的场景,例如exec相关的函数,所以并不是涉及到多进程都需要上方扩展,推荐symfony/process,对php此类相关函数进行的封装和兼容处理,同时支持linux和windows,非常好用的多进程库,一般的中小项目完全满足需求。
  • 我们只从上方扩展中找进程相关的函数来总结,信号相关的在下节中总结

概念

这里涉及到一些linux系统相关的的概念,可能需要先做了解

  • 僵尸进程和孤儿进程,简单来说就是子进程结束而父进程没有回收善后处理则产生僵尸进程,而父进程提前结束而子进程还存在则产生孤儿进程,可参考PHP多进程初探 — 孤儿和僵尸,
  • 用户(uid)和用户组(gid),这个大家应该都清楚,创建linux用户、指定nginx等执行用户和用户组、设置文件权限都会用到,还有有效用户(euid)和有效组(egid),感兴趣的可以去自行搜索。
  • 进程(pid,英文:process ID)、父进程(ppid)、进程组(pgid)、会话组(sid),例如我们从终端xshell登陆,就会产生一个会话组,然后我们执行命令,会在会话组下产生进程组,而进程组中就包含各种进程。可以参考从进程组、会话、终端的概念深入理解守护进程、linux内核之进程的基本概念(进程,进程组,会话关系)、PHP多进程初探 — 再次谈daemon进程

相关函数

  • PCNTL函数

    • pcntl_fork ( void ) : int
      • 产生子进程分支
      • 此函数后的代码,父子进程都会执行,执行顺序则是随机的。此函数会在父进程和子进程内产生不同的返回值,子进程返回0,父进程则返回子进程的pid,如果失败则返回-1,由于父子进程都会执行后续代码,所以我们一般用if来判定哪个是父进程哪个是子进程。
    • pcntl_wait ( int &$status [, int $options = 0 ] ) : int
      • 用于子进程结束时,回收子进程资源
    • pcntl_waitpid ( int $pid , int &$status [, int $options = 0 ] ) : int
      • pcntl_wait基本一样,但是多了指定pid的参数,当$pid=-1时,相当于pcntl_wait,其它的$pid参数自行查看文档
    • 上面两函数第二个参数$status是个引用,可以获取到子进程结束时的状态信息,通过调用其它函数来解析status具体含义
    • 上面两函数第三个参数则比较费解,除了0外还有两个选项WNOHANGWUNTRACED。上面两个函数默认是阻塞的,直到有子进程结束时,才会有响应动作,如果想不阻塞,可以将$options参数设置为WNOHANG,当没有子进程退出时不再阻塞等待,而是直接返回0,而如果设置为WUNTRACED参数则和设置为0一样还是阻塞,但是除了子进程结束会响应外SIGTTIN, SIGTTOU, SIGTSTP,SIGSTOP这些信号也会响应。
  • POSIX函数

    • posix_getpid ( void ) : int
      • 获取当前进程ID,常用于区分是否为父进程
    • posix_getppid ( void ) : int
      • 获取父进程ID
    • posix_getuid ( void ) : int
      • 获取当前进程的用户ID
    • posix_setuid ( int $uid ) : bool
      • 设置当前进程的用户ID
    • posix_getgid( void ) : int
      • 获取当前进程所属组ID
    • posix_setgid( int $gid ) : bool
      • 设置当前进程组ID
    • posix_getsid ( int $pid ) : int
      • 获取某进程的会话ID,需要注意的是参数需要传递,可以先通过posix_getpid获取pid参数,再获取sid
    • posix_setsid( void ) : int
      • 设置当前进程为会话的领头进程,需要注意参数,只能设置当前的进程
    • posix_getpwuid( int $uid ) : array
      • 根据用户ID获取此用户的信息,例如用户的名称、密码,组等信息
    • posix_getpwnam ( string $username ) : array
      • 根据用户名获取此用户的信息,例如用户的ID、密码、组等信息
    • posix_getgrgid ( int $gid ) : array
      • 根据组ID获取此组的信息,例如组名称、密码、成员等信息
    • posix_getgrnam( string $name ) : array
      • 根据组名称获取此组的信息,例如组ID、密码、成员等信息
    • posix_initgroups ( string $name , int $base_group_id ) : bool
      • 初始化组清单,将$name所属的附加组也加入到此进程的组权限中。
      • 这个函数不太好理解,因为和setgid有些类似。用户是有附加组的,同一个用户可能属于多个组,setuid只是设置用户ID,setgid是设置组ID,但用户ID可能同时还有其它组的权限,要想获取其它组的权限,则需要运行此命令。

相关代码

  • 创建子进程
    $pid = pcntl_fork();
    //父进程和子进程都会执行下面代码
    if ($pid == -1) {
        //错误处理:创建子进程失败时返回-1.
        die("could not fork");
    } else if ($pid) {
        //父进程会得到子进程号,所以这里是父进程执行的逻辑
        pcntl_wait($status); //等待子进程中断,防止子进程成为僵尸进程。
    } else {
        //子进程得到的$pid为0, 所以这里是子进程执行的逻辑。
    }
    
  • 设置用户ID和组
    $user = "www";
    $group = "www";
    // Get uid.
    $user_info = posix_getpwnam($user);
    if (!$user_info) {
        echo "Warning: User {$user} not exsits";
        exit;
    }
    $uid = $user_info["uid"];
    // Get gid.
    if ($group) {
        $group_info = posix_getgrnam($group);
        if (!$group_info) {
            echo "Warning: Group {$group} not exsits";
            exit;
        }
        $gid = $group_info["gid"];
    } else {
        $gid = $user_info["gid"];
    }
    
    // Set uid and gid.
    if ($uid != posix_getuid() || $gid != posix_getgid()) {
        if (!posix_setgid($gid) || !posix_initgroups($user_info["name"], $gid) || !posix_setuid($uid)) {
            echo "Warning: change gid or uid fail.";
            exit;
        }
    }
    

需要注意

  • 在fork前的变量,fork后父子都可以引用,fork后可以各自修改,但已经是独立的会互不影响,在fork后父子各自创建的变量也不会互相影响,如果想要传递信息,就需要用到进程间的通讯了,可以参考进程间通信的方式——信号、管道、消息队列、共享内存 、PHP多进程初探 — 进程间通信二三事。
  • 在循环中的fork,第一次fork后,会产生两个进程,而这两个进程是在循环中的,所以再次循环时,这两个进程都会创建各自的子进程,类似细胞分裂,所以循环的个数会远小于实际产生的子进程数。那如何避免呢,一种方法是让子进程一直while循环,这样他就无法继续执行下一次的fork(保险期间还是加上exit比较好),另一种就是子进程处理完后就exit退出,自然是不会执行下一次的fork了。
  • 上方POSIX函数的主要应用场景是设置子进程以非root的权限运行,例如我们在nginx中设置的参数user:www www,就是在指定子进程运行的用户名和用户组,否则子进程也用root来运行的话权限过大,存在安全隐患。
  • 回收子进程的几种方式:
    • 在主进程中通过waitwaitpid来阻塞或者是循环来处理子进程回收
    • 通过信号SIGCHLD来实现,每个子进程结束时都会发送SIGCHLD信号,父进程进行捕捉调用waitwaitpid来回收
    • linux下还可以通过直接忽略SIGCHLD来实现:signal(SIGCHLD, SIG_IGN),此时子进程结束时,直接由内核init进程来负责回收
      上面几种方式看似第一种不太好,因为主进程都用来处理子进程的回收了,就不能做其它的事情了,但实际上主进程本来就是来管理子进程的,并没有多少自己的逻辑,而且越少处理逻辑越安全,所以直接在循环中处理并没有什么问题。

关联

  • 上篇:php socket网络编程基础知识(三):stream函数
  • 下篇:php socket网络编程基础知识(五):信号(未完成)
hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » php socket网络编程基础知识(四):多进程