wxvirus wxvirus
首页
  • Go文章

    • Go语言学习
  • Rust

    • Rust学习
  • Java

    • 《Java》
  • Python文章

    • Python
  • PHP文章

    • PHP设计模式
  • 学习笔记

    • 《Git》
  • HTML
  • CSS
  • JS
  • 技术文档
  • GitHub技巧
  • 刷题
  • 博客搭建
  • 算法学习
  • 架构设计
  • 设计模式
  • 学习
  • 面试
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

无解的lifecycle

let today = new Beginning()
首页
  • Go文章

    • Go语言学习
  • Rust

    • Rust学习
  • Java

    • 《Java》
  • Python文章

    • Python
  • PHP文章

    • PHP设计模式
  • 学习笔记

    • 《Git》
  • HTML
  • CSS
  • JS
  • 技术文档
  • GitHub技巧
  • 刷题
  • 博客搭建
  • 算法学习
  • 架构设计
  • 设计模式
  • 学习
  • 面试
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • C&C++

  • PHP

    • PHP基础或常见方法

    • Laravel

    • ThinkPHP

    • PHP多进程编程

      • 程序与进程
      • PHP解释器的执行过程
      • 进程环境
      • 解释器文件
      • 进程标识与fork
      • 进程退出
        • 进程退出
          • 使用exit函数来退出
          • 非正常退出状态码
          • 中断信号退出【异常终止退出】
      • 进程exec
      • 进程调度
      • 多进程编写
      • SUID特权进程
      • 进程查看
      • 中断信号
      • 进程关系与守护进程
      • 进程组
      • 会话
      • 守护进程
      • 作业控制
      • 进程间通信
    • swoole

  • Python

  • Go

  • microservice

  • rust

  • Java

  • 学习笔记

  • 后端
  • PHP
  • PHP多进程编程
wxvirus
2022-04-07

进程退出

# 进程退出

一个程序启动后,变成了一个进程,进程在以下情况下会退出:

正常的情况:

  • 运行到最后一行语句
  • 运行时遇到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) {
        ;
    }
}
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) {
        ;
    }
}
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
1
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>
1
2

进程运行状态

此时再看子进程的状态为:Z+,即zombies的意思,就是变成了僵尸进程。父进程还是在运行中。

还有后面有一个<defunct>,即死了的,僵尸的意思

提示

这种进程,称为僵尸进程,虽然死亡了,但是还在内存中驻留,是占用内存的。

一个进程运行时,会在linux下的/proc/PID目录文件,生成进程的数据,这个目录是动态产生的。

我们使用命令进入对应的目录

cd /proc/21615
1

僵尸进程的缓存文件

这里因为进程已经结束了,所以暂时看不到啥内容。但是这里的cmdline是可以查看到是由什么命令来启动的。

这里还可以看到这些资源还保留在服务器,没有清理掉,所以这是有问题的,所以必须要对子进程结束之后进行回收。

注意

如果我们开发一个守护进程的web项目,如果说开启了大量的子进程,并且没有回收,那么服务器的内存和存储空间可能会被挤满,所以我们必须回收。


父进程调用pcntl_wait有几种情况:

  1. 如果说没有子进程,调用可能会返回错误
  2. 如果说子进程还没有结束,就调用了,就会阻塞父进程
  3. 我们给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());

1
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());

1
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());

1
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

exitstatus

现在退出状态码就是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());

1
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());

1
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);
1
2
3
4
5
[root@VM-16-4-centos process]# php demo6_3.php
8893-1[root@VM-16-4-centos process]#

1
2
3

可以看到它返回的是-1

我们可以使用pcntl_errno()来获取错误码,前提你得去把禁用函数打开。

<?php

echo posix_getpid();

echo pcntl_wait($status);

$error = pcntl_errno();

print_r(pcntl_strerror($error));
1
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]#
1
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);
}
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这里 31 以上的几乎很少使用。

kill -s 10 子进程pid
1

再来试一下

父进程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,运行完后,我就没事了
1
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);
}
1
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);
}
1
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);
}
1
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进行唤醒。

编辑 (opens new window)
上次更新: 2022/04/07, 22:29:36
进程标识与fork
进程exec

← 进程标识与fork 进程exec→

最近更新
01
vue3配合vite初始化项目的一些配置
07-26
02
网盘系统开发学习
07-24
03
linux多进程
06-19
更多文章>
Theme by Vdoing | Copyright © 2021-2024 wxvirus 苏ICP备2021007210号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式