【Linux】进程的创建、退出、等待和替换

一. 进程创建 — fork

1. 什么是fork()函数?

头文件:#include <unistd.h>
函数原型:pid_t fork(void);

作用:从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

返回值:给子进程中返回0,父进程返回子进程的pid,创建失败返回-1。

当一个进程调用fork()之后,就有代码完全相同的进程。而且它们都运行到相同的地方。通过判断fork()的返回值并配合if语句可以让父子进程分流,执行各自的代码,看如下程序:
在这里插入图片描述

编译运行:
$ ./myproc
child pid is 1645, fork return 0
father pid is 1000, fork return 1645

2. fork函数的作用

fork()函数是存在于内核空间的,进程调用fork(),控制转移到内核中的fork()代码,内核完成如下任务:

  1. 分配新的内存块(物理空间)和内核数据结构(PCB、页表、虚拟地址空间等)给子进程。
  2. 将父进程大部分数据结构的内容拷贝给子进程(采用写实拷贝,为了节省物理空间)。
  3. 添加子进程到系统进程列表当中(到这步时子进程已经创建成功)。
  4. fork()返回,开始调度器调度,先返回谁由调度器决定。

在这里插入图片描述

3. fork()函数补充

为何给子进程返回0,给父进程返回子进程的pid?

在现实生活中,父亲:孩子 = 1:n,即一个父亲可以有多个孩子,但一个孩子只能有一个父亲。父亲的多个孩子在一起时,父亲会具体叫某个孩子的名字,这样这个孩子才会知道父亲在叫自己;但所有孩子都只会叫他们的父亲爸爸。

进程也一样,一个父进程有多个子进程,每个子进程要执行父进程交给它们的任务,想要知道子进程执行的怎么样,父进程必须明确区分每个子进程,所以必须得到它们的pid,即子进程必须要被父进程特殊标识,而父进程不需要被子进程特殊标识。

子进程从哪里开始运行?

fork()之后,父进程继续往后运行,而子进程也是从fork()之后的位置开始运行,谁先运行有调度器决定。当然子进程也跟父进程代码是共用同一份的,只是子进程不执行fork()之前的代码罢了。
在这里插入图片描述

什么是写实拷贝?

通常,父子代码共享,当二者都不对代码里的共用数据写入时,数据也是共享的,当任意一方试图写入,操作系统会另外给要写入的数据再开辟一块空间,并更新页表的映射关系。
在这里插入图片描述
在这里插入图片描述

fork()使用场景

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个与父进程毫不相干的程序。例如子进程从fork()返回后,调用exec函数。

fork()调用失败的原因

  • 系统中进程总的数量过多。
  • 实际用户的可创建进程数超过了限制。

二. 进程退出

1. 进程退出的场景

在这里插入图片描述

1.1 正常退出

  • 在main()函数中执行return代表进程的退出。其他普通函数的return不算。
  • 任意位置调用 exit() 或 _exit(),都表示退出当前进程。

PS:通过进程的返回值(也叫作退出码)判断运行结果是否正确,一般规定返回0表示正确,非0表示错误。

我们可以通过命令:echo $? 来查看最近一次进程运行结果的退出码。
在这里插入图片描述

1.2 异常退出

进程收到某个信号,而该信号使程序终止。比如下面程序,我们有进行野指针的访问,编译器检查到后会报告给操作系统,之后系统发送段错误信号并终止进程:
在这里插入图片描述
PS:进程如果是异常退出,那么它的退出码是没有任何意义的。

2. exit 和 _exit

下面我们讨论进程正常退出时的其中两种方式,即exit和_exit,他们是两个不同的函数。

2.1 函数介绍

_exit函数

头文件:#include <unistd.h>
原型:void _exit(int exit_code);

作用:直接终止整个进程。

参数:进程的退出码。

exit函数

头文件:#include <unistd.h>
原型:void exit(int status)

作用:先执行用户通过 atexit或on_exit定义的清理函数,然后刷新缓冲区,最后调用_exit来终止整个进程。

参数:进程的退出码。

2.2 二者的区别

exit()函数的底层最终还是会调用_exit()来终止整个进程,不过在这之前会完成一些该进程相关清理工作。
在这里插入图片描述
通过一段代码感受一下,_exit因为没有刷新缓冲区,所以什么都没输出。
在这里插入图片描述

三. 进程等待

1. 什么是进程等待

子进程想要完全退出,最后必须由父进程调用wait、waitpid函数来等待子进程退出,回收资源和获取子进程退出状态。

2. 为什么要有进程等待

子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而出现内存泄漏,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。而且,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。

总结进程等待的作用有两个:

  1. 回收子进程资源,防止内存泄漏。
  2. 获取子进程的退出状态。

3. 如何完成进程等待

有两个函数可以完成进程等待:wait和waitpid,它们两个都属于系统调用函数。

3.1 函数介绍

wait

头文件:#include<sys/types.h> 和 #include<sys/wait.h>
原型:pid_t wait(int* status)

参数:输出型参数,获取任意一个子进程退出状态,不关心则可以设置成为NULL。

返回值:等待成功(包括子进程正常和异常退出)返回被等待进程pid、子进程还没结束就继续等、等待失败返回-1(进程不存在)。

waitpid

头文件:#include<sys/types.h> 和 #include<sys/wait.h>
原型:pid_ t waitpid(pid_t pid, int *status, int options)

参数:

  1. pid:指明要等待的子进程的pid,当设置为-1时代表等待任意子进程(此时与wait等效)。
  2. status:子进程的状态码,从中可以得到子进程的退出情况(正常退出还是异常退出)和退出码。不关心的话可以设置为NULL。
  3. options:当其为0时代表阻塞式等待,为WNOHANG时,为非阻塞式等待(检测到子进程还未退出,会返回0)。

返回值:

  1. 等待成功(子进程正常或异常退出)返回被等待进程的pid。
  2. 子进程若还没退出就继续等或者返回0,这取决于options是阻塞式等待或非阻塞式等待。
  3. 等待失败(子进程不存在)返回-1。

总结:waitpid就是wait的升级版,它相比于wait而言可以指定要等待那个子进程(wait是等待任意一个子进程)和可以实现非阻塞式等待(wait只能阻塞式等待)。

3.2 状态码

3.2.1 状态码的基本认识

wait和waitpid,都有一个status参数,即子进程的退出状态码,该参数是一个输出型参数,由操作系统赋值。如果传递NULL,表示不关心子进程的退出信息,否则操作系统会把这些子进程的退出信息(包括信号和退出码)通过status这个输出型参数反馈给父进程。
在这里插入图片描述
status不能简单的当作整形,而应当作位图来看待,我们只研究status低16比特位。

  • 其中低7位(对应下标0 - 6)代表子进程的退出信号,如果它非0代表子进程异常退出。
  • 第8位:core dump 标志
  • 次低8位(对应下标8 - 15)代表子进程的退出码,就是我们main函数中 return 和 exit或_exit的返回值。只有等待成功(即子进程正常退出)退出码才有意义。

具体细节如下图:
在这里插入图片描述

3.2.2 状态码的解析方法

解析状态码的方法有两种:位运算和宏。

方法一:通过位运算解析状态码

  1. status & 0x7f :得到低7位的数据。即子进程的退出信号:如果为0表示子进程正常退出,非0表示异常退出。
  2. (status>>8) & 0xff:得到次低8位的数据,即正常退出前提下子进程的退出码。

样例:

#include <stdio.h>                                                                                             
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
  pid_t pid = fork();
  if(pid==0)//子进程
  {
    // 子进程等待30秒后才退出
    sleep(30)
    exit(2);// 子进程退出码为2
  }
  else if(pid>0)//父进程
  {
    // 父进程里定义的输出型参数,传入wait,用来获取子进程的退出状态
    int st=0;
    int ret=wait(&st);
    if(ret>0 && (st&0x7f)==0)//等待成功且子进程正常退出
    {
      printf("child exit code is:%d\n",(st>>8)&0xff);
    }
    else if(ret>0 && (st & 0x7f)>0)//等待成功且子进程异常退出
    {
      printf("child sig code is:%d\n",st&0x7f);
    }
  }
  return 0;
}  

正常情况等待30秒后输出:
child exit code is:2

如果在等待30s期间,在另外一个终端通过kill -9 杀死子进程,会出现:
child sig code is:9

方法二:通过宏解析状态码

1、WIFEXITED(status) 即 "wait if exited"缩写,若此值为真,表明进程正常结束。此时可通过 WEXITSTATUS(status) 即 "wait exit status"缩写,来获取进程退出码。

 // 其中status为输出型参数,就是子进程的退出状态
 if(WIFEXITED(status))
 {
 	printf("退出码为:%d\n", WEXITSTATUS(status));
 }

2、WIFSIGNALED(status) 即"wait signaled"缩写,非0表明进程异常终止。此时可通过 WTERMSIG(status) 即"wait term signal"缩写,获取进程的退出信号。

 // 其中status为输出型参数,就是子进程的退出状态
 if(WIFSIGNALED(status))
 {
 	printf("退出信号为:%d\n", WTERMSIG(status));
 }

样例:

#include <stdio.h>                                                                                             
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
  pid_t pid = fork();
  if(pid==0)//子进程
  {
    sleep(30);
    exit(2);
  }
  else if(pid>0)//父进程
  {
    int st=0;
    int ret=wait(&st);
    if(ret>0 && WIFEXITED(st))// 等待成功且子进程正常退出
    {
      printf("child exit code is:%d\n",WEXITSTATUS(st));
    }
    else if(ret>0 && WIFSIGNALED(st))// 等待成功且子进程异常退出
    {
      printf("child sig code is:%d\n",WTERMSIG(st));
    }
  }
  return 0;
}       

等待30秒
child exit code is:2

如果在等待30秒期间,在另外一个终端kill -9 杀死子进程,会出现
child sig code is:9

3.3 进程的等待方式

3.3.1 阻塞式等待

运行到wait或waitpid时,如果子进程还没退出,父进程就停在这里不动直至子进程退出,这就叫做阻塞式等待。其中wait只能阻塞式等待,而waitpid的第三个参数option传0时才是阻塞式等待。

#include <stdio.h>      
#include <stdlib.h>      
#include <unistd.h>      
#include <sys/wait.h>      
      
int main()    
{    
  pid_t pid = fork();                                                                                                
  if(pid==0)//子进程      
  {      
    sleep(30);      
    exit(2);      
  }      
  else if(pid>0)//父进程      
  {      
    int st=0;      
    int ret=waitpid(-1,&st,0);//阻塞式等待      
    if(ret>0 && WIFEXITED(st))    
    {                    
      printf("child exit code is:%d\n",WEXITSTATUS(st));
    } 
    else                                                                                                       
    {
      printf("wait child fail\n");
    }
    cout<<"I'm here<<endl;                               
  }      
  return 0;                        
}       

等待30秒
child exit code is:2
I’m here

3.3.2 非阻塞式等待

父进程运行到waitpid时,若子进程还在运行中就继续做父进程自己的事情,完成后再来检测子进程是否退出了,一直重复这个过程就是非阻塞式等待。waitpid的options传WNOHANG即"wait no hang",如果检测到子进程还未退出,返回0。

#include <stdio.h>                                                                                             
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
  pid_t pid = fork(); 
  if(pid==0)//子进程    
  {    
    sleep(3);    
    exit(2);    
  }
  else if(pid>0)//父进程    
  {    
    int st=0;    
    int ret=0;    
    do    
   {              
      ret=waitpid(-1, &st, WNOHANG);//非阻塞式等待
      // 检测到子进程还没有退出,父进程先做自己的事
      // 做完成后,再来检测子进程是否退出
      if(ret==0)
      {                  
        sleep(1);                                                                                       
        printf("haha\n");   
      }                      
    }while(ret==0);
    // 等待完成后的处理         
    if(ret>0 && WIFEXITED(st))
    {  
      printf("child exit code is:%d\n",WEXITSTATUS(st));
    }  
    else         
    {           
      printf("wait child fail\n");
    }
    cout<<"I'm here<<endl;                       
  }    
  return 0;      
}                

编译运行:
haha
haha
haha
child exit code is:2
I’m here

四. 进程替换

1. 什么是进程替换

进程替换是在当前进程pcb并不退出的情况下,替换当前进程正在运行的程序为新的程序(加载另一个程序在内存中,更新页表信息,初始化虚拟地址空间)。

2. 为什么要有进程替换

子进程可以执行与父进程不同的程序,这样子进程的执行就会更加独立、灵活。

3. 进程替换的方法

3.1 exec系列函数介绍

其中有六种以exec开头的函数,这一系列统称exec函数。

头文件:#include <unistd.h>

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

返回值

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
  • 如果调用出错则返回-1。原因包括:要替换程序的权限不允许、命令、选项不存在或者拼写错误。
  • 所以exec函数只有出错的返回值而没有成功的返回值。

参数理解

  • path:可执行程序的路径
  • file:可执行程序名称,默认到PATH环境变量下的各目录中搜寻该可执行程序。
  • arg:即agrement,是一系列字符串指针,才开始到结束每一个字符串指针对应你要指向的命令或选项,最后必须以NULL结束。
  • argv[]:即agrement value,是一个字符串指针数组,每一个元素对应可执行程序名称及其所带的参数,第一个参数为可执行文件名字,最后一个元素必须以NULL结束。

命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  • l : 即list,使用参数列表。
  • v:即vector,把各个参数统一存储到一个数组里。
  • p:使用可执行程序名,并从PATH环境变量下的路径里进行寻找该可执行程序。
  • e:多了envp[]数组,使用新的环境变量代替调用进程的默认的环境变量。

3.2 exec系列函数使用

1、带p与不带p
带p的话第一个参数就不用写明可执行程序的路径(绝对路径或相对路径都可以),只需写出命令的名字就行,它会像系统执行命令一样到PATH环境变量里它所指定的各目录中搜寻该可执行文件。

我们有两个同一目录下编译后的可执行程序:myproc和myexec,他们的代码如下
在这里插入图片描述

在这里插入图片描述
我们运行myproc程序,里面又通过 execl() 函数把当前程序替换为另一个程序myexec。
$ ./myproc
**** before exec ****
agrv[0] = myexec
agrv[1] = hello world

可以看到,经过 execl() 函数替换后,原程序最后的“after exec”不再执行,而是去执行另外一个程序myexec去了,即替换成功后不再返回。

接下来我们使用带有p的 execlp(),这样我们第一个参数只需写我们想要执行的程序的名字就可以了。
在这里插入图片描述
$ ./myproc
**** before exec ****
**** after exec ****

看结果,我们并没有替换成功,因为环境变量PATH里的路径中没有myexec这个程序,我们把myexec拷贝到PATH下的其中一个路径/bin后在试试看:
$ sudo cp ./myexec /bin
$ ./myproc
**** before exec ****
agrv[0] = myexec
agrv[1] = hello world

这次成功了,所以对于带p的函数,必须先保证我们想要替换的程序必须能在环境变量PATH里找到才行

2、 带l和带v
带l(即list)的函数:你需要把命令和选项作为参数依次、逐个的传入,最后要用空指针标识结束。

execl("myexec","myexec","hello world",NULL);

带v(即vector)的函数:要求把命令和选项同时放到一个数组里,最后要用空指针标识结束。调用时只需把数组传入即可。

char* const arr[]={"myexec","hello world",NULL};                                                       
execv("./myexec",arr);

3. 带e的函数
包括 execle()、execvpe(),替换前可以传递一个指向环境字符串的指针数组

参数例如char* myenv[ ] = {“AA=111”,“BB=222”,“CC=333”,NULL},带e的话就表示该函数读取myenv[ ]数组,而不使用默认的系统配置的环境变量。即使用传入的环境变量,替换了默认的环境变量

execle() 为例,我们重新编写同一目录下的myproc.c和myexec.c两个文件
在这里插入图片描述
在这里插入图片描述
生成可执行程序后,编译运行

$ ./myproc
**** before exec ****
AA=111
BB=222
CC=333

可以看到替换后的程序myexec确实使用了我们替换前传入的自己写的环境变量myenv。

3.3 小程序 — 实现一个简易的Shell

什么是Shell

hell是指提供使用者使用界面的软件,它接收命令,然后调用相关的应用程序。

Shell实现原理

shell作为父进程用fork建立子进程,用exec系列函数簇在子进程中运行用户指定的程序,父进程shell用wait命令等待其子进程结束。wait系统调用同时从内核取得退出状态或者信号序列以告知子进程是如何结束的。
在这里插入图片描述

代码实现

include <iostream>                                                                                                   
#include <vector>    
#include <string.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
#include <unistd.h>    
using namespace std;    
    
int main()    
{    
  while(1)    
  {    
    // 获取命令行 + 解析命令行    
    cout<<"[myshell]$ ";    
    char* argList[20] = {nullptr};    
    string s;    
    string tmp;    
    int i = 0;    
    vector<string> v(20);    
    // 1、获取一行命令行,存储在string类型对象s中    
    getline(cin, s);    
    // 2、遍历s,取出其中的每一个命令和选项,先放到数组v中,完成字符串内容的深拷贝    
    // 在把每一个元素的指针存到指针数组argList里    
    for(auto e : s)    
    {    
      if(e == ' ')
      {
        v[i] = tmp;
        argList[i] = (char*)v[i].c_str();
        ++i;
        tmp.clear();
      }
      else 
      {
        tmp += e;
      }
    }
    // 最后一个命令还没有存放,因为我们输入的一行字符串最后一个字符不是以空格结尾的
    argList[i] = (char*)tmp.c_str();
    // 3、父子进程分流完成各自的任务
    // 子进程:用execvp完成程序替换
    // 父进程:等待子进程
    pid_t id = fork();                                                                                                
    if(id == 0)// 子进程
    {
      execvp(argList[0], argList);
      exit(-1);
    }
    else if(id > 0)// 父进程
    {
      int status = 0;
      int ret = wait(&status);
      if(ret > 0)// a、等待成功
      {
        if(WIFEXITED(status))// 正常退出
        {
          cout<<"child exit code is:"<<WEXITSTATUS(status)<<endl;
        }
        else// 异常退出
        {
          cout<<"abnormal exited"<<endl;
        }
      }
      else// b、等待失败
      {
        cout<<"wait fail"<<endl;
      }                                                                                                               
    }
  }
  return 0;
}

效果演示:
在这里插入图片描述

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Linux进程创建实验可以通过编写一个简单的程序来完成。 首先,在Linux命令行中打开一个文本编辑器,如vi或nano。在编辑器中,编写一个简单的程序来创建进程。程序可以使用fork()函数来创建一个子进程,使用exec()函数来执行另一个程序。 例如,以下是一个使用fork()和exec()函数创建进程的简单C程序: ``` #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { pid_t pid; pid = fork(); if (pid == -1) { printf("Error: fork failed\n"); exit(1); } else if (pid == 0) { printf("Child process created\n"); execl("/bin/ls", "ls", "-l", NULL); } else { printf("Parent process running\n"); wait(NULL); } return 0; } ``` 在程序中,首先使用fork()函数创建一个新的子进程。如果fork()函数返回-1,则表示创建进程失败。如果fork()函数返回0,则表示当前正在运行的进程为子进程。如果fork()函数返回一个正整数,则表示当前正在运行的进程为父进程,返回的整数是新创建进程进程ID。 在子进程中,使用execl()函数执行/bin/ls命令。execl()函数将当前进程替换为新的进程,因此在这个例子中,子进程将执行/bin/ls命令,然后退出。 在父进程中,使用wait()函数等待进程结束。 编译并运行这个程序,可以看到它创建了一个新的子进程,并执行了/bin/ls命令,然后父进程等待进程结束。这个简单的程序可以帮助你了解Linux进程创建过程。 ### 回答2: Linux进程创建是操作系统中一个非常重要的过程。在Linux系统中,进程是由内核(kernel)创建的,它被用来执行各种程序。进程创建可以通过系统函数fork()或exec()来完成。 在Linux中,每个进程都有一个唯一的进程标识符(PID),也就是一个整数。在进程创建的时候,新进程会继承父进程的一些特性,例如环境变量和进程工作目录。但是,它会有自己的PID,并且会有自己的寄存器值和程序计数器(PC)。 实际上,Linux系统中的进程创建流程非常复杂。首先,内核需要为新进程分配资源,包括进程的内存空间、堆栈空间、文件描述符等。在这个过程中,内核还需要建立进程和其他进程之间的访问权限。这个过程需要耗费大量的时间和系统资源。 Linux进程创建实验则是为了探究进程创建过程中的一些细节和关键点。通过实验,我们可以更深入地了解操作系统内核的设计原理,并掌握进程管理的相关技术。 在实验中,我们需要了解进程的基本概念,例如进程标识符、内存空间、堆栈空间和文件描述符。我们还需要了解系统调用和进程通信的一些基本知识。 在实验过程中,我们需要编写相应的程序,探究程序的运行机制和进程创建过程中的关键点。我们还需要使用一些工具进行分析和调试,例如strace、gdb等。这些工具可以帮助我们定位代码中的问题和调试程序。 总之,Linux进程创建实验是一项非常有价值的学习活动。通过实验,我们可以加深对操作系统内核的理解,掌握进程管理和系统调用的技术,为进一步学习Linux系统提供坚实的基础。 ### 回答3: Linux进程创建实验是对操作系统进程管理机制理解的深入考察。通过此实验,可以加深对进程的认识和使用,同时也可以更好地了解操作系统的内部机制。 在Linux系统中,进程创建使用的是fork()函数。其主要作用是创建一个与父进程几乎完全相同的子进程。同时,子进程可以继承父进程的环境,包括变量、文件等。 在实验中,我们可以通过编写代码、运行程序的方式,来创建多个进程,并观察它们之间的相互作用。如创建多个子进程,可以在父进程中使其等待并接收子进程的返回值。在进程管理中,这也是非常实用的技巧之一。 除此之外,还有一些其他的进程操作,如signal()函数。通过该函数可以改变进程的处理方式,如注册信号处理器等。这些操作都是操作系统进程管理中常用的工具和技能,掌握后对于我们的编程和调试都会有很大的帮助。 总之,在Linux进程创建实验中,我们需要认真掌握代码编写方式,技巧方法,并且注重实践操作,才能真正掌握并理解操作系统内核的进程管理机制,提高我们的技术水平。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值