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

news2024/11/17 3:45:37

文章目录

    • 🦖 前言
    • 🦖 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并不会替换,即在进行进程替换的时候将对应的代码和数据载入内存当中并不会产生一个新的进程;


💥 进程替换的原理

请添加图片描述

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

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

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

  • 加载新程序

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

  • 清理资源

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

  • 替换内存映像

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

  • 执行新程序

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

以该图为例;

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

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

相关文章

单链表的实现(数据结构)

本篇博客主要是单链表&#xff08;无头单项不循环&#xff09;的实现的代码分享 说明&#xff1a;因为此单链表无头&#xff08;哨兵位&#xff09;&#xff0c;可以说成没有初始化也可以说初始化时没有一个有效地址作为单链表的起始地址 例如下面代码中的plist NULL。 所以在…

MS5188N——16bit、8 通道、500kSPS、 SAR 型 ADC

产品简述 MS5188N 是 8 通道、 16bit 、电荷再分配逐次逼近型模数 转换器&#xff0c;采用单电源供电。 MS5188N 拥有多通道、低功耗数据采集系统所需的所有 组成部分&#xff0c;包括&#xff1a;无失码的真 16 位 SAR ADC &#xff1b;用于将输入配 置为单端输入…

开源爬虫技术在金融行业市场分析中的应用与实战解析

一、项目介绍 在当今信息技术飞速发展的时代&#xff0c;数据已成为企业最宝贵的资产之一。特别是在${industry}领域&#xff0c;海量数据的获取和分析对于企业洞察市场趋势、优化产品和服务至关重要。在这样的背景下&#xff0c;爬虫技术应运而生&#xff0c;它能够高效地从互…

字符串索引错误解决方案

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

运维知识点-Tomcat

Tomcat tomcat日志告警tomcat文件包含读取漏洞Tomcat ### 远程代码执行(7.0.0-7.0.81) 开启PUT,访问127.0.0.1:8080改PUT,创建x.jsp,写入shellwar后文件部署,登入特定后台,上传包含jsp写的war文件。文件解析tomcat日志告警 cat catalina.sh /usr/local/tomcat/confTo…

运维知识点-JBoss

JBoss 介绍介绍 JBoss是一个基于J2EE的开放源代码的应用服务器,也是一个运行EJB(Enterprise JavaBean)的容器和服务器。它支持EJB 1.1、EJB 2.0和EJB3的规范,体现了J2EE规范中最新的技术。JBoss遵循LGPL许可,可以在任何商业应用中免费使用,并且由开源社区开发,这使得JB…

345.反转字符串中的元音字母

题目&#xff1a;给你一个字符串 s &#xff0c;仅反转字符串中的所有元音字母&#xff0c;并返回结果字符串。 元音字母包括 a、e、i、o、u&#xff0c;且可能以大小写两种形式出现不止一次。 class Solution {//画图&#xff0c;好理解点public String reverseVowels(String…

MySQL常见的存储引擎介绍

我将为您详细讲解 MySQL 常见的存储引擎&#xff0c;以及它们的使用场景、特点、区别和优势。MySQL 支持多种存储引擎&#xff0c;每种存储引擎都有其特定的优势和局限性。了解这些存储引擎的特点和适用场景对于选择合适的存储引擎以及优化数据库性能至关重要。 1. InnoDB 存储…

搞不完的事情,大不了加班?

工作是生活的一部分&#xff0c;但当你发现搞不完的事情&#xff0c;大不了加班&#xff01;你就会陷入无限的循环。 如果你想早点下班 &#xff0c;并且好好做自己的账号&#xff0c;还是少看哪些月入5万-10万的博主&#xff01; 如果你照着大V账号模仿大概率会失败&#xff…

python统计日志中数据从开始到结束的响应时间的最大值、最小值、平均值、中位数

应用场景&#xff1a;需要根据日志文件&#xff0c;统计出数据从开始下发到收到回复所需的时间&#xff0c;包括最大值、最小值、平均值、中位数。 日志格式如图类似&#xff0c;每一行日志开始部分就是所需要截取的时间&#xff1b;1条日记是以某些关键词作为开始&#xff0c;…

R语言自定义颜色

一、创建颜色梯度&#xff08;渐变色&#xff09; 在绘热图时&#xff0c;需要将数值映射到不同的颜色上&#xff0c;这时就需要一系列的颜色梯度colorRampPalette 函数支持自定义的创建一系列的颜色梯度。 代码示例&#xff1a; library(RColorBrewer)x <- colorRampPal…

从新能源汽车行业自动驾驶技术去看AI的发展未来趋势

自动驾驶汽车关键技术主要包括环境感知、精准定位、决策与规划、控制与执行、高精地图与车联网V2X以及自动驾驶汽车测试与验证技术等。 &#x1f413; 自动驾驶技术 这是AI在汽车行业中应用最广泛的领域之一。自动驾驶技术利用AI算法和传感器来感知环境、识别障碍物&#xff0c…

购买阿里云服务器如何选择实例?根据业务场景与细分场景推荐实例规格

对于很多初次购买阿里云服务器的用户来说&#xff0c;面对众多可选择的云服务器实例规格&#xff0c;往往不知道如何选择&#xff0c;不同实例规格适用于不同的业务场景&#xff0c;本文为大家汇总了不同业务场景和细分场景下应该选择的主要实例规格&#xff0c;以及这些实例规…

传统应急照明解决方案和新标准下地铁应急照明的方案区别

传统地铁站应急照明系统方案 传统地铁站应急照明系统一般设置2套或4套,给相应端区域的应急照明提供电源。由于地铁站应急照明系统设计比较成熟&#xff0c;几乎所有的地铁站接线方案均采用了经典的双电源切换加蓄电池逆变交流220/380V的配电方式.以南方某地铁站为例,此地铁站是…

YoloV8改进策略:Block改进|自研Block,涨点超猛|代码详解|附结构图

涨点效果 涨点效果:在我自己的数据集上,mAP50 由0.986涨到了0.993,mAP50-95由0.737涨到0.757,涨点明显! 参考模型 参考的Block,如下图: 我对Block做了修改,修改后的结构图如下: 代码详解 from timm.models.layers import DropPathfrom torch import Tensor def …

阿里云服务器使用教程_2024建站教程_10分钟网站搭建流程

使用阿里云服务器快速搭建网站教程&#xff0c;先为云服务器安装宝塔面板&#xff0c;然后在宝塔面板上新建站点&#xff0c;阿里云服务器网aliyunfuwuqi.com以搭建WordPress网站博客为例&#xff0c;来详细说下从阿里云服务器CPU内存配置选择、Web环境、域名解析到网站上线全流…

selenium 4.17正式发布,这几项更新值得关注

&#xff08;全文约1400字&#xff0c;阅读约需4分钟&#xff0c;首发于公众号&#xff1a;测试开发研习社&#xff0c;欢迎关注&#xff09; 两天前&#xff0c;selenium 正式发布了新版本 4.17&#xff0c;其中 python 版本的变化有更新更新值得关注 1. CDP 支持范围调整到 &…

117.龙芯2k1000-pmon(16)- linux下升级pmon

pmon的升级总是有些不方便&#xff0c;至少是要借助串口和串口工具 如果现场不方便连接串口&#xff0c;是不是可以使用网线升级pmon呢&#xff1f; 答案当然是可行的。 环境&#xff1a;2k1000linux3.10麒麟的文件系统 如今我已经把这个工具开发出来了。 GitHub - zhaozhi…

做抖店用平板能代替电脑操作吗?抖店运营相关注意事项,注意规避

我是王路飞。 之前给你们讲在抖音开店流程的时候&#xff0c;说过开店需要用到电脑&#xff0c;还需要执照、资金、时间等等。 那么做抖店用平板能代替电脑操作吗&#xff1f; 这个问题其实有很多新手问过我&#xff0c;有的甚至是想直接在手机上操作&#xff0c;想着能省点…

怎么对接迅雷网盘拉新项目?迅雷网盘怎么做才有效果?

自网盘拉新项目上线以来&#xff0c;网盘市场日益繁荣&#xff0c;各大厂商纷纷进军这一领域。头条网盘、悟空网盘、UC网盘、迅雷网盘等都成为了各个推广达人喜欢的推广项目。其中&#xff0c;迅雷网盘凭借其稳定的服务、强大的功能和广泛的用户基础&#xff0c;成为了市场中的…