Linux进程管理

Linux进程

在Linux系统中,进程是正在运行的程序的实例。每个进程都有一个唯一的进程ID(PID),并且可以包含多个线程。以下是关于Linux进程的一些重要信息:

  1. 进程状态:Linux中的进程可以处于不同的状态,包括运行(running)、等待(waiting)、停止(stopped)、僵尸(zombie)等。可以使用ps命令查看进程状态。

  2. 进程管理:可以使用ps命令查看系统中正在运行的进程,使用kill命令终止进程,使用tophtop命令查看系统资源占用情况。

  3. 进程优先级:Linux中的进程可以有不同的优先级,可以使用nice命令或renice命令来设置进程的优先级。

  4. 父子进程:在Linux中,进程可以通过fork系统调用创建子进程,子进程将继承父进程的大部分属性。

  5. 进程通信:Linux进程可以通过多种方式进行通信,包括管道、消息队列、共享内存、信号等。

  6. 进程调度:Linux内核使用调度算法来决定哪个进程在何时运行,以及运行多长时间。常见的调度算法包括先来先服务(FCFS)、最短作业优先(SJF)、时间片轮转等。

  7. 进程监控:可以使用pstophtop等命令来监控系统中的进程,查看资源占用情况,以及进行进程管理。

总的来说,Linux进程是系统中最基本的执行单元,了解进程的概念和管理方法对于理解和优化系统运行非常重要。

jammy:
1.进程:正在运行的程序。

操作系统根据进程会生成一个内核结构体,pcb结构体(存放的信息数据)。一个进程对应一个PCB结构体。操作系统只需通过此结构体就能找到对应的二进制指令让CPU运行。当多个程序被加载到内存时,就会生成多个PCB结构体,操作系统非常聪明,把这些结构体通过某种数据结构组织起来,这样操作系统可以非常方便的访问各个PCB结构体。

img

2.进程的状态

每个进程对应的由PID。会有相应的状态。(运行状态、阻塞状态、挂起状态)

  • 运行状态:cpu正在处理的进程。这句话本没有错,但我认为运行状态是指:进程的PCB结构体正在cpu的运行队列上排队

  • 阻塞状态:硬件都有一个等待队列,那么进程的PCB结构体被操作系统放在这个等待队列时,这个进程就处于阻塞状态

  • 挂起状态进程的代码和数据短期内置换到磁盘上,仅保留此进程的PCB。腾出的这一块空间供新的进程使用。这个动作,就叫做挂起。(为什么会出现挂起呐,内存满负荷时,又要增加新的进程显然是不行的。所以操作系统会观察内存中的哪些进程没有被放在任何一个队列里面(在内存里面什么活也不干的))。被挂起的进程,状态处于挂起状态。

3.Linux下是怎样的

1.肯定有进程。

2.进程的状态。

源码关于进程状态的描述是一个指针数组,其中各个字母的含义为:

R (Running):该进程正在运行中。 S (Sleep):该进程目前正在睡眠状态,但可以被唤醒。 D :不可被唤醒的睡眠状态,通常这个进程可能在等待I/O的情况。 T :停止状态,发送一个暂停信号给进程,进程就暂停了。 t :追踪停止状态,通常在断点调试时,进程处于此状态。 X :死亡状态,这个状态是用来告诉操作系统的,所以我们观察不到此状态。 Z (Zombie):僵尸状态,进程已经死亡,但是却无法被删除至内存外。 ———————————————------------------

Linux环境下观察进程的状态,我们可用的指令有两个:

 ps aux 或者 ps ajx              //  <==查看系统所有的进程
 ps -lA              //  <==也是能够查看系统的所有进程

输入指令观察一下:

img

STAT就是咱们要找的进程状态

*PID是进程的唯一标识符,这个标识符是操作系统给的,操作系统把PID放在进程的PCB中。*

现在,我们学会了最基本的查看进程。此时我们需要结合我们自己写的程序来进一步熟悉进程和Linux。

这段代码是一个简单的 C 程序,用于获取当前进程的进程 ID(PID)并将其打印出来。

  1. #include <stdio.h>:包含标准输入输出库,以便使用 printf 函数打印输出。

  2. #include <sys/types.h>:包含了一些系统数据类型的定义。

  3. #include <unistd.h>:包含了对 POSIX 操作系统 API 的访问,其中包含了获取进程 ID 的函数 getpid()

  4. int main():程序的入口函数。

  5. pid_t pid;:声明一个 pid_t 类型的变量 pid,用于存储进程 ID。

    在Linux系统中,pid_t 是一种数据类型,用来表示进程ID(Process ID)。它是定义在头文件 <sys/types.h> 中的类型别名,本质上通常是一个整型(integer type),用来存储进程的唯一标识符。

    pid_t 被定义为 typedef,其目的是为了提供一种类型,该类型可以保证在所有平台上都有足够的位数来存储任何进程的ID。在32位系统上,pid_t 可能是 int 类型,而在64位系统上,可能足够大以至于可以是一个 long 类型。

    通常,你会看到 pid_t 在以下场合中使用:

    • 当你想要获取当前进程的ID时,你会使用 getpid() 函数,它的返回类型就是 pid_t

    • 当你想要获取父进程的ID时,你会使用 getppid() 函数,它也返回一个 pid_t 类型的值。

    • 在进程控制函数,如 fork(), wait(), kill() 等中,进程ID都是使用 pid_t 类型来传递的。

  6. pid = getpid();:调用 getpid() 函数获取当前进程的进程 ID,并将其赋值给 pid 变量。

  7. printf("my pid is %d\\n", pid);:使用 printf 函数打印输出当前进程的进程 ID,%d 是格式化字符串,用于输出整数。

  8. return 0;:返回程序执行的结果,0 表示程序成功执行。

注释代码如下:

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 ​
 int main()
 {
     pid_t pid; // 声明一个变量用于存储进程 ID
 ​
     pid = getpid(); // 获取当前进程的进程 ID //调用 `getpid()` 函数获取当前进程的进程 ID
     while(1);
     printf("my pid is %d\\n", pid); // 打印当前进程的进程 ID
 ​
     return 0; // 返回程序执行结果
 }

1.运行此程序,观察此进程的状态。
 ps ajx | head -1 && ps ajx | grep demo1

我们自己的程序处于运行状态啦!并且拥有自己的PID。但是我们看到,还有一个叫做PPID的东西,这个PPID是我们的进程创建时所依赖的进程的标识符

观察PPID谁的标识。

 ps ajx | head -1 && ps ajx | grep 2455

可以看到,这个2437的PID是bash进程的标识符,这个bash进程也有一个PPID。

bash是什么?我们可以理解为命令输入行。

这个命令行输入也就是让我们输入指令的地方,是不是我们一打开Linux就有了?所以,bash是当我们登录Linux操作系统时,操作系统会自动获取一个bash的shell,那么此时bash就被加载到内存里面去啦!所以bash是登录操作系统时自动加载到内存里的进程。

在这个bash进程的基础上,我们才可以输入指令去让Linux完成某些操作,所以,指令的运行是依赖bash的。那么这种依赖关系,我们称作[父子进程],指令需要依赖bash,所以指令称为[子进程],bash称为[父进程]。同理,我们在命令输入行输入[ls]这个指令,ls是子进程,bash是父进程。 ————————————————

当修改代码

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 ​
 int main()
 {
         pid_t pid;
 ​
         pid = getpid();
 ​
         while(1){
 ​
         printf("my pid is %d\n",pid);
         }
 ​
         return 0;
 }
 ​

我们明明运行程序了啊!为什么是S状态!而不是R状态!

实际上这两种状态都有!我们要清楚一个概念,硬件的处理速度有cpu快吗?当然没有!cpu处理指令的单位时间是纳秒!所以可以打一个比方,执行一个程序要的时间为100秒,那么这个进程在硬件的等待队列当中就可能待了99.99秒,所以从概率上来看,我们的test进程是S状态

还有一个D状态,我在这里浅浅的普及一下:D状态是平时使用时几乎不会碰到的东西。D状态是一个深度睡眠的状态,通常发生在与硬件的交互中。一旦出现了D状态的进程,就相当于这个操作系统无法正常执行任务了。解决的办法只有两种:一是等这个进程处理完,而是掉电重启。

4.Linux进程管理

进程之间是可以互相控制的。最典型的例子就是打开某个软件、关闭某个软件,我们无法直接隔着屏幕大喊一声:”我要关掉你!“这个软件就会关闭掉,而是需要通过一个进程给这个软件(也是进程)一个关闭信号,这样就形成了进程的互相控制。

刚才提到了一个操作,叫做使用kill指令杀掉进程。实际上在以前,电脑还很贵的时候,那时候玩游戏都得看脸,经常各种程序死机,然后都会统一的调出任务管理器强制结束进程: img

Linux也有这样的操作,就是使用kill指令:

 kill -9 [PID]        <==通过PID去杀掉进程

[-9]是什么?它是一个信号,这个信号代表的意思为强制中断一个进程的执行,也就是从内存当中删除这个进程。那么我在这里列举几个常用的控制信号,供大家作为参考:

代号名称内容
9SIGKILL强制删除一个进程
18SIGCONT继续一个进程的运行
19SIGSTOP暂停一个进程的运行

命令输入的格式,我们以杀掉某个进程举例,还可以这么写:

 kill -SIGKILL [PID]                <==与上面的效果等价

展示一下,kill命令。

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 ​
 int main()
 {
         pid_t pid;
 ​
         pid = getpid();
 ​
         while(1){
 ​
         printf("my pid is %d\n",pid);
         }
 ​
         return 0;
 }
 ​

此时进程停止。

继续进程,删除进程。

可以看到,随着不同的信号输入,进程的状态也随之变化。

那么我们可以向进程发送多少种信号?我们可以通过这个指令来查看一共有多少种信号:

 kill -l                <==l是小写的L,查看所有的信号

我们还可以发现一个非常麻烦的东西:因为[kill]这个指令是专门针对进程的PID的,所以每次使用[kill]命令时都得配合[ps]命令来查看要操作的进程的PID。那有没有什么办法,我们直接操作进程的名称而不是它的PID呢?——[killall]命令。我们使用[killall]命令重复一遍上面的操作:

是不是方便许多呢?

1.创建子进程fork

在Linux系统中,fork() 是一个系统调用,用于创建一个新的进程,新进程是调用进程的副本。fork() 系统调用会在当前进程的地址空间中创建一个新的进程,新进程会继承父进程的内存映像、文件描述符、信号处理等信息。

fork() 系统调用的原型如下:

 #include <unistd.h>
 ​
 pid_t fork(void);
  • 如果 fork() 调用成功,它将在父进程中返回子进程的进程ID(PID),在子进程中返回0。

  • 如果 fork() 调用失败,它将返回一个负值。

下面是一个简单的示例,演示了如何使用 fork() 创建一个子进程:

 #include <stdio.h>
 #include <unistd.h>
 ​
 int main() {
     pid_t pid = fork();
 ​
     if (pid == -1) {
         // fork() 失败
         perror("fork");
         return 1;
     } else if (pid == 0) {
         // 子进程执行这里
         printf("This is the child process, PID: %d\\n", getpid());
     } else {
         // 父进程执行这里
         printf("This is the parent process, child PID: %d\\n", pid);
     }
 ​
     return 0;
 }

在上面的示例中,调用 fork() 后,父进程会得到子进程的PID,而子进程会得到0。这样,父子进程可以通过返回值的不同来区分自己是父进程还是子进程,从而执行不同的逻辑。

需要注意的是,fork() 创建的子进程会复制父进程的内存空间,包括数据段、堆、栈等。因此,父进程和子进程之间的数据是相互独立的,它们各自有自己的内存空间。(首先,子进程是完全复制父进程的,所以num的地址是一样的,可是子进程复制的是虚拟地址空间,而非物理空间。如果,子进程和父进程对变量只读,也就是说变量不会被改变,这时候,变量表现为共享的,此时物理空间只有一份。如果说父进程或者子进程需要改变变量,那么进程将会对物理内存进行复制,这个时候变量是独立的,也就是说,物理内存中存在两份空间。

在使用 fork() 系统调用时,操作系统会创建一个新的子进程,该子进程是父进程的副本,包括代码段、数据段、堆栈等。下面是 fork() 调用的一般流程以及父子进程的运行情况:

  1. 调用 fork() 后,操作系统会复制父进程的内存空间,并将其分配给子进程。

  2. 父子进程都会继续执行 fork() 调用之后的代码,但是它们会在不同的地址空间中运行。

  3. fork() 调用成功后,父进程会得到子进程的进程 ID(PID),而子进程得到的返回值为 0。

  4. 父子进程会并发运行,操作系统会根据调度算法决定它们的执行顺序。

  5. 父子进程之间的主要区别在于它们的 PID 不同,以及在某些情况下,父子进程会有不同的资源限制。

  6. 父子进程共享代码段,但各自有独立的数据段和堆栈,因此它们可以独立地运行和修改变量值。

总的来说,fork() 调用成功后会创建一个父进程和一个子进程,它们在各自的地址空间中运行,但共享相同的代码段。父子进程之间可以通过进程间通信机制来进行数据交换和同步操作。在实际编程中,通常会根据 fork() 的返回值来区分父子进程,从而执行不同的逻辑。

fork()使用场景:

(1)一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当请求到达时使用fork函数,使子进程处理此请求。父进程则继续下一个服务请求到达。

(2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

jammy:

fork()函数调用

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 ​
 int main()
 {
   pid_t pid;
   pid = getpid();
   fork();
   printf("my pid is %d\n",pid);
   return 0;
 }
 ​

为什么出现两次,

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 ​
 int main()
 {
      pid_t pid;
      fork();
      pid = getpid();
      printf("my pid is %d\n",pid);
      return 0;
 }
 ​

为什么两次不一样,看来不是同一个进程

那么哪一个是自身进程的呐

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 ​
 int main()
 {
      pid_t pid;
      pid = getpid();
      fork();
      printf("befor pid is %d,this is pid%d\n",pid,getpid());
      return 0;
 }
 ~        

打印的第一个是“自身的”进程

打印的第二个是“产生的”进程

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 ​
 int main()
 {
      pid_t pid;
      pid_t pid1;
      pid = getpid();
      printf("fork befor pid is %d\n",pid);
      fork();
      pid1 = getpid();
      printf("fork after pid is %d\n",pid1);
      if(pid == pid1){ //如果重新获取的pid和之前的pid一样,就是父进程。
      printf("this is Parent process.\n");
      }else{
      printf("this is Child process.\n");
      }
      return 0;
 }
 ​

于是乎:父子进程产生了。并且fork()之前的不会变,fork之后开始不同。


那么如何在代码中把父子进程彻底的清晰的分辨出呐,根据fork()返回值。

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 ​
 int main()
 {
      pid_t pid;
      printf("fork befor pid is %d\n",getpid());
      pid = fork();//fork`return
      if(pid > 0){
              printf("this is parent process.pid:%d\n",getpid());
      }else if(pid == 0){
              printf("this is child process.pid:%d\n",getpid());
      }
 ​
      return 0;
 }
 ​

父子进程的运行,严格来讲(由进程调度来决定)

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 ​
 int main()
 {
      pid_t pid;
      printf("fork befor pid is %d\n",getpid());
      pid = fork();//fork`return
      if(pid > 0){ //父进程
              printf("this is parent process.pid:%d\n",getpid());
      }
      }
 ​
      return 0;
 }
 ;#include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 ​
 int main()
 {
      pid_t pid;
      printf("fork befor pid is %d\n",getpid());
      pid = fork();//fork`return
      if(pid == 0){  //子进程
              printf("this is child process.pid:%d\n",getpid());
      }
 ​
      return 0;
 }

父子进程的产生和内存分配

存储空间的分配:对于老版Linux,是全部复制。对于新版Linux,是对不改变值得采用共享,当改变的时候复制。


fork()应用一,模拟客户端

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 ​
 int main()
 {
      pid_t pid;
      int data;
      while(1){
      printf("please put your data.\n");
      scanf("%d",&data);
      if(data == 1){
          pid = fork();  //创建父子进程·
          if(pid > 0){ //父进程
              printf("my:%d,child:%d\n",getpid(),pid); 打印父进程pid,并且它的返回值(新的子进程的pid)
          }else if(pid == 0){ //child   模拟客户端
          while(1){   
          printf("pid:%d\n",getpid());
          sleep(1);
          }
          }
      }else{
      printf("faild!\n");
      }
      }
      return 0;
 }
 ~                //循环嵌套   一个父进程,多个子进程

2.创建子进程vfork

vfork函数是类似于fork函数的系统调用,在Unix/Linux系统中用于创建新进程。vfork函数与fork函数的主要区别在于创建子进程时的行为方式不同。下面是对vfork函数的讲解:

  1. vfork函数的特点

    • vfork函数创建的子进程与父进程共享内存空间,即子进程会在父进程的地址空间中运行,而不会像fork函数那样复制父进程的地址空间。

    • vfork函数创建的子进程中,子进程会先执行,直到调用exec系列函数或者调用_exit函数为止,子进程才会释放父进程的地址空间并运行新的程序。

    • 父进程在vfork函数创建子进程期间会被阻塞,直到子进程调用exec系列函数或者_exit函数,或者父进程显式调用exit函数为止。

  2. vfork函数的使用场景

    • vfork函数通常用于创建一个新进程,该新进程会立即调用exec函数来加载并执行另一个程序,而不需要执行其他操作。

    • vfork函数适合用于创建临时进程,且子进程需要立即执行一个新程序的情况。

  3. 注意事项

    • 在使用vfork函数时,需要确保子进程不会修改父进程的地址空间中的数据,否则可能导致未定义的行为。

    • 子进程在调用exec函数或_exit函数之前,应当避免访问父进程的内存空间,以免造成数据混乱或错误。

总之,vfork函数与fork函数相比,更适合用于创建临时进程并立即执行一个新程序的场景,但需要注意子进程对父进程内存空间的访问和修改,以避免潜在的问题。在实际应用中,根据具体需求选择合适的创建进程的方式。

Linux进程 fork与vfork 区别

在Linux系统中,除了fork()外,还有vfork()这个系统调用,它也用于创建一个新的进程。vfork()fork()的主要区别在于子进程对父进程地址空间的使用方式。

  1. fork()

    • fork()系统调用会创建一个子进程,子进程会复制父进程的地址空间,包括数据段、堆、栈等,父子进程之间是相互独立的。

    • 子进程在执行fork()后会复制父进程的地址空间,因此子进程可以修改自己的内存副本而不会影响父进程。

    • fork()是比较安全的,但是由于需要复制父进程的地址空间,可能会比较耗时。

  2. vfork()

    • vfork()系统调用也会创建一个子进程,但是子进程会共享父进程的地址空间,子进程会在父进程的地址空间中运行直到调用execexit

    • vfork()中,子进程会暂时共享父进程的地址空间,因此子进程不应该修改父进程的内存数据,否则会导致未定义的行为。

    • vfork()fork()更高效,因为子进程不需要复制父进程的地址空间,但是需要注意子进程在调用execexit之前不应该对内存进行写操作。

总的来说,vfork()适用于需要在子进程中立即调用exec的情况,因为它避免了不必要的内存复制。而fork()适用于需要在子进程中继续执行一些逻辑的情况。在实际编程中,根据具体需求选择合适的系统调用是很重要的。

jammy:

fork运行情况:父子进程同时运行,互不打扰。(进程调度来决定)

vfork运行情况:子进程运行结束,父进程开始,并且共用内存。

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <stdlib.h>
 ​
 int main()
 {
   pid_t pid;
   pid_t pid2;
   int data = 0;
 ​
   pid = vfork();
   if(pid > 0){
           while(1){
           printf("data=%d\n",data);
           printf("this is father printf id:%d\n",getpid());
           sleep(1);
           }
   }else if(pid == 0){
           while(1){
           printf("this is child printf id:%d\n",getpid());
           sleep(1);
           data++;
           if(data == 3){
                   exit(0);//针对vfork
           }
           }
   }
 ​
   return 0;
 }
 ​
 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <stdlib.h>
 ​
 int main()
 {
      pid_t pid;
      pid_t pid1;
      pid = vfork();
      int data = 0;
      if(pid > 0){
      //      printf("data:%d\n",data);
              while(1){
              printf("data:%d\n",data);
              printf("this is far pid:%d\n",getpid());
              sleep(1);
              }
      }else if(pid == 0){
              while(1){
              printf("this is child pid:%d\n",getpid());
              sleep(1);
              data++;
              if(data == 3){
              exit(0);
              }
              }
      }
 ​
 ​
 ​
    return 0;
 ​
 }
 ​

3.退出进程

进程退出是指一个进程结束其执行并释放其占用的系统资源的过程。在Unix/Linux系统中,可以使用 exit() 系统调用来终止一个进程的执行。

在C语言中,可以通过调用 exit() 函数来退出一个进程。exit() 函数接受一个整数参数,表示进程的退出状态码。通常,0 表示进程正常退出,非零值表示进程异常退出。例如,可以使用以下方式退出进程:

 #include <stdlib.h>
 ​
 int main() {
     // 进行一些操作
     // ...
 ​
     // 退出进程,返回状态码为0表示正常退出
     exit(0);
 ​
     // 注意:exit() 函数之后的代码不会执行
 }

除了调用 exit() 函数外,进程还可以通过返回一个值来退出。在 main 函数中,如果没有显式调用 exit() 函数,当 main 函数执行结束时,进程会自动退出,并将 main 函数中的返回值作为进程的退出状态码。例如:

 #include <stdio.h>
 ​
 int main() {
     // 进行一些操作
     // ...
 ​
     // 返回状态码为0表示正常退出
     return 0;
 }

无论是调用 exit() 函数还是通过返回值退出,进程结束时都会释放其占用的资源,并向其父进程发送一个终止信号,告知父进程其结束状态。父进程可以通过 wait()waitpid() 等系统调用来获取子进程的退出状态(退出状态需要解析),进而进行相应的处理。

jammy:
 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <stdlib.h>
 ​
 int main()
 {
   pid_t pid;
 pid_t pid2;
 int data = 0;
 ​
 pid = vfork();   //v
 ​
 if(pid > 0){
        while(1){
        printf("data=%d\n",data);
        printf("this is father printf id:%d\n",getpid());
        sleep(1);
           }
   }else if(pid == 0){
           while(1){
           printf("this is child printf id:%d\n",getpid());
           sleep(1);
           data++;
           if(data == 3){
                   _Exit(0);
           }
           }
   }
 ​
   return 0;
 ​
 ammy@jammy-virtual-machine:~$ ps -aux|grep a.out
 jammy      11666  0.0  0.0   2776  1408 pts/0    S+   20:45   0:00 ./a.out
 jammy      11667  0.0  0.0      0     0 pts/0    Z+ //僵尸进程  20:45   0:00 [a.out] <defunct>
 jammy      11672  0.0  0.0  17872  2816 pts/1    S+   20:45   0:00 grep --color=auto a.out
 ​

子进程退出,返回值没有被收集,变成僵尸进程。

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <stdlib.h>
 ​
 int main()
 {
   pid_t pid;
 pid_t pid2;
 int data = 0;
 ​
 pid = fork();   //
 ​
 if(pid > 0){
        while(1){
        printf("data=%d\n",data);
        printf("this is father printf id:%d\n",getpid());
        sleep(1);
           }
   }else if(pid == 0){
           while(1){
           printf("this is child printf id:%d\n",getpid());
           sleep(1);
           data++;
           if(data == 3){
                   _Exit(0);
           }
           }
   }
 ​
   return 0;
 ​

同样,子进程退出,返回值没有被收集,变成僵尸进程。

1.父进程等待子进程

父进程等待子进程的主要原因是为了避免僵尸进程的产生,同时也可以获取子进程的退出状态码。下面具体解释一下:

  1. 避免僵尸进程:当子进程退出时,系统会保留子进程的一些信息(如进程号、退出状态码等),但是并不会立即释放子进程占用的资源。如果父进程不等待子进程,那么子进程的信息就会一直保留在系统中,形成僵尸进程。僵尸进程会占用系统资源,如果过多积累会导致系统资源耗尽。

  2. 获取子进程的退出状态码:父进程通过等待子进程可以获取子进程的退出状态码,进而了解子进程的运行情况。退出状态码通常用来表示子进程是正常退出还是异常退出,以及具体的退出原因。

父进程等待子进程的一般流程如下:

  • 父进程创建子进程后,通过系统调用 wait()waitpid() 等来等待子进程退出。

  • 当子进程退出时,父进程会收到子进程退出的信号,可以通过这个信号获取子进程的退出状态码。

  • 父进程根据子进程的退出状态码来决定后续的操作,比如处理子进程的输出结果、记录日志、重新启动子进程等。

总之,父进程等待子进程是为了保证程序的正常运行,避免资源泄漏和获取子进程的退出状态信息。

检查wait所返回的终止状态的值

在父进程中使用 wait()waitpid() 等系统调用等待子进程退出时,可以通过以下方式检查返回的终止状态值:

  1. 检查是否正常退出:通过宏 WIFEXITED(status) 判断子进程是否正常退出,如果返回真,则可以使用 WEXITSTATUS(status) 获取子进程的退出状态码。

    if (WIFEXITED(status)) { int exit_status = WEXITSTATUS(status); printf("子进程正常退出,退出状态码为:%d\n", exit_status); }

  2. 检查是否异常退出:通过宏 WIFSIGNALED(status) 判断子进程是否异常退出,如果返回真,则可以使用 WTERMSIG(status) 获取导致子进程异常退出的信号编号。

    if (WIFSIGNALED(status)) { int signal_number = WTERMSIG(status); printf("子进程异常退出,导致退出的信号编号为:%d\n", signal_number); }

  3. 其他状态检查:还可以通过其他宏来检查子进程的其他状态,比如是否被暂停、继续等。具体可以参考系统头文件 sys/wait.h 中定义的宏。

    WIFEXITED(status):用于检查子进程是否正常退出。如果子进程是正常退出的,则该宏返回非零值。

    WEXITSTATUS(status):如果子进程是通过正常退出方式终止的(即 WIFEXITED(status) 返回非零),则可以使用该宏来获取子进程的退出状态。它返回子进程调用 exit()_exit() 时传递的退出状态值。

    WIFSIGNALED(status):用于检查子进程是否因为信号而终止。如果子进程是因为信号而终止的,则该宏返回非零值。

    WTERMSIG(status):如果子进程是因为信号而终止的(即 WIFSIGNALED(status) 返回非零),则可以使用该宏获取导致子进程终止的信号编号。

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <stdlib.h>
 ​
 int main()
 {
      pid_t pid;
   pid_t pid2;
   int data = 0;
 ​
   pid = fork();   //
 ​
   if(pid > 0){
           while(1){
           wait(NULL);  //接受一下,(子进程)的退出状态先不存放
           printf("data=%d\n",data);
           printf("this is father printf id:%d\n",getpid());
           sleep(1);
              }
      }else if(pid == 0){
              while(1){
              printf("this is child printf id:%d\n",getpid());
              sleep(1);
              data++;
              if(data == 3){
                      _Exit(0);
              }
              }
      }
 ​
      return 0;
 ​

子进程的退出状态被接收,自然释放。

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <stdlib.h>
 ​
 int main()
 {
      pid_t pid;
   pid_t pid2;
   int data = 0;
   int status;
   pid = fork();   //
 ​
   if(pid > 0){
           while(1){
           wait(&status);  //接受一下,存放(子进程)的退出状态
           printf("status:%d data=%d\n",data,WIFEXITED(status));  //接受的退出状态需要用WIFEXITED(status)解析
           printf("this is father printf id:%d\n",getpid());
           sleep(1);
              }
      }else if(pid == 0){
              while(1){
              printf("this is child printf id:%d\n",getpid());
              sleep(1);
              data++;
              if(data == 3){
                      _Exit(0);
              }
              }
      }
 ​
      return 0;
 ​

虚假预期

草台班子

综上所述,通过检查终止状态的值,父进程可以根据子进程的不同退出状态做出相应的处理,从而更好地管理子进程的生命周期。

waitpid() 函数

wait() 函数类似,都是用于等待子进程结束并获取其状态,但 waitpid() 具有更多的灵活性。它可以指定等待哪个子进程结束,以及是否阻塞父进程等待。

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <stdlib.h>
 ​
 int main()
 {
      pid_t pid;
   pid_t pid2;
   int data = 0;
   int status;
   pid = fork();   //
 ​
   if(pid > 0){
           while(1){
           waitpid(pid,&status,WNOHANG); //子进程pid 存放一下 参数
           printf("status:%d data=%d\n",data,WIFEXITED(status));  //接受的退出状态需要用WIFEXITED(status)解析
           printf("this is father printf id:%d\n",getpid());
           sleep(1);
              }
      }else if(pid == 0){
              while(1){
              printf("this is child printf id:%d\n",getpid());
              sleep(1);
              data++;
              if(data == 3){
                      _Exit(0);
              }
              }
      }
 ​
      return 0;
 ​

父子进程一起走,子进程接收后,父进程依然接受退出符,

父进程使用 waitpid(pid, &status, 0) 来等待特定的子进程结束,其中 pid 是要等待的子进程的进程ID,status 是存储子进程退出状态的变量,最后一个参数是选项,0 表示父进程会阻塞直到指定的子进程结束。如果不想阻塞父进程,可以传入 WNOHANG 标志。

孤儿进程

孤儿进程是指其父进程先于它结束,从而使得操作系统将其父进程的进程组号设置为1(通常情况下由 init 进程接管)。因为每个进程都必须有一个父进程,如果父进程先于子进程结束,操作系统将会将孤儿进程的父进程设置为 init 进程(在 Unix 系统中通常是进程号为1的 init 进程),这样 init 进程就会成为孤儿进程的新父进程。

孤儿进程的特点如下:

  1. 孤儿进程会被 init 进程收养,init 进程会负责对孤儿进程进行清理,以避免其成为僵尸进程。

  2. 孤儿进程的父进程 ID 变为 1。

  3. 孤儿进程会继承操作系统的资源,如文件描述符等。

  4. 当孤儿进程结束时,会向其父进程发送 SIGCHLD 信号,通知其父进程子进程的结束状态,父进程可以通过处理该信号来执行相关清理操作。

孤儿进程的一个常见场景是,父进程创建一个子进程,然后父进程退出,而子进程仍在运行。这样子进程就成为了孤儿进程。

以下是一个简单的示例说明孤儿进程的概念:

 #include <stdio.h>
 #include <stdlib.h>
 #include <sys/types.h>
 #include <unistd.h>
 ​
 int main() {
     pid_t pid = fork();
 ​
     if (pid < 0) {
         perror("fork");
         exit(EXIT_FAILURE);
     } else if (pid == 0) {
         // 子进程
         printf("Child process (PID: %d) is running, parent PID: %d\\n", getpid(), getppid());
         sleep(2); // 模拟子进程执行
         printf("Child process (PID: %d) is exiting\\n", getpid());
         exit(EXIT_SUCCESS);
     } else {
         // 父进程
         printf("Parent process (PID: %d) is exiting\\n", getpid());
         exit(EXIT_SUCCESS);
     }
 ​
     return 0;
 }

在这个示例中,父进程创建了一个子进程,然后父进程立即退出,而子进程继续执行。此时子进程成为了孤儿进程,其父进程 ID 变为1(即 init 进程)。

4.exec函数

在Linux系统中,exec()函数是用于执行一个新的程序的系统调用。当调用exec()函数时,当前进程的地址空间会被新程序的代码和数据替换,从而使得当前进程成为了一个全新的程序。exec()函数通常与fork()vfork()配合使用,用于在子进程中执行新的程序。

exec()函数有几种不同的变体,包括:

  1.  int execve(const char *filename, char *const argv[], char *const envp[])

    :这是最通用的exec()函数,它接受一个文件名(要执行的程序的路径)、参数列表和环境变量列表。它会加载并执行指定的可执行文件。

  2.  int execl(const char *path, const char *arg, ...)

    :这个函数接受一个文件路径和一系列以空指针结尾的参数,用于指定要执行的程序和传递给程序的参数。

  3.  int execvp(const char *file, char *const argv[])

    :这个函数接受一个文件名和参数列表,会在$PATH环境变量指定的路径中查找可执行文件并执行。

  4.  int execlp(const char *file, const char *arg, ...)

    :这个函数接受一个文件名和一系列以空指针结尾的参数,会在$PATH环境变量指定的路径中查找可执行文件并执行。

当调用exec()函数时,当前进程的代码和数据会被替换为新程序的代码和数据,但是进程的PID和文件描述符等信息会被保留。这使得exec()函数可以在不创建新进程的情况下替换当前进程的执行内容。

需要注意的是,一旦调用了exec()函数,当前进程的代码、数据和堆栈都会被替换,因此exec()函数调用后面的代码将不会被执行。通常在fork()之后,子进程会调用exec()来执行新程序,这样可以实现进程的替换和程序的重载。

1.Linux进程,exec函数配合fork函数使用

在Linux系统中,exec()函数通常与fork()函数结合使用,以实现进程的替换和程序的执行。下面是一个简单的示例代码,演示了如何在子进程中使用exec()函数来执行一个新程序:

 #include <stdio.h>
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 ​
 int main() {
     pid_t pid = fork();
 ​
     if (pid == -1) {
         // fork失败
         perror("fork");
         return 1;
     } else if (pid == 0) {
         // 子进程
         char *args[] = {"/bin/ls", "-l", NULL};
         execv("/bin/ls", args);
         
         // 如果execv执行成功,这里的代码不会被执行
         perror("execv");
         return 1;
     } else {
         // 父进程
         wait(NULL); // 等待子进程结束
         printf("Child process finished.\\n");
     }
 ​
     return 0;
 }

在上面的代码中,首先调用fork()函数创建一个子进程。子进程中使用execv()函数来执行/bin/ls命令,显示当前目录的文件列表。父进程则等待子进程执行完毕。

需要注意的是,子进程中调用exec()函数后,如果执行成功,子进程的代码段将被替换,因此后续的代码不会被执行。如果exec()函数执行失败,会返回-1,并且会设置errno变量以指示错误原因。因此,在调用exec()函数后通常会检查返回值,以确保执行成功。

5.system函数

在Linux系统中,system()函数可以用来执行shell命令。system()函数会创建一个子进程,在子进程中调用/bin/sh -c来执行指定的命令。下面是一个简单的示例代码,演示了如何使用system()函数执行一个shell命令:

 #include <stdlib.h>
 ​
 int main() {
     int ret = system("ls -l");
 ​
     if (ret == -1) {
         perror("system");
         return 1;
     }
 ​
     return 0;
 }

在上面的代码中,system("ls -l")会执行ls -l命令,显示当前目录的文件列表。system()函数会返回命令的退出状态码,如果执行成功则返回命令的退出码,如果执行失败则返回-1。

需要注意的是,system()函数是一个比较简单方便的方法来执行shell命令,但在某些情况下可能会存在安全风险,因为它直接调用shell来执行命令。为了避免潜在的安全问题,最好使用exec()系列函数来执行特定的程序,而不是直接调用system()函数。

6.popen函数

在Linux系统中,popen()函数可以用来创建一个进程来执行一个shell命令,并建立一个到该进程的管道。通过这个管道,可以向子进程发送数据(输入),并从子进程接收数据(输出)。

下面是一个简单的示例代码,演示了如何使用popen()函数执行一个shell命令并读取其输出:

 #include <stdio.h>
 ​
 int main() {
     FILE *fp;
     char buffer[1024];
 ​
     fp = popen("ls -l", "r");
     if (fp == NULL) {
         perror("popen");
         return 1;
     }
 ​
     while (fgets(buffer, sizeof(buffer), fp) != NULL) {
         printf("%s", buffer);
     }
 ​
     pclose(fp);
 ​
     return 0;
 }

在上面的代码中,popen("ls -l", "r")会执行ls -l命令,并返回一个文件指针fp,通过这个文件指针可以读取子进程的输出。然后,我们使用fgets()函数逐行读取子进程的输出,并将其打印到标准输出中。最后,使用pclose()函数关闭管道。

需要注意的是,popen()函数同样存在一些安全风险,因为它也是通过shell来执行命令。在实际应用中,需要谨慎处理输入的命令,以避免潜在的安全问题。

  • 34
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值