PHP 中执行命令的几种方式比较

system/exec 函数:

这两个函数会利用/bin/sh 指定的shell来执行指定的命令,所以,指定的命令实际上不仅可以包含可以执行的命令和参数,还可以是对应shell的内置命令以及shell支持的控制结构。

缺点: 无法区分标准输出与标准错误

注意: 这两个函数执行的外部命令默认继承了php进程的标准输入、标准输出和标准错误,你可以通过返回值得到进程的标准输出,但是标准错误却悄悄地流进了php进程定义的标准错误输出里面去了;更加隐晦的是,对于php-fpm进程来讲,该外部进程也默认listen(继承来的)了php-fpm监听的端口,但是肯定不会去处理php-fpm请求,一般情况下不会有问题,特殊情况下回让你莫名其妙

pcntl_exec 函数

该函数会取代当前执行的进程空间,而且是不会再回到当前进程空间,可能你很少会用到该函数,但是,当你确实需要该函数的时候,却又是没有其他函数可以替代的,如:

echo 2 是不可能被执行到的

注意: pcntl_exec不同于其他函数,并不会使用shell来执行指定的命令,而且指定的命令需要是要执行的可执行文件的全路径

proc_open 函数

这个是我最喜欢的函数:

  1. 可以通过proc_close得到外部进程的返回值
  2. 可以定义外部进程的标准输入、标准输出、标准错误,并且可以区分外部进程的标准输出和标准错误
  3. 一定程度上还可以实现并行执行多个外部命令

 

proc_open 示例:

 

Linux之进程会计

什么是进程会计?

进程会计记录了什么时间启动了什么进程,进程的属主是什么,进程的结束时间等信息。

linux默认并没有开启进程会计,那么如何开启进程会计呢?

centos中提供了一个叫做psacct的软件包,其中包含accton 、lastcomm、ac、sa、dump-acct、dump-utmp几个命令,可以帮助我们开启、关闭、查看进程会计信息,简要介绍如下:

accton   on|off|file

打开、关闭进程会计,也可以直接accton file来指定进程会计信息的记录位置,同时开启进程会计;注意:

  1. 进程会计信息的默认存储位置: /var/account/pacct
  2. 进程会计信息存储文件需要预先手动创建(touch一个就行)

实现原理:

linux提供了一个acct系统调用,用来通知内核信息进程会计:

accton on 等同于: acct(“/var/account/pacct”)

accton off等同于: acct(NULL)
(注意:有的版本可能不能accton off,而是不带任何参数就意味着off)

 

lastcomm: 查看进程会计信息

 

sa:统计进程会计信息

dump-utmp

 

php-fpm 与 so_reuseport

问题:

如何让php-fpm能优雅地重启?

办法:

  1. 修改php-fpm listen的方式,开启so_reuseport
  2. 重启php-fpm时,先启动新进程,再停止旧进程

问题:

  1. 新进程和旧进程有不同的tcp backlog队列
  2. 系统会将新建立的连接平均分配到每个backlog队列中,新旧进程只能消费自己的backlog队列
  3. 新进程的所有子进程共享一个新的backlog队列,就进程的所有子进程共享一个旧的backlog队列
  4. 旧进程如何才能将backlog中的连接消费完?
  5. 旧进程如何阻止系统不要再分配连接到自己的backlog队列?
  6. 旧进程如何判断自己的backlog队列已空?
  7. 如果php-fpm进程通过exec、system、popen、proc_open等方式执行了外部的程序,则外部程序会继承所有的文件描述符(如果不做有意或无意的特殊处理的话,也会包括listen 9000 的文件描述符);问题在于,这个外部的程序虽然listen了9000端口,但是它绝对不会帮你处理9000端口的请求的,然后,系统分配过来的请求就会被阻塞

kill -SIGQUIT 可以解除子进程对9000端口的listen,却不能解除master进程对9000端口的listen

子进程的listen是继承父进程的

子进程接到SIGQUIT 信号后,close掉listen的文件描述符,然后创建了一个新的socket,如下:

(为什么要创建一个新的socket?)

同一个文件描述符被fork到多个子进程后,每个进程的关闭只会在本进程内关闭(让该资源和本进程脱离关系),然后减少该资源的引用计数,直到最后一个引用被解除,该资源才真正销毁

 

关于上述的问题7:

  1. 无意中对listen 9000的覆盖
    1. 当使用popen(“sleep” , “w”)的方式,就是以“写”的方式打开外部程序,则外部程序继承过来的标准输入0(原本listen 9000的文件描述符)会被覆盖
    2. 这时候就不会出现问题。 不过,你很少会如此打开一个外部程序的,常见的方式是以“读”的方式打开外部程序的,这时候,外部程序要覆盖掉的是文件描述符1,而0不会受影响
    3. 能否刻意覆盖掉文件描述符0呢? 比如: popen(“sleep”, “rw”) 读写方式打开?答案是:不可以; popen只允许单向打开(原因未深究,参考文档: http://php.net/manual/en/function.popen.php )
    4. 那么通过proc_open 实现呢?答案是: 可以; 如下:

      则sleep进程的文件描述符如下:

      这样就对了; 而且,proc_close() 可以获取外部进程的退出码

    5. 如果觉得proc_open 用起来比较麻烦,那么,或许可以给启动的外部程序来一个自己的shell,该shell负责关闭文件描述符0, 如: a.sh

      注意: 这个shell会存在一些问题,如下:

      改进如下

到现在为止,我们有能力不让外部的子进程来影响fpm请求的处理了;但是,还有一个listen 9000 但是不能处理请求的进程,那就是: php-fpm master进程

我们的处理方法可以如下:

  1. kill -SIGQUIT pid-of-php-fpm-master  && sleep 1 && kill -9 pid-of-php-fpm-master   (里面有 -9 ,好不和谐)

最后:

我们还没有解决如何让旧进程消费完旧进程的backlog中剩余的连接的问题,稍后研究。

分析:

socket中有一个shutdown的方法,可以关闭socket的读或(和)写,会触发关闭已建立的连接的方法,(对于listen的socket没有对端,想必不能用shutdown方法),测试发现:

  1. shutdown可以停止socket的listen,而不关闭文件描述符,确实可以实现关闭进程的listen,但是同时也会丢弃backlog中的连接,不管shutdown的是读还是写
  2. 测试脚本:

    (如果能通过单向关闭的方式通知内核不要再往该backlog分配连接不是很好吗?)

一种其它的实现方式:

在so_reuseport出现之前(包括现在)有一些程序是可以实现优雅重启的,原理就是,新旧进程时间继承lisen的文件描述符; 其实,继承文件描述符需要不小的工作量的。

如果能有一种标准的接口来处理继承文件描述符的事情,那么完全可以开发一个和应用无关的小程序,用来创建socket端口,然后遗传给真正要干活的应用程序,只需要遗传给应用程序的master进程就算OK了,如果需要重启程序,则只需要fork新的应用程序的master进程,然后发信号让旧的空闲的应用进程退出就可以了,这样就不会丢弃已创建的连接。

 

 

参考:

https://my.oschina.net/miffa/blog/390931