『 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 COMMAND
 9338  9877  9877  9338 pts/2     9877 S+    1002   0:00 ./myproc
 9877  9878  9877  9338 pts/2     9877 S+    1002   0:00 ./myproc
------------------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 9338  9877  9877  9338 pts/2     9877 S+    1002   0:00 ./myproc
 9877  9878  9877  9338 pts/2     9877 S+    1002   0:00 ./myproc
------------------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 9338  9877  9877  9338 pts/2     9877 S+    1002   0:00 ./myproc
 9877  9878  9877  9338 pts/2     9877 Z+    1002   0:00 [myproc] <defunct>
------------------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 9338  9877  9877  9338 pts/2     9877 S+    1002   0:00 ./myproc
 9877  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 -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
    
  • 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并不会替换,即在进行进程替换的时候将对应的代码和数据载入内存当中并不会产生一个新的进程;


💥 进程替换的原理

请添加图片描述

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

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

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

  • 加载新程序

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

  • 清理资源

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

  • 替换内存映像

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

  • 执行新程序

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

以该图为例;

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

高中数学:初等函数之幂函数

1、定义 注意&#xff1a;x项系数&#xff0c;只能是1&#xff01; 例题&#xff1a; 2、常见幂函数图像 3、分数指数幂 x定义域&#xff1a;分母为偶数时&#xff0c;如&#xff1a;2、4、6等&#xff0c;则x≥0 x≥0时 4、画草图 步骤&#xff1a; 1、利用结论画出第…

2024最新图标设计趋势!附超好用的图标工具清单

图标&#xff0c;在界面设计中的作用不容小觑。正所谓浓缩的就是精华&#xff0c;一个小小的图标&#xff0c;却有着高效传递信息、美化界面排版、提升用户体验的巨大能力。 既然图标如此重要&#xff0c;了解图标设计趋势对设计师来说几乎是必须要做的事&#xff0c;它可以让…

字符串索引错误解决方案

字符串索引错误通常是由于尝试访问字符串中不存在的索引位置而引起的。我在Python编译中&#xff0c;字符串是一个不可变的序列&#xff0c;可以通过索引访问其中的字符。如果尝试访问超出字符串长度范围的索引位置&#xff0c;将引发IndexError异常。所以下面的问题如果遇到了…

阿里云k8s内OSS报错UnKnownHost。

这个问题就是链接不上oss属于网络问题&#xff1a; 1.排查服务器 在服务器&#xff08;ecs&#xff09;上直接ping oss地址看是否能够通。 不通就要修改dns和hosts&#xff08;这个不说&#xff0c;自己网上查&#xff09; 2.排查容器 进去ping一下你的容器是否能访问到oss…

新能源车高压线束更换VR虚拟互动教学保障了培训安全可控

随着新能源汽车市场的快速发展&#xff0c;对于新能源汽车检修人才的需求也日益增长。然而&#xff0c;传统的培训模式往往存在一些限制&#xff0c;如培训周期长、成本高、实践机会少等。为了解决这些问题&#xff0c;新能源车检修VR互动培训应运而生&#xff0c;成为一种创新…

电脑怎么强制关机?让电脑不再“任性”!

在使用电脑的过程中&#xff0c;有时可能会遇到程序无响应、系统崩溃或遭遇恶意软件攻击等情况&#xff0c;这时就需要我们采取紧急措施&#xff0c;强制关闭电脑。强制关机虽然是一种非常手段&#xff0c;但在必要时能够保护电脑硬件和数据安全。本文将为您介绍电脑怎么强制关…

智慧视频终端解决方案

依托富瀚微智慧视频SOC&#xff0c;提供以视频为核心的智能产品及解决方案

【数据结构与算法】二分查找题解(二)

这里写目录标题 一、81. 搜索旋转排序数组 II二、167. 两数之和 II - 输入有序数组三、441. 排列硬币四、374. 猜数字大小五、367. 有效的完全平方数 一、81. 搜索旋转排序数组 II 中等 已知存在一个按非降序排列的整数数组 nums &#xff0c;数组中的值不必互不相同。 在传递…

搞钱必看 盘点那些靠谱的程序员副业,狠狠提升财富值

这是一个职业生涯三叶草模型&#xff0c;它分为兴趣、价值、能力三个维度&#xff0c;完美的主职业最好同时满足这三项。但事情往往未必那么如意&#xff0c;如果主职业没能同时满足&#xff0c;那么剩下的部分&#xff0c;完全可以用副业填充。 或者&#xff0c;通俗点说&…

蓝牙耳机怎么选择比较好?2024年热门机型推荐大揭秘!

​蓝牙耳机已经成为了我们日常生活中不可或缺的一部分&#xff0c;随着技术的发展&#xff0c;人们对蓝牙耳机的要求也在不断提升&#xff0c;不仅希望音质出色&#xff0c;还希望能够在不同的场景下使用。然而&#xff0c;如何挑选一款适合自己的蓝牙耳机却是一门学问。今天&a…

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

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

stm32学习笔记:I2C通信外设原理+实验

软件实现和硬件实现 串口通信为异步时序&#xff0c;用软件实现很麻烦&#xff0c;基本上用硬件实现 而I2C协议通信为同步时序&#xff0c;软件实现简单且灵活&#xff0c;硬件实现比较麻烦&#xff0c;故软件比较常用 但I2C硬件实现功能比较大&#xff0c;执行效率高&#xff…

使用 Mendix 中的 OIDC 模块集成 Azure AD SSO

前言 在当今快速发展的数字化世界中&#xff0c;企业追求高效率和灵活性已成为常态。Mendix&#xff0c;作为一个先进的低代码开发平台&#xff0c;正是企业快速响应市场需求、加速数字化转型过程的利器。通过其直观的可视化开发环境&#xff0c;即使是非技术背景的用户也能设…

原型设计工具有哪些值得推荐?列举6个!

原型设计是一个可视化项目需求的过程&#xff0c;没有产品原型的创建&#xff0c;就无法从事产品设计。因此&#xff0c;原型工具的选择不容忽视。一个好的原型工具不仅可以高效输出页面设计&#xff0c;规范产品原型&#xff0c;还可以有效降低开发设计师的理解和沟通成本。在…

Ulysses for macOS v33.1 写作应用 兼容 M1/M2

Ulysses 是面向 Mac、iPhone 和 iPad 的一站式写作环境。Ulysses 提供令人愉悦、专注的写作体验&#xff0c;加上高效文稿管理、无缝同步以及灵活导出&#xff0c;因此是各种写作人士的第一选择。 应用介绍 Ulysses 是面向 Mac、iPhone 和 iPad 的一站式写作环境。Ulysses 提供…

数据可视化原理-腾讯-分类散点图

在做数据分析类的产品功能设计时&#xff0c;经常用到可视化方式&#xff0c;挖掘数据价值&#xff0c;表达数据的内在规律与特征展示给客户。 可是作为一个产品经理&#xff0c;&#xff08;1&#xff09;如果不能够掌握各类可视化图形的含义&#xff0c;就不知道哪类数据该用…

《小学科学》是什么级别的期刊?是正规期刊吗?能评职称吗?

问题解答&#xff1a; 问&#xff1a;《小学科学》期刊是正规期刊吗&#xff1f; 答&#xff1a;是正规期刊&#xff0c;下面会有具体介绍 问&#xff1a;《小学科学》期刊是什么级别的&#xff1f; 答&#xff1a;省级&#xff1b;主管单位&#xff1a;长春出版传媒集团有…

软考认证该怎么选择,哪个等级更适合自己?

软考选择等级和方向的时候对于新手来说可能有点难&#xff0c;不过我们可以根据官方的描述和自己的发展意向去选择&#xff0c;就算选择错了也无所谓&#xff0c;毕竟这只是个证书&#xff0c;备考过程学到的内容与时间上的对比来说还是比较值得的&#xff0c;所以大家大可大胆…

用MATLAB求解微分方程

第一篇为 基础概念 &#xff0c;第二篇为 R-K法的具体实现方法。 &#xff08;一&#xff09;常微分方程的MATLAB求解 概要&#xff1a; 常微分方程的MATLAB求解分为解析解、数值解解析解(只有少数微分方程组有解析解)&#xff1a;dsolve函数数值解&#xff1a;solver函数&a…

实现消息队列(Kafka、ActiveMQ、RabbitMQ和RocketMQ)高可用

概述 单机没有高可用可言&#xff0c;高可用都对集群来说的 要保证消息队列系统&#xff08;如Kafka、ActiveMQ、RabbitMQ和RocketMQ&#xff09;的高可用性&#xff0c;可以采取以下一些通用的措施&#xff1a; 集群部署&#xff1a;将消息队列系统部署为集群&#xff0c;包…