进程退出
# 进程退出
一个程序启动后,变成了一个进程,进程在以下情况下会退出:
正常的情况:
- 运行到最后一行语句
- 运行时遇到
return
时 - 运行时遇到
exit()
函数的时候
不正常的情况:
- 程序异常的时候
主动退出:
- 进程接收到中断信号
一个进程要么是正常结束,要么是异常结束【信号有关】,不管是何种方式导致进程退出,它都有一个终止的状态码,进程结束时并不会真的退出,还会驻留在内存中,父进程可以使用
wait【pcntl_wait()】
函数来获取进程的终止状态码,同时该函数会释放终止进程的内存空间。否则会容易造成僵尸进程过多占用大量的内存空间。
僵尸进程:
就是指子进程已经结束,但是父进程还没使用
wait/wait_pid
函数来回收和释放内存空间。
$pid = pcntl_fork();
if (0 == $pid) {
fprintf(STDOUT, "我是子进程pid=%d,运行完后,我就没事了\n", posix_getpid());
} else {
fprintf(STDERR, "我是父进程,pid=%d\n", posix_getpid());
// 先让子进程先运行完
sleep(1);
// 父进程不结束
while (1) {
;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
这里可以看到,下面有一个带括号的子进程的,这个已经属于僵尸进程了
如果我们这里在子进程也加死循环
$pid = pcntl_fork();
if (0 == $pid) {
fprintf(STDOUT, "我是子进程pid=%d,运行完后,我就没事了\n", posix_getpid());
while (1) {
;
}
} else {
fprintf(STDERR, "我是父进程,pid=%d\n", posix_getpid());
// 先让子进程先运行完
sleep(1);
// 父进程不结束
while (1) {
;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
使用ps -aux
查看进程状态
root 20410 95.8 0.4 254784 15548 pts/2 R+ 15:59 2:42 php demo6.php
root 20411 96.3 0.1 254784 5924 pts/2 R+ 15:59 2:43 php demo6.php
2
这里的状态都是R+
,代表都是在运行的。即running
的意思。
我们再次把子进程的死循环去除进行运行
root 21614 97.6 0.4 254784 15548 pts/2 R+ 16:04 0:43 php demo6.php
root 21615 0.0 0.0 0 0 pts/2 Z+ 16:04 0:00 [php] <defunct>
2
此时再看子进程的状态为:Z+
,即zombies
的意思,就是变成了僵尸进程。父进程还是在运行中。
还有后面有一个<defunct>
,即死了的,僵尸的意思
提示
这种进程,称为僵尸进程,虽然死亡了,但是还在内存中驻留,是占用内存的。
一个进程运行时,会在linux
下的/proc/PID
目录文件,生成进程的数据,这个目录是动态产生的。
我们使用命令进入对应的目录
cd /proc/21615
这里因为进程已经结束了,所以暂时看不到啥内容。但是这里的cmdline
是可以查看到是由什么命令来启动的。
这里还可以看到这些资源还保留在服务器,没有清理掉,所以这是有问题的,所以必须要对子进程结束之后进行回收。
注意
如果我们开发一个守护进程的web
项目,如果说开启了大量的子进程,并且没有回收,那么服务器的内存和存储空间可能会被挤满,所以我们必须回收。
父进程调用pcntl_wait
有几种情况:
- 如果说没有子进程,调用可能会返回错误
- 如果说子进程还没有结束,就调用了,就会阻塞父进程
- 我们给
pcntl_wait
传递第三个参数option
选项,可以让父进程不阻塞
<?php
$pid = pcntl_fork();
if (0 == $pid) {
fprintf(STDOUT, "我是子进程pid=%d,运行完后,我就没事了\n", posix_getpid());
} else {
fprintf(STDERR, "我是父进程,pid=%d\n", posix_getpid());
$exitPid = pcntl_wait($status);
if ($exitPid > 0) {
// 正常结束
fprintf(STDOUT, "pid=%d子进程已经挂了,并且它的终止状态码是: %d,并且已经完全释放了它所占用的资源\n", $pid, $status);
} else {
fprintf(STDOUT, "wait error...\n");
}
while (1) {
fprintf(STDOUT, "我在打印\n");
sleep(3);
}
}
// getmypid 这个也可以获取进程号
fprintf(STDOUT, "pid=%d\n", getmypid());
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
此时,子进程会正常退出,父进程也会进行对应的回收,没有产生僵尸进程。
# 使用exit
函数来退出
<?php
$pid = pcntl_fork();
if (0 == $pid) {
fprintf(STDOUT, "我是子进程pid=%d,运行完后,我就没事了\n", posix_getpid());
exit(10);
} else {
fprintf(STDERR, "我是父进程,pid=%d\n", posix_getpid());
$exitPid = pcntl_wait($status);
if ($exitPid > 0) {
// 正常结束
fprintf(STDOUT, "pid=%d子进程已经挂了,并且它的终止状态码是: %d,并且已经完全释放了它所占用的资源\n", $pid, $status);
} else {
fprintf(STDOUT, "wait error...\n");
}
while (1) {
fprintf(STDOUT, "我在打印\n");
sleep(3);
}
}
// getmypid 这个也可以获取进程号
fprintf(STDOUT, "pid=%d\n", getmypid());
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
我们会发现它的终止状态码,并不是我们输入的exit(10);
,并不是 10,我们必须使用pcntl_wexitstatus()
函数
<?php
$pid = pcntl_fork();
if (0 == $pid) {
fprintf(STDOUT, "我是子进程pid=%d,运行完后,我就没事了\n", posix_getpid());
exit(10);
} else {
fprintf(STDERR, "我是父进程,pid=%d\n", posix_getpid());
$exitPid = pcntl_wait($status);
if ($exitPid > 0) {
// 正常结束
fprintf(STDOUT, "pid=%d子进程已经挂了,并且它的终止状态码是: %d,并且已经完全释放了它所占用的资源\n", $pid, pcntl_wexitstatus($status));
} else {
fprintf(STDOUT, "wait error...\n");
}
while (1) {
fprintf(STDOUT, "我在打印\n");
sleep(3);
}
}
// getmypid 这个也可以获取进程号
fprintf(STDOUT, "pid=%d\n", getmypid());
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
现在退出状态码就是exit(10)
里的 10 了。
它的退出状态码,一般 0 是标识成功,-1 标识失败,最大值就是 255
pcntl_wexitstatus
函数不但获取退出进程的状态码和退出的进程标识 PID,还能释放子进程占用的系统资源,所以我们在开发多进程的时候,一定要用这个函数来回收退出的子进程。如果子进程加上死循环,这个函数没有第二参数,就会让父进程阻塞,没法做其他的事,所以有时候父进程不但要能回收子进程的内存资源,还要能做其他的事情。
父进程非阻塞方式
<?php
$pid = pcntl_fork();
if (0 == $pid) {
fprintf(STDOUT, "我是子进程pid=%d,运行完后,我就没事了\n", posix_getpid());
while (1) {;}
exit(10);
}
while (1) {
fprintf(STDERR, "我是父进程,pid=%d\n", posix_getpid());
// wait 函数以未阻塞方式运行,如果有子进程退出,则返回子进程的pid,如果没有子进程退出,则返回0
$exitPid = pcntl_wait($status, WNOHANG);
if ($exitPid > 0) {
// 正常结束
fprintf(STDOUT, "pid=%d子进程已经挂了,并且它的终止状态码是: %d,并且已经完全释放了它所占用的资源\n", $pid, pcntl_wexitstatus($status));
} else if (0 == $exitPid) {
fprintf(STDOUT, "我在打印1\n");
} else {
fprintf(STDOUT, "wait error...\n");
}
fprintf(STDOUT, "我在打印2\n");
}
// getmypid 这个也可以获取进程号
fprintf(STDOUT, "pid=%d\n", getmypid());
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
如果你有子进程退出了就执行wait
,否则就执行打印 1,接着再打印 2,父进程就可以去做别的事情,不会阻塞。
<?php
$pid = pcntl_fork();
if (0 == $pid) {
fprintf(STDOUT, "我是子进程pid=%d,运行完后,我就没事了\n", posix_getpid());
exit(10);
}
while (1) {
fprintf(STDERR, "我是父进程,pid=%d\n", posix_getpid());
// wait 函数以未阻塞方式运行,如果有子进程退出,则返回子进程的pid,如果没有子进程退出,则返回0
$exitPid = pcntl_wait($status, WNOHANG);
if ($exitPid > 0) {
// 正常结束
fprintf(STDOUT, "pid=%d子进程已经挂了,并且它的终止状态码是: %d,并且已经完全释放了它所占用的资源\n", $pid, pcntl_wexitstatus($status));
// 子进程挂了之后,退出循环
break;
} else if (0 == $exitPid) {
fprintf(STDOUT, "我在打印1\n");
} else {
fprintf(STDOUT, "wait error...\n");
}
fprintf(STDOUT, "我在打印2\n");
}
// getmypid 这个也可以获取进程号
fprintf(STDOUT, "pid=%d\n", getmypid());
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
通过加上break
,如果子进程正好退出了,就退出循环,父进程可以释放资源
# 非正常退出状态码
<?php
echo posix_getpid();
echo pcntl_wait($status);
2
3
4
5
[root@VM-16-4-centos process]# php demo6_3.php
8893-1[root@VM-16-4-centos process]#
2
3
可以看到它返回的是-1
我们可以使用pcntl_errno()
来获取错误码,前提你得去把禁用函数打开。
<?php
echo posix_getpid();
echo pcntl_wait($status);
$error = pcntl_errno();
print_r(pcntl_strerror($error));
2
3
4
5
6
7
8
9
[root@VM-16-4-centos process]# php demo6_3.php
9347-1No child processes[root@VM-16-4-centos process]#
2
它这样就会很明确的告诉你 ,没有子进程的错误信息。
# 中断信号退出【异常终止退出】
进程不管是何种方式退出,都会有一部分数据驻留在内存中,比如:终止状态,所以父进程必须使用
wait
函数来回收退出进程,也就是终止进程所占用的系统资源。我们都可以使用函数来判断进程的退出方式,我们可以获取终止状态码,我们可以获取到它的中断信号编号。
<?php
$pid = pcntl_fork();
if (0 == $pid) {
while (1) {
fprintf(STDOUT, "我是子进程pid=%d,运行完后,我就没事了\n", posix_getpid());
sleep(2);
}
// exit(10);
}
while (1) {
// WNOHANG 不阻塞
$exitPid = pcntl_wait($status, WNOHANG);
if ($exitPid > 0) {
if (pcntl_wifexited($status)) {
// 正常结束
fprintf(STDOUT, "pid=%d子进程已经挂了,并且它的终止状态码是: %d\n", $pid, pcntl_wexitstatus($status));
}
}
fprintf(STDOUT, "父进程pid=%d\n", posix_getpid());
sleep(3);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这边我只是截取的一部分,他们还是会不断的执行下去的,是交替的方式执行。
我们可以在终端使用kill
命令,它是用来发送一个中断信号给【进程或者说是一个进程组】。
中断信号:它有自己的信号编号和对应的信号的名字, 信号编号是以非负数值来表示,信号是以SIG
开头的,可以使用kill -l
来查看信号的选项
[root@VM-16-4-centos process]# kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
2
3
4
5
6
7
8
9
10
11
12
13
14
这里 31 以上的几乎很少使用。
kill -s 10 子进程pid
再来试一下
父进程pid=14431
我是子进程pid=14432,运行完后,我就没事了
exit pid=0
父进程pid=14431
我是子进程pid=14432,运行完后,我就没事了
我是子进程pid=14432,运行完后,我就没事了
exit pid=0
父进程pid=14431
我是子进程pid=14432,运行完后,我就没事了
exit pid=0
父进程pid=14431
我是子进程pid=14432,运行完后,我就没事了
我是子进程pid=14432,运行完后,我就没事了
exit pid=0
父进程pid=14431
我是子进程pid=14432,运行完后,我就没事了
exit pid=0
父进程pid=14431
我是子进程pid=14432,运行完后,我就没事了
我是子进程pid=14432,运行完后,我就没事了
exit pid=0
父进程pid=14431
我是子进程pid=14432,运行完后,我就没事了
exit pid=0
父进程pid=14431
我是子进程pid=14432,运行完后,我就没事了
我是子进程pid=14432,运行完后,我就没事了
exit pid=0
父进程pid=14431
我是子进程pid=14432,运行完后,我就没事了
exit pid=0
父进程pid=14431
我是子进程pid=14432,运行完后,我就没事了
我是子进程pid=14432,运行完后,我就没事了
exit pid=0
父进程pid=14431
我是子进程pid=14432,运行完后,我就没事了
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
现在由于没有子进程了,所以这个返回-1
了。
<?php
$pid = pcntl_fork();
if (0 == $pid) {
while (1) {
fprintf(STDOUT, "我是子进程pid=%d,运行完后,我就没事了\n", posix_getpid());
sleep(2);
}
// exit(10);
}
while (1) {
// WNOHANG 不阻塞
$exitPid = pcntl_wait($status, WNOHANG);
fprintf(STDOUT, "exit pid=%d\n", $exitPid);
if ($exitPid > 0) {
if (pcntl_wifexited($status)) {
// 正常结束
fprintf(STDOUT, "pid=%d子进程已经挂了,并且它的终止状态码是: %d\n", $pid, pcntl_wexitstatus($status));
} else if (pcntl_wifsignaled($status)) {
fprintf(STDOUT, "中断信号退出: pid=%d: %d\n", $pid, pcntl_wtermsig($status));
}
}
fprintf(STDOUT, "父进程pid=%d\n", posix_getpid());
sleep(3);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
提示
现在由于没有子进程了,返回了-1
,我们使用另一个函数来判断pcntl_wifsignaled
,而且这个是中断退出,那么这个退出的信号编号,必须使用pcntl_wtermsig
函数,而pcntl_wexitstatus
函数只适用于正常情况退出。
此时我们可以看到它的中断结果,且中断信号为 10。
<?php
$pid = pcntl_fork();
if (0 == $pid) {
while (1) {
fprintf(STDOUT, "我是子进程pid=%d,运行完后,我就没事了\n", posix_getpid());
sleep(2);
}
// exit(10);
}
while (1) {
// WNOHANG 不阻塞
$exitPid = pcntl_wait($status, WNOHANG);
fprintf(STDOUT, "exit pid=%d\n", $exitPid);
if ($exitPid > 0) {
if (pcntl_wifexited($status)) {
// 正常结束
fprintf(STDOUT, "pid=%d子进程已经挂了,并且它的终止状态码是: %d\n", $pid, pcntl_wexitstatus($status));
} else if (pcntl_wifsignaled($status)) {
fprintf(STDOUT, "中断信号1退出: pid=%d: 中断信号编号: %d\n", $pid, pcntl_wtermsig($status));
} else if (pcntl_wifstopped($status)) {
// 一般是发送 SIGNSTOP 或 SIGNTSTP 信号才管用
fprintf(STDOUT, "中断信号2退出: pid=%d: 中断信号编号: %d\n", $pid, pcntl_wtermsig($status));
}
}
fprintf(STDOUT, "父进程pid=%d\n", posix_getpid());
sleep(3);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
我们可以看到右侧的状态为T+
,为停止状态,但是我们为啥没有捕获到。
我们可以查看php.net
文档:
检查导致返回的子进程当前是否停止;这只有在使用选项完成 对pcntl_waitpid() (opens new window)的调用时才有可能
WUNTRACED
。
<?php
$pid = pcntl_fork();
if (0 == $pid) {
while (1) {
fprintf(STDOUT, "我是子进程pid=%d,运行完后,我就没事了\n", posix_getpid());
sleep(2);
}
// exit(10);
}
while (1) {
// WNOHANG 不阻塞
$exitPid = pcntl_wait($status, WUNTRACED);
fprintf(STDOUT, "exit pid=%d\n", $exitPid);
if ($exitPid > 0) {
if (pcntl_wifexited($status)) {
// 正常结束
fprintf(STDOUT, "pid=%d子进程已经挂了,并且它的终止状态码是: %d\n", $pid, pcntl_wexitstatus($status));
} else if (pcntl_wifsignaled($status)) {
fprintf(STDOUT, "中断信号1退出: pid=%d: 中断信号编号: %d\n", $pid, pcntl_wtermsig($status));
} else if (pcntl_wifstopped($status)) {
// 一般是发送 SIGNSTOP 或 SIGNTSTP 信号才管用
fprintf(STDOUT, "中断信号2退出: pid=%d: 中断信号编号: %d\n", $pid, pcntl_wstopsig($status));
}
}
fprintf(STDOUT, "父进程pid=%d\n", posix_getpid());
sleep(3);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
此时,父进程会阻塞。
此时就已经捕获到了,上面那些错误,是因为这些禁用函数没有打开。这边我上面的第二个终端的消息的信号,使用的函数错了,导致这里显示的是 127,其实应该是 19,上面的代码以及改过来了。
注意
这个功能只是让你停止,不是让你退出。还可以使用kill -s SIGCONT 进程pid
进行唤醒。