【Linux进阶之路】进程(下)—— 进程控制

文章目录

  • 前言
  • 一.再识fork
    • 1.为啥有两个返回值?
    • 2.为啥给父进程返回子进程的pid,给子进程返回0?
    • 3.为啥返回的同一个变量,地址相同,但值不同?
  • 二.进程退出
    • 1.退出情况
      • 1.1正常退出,退出码正常
      • 1.2正常退出,退出码异常
      • 1.3异常退出
    • 2.退出码
      • 2.1转化错误码信息
      • 2.2全局错误码
    • 3. exit系列
      • 3.1exit
      • 3.2_exit
  • 三.进程等待
    • 1.wait
      • 1.1基本信息
      • 1.2接口的简单使用
      • 1.3参数
      • 1.4原理
    • 2. waitpid
      • 2.1基本介绍
      • 2.2非阻塞等待
  • 四.进程替换
    • 1.基本原理
    • 2.exec系列
      • 2.1execl
        • 2.1.1基本介绍
        • 2.1.2简单使用
      • 2.2execlp
        • 2.2.1基本介绍
        • 2.2.2简单使用
      • 2.3execvp
        • 2.3.1基本介绍
        • 2.3.2基本使用
      • 2.4execvpe
        • 2.4.1简单介绍
        • 2.4.2简单使用
  • 总结

前言

 回过头来,补充一个小知识点,上篇进程文章,我们并没证明命令行参数与环境变量的参数在进程地址空间的具体位置,下面我们写一段代码简要证明:

#include
#include
#include
#include
//已初始化全局数据区
int d = 0;
//未初始化全局数据区
int e;
void func()
{}
int main(int argc,const char* argv[],const char * env[])
{
  //1.命令行参数
  for(int i = 0; i < argc ; i++)
  {
    printf("argv[%d]:%p\n",i,argv[i]);
  }
  printf("\n");
  //2.环境变量
  for(int i = 0; env[i]; i++)
  {
    printf("env[%d]:%p\n",i,env[i]);
  }
  printf("\n");
  //栈区
  int a = 0;
  //堆区
  int* p = (int*)malloc(sizeof(int));
  //已初始化静态区
  static int b = 0;
  //未初始化静态区
  static int c;
  //代码区
  void (*func_ptr)() = func;
  //常量区
  const char* str = "hello world";

  printf("stack:%p\n",&a);
  printf("heap:%p\n",p);
  printf("uninit_g_val:%p\n",&e);
  printf("uninit_static_val:%p\n",&c);
  printf("init_g_val:%p\n",&d);
  printf("init_static_val:%p\n",&b);
  printf("const_val:%p\n",str);
  printf("code_val:%p\n",func_ptr);
  return 0;
}

运行结果:
【Linux进阶之路】进程(下)—— 进程控制_第1张图片

  • 结论:环境变量的地址高于命令行参数的地址且高于栈区地址

一.再识fork

1.为啥有两个返回值?

解释:

  1. 本质在于fork针对父进程进行拷贝创建了子进程,并进行了写时拷贝,因此父子进程代码完全一样。
  2. 两个进程意味着有两个程序,且执行的是同一处的代码,因此有两个返回值。

2.为啥给父进程返回子进程的pid,给子进程返回0?

解释:

  1. 父进程管理子进程,返回子进程的pid,便于进行管理。
  2. 为了区分父子进程,因此给子进程返回0,便于判断。
  3. 父进程管理子进程,本质上是通过多叉树的数据结构进行管理。

3.为啥返回的同一个变量,地址相同,但值不同?

解释:

  1. 在进程地址空间中,地址是虚拟地址不是物理地址。
  2. 虚拟地址与物理地址通过页表映射进行关联。
  3. 进程之间相互独立,也就意味着父子进程有着各种独立进程地址空间。
  4. 由于子进程在对父进程进行拷贝时,生成的进程地址空间与页表几乎一样,因此对应的变量在不同进程地址空间所对应的地址相同。
  5. 在子进程对父进程拷贝过程中发生了浅拷贝,因此在未对子进程的数据进行修改之前其都指向同一块物理空间。
  6. 但页表的中权限位发生了修改,因此在对子进程的变量进行写入时触发缺页中断(写时拷贝)。
  7. 同时操作系统会自动给子进程申请一块空间,并将数据填入新申请的空间。

补充小知识:虚拟内存 == 进程地址空间 == 虚拟地址空间。

  • 深入理解写时拷贝

问题1:为什么要进行写时拷贝?其原理为什么?

  1. 解决数据冗余,提高内存效率。
  2. 原理:页表权限修改,延迟申请,按需申请。

问题2.进程拷贝时直接进行深拷贝,有什么问题吗?

  1. 数据冗余。拷贝父进程,但又不用其数据。
  2. 效率不高。内存的拷贝也需要花费时间。
  3. 增加内存负担。进程占用内存,如果数据与代码过冗余,可能会影响其它进程的开辟与正常使用。

二.进程退出

1.退出情况

1.1正常退出,退出码正常

补充一个小知识:

echo $? //可打印出最近一次进程的退出信息

我们先来写出一个正常的代码:

#include
int main()
{
	 int a = 0;
	 int b = 1;
	 int c = a + b;
	 return 0;
}
  • 查看退出码:
    【Linux进阶之路】进程(下)—— 进程控制_第2张图片
    可见:退出码为0,代码是正确的。

1.2正常退出,退出码异常

#include
#include
#include
#include
int main()
{
  int *p = (int*)malloc(2100000000);//大概21个G
  if(p == NULL)
    exit(1);
    
  return 0;
}

运行结果:

【Linux进阶之路】进程(下)—— 进程控制_第3张图片
可见:退出码不正确。

1.3异常退出

#include
#include
#include
#include
int main()
{
  //对空指针进行解引用
  int *p = NULL;
  *p = 0;
  return 0;
}
  • 运行结果:
    【Linux进阶之路】进程(下)—— 进程控制_第4张图片
  • 先来解决一个小细节:第二次再打印 echo $?为啥变为了0?
  • 解释:echo也是进程,也要对bash做出返回错误信息,由于正确运行自然返回0。

我们接着分析:

  • 这里的代码既然都没有运行到退出的那一步,那是如何获取到进程的异常信息呢?

其实是收到了操作系统发出的信号,下面我们写一个例子进行验证:

#include
#include
#include
#include
int main()
{
  printf("%d\n",getpid());
  while(1)
  {
    ;
  }
  return 0;
}

思路:
 打印进程的pid,对此死循环进程发送11号信号,即段错误( Segmentation fault 也叫core dumped) 查看此进程的退出结果。

【Linux进阶之路】进程(下)—— 进程控制_第5张图片

  • 结论:进程是收到了对应的信号,才进行终止的,且对父进程返回了异常信息。

2.退出码

我们先来想这样一个问题:为啥要用退出码?
解释:

  1. 代码执行的结果不一定会直接回显到屏幕上,从而判断程序是否正确。
  2. 父进程可能会关心子进程的执行结果,但由于进程之间是相互独立的,因此父进程无法直接访问子进程,需要错误码来间接的获取错误信息,从而对用户做出及时的反馈。

2.1转化错误码信息

C语言提供接口:

头文件: #include<string.h>
函数声明: char * strerror(int errnum)
参数: 错误码
返回值: 错误码对应的错误信息

代码样例:

#include
#include
int main()
{
  for(int i = 0; i < 140; i++)
  {
      printf("errno[%d]:%s\n",i,strerror(i));
  }
  return 0;
}
  • 运行结果:
    【Linux进阶之路】进程(下)—— 进程控制_第6张图片
    说明:图片过长,其中总共有134种错误信息。

那异常也有错误信息,如何查看呢?

kill -l
//说明: kill -[信号编号] 【进程pid】——对指定进程传递信号

【Linux进阶之路】进程(下)—— 进程控制_第7张图片
下面这是简要的信号(目前了解即可):

  1. SIGHUP(1):挂起信号,通常用于通知进程终端已断开连接。

  2. SIGINT(2):中断信号,通常由用户按下 Ctrl-C 产生,用于中断进程。

  3. SIGQUIT(3):退出信号,通常由用户按下 Ctrl-\ 产生,用于退出进程并生成核心转储文件。

  4. SIGILL(4):非法指令信号,当进程执行非法指令时产生。

  5. SIGTRAP(5):跟踪/断点陷阱信号,当进程执行单步调试或遇到断点时产生。

  6. SIGABRT(6):异常终止信号,当进程调用 abort() 函数时产生。

  7. SIGBUS(7):总线错误信号,当进程访问无效内存地址时产生。

  8. SIGFPE(8):浮点异常信号,当进程执行浮点运算错误时产生。

  9. SIGKILL(9):终止信号,用于强制终止进程。此信号不能被捕获或忽略。

  10. SIGUSR1(10):用户定义信号 1,用于用户自定义目的。

  11. SIGSEGV(11):段错误信号,当进程访问无效内存地址时产生。

  12. SIGUSR2(12):用户定义信号 2,用于用户自定义目的。

  13. SIGPIPE(13):管道破裂信号,当进程向一个没有读端的管道写入数据时产生。

  14. SIGALRM(14):闹钟信号,当由 alarm() 函数设置的定时器超时时产生。

  15. SIGTERM(15):终止信号,用于请求终止进程。此信号可以被捕获或忽略。

  16. SIGSTKFLT(16):协处理器栈错误信号,在 Linux 上未使用。

  17. SIGCHLD(17):子进程状态改变信号,当子进程停止或终止时产生。

  18. SIGCONT(18):继续执行信号,用于恢复先前停止的进程。

  19. SIGSTOP(19):停止执行信号,用于强制停止进程。此信号不能被捕获或忽略。

  20. SIGTSTP(20):键盘停止信号,通常由用户按下 Ctrl-Z 产生,用于请求停止进程。

  21. SIGTTIN(21):后台读取终端信号,在后台进程从控制终端读取数据时产生。

  22. SIGTTOU(22):后台写入终端信号,在后台进程向控制终端写入数据时产生。

  23. SIGURG(23):紧急情况信号,在套接字上接收到紧急数据时产生。

  24. SIGXCPU(24):超出 CPU 时间限制信号,在进程超出 CPU 时间限制时产生。

  25. SIGXFSZ(25):超出文件大小限制信号,在进程超出文件大小限制时产生。

  26. SIGVTALRM(26):虚拟定时器超时信号,在由 setitimer() 函数设置的虚拟定时器超时时产生。

  27. SIGPROF(27):统计分析定时器超时信号,在由 setitimer() 函数设置的统计分析定时器超时时产生。

  28. SIGWINCH(28):窗口大小改变信号,在控制终端的窗口大小改变时产生。

  29. SIGIO(29):异步 I/O 事件信号,在文件描述符准备就绪时产生。

  30. SIGPWR(30):电源故障信号,在系统检测到电源故障时产生。

  31. SIGSYS(31):错误的系统调用信号,在进程执行错误的系统调用时产生。

  32. 其他:34-64. SIGRTMIN 至SIGRTMAX(34-64):实时信号,用于用户自定义目的。

2.2全局错误码

 C语言提供的全局变量存放错误码:errno

  • 功能:存放最近一次函数运行失败时的错误码。

例:

#include
#include
#include
#include
int main()
{
  int *p = (int*)malloc(2100000000);
  if(p == NULL)
  {
    printf("errno:%d,strerror:%s\n",errno,strerror(errno));  
  }
  return 0;
}

预测结果:打印开辟内存过大的信息。

运行结果:
在这里插入图片描述

3. exit系列

问题1:exit与return有什么区别吗?
解释 :

  1. exit是直接终止进程,而return是做函数的返回值。
  2. 在main函数的return时,与exit的作用相同。
  3. 在其它函数的return时,与exit的作用不同。

3.1exit

示例代码:

#include
#include
int main()
{
  printf("hello Linux");
  //此时并没有刷新缓冲区
  exit(0);
  return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 可见缓存区被刷新了。

3.2_exit

```cpp
#include
#include
int main()
{
  printf("hello Linux");
  //此时并没有刷新缓冲区
  _exit(0);
  return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 可见:_exit并没有刷新缓存区。

  • 总结:exit与_exit的区别与联系

区别:

  1. exit是库函数,_exit是系统调用接口。
  2. exit会刷新缓存区,而_exit不会刷新缓存区。

联系:

  • C语言的exit的实现必然要封装系统调用接口_exit。

拓展:

  • 既然系统调用接口不刷新缓冲区,那么缓存区必然不在Linux内核中。

三.进程等待

问题1:为什么要进行进程等待?
解释:

  1. 子进程受父进程进行管理,父进程有义务对子进程的资源进行回收释放,并且能够有效的解决内存泄漏的问题。
  2. 子进程在陷入僵尸状态后,无法再对子进程发送9号信号进行终止。
  3. 其次子进程正常运行时,发送9号信号进行终止,子进程会立马陷入僵尸状态,因此无法再使用9号信号杀子进程。
  4. 解决方法有两种,第一种是父进程终止,其子进程会交由init进程(操作系统)进行管理,第二种是父进程等待子进程结束,然后对子进程的资源进行释放。
  5. 父进程可能会关心子进程的退出情况,因此需要进行等待获取退出信息。

下面我们来正式认识两个常用的等待进程的系统调用接口。

1.wait

1.1基本信息

头文件:
1、 #include<sys/wait.h>//这是wait函数的头文件
2、 #include<sys/type.h>//这是pid_t的类型的头文件

函数声明:
pid_t wait(int*status);

返回值:所等待进程的pid
参数:输出型参数,传的是父进程的一个int变量的地址,执行结束status指向的
     int变量存储的是进程的退出信息(包括错误信息与异常信息)。

1.2接口的简单使用

  • 简单使用
#include
#include
#include
#include
#include
int main()
{
  pid_t id = fork();

  if(id == 0)
  {
    //子进程
    printf("I am child,my pid is %d \n",getpid());
  }
  else
  {
    sleep(10);
    //此处先不关心staus,因此传入NULL
    pid_t ID = wait(NULL);
    if(ID == id)
    {
      printf("等待成功!\n");
    }
    else 
    {
      printf("等待失败!\n");
    }
  }
  sleep(3);
  return 0;
}

  • 简单说明:此处我们创建了一个子进程,并且在子进程结束时,父进程并没有及时的回收,此时子进程会陷入僵尸状态,然后父进程休眠过后回收子进程,并且休眠3秒之后,正常退出。

补充:在运行进程时多开辟一个会话窗口,运行以下脚本便于观察进程状态:

while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;sleep 1;done
  • 实验结果:
    【Linux进阶之路】进程(下)—— 进程控制_第8张图片
    上述实验只是单进程,下面我们用fork创建多个子进程,让父进程进行回收。

  • 实验2:

	#include
	#include
	#include
	#include
	#include
	#define MAX_NUM (3)
	int main()
	{
	  int arr[MAX_NUM];
	  for(int i = 0; i < MAX_NUM; i++)
	  {
	    pid_t id = fork();
	    //在父进程中存放对应的子进程的pid
	    arr[i] = id;
	    if(id == 0)
	    {
	      //子进程
	      printf("I am child%d,my pid is %d\n",i+1,getpid());
	      exit(0);
	    } 
	  }
	  for(int i = 0; i < MAX_NUM; i++)
	  {
	    pid_t id = wait(NULL);
	    for(int j = 0; j < MAX_NUM; j++)
	    {
	      if(id == arr[j])
	      {
	        printf("this is child%d,my pid is %d\n",j+1,id);
	        break;
	      }
	    }
	  }
	  
	  return 0;
	}
  • 思路:此处我们不只是单单的回收子进程,还验证了进程的创建顺序是否与进程的等待顺序是否相关。
  • 实验结果:
    【Linux进阶之路】进程(下)—— 进程控制_第9张图片

 以上我们简单的了解了一下,wait接口的用处,但是我们还没有了解其参数——int* status,下面我们来正式认识一下status。

1.3参数

问题:为什么不直接从子进程中获取数据,而非得调用接口函数还要传地址?

  1. 首先父进程与子进程独立,互不影响,这也意味着父进程无法直接访问子进程。
  2. 其次父进程要从子进程获取数据,本质上还是用户从操作系统中获取数据,因为进程是在操作系统中的。
  3. 而用户可能会恶意的篡改操作系统的数据,因此为了防止恶意用户,操作系统使用了系统调用接口来保护自己。
  4. 最后接口已经有返回值了,可以通过传变量地址的方式间接的增加一个返回值,也是一种不错的办法。
  5. 因此,需要传入一个参数从而让操作系统获取到子进程的退出信息,从而让父进程获取到子进程的退出信息。

解决上述问题,我们再来了解,退出信息指的是什么?

下面实验写两个小demo来观察:

  • 实验1:正常运行
#include
#include
#include
#include
#include
int main()
{
  pid_t id = fork();

  if(id == 0)
  {
    //子进程
    printf("I am child,my pid is %d \n",getpid());
   	exit(11);
  }
  else
  {
    int status = 0; 
    pid_t ID =  wait(&status);

    if(ID == id)
    {
      printf("子进程的退出信息为:%d\n",status);
      printf("等待成功!\n");
    }
    else 
    {
      printf("等待失败!\n");
    }
  }
  return 0;
}

实验结果:
【Linux进阶之路】进程(下)—— 进程控制_第10张图片

  • 实验2:异常中断
#include
#include
#include
#include
#include
int main()
{
  pid_t id = fork();

  if(id == 0)
  {
    //子进程
    printf("I am child,my pid is %d \n",getpid());
    //异常退出
    int *p = NULL;
    *p = 0;
  }
  else
  {
    int status = 0; 
    pid_t ID =  wait(&status);

    if(ID == id)
    {
      printf("子进程的退出信息为:%d\n",status);
      printf("等待成功!\n");
    }
    else 
    {
      printf("等待失败!\n");
    }
  }
  return 0;
}

实验结果:
【Linux进阶之路】进程(下)—— 进程控制_第11张图片
 通过观察我们大致可以得出,status的退出信息包括正常运行的退出码与异常信息,下面我们来正式认识一下其信息分布。

说明:系统的退出信息总共有134种错误信息,信号信息总共有64种

  • 一张图理解:进程退出信息分布
    【Linux进阶之路】进程(下)—— 进程控制_第12张图片

补充:第17位到第32位这之间的16位,目前我们不做考虑。

如何获取退出状态与终止信号呢?

退出状态:(status >> 8) & 0xFF
作用:左移8位,并移动之后的前16位清零(从前外后数16位)。

终止信号: staus & 0x7F
作用:将前25位清0.

其实系统里面早已提供了两个宏函数,来帮助我们获取退出状态与终止信号:

判断是否正常退出: WIFEXITED(status)
获取退出状态:     WEXITSTATUS(status)
判断是否被信号终止:WIFSIGNALED(status)
获取信号信息:     WTERMSIG(status)

同时我们用这些宏再来看看上述两个demo的实验结果:

  • 在两个demo的等待进程之后的代码处第一个判断出替换为此代码:
	if(ID == id)
    {
      if(WIFEXITED(status))
      {
         //正常退出
         printf("子进程的退出结果为:%d\n",WEXITSTATUS(status));
      }
      if(WIFSIGNALED(status)) 
      {
        //异常退出
        printf("子进程的异常信号为:%d\n",WTERMSIG(status));
      }
      printf("等待成功!\n");
    }
  • demo1:
    在这里插入图片描述
  • demo2
    【Linux进阶之路】进程(下)—— 进程控制_第13张图片

1.4原理

其实很简单:

  • 首先在子进程结束时,变为僵尸状态,其代码和数据先被释放,task_struct是没有释放的。
  • 其次子进程的task_struct中,存放有两个整形变量——
		int exit_code, exit_signal;
  • 并且父进程调用的wait是系统调用接口,并且进程之间互相独立。
  • 因此应由操作系统进行获取这两个变量;
  • 然后合并之后写入传进去的指针(非空)指向的内容当中。
  • 最终父进程获取到了退出信息。

图解:
【Linux进阶之路】进程(下)—— 进程控制_第14张图片

  • 总结:wait函数有一个特点就是非等不可,如果子进程不结束,那就一直等,我们称这种等待为阻塞等待。

有没有非阻塞等待呢?下面我们介绍另外一种函数。

2. waitpid

2.1基本介绍

头文件: 
#include
#include
函数声明: 
pid_t waitpid(pid_t id,int *status,int option);
参数:

1.id  
	情况1:小于-1暂时不做考虑。
	情况2: 等于-1意味着等待任意一个子进程。
	情况3: 等于0暂时不做考虑。
	情况4: 大于0等待与id值相等的子进程。
2.status
	情况1:为空,不考虑状态信息。
	情况2:非空,将退出信息写入status指向的变量当中。
3.option
	情况1:零,表示阻塞等待。
	情况2:WNOHANG如果子进程没有退出就立马返回。
	情况3:WUNTRACED情况2 + 如果子进程stoped就立马返回
	情况4:情况2 + 情况3 + 如果一个stoped子进程被发送SIGCONT信号。

返回值:
	情况1:如果参数30,则等待成功返回子进程的pid,失败返回-1.
	情况2:如果参数3为WOHANG,则等待成功返回子进程的pid,子进程没有
		 退出返回0,失败返回-1

上面wait的使用已经很详细了,下面我们只做一个简单的非阻塞等待的实验。

2.2非阻塞等待

#include
#include
#include
#include
#include
#define MAX_NUM (3)
int main()
{
  for(int i = 0; i < MAX_NUM; i++)
  {
      pid_t id = fork();
      if(id == 0)
      {
          //子进程
          printf("I am child%d,my pid is %d\n",i,getpid());
          sleep(3);
          exit(0);
      }
  }
  sleep(1);//先让子进程的打印信息先执行。
  int cnt = MAX_NUM;//需要等待的子进程的个数
  //非阻塞等待需要死循环,因为子进程没有结束也进行返回。
  while(1)
  {
    pid_t id = waitpid(-1,NULL,WNOHANG);
    if(id < 0)
    {
      printf("进程等待失败!\n");
      break;
    }
    else if(id == 0)
    {
      printf("子进程还没有退出,仍需等待....\n");

      //这时我们可以让父进程干自己的事情,这个事情不能太重。
      printf("监视任务...\n");
      printf("网络任务...\n");
      sleep(1);//防止打印过快。
    }
    else 
    {
      //进程等待成功
      printf("进程等待成功!\n");
      if(--cnt == 0)
      {
        break;
      }
    }
  }
  return 0;
}

  • 实验思路:创建3个子进程,让父进程进行非阻塞等待的同时,执行自己的任务,直到等待成功之后,父进程也正常结束。

  • 运行结果:
    【Linux进阶之路】进程(下)—— 进程控制_第15张图片

总结:非阻塞与阻塞

  1. 阻塞等待是直到等到一个子进程退出才往下执行,否则一直陷入阻塞状态。
  2. 非阻塞等待是等一次,看看子进程如果还在运行,也进行返回。
  3. 阻塞等待就像是打一次电话,接通之后,直到对面回应,才挂电话。
  4. 非阻塞等待就像是打多次电话,如果对面没回应,就挂电话再打,直到对面回应挂了,就不再打了。
  5. 非阻塞等待相比较阻塞等待要执行多次,但是其可以在子进程没结束的同时,做一些自己的事情。一般我们称这种现象为非阻塞轮询。

四.进程替换

1.基本原理

故事引入:
 某位上古大能,带着绝世功法,轮回降临世间,夺舍了一位手无缚鸡之力的小菜鸡,之后便开始了主人公的巅峰之路。

 不知这样的思路的爽文,你是否看过,其实回归到正题——进程替换,这里进程替换,跟上古大能夺舍的原理类似,夺舍的是灵魂(代码和数据),不变的是肉体(进程)。

带着这样的理解,下面我们来简单的认识接口,并进行简单的使用。

2.exec系列

2.1execl

2.1.1基本介绍
头文件:
#include
函数声明:
int execl(const char *path, const char *arg, ...);
参数:
1.path:指的是所要打开文件具体的路径(分为绝对路径和相对路径),
2.arg: 指的是所要打开的文件名。
3. ...:可变参数列表,传的是具体的选项,且(标准写法)最后一个必须以NULL结尾。
返回值:
情况1:如果替换成功,则直接按照替换之后的代码往下执行,原先的代码不执行。
情况2:替换失败,返回-1,按照原先的代码往下继续运行,并且错误码将被设置。 
2.1.2简单使用
  • 实验一:路径正确,替换成功。
#include
#include
#include
int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    //进行进程替换
    int ret =  execl("/usr/bin/ls","ls","-l",NULL);
    //此处我们执行的是ls -l 命令。
    if(ret == -1)
    {
      printf("进程替换失败!\n");
    }
  }
  return 0;
}
  • 运行结果:符合预期。
    【Linux进阶之路】进程(下)—— 进程控制_第16张图片

  • 实验二: 路径错误,替换失败。

#include
#include
#include
int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    //进行进程替换
    int ret =  execl("ls","ls","-l",NULL);
    if(ret == -1)
    {
      printf("进程替换失败!\n");
    }
  }

  return 0;
}
  • 运行结果: 符合预期
    在这里插入图片描述

2.2execlp

2.2.1基本介绍
头文件:
#include
函数声明:
int execlp(const char *file, const char *arg, ...);
参数:
1.file:指的是所要打开文件的路径(分为绝对路径和相对路径),如果不加绝对路径
	   默认在相对路径与path环境变量下找。
2.arg: 指的是所要打开的文件名。
3. ...:可变参数列表,传的是具体的选项,且(标准写法)最后一个必须以NULL结尾。
返回值:
情况1:如果替换成功,则直接按照替换之后的代码往下执行,原先的代码不执行。
情况2:替换失败,返回-1,按照原先的代码往下继续运行,并且错误码将被设置。 

补充: 函数名的结尾有p,说明会在环境变量path下的默认路径进行查找。

2.2.2简单使用
#include
#include
#include
int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    //进行进程替换
    int ret =  execlp("ls","ls","-l",NULL);
    if(ret == -1)
    {
      printf("进程替换失败!\n");
    }
  }
  return 0;
}
  • 运行结果: 符合预期。
    【Linux进阶之路】进程(下)—— 进程控制_第17张图片

  • path(大写)环境变量:
    在这里插入图片描述

  • 小总结一下:

    1. execl比execl,多了在环境变量path的下进行查找路径。
    1. 本质上是参数1的不同,即file与path的区别。

2.3execvp

2.3.1基本介绍
#include
函数声明:
int execlvp(const char *file, char *const argv[]);

参数:
1.file:指的是所要打开文件的路径(分为绝对路径和相对路径),如果不加绝对路径
	   默认在相对路径与path环境变量下找。
2.arg: 指的是所要打开的文件名及其选项,且其中的指针的指向不能被更改。

返回值:
情况1:如果替换成功,则直接按照替换之后的代码往下执行,原先的代码不执行。
情况2:替换失败,返回-1,按照原先的代码往下继续运行,并且错误码将被设置。
2.3.2基本使用
#include
#include
#include
#define MAX_NUM (3)
int main()
{
  char* const argv[MAX_NUM] = {(char* const)"ls",\
  (char* const)"-l",NULL};

  pid_t id = fork();
  if(id == 0)
  {
    //进行进程替换
    int ret =  execvp("ls",argv);
    if(ret == -1)
    {
      printf("进程替换失败!\n");
    }
  }

  return 0;
}
  • 实验结果:
    【Linux进阶之路】进程(下)—— 进程控制_第18张图片

  • 小小总结:

    1. execlp选项与文件是一个一个传,而execvp的文件和选项是一次性传。
    1. 本质在于const char* arg 与char* const argv[]的区别。
    1. 记忆可按照execlp的 l 意为list,execvp的v意为vector进行记忆。

2.4execvpe

2.4.1简单介绍
#include
函数声明:
int execlvp(const char *file, char *const argv[]);

参数:
1.file:指的是所要打开文件的路径(分为绝对路径和相对路径),如果不加绝对路径
	   默认在相对路径与path环境变量下找。
2.arg: 指的是所要打开的文件名及其选项,且其中的指针的指向不能被更改。

返回值:
情况1:如果替换成功,则直接按照替换之后的代码往下执行,原先的代码不执行。
情况2:替换失败,返回-1,按照原先的代码往下继续运行,并且错误码将被设置。
2.4.2简单使用
  • 此处我们直接向子进程再导入我们自己定义的环境变量。

  • test.c

#include
#include
#include
#include
#define MAX_NUM (3)
extern char** environ;
int main()
{
  char* const argv[MAX_NUM] = {(char* const)"test4",NULL};
  putenv("MY_VAL=66666666666");
  pid_t id = fork();
  if(id == 0)
  {
    //进行进程替换
    int ret =  execvpe("./test4",argv,environ);
    if(ret == -1)
    {
      printf("进程替换失败!\n");
    }
  }
  return 0;
}
  • test4.cpp
#include
using namespace std;
int main(int argc,char* const argv[],char* const env[])
{
  for(int i = 0; env[i]; i++)
  {
    cout << "env[" << i << "]:" << env[i] << endl;
  }
  return 0;
}

补充小知识:当需要多文件一次生成可执行程序时,可再添加一个伪目标。
【Linux进阶之路】进程(下)—— 进程控制_第19张图片
除此之外,这里的环境变量还可以进行自定义,进而传给子进程。

总结一下:

  1. 我们目前初步理解了exec系列的4种经典的库函数接口,还有execl,execv这两个函数就不过多介绍了,其用法一致。
  2. 接口功能相同,这里我们只是强调了记忆方法与参数区别,llist,vvector,ppatheenvironment,区别上文很详细,这里就不在说了。
  3. 还有一个execve是一个系统调用接口,上面所提及的exec系列的库函数的接口必然在底层会调用此接口,有兴趣可查看man 2号手册。

拓展:

问题1:替换之后会影响父进程吗?

  • 解释:
  1. 因为替换是将代码与数据进行替换,由于父子进程共享代码与数据,因此这里必然不会将共享代码和数据,进行直接替换,而是让操作系统再腾出一块物理空间,并将页表的映射关系一改就达成了替换的目标。
  2. 因此并没有全部真的进行替换,因此并不影响父进程。


问题2:上面的C代码为什么可以替换为C++的程序再进行运行?

  • 解释:
  1. 在操作系统看来所有程序执行的单位都为进程,程序在运行本质是进程在运行,而进程是操作系统级别的概念,所有语言最后都会变为进程运行。
  2. 因此替换与语言并没有什么关系。


问题3:替换时创建新进程了吗?

  • 答案:是没有,只是数据和代码发生了替换。


问题4:子进程与父进程的环境变量是什么关系?

  • 解释:
  1. 父子进程独立,因此父子进程都有自己独立的环境变量。
  2. 子进程的环境变量未做说明,默认直接继承父进程的环境变量。
  3. 父子进程互不影响,子进程的环境变量的修改并不会影响父进程。


问题5: 最后我们再来讨论以前的问题,shell的环境变量从哪来?

  • 环境变量在用户登入时,会自动从家目录下的.bash_profile文件中进行加载。

总结

 本篇文章总计一万三千多字,内容较长,如果能耐心看完,想必进程控制已经OK了,最后如果有所收获的话,不妨点个赞鼓励一下吧!
我是舜华,期待与你的下一次相遇!

你可能感兴趣的:(Linux进阶之路,linux,进程控制,进程退出,进程等待,进程替换)