『 Linux 』Process Control进程控制(万字)

文章目录

    • 🦖 前言
    • 🦖 fork()函数调用失败原因
    • 🦖 进程终止
      • 💥 进程退出码
      • 💥 进程正常退出
    • 🦖 进程等待
      • 💥 僵尸进程
      • 💥 如何解决僵尸进程的内存泄漏问题
      • 💥 wait( )/waitpid( )函数
        • 🌟 进程退出信息
      • 💥 非阻塞式等待
      • 💥 父进程如何获取子进程的退出信息
    • 🦖 进程替换
      • 💥 进程替换的原理


🦖 前言

请添加图片描述

进程控制是一种在操作系统上对进程进行管理和调度的一个过程;

这包括创建进程,终止进程,等待进程,暂停和恢复进程,进程间的通信和调度进程等待;

在之前关于Linux的内容中谈论了大量的关于进程的内容;

本文将重点对于基础进程控制进行一定的讲解;


🦖 fork()函数调用失败原因

请添加图片描述

在『 Linux 』使用fork函数创建进程与进程状态的查看-CSDN博客中提到了使用fork()函数对进程进行创建等操作;

fork()函数本质上就是在一个已经存在的进程当中创建出该进程的子进程,在此不再进行赘述;

知道了fork()函数的大致原理,那么有一个问题:

  • fork()函数调用失败的原因是什么?

fork()函数调用失败的原因本质上分为两种:

  • 系统中存在大量进程

    当内存当中存在大量进程时,由于进程需要维护对应的PCB结构体与对应的内存数据;

    当出现大量的进程时将会极度占用内存资源;

    OS为了防止崩溃的情况,当内存吃紧或是当前进程数过多的情况将会驳回创建进程的请求从而导致子进程创建失败;

  • 实际用户的进程数超过了限制

    一般情况下,操作系统会限制每个用户可以拥有的进程数量,以确保系统资源的合理分配和管理;

    故当实际用户的进程数超过了限制时,操作系统也将驳回创建进程的请求;


🦖 进程终止

请添加图片描述

进程终止即字面意思理解;

一个正在运行的进程运行结束并释放对应的内存资源;

一般进程终止存在以下几种状态:


💥 进程退出码

请添加图片描述

在上文中提到的三种状态其中两种为程序正常终止的状态;

分别为:

  • 代码运行完毕且结果正确
  • 代码运行完毕但结果错误

在这两种情况下,程序(进程)的代码数据已经被执行完毕,只是对应的结果是错误,这种进程终止方式统称为进程的正常终止;

以我平时写代码的习惯而言:

int main(){//代码数据return 0;
}

在这段代码当中,或许有些人并不理解为什么在main()函数当中需要返回一个0值;

可能从某些编译器的源代码当中向下进行追述可以明白这个函数返回值最终将会传给操作系统;

实际上这个return 0所返回的0值被称为一个进程的退出码;

在c/C++中可以通过strerror()打印退出码对应的退出信息;

#include <cstring>/#include <string.h> 
char* strerror(int errnum); //声明
  • 存在一个程序

    #include<cstring>
    int main() {for (int i = 0; i < 150;++i){printf("strerror(%d) : %s \n", i, strerror(i));}return 0;
    }
    

    即为打印出150以内的退出码;

    运行该进程结果为:

    $ ./myproc 
    strerror(0) : Success 
    strerror(1) : Operation not permitted 
    strerror(2) : No such file or directory 
    strerror(3) : No such process 
    strerror(4) : Interrupted system call 
    strerror(5) : Input/output error 
    strerror(6) : No such device or address 
    strerror(7) : Argument list too long 
    strerror(8) : Exec format error 
    strerror(9) : Bad file descriptor 
    strerror(10) : No child processes 
    strerror(11) : Resource temporarily unavailable 
    strerror(12) : Cannot allocate memory 
    strerror(13) : Permission denied 
    strerror(14) : Bad address 
    strerror(15) : Block device required 
    strerror(16) : Device or resource busy 
    strerror(17) : File exists 
    strerror(18) : Invalid cross-device link 
    strerror(19) : No such device 
    strerror(20) : Not a directory 
    strerror(21) : Is a directory 
    strerror(22) : Invalid argument 
    strerror(23) : Too many open files in system 
    strerror(24) : Too many open files 
    strerror(25) : Inappropriate ioctl for device 
    strerror(26) : Text file busy 
    strerror(27) : File too large 
    strerror(28) : No space left on device 
    strerror(29) : Illegal seek 
    strerror(30) : Read-only file system 
    strerror(31) : Too many links 
    strerror(32) : Broken pipe 
    strerror(33) : Numerical argument out of domain 
    strerror(34) : Numerical result out of range 
    strerror(35) : Resource deadlock avoided 
    strerror(36) : File name too long 
    strerror(37) : No locks available 
    strerror(38) : Function not implemented 
    strerror(39) : Directory not empty 
    strerror(40) : Too many levels of symbolic links 
    strerror(41) : Unknown error 41 
    strerror(42) : No message of desired type 
    strerror(43) : Identifier removed 
    strerror(44) : Channel number out of range 
    strerror(45) : Level 2 not synchronized 
    strerror(46) : Level 3 halted 
    strerror(47) : Level 3 reset 
    strerror(48) : Link number out of range 
    strerror(49) : Protocol driver not attached 
    strerror(50) : No CSI structure available 
    strerror(51) : Level 2 halted 
    strerror(52) : Invalid exchange 
    strerror(53) : Invalid request descriptor 
    strerror(54) : Exchange full 
    strerror(55) : No anode 
    strerror(56) : Invalid request code 
    strerror(57) : Invalid slot 
    strerror(58) : Unknown error 58 
    strerror(59) : Bad font file format 
    strerror(60) : Device not a stream 
    strerror(61) : No data available 
    strerror(62) : Timer expired 
    strerror(63) : Out of streams resources 
    strerror(64) : Machine is not on the network 
    strerror(65) : Package not installed 
    strerror(66) : Object is remote 
    strerror(67) : Link has been severed 
    strerror(68) : Advertise error 
    strerror(69) : Srmount error 
    strerror(70) : Communication error on send 
    strerror(71) : Protocol error 
    strerror(72) : Multihop attempted 
    strerror(73) : RFS specific error 
    strerror(74) : Bad message 
    strerror(75) : Value too large for defined data type 
    strerror(76) : Name not unique on network 
    strerror(77) : File descriptor in bad state 
    strerror(78) : Remote address changed 
    strerror(79) : Can not access a needed shared library 
    strerror(80) : Accessing a corrupted shared library 
    strerror(81) : .lib section in a.out corrupted 
    strerror(82) : Attempting to link in too many shared libraries 
    strerror(83) : Cannot exec a shared library directly 
    strerror(84) : Invalid or incomplete multibyte or wide character 
    strerror(85) : Interrupted system call should be restarted 
    strerror(86) : Streams pipe error 
    strerror(87) : Too many users 
    strerror(88) : Socket operation on non-socket 
    strerror(89) : Destination address required 
    strerror(90) : Message too long 
    strerror(91) : Protocol wrong type for socket 
    strerror(92) : Protocol not available 
    strerror(93) : Protocol not supported 
    strerror(94) : Socket type not supported 
    strerror(95) : Operation not supported 
    strerror(96) : Protocol family not supported 
    strerror(97) : Address family not supported by protocol 
    strerror(98) : Address already in use 
    strerror(99) : Cannot assign requested address 
    strerror(100) : Network is down 
    strerror(101) : Network is unreachable 
    strerror(102) : Network dropped connection on reset 
    strerror(103) : Software caused connection abort 
    strerror(104) : Connection reset by peer 
    strerror(105) : No buffer space available 
    strerror(106) : Transport endpoint is already connected 
    strerror(107) : Transport endpoint is not connected 
    strerror(108) : Cannot send after transport endpoint shutdown 
    strerror(109) : Too many references: cannot splice 
    strerror(110) : Connection timed out 
    strerror(111) : Connection refused 
    strerror(112) : Host is down 
    strerror(113) : No route to host 
    strerror(114) : Operation already in progress 
    strerror(115) : Operation now in progress 
    strerror(116) : Stale file handle 
    strerror(117) : Structure needs cleaning 
    strerror(118) : Not a XENIX named type file 
    strerror(119) : No XENIX semaphores available 
    strerror(120) : Is a named type file 
    strerror(121) : Remote I/O error 
    strerror(122) : Disk quota exceeded 
    strerror(123) : No medium found 
    strerror(124) : Wrong medium type 
    strerror(125) : Operation canceled 
    strerror(126) : Required key not available 
    strerror(127) : Key has expired 
    strerror(128) : Key has been revoked 
    strerror(129) : Key was rejected by service 
    strerror(130) : Owner died 
    strerror(131) : State not recoverable 
    strerror(132) : Operation not possible due to RF-kill 
    strerror(133) : Memory page has hardware error 
    strerror(134) : Unknown error 134 
    ......
    

    其中return 0时表示程序正常退出且 结果正确 ;

当一个进程结束后,可以对应的使用echo打印出上一个进程结束时的退出码;

echo $?

同样以上一个程序为例,此时将退出信息return 0改为return 111;

#include<cstring>
int main() {for (int i = 0; i < 150;++i){printf("strerror(%d) : %s \n", i, strerror(i));}return 111;//此处进行修改
}

将该程序运行后再使用echo $?打印出上一个进程运行结束后的退出码;

$ ./myproc
strerror(0) : Success 
strerror(1) : Operation not permitted 
strerror(2) : No such file or directory 
......
strerror(149) : Unknown error 149 $ echo $?
111
$ echo $?
0
$ echo $?
0

从该处的结果可以得出结论;

  • 为什么此处的echo $?只打印出了一次111;

在之前的文章中提到,在Linux当中,命令也属于文件,也需要被执行;

既然要被执行那么就会变成进程,对应的也有属于自身的退出码;

故当一个命令在被执行过后再使用echo $?打印退出码时将会打印出该命令的退出码;


💥 进程正常退出

请添加图片描述

在上文当中谈到了进程退出的三种情况其中两种情况都为正常退出的情况;

同时在上文当中提到了 进程退出码 的概念;

对于进程退出码而言,只有当进程正常退出的情况退出码才有意义;

进程退出时除了return可以对进程进行结束以外还有对应的exit(),_exit();

#include<stdlib.h>/<cstdlib>
exit();#include<unistd.h>
_exit();
  • 存在一段程序

    #include<iostream>
    using namespace std;
    int func(){return 10;
    }
    int main(){cout<<func()<<endl;return 0;
    }
    

    在这段程序当中出现了两个return,分别为main()函数与普通函数func()函数;

那么在上段代码中的两个return的意义都是什么;

这可以将return分为两种情况;

  • return存在于main()函数当中

    return存在main()函数中,其意义表示为程序(进程)的退出;

  • return存在于普通函数当中

    return存在普通函数中,其意义表示为当前函数的返回;

return不同,exit()_exit()无论处于哪都表示进程的退出;

  • 存在一段代码

    int func() { // return 10;exit(10);//由于_exit 与 exit在此处所展示的效果相同故不进行演示
    }
    int main() {func();return 111;
    }
    

    当运行这段代码后再使用echo $?打印出进程对应的退出码;

    $ ./myproc 
    $ echo $?
    10
    

    对应的退出码变成了10;

    由此也可以验证对于exit()_exit()而言,无论是普通函数还是main()函数,都充当着结束进程的功能;

那么对应的这两个函数的功能是否完全相同?

int main()
{cout<<"hello world\n";exit(-1);
}

已知当printf()或者cout输出字符串并包含\n时,输出流将会被刷新并将缓冲区的内容写入到标准输出设备;

故这个程序的结果为:

hello world

那么将该处的\n进行删除并再次运行程序;

int main()
{cout<<"hello world";exit(-1);
}
hello world

对应的结果还是不变;

此时将exit()换成_exit()重新编译后再次运行程序;

int main() {cout << "hello world";_exit(-1);
}
$ ./myproc 
$ 

从该段当中可以看出,当exit()被换成_exit()时,将不再刷新缓冲区进行输出;

  • 为什么使用exit()会进行打印而_exit()不会?
  • exit()_exit()之间的区别是什么?

实际上,_exit()属于系统调用,当程序调用_exit()对进程进行结束时将通过系统调用直接结束进程;

而对于exit()而言,其本质是一个对_exit()封装后的C标准库函数;


🦖 进程等待

请添加图片描述

在操作系统当中, 进程等待(Process Waiting) 指一个进程暂时停止执行,知道某个特定事件发生或者某个特定条件得到满足后再继续执行;


💥 僵尸进程

请添加图片描述

在『 Linux 』僵尸进程与孤儿进程-CSDN博客中提到了对于僵尸进程的概念;

一个进程的创建与资源回收是由其父进程或者是OS(操作系统)进行的;

而僵尸进程的概念即为,当进程退出时其并不被允许进行资源回收而回处于僵尸状态(Z);

本质上一个进程在结束之后其对应的内存资源将会被释放,但是若是该进程的父进程并未读取到该进程退出的返回状态时该僵尸进程虽然对应的内存资源被释放但仍有一部分的PCB结构体未被释放;

过多的僵尸进程将在操作系统当中存在过多的PCB结构体使得大量的占用内存,属于是一种内存泄漏的问题;

那么在这里存在一个问题:

  • 子进程由父进程进行创建并且子进程将会帮助父进程完成对应的工作,那么父进程是否需要关心子进程完成工作的完成情况?且若是父进程需要关注又该如何得知?父进程若是不需要又该如何处理?

💥 如何解决僵尸进程的内存泄漏问题

请添加图片描述

在上文当中提到了对于僵尸进程的内存泄露问题;

那么如何解决僵尸进程的内存泄露问题?

POSIX标准库中存在着这样的两个函数,分别为wait()waitpid()两个函数;

#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

这两个函数实际上是一个用于进程等待的函数;

这两个函数的函数名简而言之即为等待子进程的状态发生变化,当父进程等待到子进程的状态变为Z(Zombie)时,父进程将回收子进程并读取其对应的退出信息,从而结局僵尸问题;

#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;int main() {pid_t id = fork();if (id == 0) {//  子进程int cnt = 2;while (cnt--) {cout << "pid : " << getpid() << "  ppid : " << getppid() << endl;sleep(1);}} else if (id > 0) {//  父进程sleep(4);wait(NULL);//wait(NULL) 与 waitpid(-1,NULL,0) 效果相等,在此不演示waitpid();} else {// 创建进程失败exit(-1);}return 0;
}

如该段代码所示;

该段代码即在一个程序中创建一个子进程,且子进程"存活"2s,父进程存活4s;

父进程将在子进程结束处于僵尸进程时将子进程进行回收;

运行该程序并且试用Shell对进程进行观察;

 while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep ; sleep 1 ; echo "-----------------------------------" ; done

当运行程序时shell语句对应显示的信息为:

PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND9338  9877  9877  9338 pts/2     9877 S+    1002   0:00 ./myproc9877  9878  9877  9338 pts/2     9877 S+    1002   0:00 ./myproc
------------------------------------PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND9338  9877  9877  9338 pts/2     9877 S+    1002   0:00 ./myproc9877  9878  9877  9338 pts/2     9877 S+    1002   0:00 ./myproc
------------------------------------PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND9338  9877  9877  9338 pts/2     9877 S+    1002   0:00 ./myproc9877  9878  9877  9338 pts/2     9877 Z+    1002   0:00 [myproc] <defunct>
------------------------------------PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND9338  9877  9877  9338 pts/2     9877 S+    1002   0:00 ./myproc9877  9878  9877  9338 pts/2     9877 Z+    1002   0:00 [myproc] <defunct>
------------------------------------PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
------------------------------------

子进程运行了2s后状态称为了Z即僵尸进程;

父进程在4s过后执行了wait(NULL)将子进程进行回收,对上述内容进行了验证;


💥 wait( )/waitpid( )函数

请添加图片描述

上文当中提到了一个问题,简而言之即为父进程如何去管理其子进程;

对于该问题的解答,首先为父进程需不需要关心子进程的工作完成情况,答案是肯定的;

若是父进程未对子进程的工作完成情况进行管理则不能很好的根据子进程的工作完成情况而做出其他处理;

那么父进程该如何得知子进程的工作完成情况?

在上文当中可以得知,当一个进程退出时,即代表它的工作完成(可能);

那么一个进程的退出情况无非分为三种:

  • 代码运行完毕且结果正确
  • 代码运行完毕但结果错误
  • 代码未运行完毕异常终止

其中第一种与第二种都属于进程正常结束,第三种属于进程的异常终止,而一般情况下父进程都是采用wait()/waitpid()的方式获取子进程的退出信息从而对后序操作进行处理;

上文当中演示了使用wait()/waitpid()解决僵尸进程的内存泄露问题;

对应的两个函数的分别为:

#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
  • 返回值

    对于两个函数的返回值都是一样的返回值,且返回值分别有几种情况:

    • return value > 0

      当返回值大于0时则表示进程等待成功且回收成功,并返回处理的子进程的PID;

    • return value == 0

      当返回值等于0时则表示当前不存在已终止的进程,即没有已终止的子进程可等待;

    • return value == -1

      当返回值为-1时则表示等待过程当中出现了错误,这通常发生在传递给waitpid()函数的参数不合法或者出现了系统错误的情况;

  • 参数

    • pid

      对于waitpid()中的参数pid表示需要等待的子进程的ID;

      pid > 0表示等待进程IDpid的子进程;

      pid == -1表示等待任意子进程;

      pid == 0表示等待和调用进程属于同一个进程组的任何进程(不作过多描述);

      pid < -1表示等待进程组IDpid的任意子进程(不作过多描述);

    • status

      这是一个输出型参数,用于存储子进程的退出状态信息,当子进程终止时,它会将退出状态信息存储在这个指针所指向的位置;

      一般用法为:

      int status = 0;
      waitpid(-1,&status,0);
      

      当进程结束后对应的进程信息将会填入status当中;

    • options

      这是一个标志位用来制定等待子进程时的一些选项;

      options = 0时表示默认等待为阻塞等待子进程结束;

      options = WNOHANG表示非阻塞式等待(WNOHANG为对魔术数字的#define的重命名``);

而对于上文中提到的问题实质性是根据参数当中的status进行处理,当父进程成功等待对应的子进程并对子进程进行处理时,status将会获取子进程对应的退出信息;


🌟 进程退出信息

请添加图片描述

对于进程的退出信息分别有两种:

  • 进程退出码

    进程正常退出(代码跑完结果正确或是代码跑完结果错误);

  • 退出信号状态

    进程异常退出(被信号杀死)时对应的退出信号状态;

存在一段代码:

#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>int main() {pid_t id = fork();if (id == 0) {//  子进程int cnt = 2;while (cnt--) {cout << "pid : " << getpid() << "  ppid : " << getppid() << endl;sleep(1);}exit(111);} else if (id > 0) {//  父进程sleep(4);int status = 0;pid_t id = waitpid(-1,&status,0);cout << id << " : " << status < < < < endl;} else {// 创建进程失败exit(-1);}return 0;
}

在这段程序中,waitpid()将结束子进程的僵尸状态并且使用status获取进程中对应的退出信息并进行打印,其子进程的退出码为111;

运行后结果为:

$ ./myproc 
pid : 10914  ppid : 10913
pid : 10914  ppid : 10913
pid = 10914 , status = 28416 

从该段答案当中发现实际上打印的子进程的退出信息并不为代码中的退出码;

而实际上退出信息与提出码并不相同,退出码是退出信息中的其中一个部分;

退出信息一般由 退出码退出终止信号以二进制的方式组成;

  • 正常退出

    当进程正常退出时,其次低八位代表该进程退出时的退出码;

    对应的可以采用位运算计算出对应的退出码;

    以该段代码为例:

    #include <iostream>
    #include <sys/types.h>
    #include <sys/wait.h>
    using namespace std;int main() {pid_t id = fork();if (id == 0) {//  子进程int cnt = 2;while (cnt--) {cout << "pid : " << getpid() << "  ppid : " << getppid() << endl;sleep(1);}exit(111);} else if (id > 0) {//  父进程sleep(4);int status = 0;pid_t id = waitpid(-1, &status, 0);printf("pid = %d , 退出码 = %d \n", id, (status>>8)&0xff);} else {// 创建进程失败exit(-1);}return 0;
    }
    

    采用了位运算,即将退出信息 右移八位 后再使用按位与&0xFF最终得到对应的进程退出码;

    运行程序后结果为:

    $ ./myproc 
    pid : 11219  ppid : 11218
    pid : 11219  ppid : 11218
    pid = 11219 , 退出码 = 111 
    
  • 异常终止

    当进程异常终止时期对应的退出码即无意义;

    但是按照对应的位运算也可以得出进程异常终止的信号状态;

    稍微将代码进行改动:

    #include <iostream>
    #include <sys/types.h>
    #include <sys/wait.h>
    using namespace std;int main() {pid_t id = fork();if (id == 0) {//  子进程int cnt = 2;while (cnt) {cout << "pid : " << getpid() << "  ppid : " << getppid() << endl;sleep(1);}exit(111);} else if (id > 0) {//  父进程sleep(4);int status = 0;pid_t id = waitpid(-1, &status, 0);printf("pid = %d , 信号状态 = %d \n", id, status&0x7f);} else {// 创建进程失败exit(-1);}return 0;
    }
    

    此处将代码改为了一个子进程中无限循环的状态;

    且由于进程退出信息中的低七位为进程异常终止时的进程信号状态,此时直接使用按位与&0x7F即可;

    在运行该段代码后采用9号信号将进程杀死;

    kill -9 xxxxx(表示子进程的pid)
    
    $ ./myproc 
    pid : 11702  ppid : 11701
    pid : 11702  ppid : 11701
    pid = 11702 , 信号状态 = 9 
    

    从该处可以看出最终的结果显示出了对应的信号状态;

    在Linux当中可以使用kill -l来查看所有的信号状态从而对进程的结束状态进行了解(再次不进行过多描述);

    $ kill -l1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP6) 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
    
  • core dump

    在上图中出现除了 进程退出码 , 信号状态 以外还存在着一个为core dump的标志;

    core dump一般指进程在异常终止时产生的核心转储文件;

    核心转储文件包括了进程在异常终止时的内存映像,也便于后序的调试分析;

    core dump1时表示生成了对应的core dump文件;

    core dump0时表示未生成对应的core dump文件;

    当然core dump需要进行配置;

当然对应的进程退出码也不一定需要使用对应的位运算进行;

POSIX中存在两个宏分别为WIFEXITED()WEXITSTATUS();

其对应的声名分别为:

#include <sys/wait.h>int WIFEXITED(int status);
int WEXITSTATUS(int status);
  • WIFEXITED(int status)

    这个宏将判断进程的退出信息判断其是否为正常退出;

    若是正常退出将返回1,若是异常终止则返回0;

  • WEXITSTATUS(int status)

    该宏可以在退出信息中提取对应的进程退出码;

一般的使用情况为利用这两个宏来判断子进程是否为正常退出从而对后序进行处理;

#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;int main() {pid_t id = fork();if (id == 0) {//  子进程int cnt = 2;while (cnt--) {cout << "pid : " << getpid() << "  ppid : " << getppid() << endl;sleep(1);}exit(111);} else if (id > 0) {//  父进程sleep(4);int status = 0;waitpid(-1, &status, 0);if(WIFEXITED(status)){  //利用WIFEXITESD判断进程是否正常退出printf("进程退出码为: %d\n", WEXITSTATUS(status));//  利用WEXITSTATUS获取进程退出信息中的退出码}else{ cout << "进程异常终止" << endl;}} else {// 创建进程失败exit(-1);}return 0;
}

💥 非阻塞式等待

请添加图片描述

在上文当中,对于wait()waitpid()中提到了一个对应的进程等待问题;

且在上文当中的进程等待属于阻塞式等待;

既然存在阻塞式等待那么必定也会存在非阻塞式等待;

waitpid()函数当中,其中的options参数若是为0时则默认为阻塞等待;

除了0以外还有一个特殊的宏为WNOHANG;

这个宏为对一个 魔术数字 1的重命名;

#define WNOHANG 1

waitpid()函数中第三个参数为WNOHANG时则表示非阻塞式等待;

  • 那么如何理解阻塞式等待与非阻塞式等待?

当父进程进行阻塞式等待时操作系统将会把父进程对应的数据放置于阻塞队列当中,当任意一个子进程变为僵尸状态时该父进程将会复苏,并获取子进程对应的退出信息再执行父进程后序的代码;

当等待成功后EIP将会继续读取父进程的下一行指令并在对应位置使CPU继续执行父进程的代码;

当父进程进行非阻塞式等待时,父进程将会直接判断是否存在需要等待的子进程,即是否存在状态发生变化(僵尸状态)的进程,其对应的返回值如下进行比较:

  • return value > 0

    当返回值大于0时则表示进程等待成功且回收成功,并返回处理的子进程的PID;

  • return value == 0

    当返回值等于0时则表示当前不存在已终止的进程,即没有已终止的子进程可等待;

  • return value == -1

    当返回值为-1时则表示等待过程当中出现了错误,这通常发生在传递给waitpid()函数的参数不合法或者出现了系统错误的情况;

一般情况下,非阻塞式等待需要配合循环进行使用,若是父进程不为一个循环且为一个非阻塞式等待(WNOHANG)时,若是父进程在调用waitpid()时其子进程并未结束;

父进程则将错失获取子进程退出信息的机会;

当然非阻塞式等待也使得多进程的状态下能够提高程序整体的效率,当子进程在进行处理时父进程进行非阻塞式等待并处理与子进程不同的工作使得整体的效率增加;

#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <vector>using namespace std;typedef void (*FunPoint)();vector<FunPoint> P_FunV;void Func1() { cout << "Func1()" << endl; }
void Func2() { cout << "Func2()" << endl; }void Load() {P_FunV.push_back(Func1);P_FunV.push_back(Func2);
}int main() {pid_t id = fork();if (id == 0) {// 子进程int cnt = 5;while (cnt--) {cout << "子进程 : "<< "pid = " << getpid() << "  ppid = " << getppid() << endl;sleep(1);}exit(111);} else if (id > 0) {// 父进程bool quite = false;while (!quite) {cout << "父进程 : "<< "pid = " << getpid() << "  ppid = " << getppid() << endl;sleep(1);int status = 0;pid_t res = waitpid(-1, &status, WNOHANG);if (res > 0) {printf("等待成功 进程执行完毕,退出码:%d\n", WEXITSTATUS(status));quite = true;} else if (res == 0){//在该条件当中 父进程并未识别到已经结束的任意子进程且在等待时进行的是非阻塞式等待,故父进程可以在等待期间通过循环参与其他工作(cout << "不存在已经结束的子进程" << endl);if(P_FunV.empty())Load();else{for(auto iter:P_FunV){iter();}}} else {perror("子进程等待失败");quite = true;}}} else {// 创建子进程失败}return 0;
}

以该段代码为例

该段代码在父进程当中fork()出了一个子进程,并设置了一个函数指针的vector;

在子进程并未结束时父进程循环边进行非阻塞式等待,边将函数加载至vector当中进行其余操作;


💥 父进程如何获取子进程的退出信息

请添加图片描述

上文当中讲了许多关于父进程调用wait()/waitpid()从而获取子进程的退出信息,此时子进程已经死亡,对应的资源也已经释放,那么父进程是如何做到的,wait()/waitpid()做了什么?

当一个进程终止时,内核会保留其PCB结构体,并将其标记为僵尸状态,以便父进程可以查询其退出状态;

此时,进程的内存资源,包括堆栈,全局变量等,通常会被释放;

但是其PCB结构体中仍然会保留进程的一些信息,例如退出状态码,资源使用情况等;

wait()/waitpid()则将在子进程的PCB结构体当中找到对应的退出信息并返回;

  • 那么既然可以使用wait()/waitpid()来获取子进程的退出信息,那么是否可以使用全局变量使得父进程在不使用wait()/waitpid()的情况下取得子进程的退出结果?

    这个答案显然为否,在上文当中提到了进程具有独立性,当父子进程中的其中一个进程试图去 写入/修改 另一个进程的数据时将会发生写时拷贝从而保证进程的独立性;

  • 那么既然进程具有独立性,且PCB结构体为内核数据结构,父进程是否有权利获取子进程的退出信息,又是如何获取子进程的退出信息的?

    实际上但以权限而言,单以进程的权限而言,其父进程不具有获取内核数据结构数据的权限;

    但实际上在调用wait()/waitpid()调用的是系统调用,故父进程在使用该函数的情况下有权力获取子进程的退出信息;


🦖 进程替换

请添加图片描述

在上文中提到,进程通过fork()创建出子进程,并使得子进程可以完成对应的工作;

而实际上,子进程不仅可以执行与父进程相同的代码,同时子进程还可以单独调用执行另一个程序;

<unistd.h>头文件中存在一个系列的函数为exec系列函数;

其功能可以将一个进程替换为另一个进程,并执行另一份代码数据,且其对应的PID也不会发生变化;

此处主要围绕execl()函数进行讲解;

int execl(const char *path, const char *arg0, ... /* (char *) NULL */);
  • 参数

    execl()函数接受一个字符串参数 path,表示要执行的程序的路径,以及一个或多个以NULL结尾的字符串参数;

#include <unistd.h>#include <iostream>using namespace std;int main() {cout << "hello world1" << endl;cout << "hello world1" << endl;cout << "hello world1" << endl;execl("/bin/ls", "ls", "-a", NULL);cout << "hello world2" << endl;cout << "hello world2" << endl;cout << "hello world2" << endl;return 0;
}

在这段代码当中,将会打印3个hello world1与3个hello world2;

其中在打印3个hello world1的下一句为执行一个新的程序ls,并传递-a参数,且以NULL作为结束;

当运行时其对应的结果为:

$ ./myproc 
hello world1
hello world1
hello world1
.  ..  makefile  myproc  myproc.cpp

当执行完前三句打印后进程替换为了ls程序;

  • 那么是否其PID不会发生变化?

    将代码进行修改,且在该路径中再添加一个文件夹并分别使用getpid()观察其PID情况;

    #include <unistd.h>#include <iostream>using namespace std;int main() {cout << "hello world1" << endl;cout << "hello world1" << endl;cout << "hello world1" << endl;printf("当前程序为myproc 且PID为:%d \n", getpid());execl("./test_/mytest", "mytest", NULL);cout << "hello world2" << endl;cout << "hello world2" << endl;cout << "hello world2" << endl;return 0;
    }
    

    该路径下存在一个名为test_的目录且目录中存在一个可执行文件为mytest;

    且对应的代码为如下:

    #include <iostream>
    #include<unistd.h>using namespace std;int main() {printf("当前程序为mytest 且PID为:%d\n", getpid());return 0;}
    

    运行后最终的结果为:

    $ ./myproc 
    hello world1
    hello world1
    hello world1
    当前程序为myproc 且PID为:14115 
    当前程序为mytest 且PID为:14115
    

    证明实际上在进行进程替换的时候起PID并不会替换,即在进行进程替换的时候将对应的代码和数据载入内存当中并不会产生一个新的进程;


💥 进程替换的原理

请添加图片描述

进程替换是指一个进程将自己的内存映像替换为另一个程序的内存映像,并开始执行该程序的过程;

这种机制允许一个进程在不创建新的进程的情况下,动态地加载和执行其他程序,从而实现程序的动态更新资源回收等功能;

进程在进行进程替换时通常需要进行以下几个步骤:

  • 加载新程序

    即将要执行的新程序的可执行文件加载至对应的内存空间当中,并将其映射至当前进程的地址空间当中;

  • 清理资源

    在加载新程序之前,原始进程将是放一部分资源从而使得新程序在运行时不会受到原始进程状态的影响;

  • 替换内存映像

    当新程序的可执行文件被加载至内存当中后,原始进程将会覆盖自己的内存映像并将新程序的代码数据替换为自己的;

  • 执行新程序

    最后,原始进程将控制权转移到新的程序的入口点并开始执行新程序,此时原始进程将不再执行;

以该图为例;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/729160.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1748页CTF竞赛入门指南,有点牛!

CTF是一种针对信息安全领域的经济性挑战&#xff0c;旨在通过解决一系列的难题来寻找隐藏的“flag”。CTF比赛战队一般是以高校、科研单位、企业、信息安全从业者或社会团体组成。对于网安爱好者及从业者来说&#xff0c;拥有“CTF参赛经验”也是求职中的加分项。 前几天分享的…

【“双碳”目标】Acrel-2000Z分布式光伏发电监测系统解决方案

1 概述 “十四五”期间&#xff0c;随着“双碳”目标提出及逐步落实&#xff0c;本就呈现出较好发展势头的分布式光伏发展有望大幅提速。就“十四五”光伏发展规划&#xff0c;国家发改委能源研究所可再生能源发展中心副主任陶冶表示&#xff0c;“双碳”目标意味着国家产业结…

基于React的低代码开发:探索应用构建的新模式

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法|MySQL| ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-OywB1Epu30PrvOJQ {font-family:"trebuchet ms",verdana,arial,sans-serif;f…

基于UDP实现直播间聊天的功能

需求&#xff1a;软件划分为用户客户端和主播服务端两个软件client.c和server.c 用户客户端负责&#xff1a;1.接收用户的昵称2.接收用户输入的信息&#xff0c;能够将信息发送给服务端3.接收服务端回复的数据信息,并完成显示主播服务端负责&#xff1a;1.对所有加入直播间的用…

网络协议栈--应用层--HTTPS协议

目录 一、HTTPS协议原理1.1 HTTPS协议是什么&#xff1f;1.2 概念准备1.2.1 什么是“加密”&#xff1f;1.2.2 为什么要加密&#xff1f;1.2.3 常见的加密方式1.2.3.1 对称加密1.2.3.2 非对称加密 1.2.4 数据摘要&&数据指纹1.2.5 数字签名1.2.6 理解链-承上启下 1.3 HT…

关于JVM的小总结(待补充)

JVM组成及他们之间的关系 装载类子系统字节码执行引擎运行时数据区 装载类子系统 类加载器字节码调节器类加载运行时数据区 字节码执行引擎 运行时数据区 线程私有 虚拟机栈本地方法栈程序计数器 线程共享 堆方法区&#xff08;元空间&#xff09;

后见之明!错过6个涨停板之后的复盘

在今年1月2日和1月3日&#xff0c;旅游板块两支个股先后涨停&#xff0c;此后一支月内三倍&#xff0c;另一支连续6个涨停。事后复盘&#xff0c;我们如何在1月2日第一支个股涨停之后&#xff0c;通过量化分析&#xff0c;找出第二支股&#xff1f; 一个3倍&#xff0c;一个6连…

M1电脑 Xcode15升级遇到的问题

遇到四个问题 一、模拟器下载经常报错。 二、Xcode15报错: SDK does not contain libarclite 三、报错coreAudioTypes not found 四、xcode模拟器运行一次下次必定死机 一、模拟器下载经常报错。 可以https://developer.apple.com/download/all/?qios 下载最新的模拟器&…

工业制氧机的使用与维护管理指南

工业制氧机是工业生产中不可或缺的重要设备&#xff0c;其高效稳定的供氧功能对于保障生产过程的顺利进行至关重要。为了确保工业制氧机能够持续高效地提供氧气&#xff0c;正确的使用方法和维护措施是必不可少的。 在使用工业制氧机时&#xff0c;我们首先要确保设备放置在通风…

网络聊天室的UDP实现以及数据库

网络聊天室UDP实现 服务器端&#xff1a; 头文件&#xff1a; #include <myhead.h>//定义客户信息结构体 typedef struct magtye {char type; //消息类型char name[100]; //客户姓名char text[1024]; //客户发送聊天信息 }msg_t;//定义结构体存储…

mysql的语法总结2

命令&#xff1a; mysql -u 用户名 -p mysql登录 命令&#xff1a;create database u1 创建数据库u1 查询数据库 使用数据库u1 创建表department 查询表department ALTER TABLE 表名 操作类型&#xff1b; 操作类型可以有以下的操作&#xff1a; 添加列&#x…

CubeMX使用教程(2)——点亮LED

在上一章&#xff0c;我们完成了CubeMX的环境配置&#xff0c;这一章我们通过CubeMX来完成点亮LED的工作。 通过LED原理图可知&#xff0c;如果我们要点亮LD1&#xff08;第一个灯&#xff09;&#xff0c;它对应开发板的PC8端口&#xff0c;因此我们应该在CubeMX中将PC8配置为…

【并查集】一种简单而强大高效的数据结构

目录 一、并查集原理 二、并查集实现 三、并查集应用 1. LeetCode并查集相关OJ题 2. 并查集的其他应用及总结 一、并查集原理 并查集&#xff08;Disjoint Set&#xff09;是一种用来管理元素分组和查找元素所属组别的数据结构。它主要支持两种操作&#xff1a;查找&…

JavaScript进阶 (1)

封装 构造函数存在问题 js可以通过构造函数进行封装&#xff0c;但存在浪费内存问题 每创建新的对象引用数据类型就开辟新的空间 原型 构造函数通过原型分配函数是所有对象所共享的 每一个构造函数都有一个prototype属性&#xff0c;指向另一个对象&#xff0c;也称为原型…

小型内衣裤洗衣机哪个牌子好?四款高热度内衣洗衣机力荐

相信很多用户从小就有个观念&#xff0c;内衣裤不能跟其他衣物一起混合洗&#xff0c;否则会感染细菌&#xff0c;所以不少人的内衣裤一直都是自己手洗的&#xff0c;清洗内衣裤不算麻烦&#xff0c;但日常都要换洗&#xff0c;对一个白天上班已经很累的人来说&#xff0c;真是…

如何 借助 AI + bat,1分钟内建立100个自定义文件和文件夹?

01 你好&#xff0c;我是云桃桃。 最近&#xff0c;我在写web系列的知识&#xff0c;做的过程中遇到过不少问题。今天&#xff0c;就来说说&#xff0c;我解决的一个批量新建文件/文件夹的问题。 事情是这样的。首先&#xff0c;我的大纲基本在幕布里已经弄好了&#xff0c;…

简析内部审计数字化转型的方法和路径【小落送书(第6期)】

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢AI编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️…

基于ACM32 MCU的电动滑板车方案介绍

随着智能科技的快速发展&#xff0c;电动滑板车的驱动系统也得到了长足的发展。国内外的电动滑板车用电机驱动系统分为传统刷式电机和无刷电机两种类型。其中&#xff0c;传统的刷式电机已经逐渐被无刷电机所取代&#xff0c;无刷电机的性能和寿命都更出色&#xff0c;已成为电…

玩转AI大模型应用开发,轻松打造热门APPai数字人直播软件!

AI大模型应用在数字人直播领域的应用愈发成熟&#xff0c;为开发者提供了更多创意和可能性。数字人直播软件是当前热门的应用之一&#xff0c;它结合了虚拟主播和人工智能技术&#xff0c;为用户带来全新的互动体验。想要打造一个火爆的数字人直播软件&#xff0c;就需要玩转AI…

从0到1快速搭建一个jeecg 企业级应用管理后台

一. 基本介绍 官网地址&#xff1a;https://jeecg.com/ JeecgBoot 是一款企业级的低代码平台&#xff01;前后端分离架构 SpringBoot2.x&#xff0c;SpringCloud&#xff0c;Ant Design&Vue3&#xff0c;Mybatis-plus&#xff0c;Shiro&#xff0c;JWT 支持微服务。强大的…