小言_互联网的博客

< Linux > 进程控制

532人阅读  评论(0)

目录

1、进程创建

        fork函数

        fork函数返回值

        写时拷贝

        fork常规用法

        fork调用失败的原因

2、进程终止

        2.1、进程退出常见场景

        2.2、进程退出码

        2.3、进程常见退出方法

                 _exit函数

                 exit函数

                 return退出

        2.4、关于终止,内核做了什么

3、进程等待

        3.1、进程等待必要性

        3.2、进程等待的方法

                 wait方法

                 waitpid方法

        3.3、获取子进程status

        3.4、阻塞等待和非阻塞等待

4、进程程序替换

        替换原理

        替换函数

        替换函数示例


1、进程创建

fork函数

在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。


   
  1. #include <unistd.h>
  2. pid_t fork(void);
  3. 返回值:子进程中返回 0,父进程返回子进程id,出错返回 -1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序:

这里看到了三行输出,一行before,两行after。进程22346先打印before消息,然后它有打印after。另一个after消息有22347打印的。注意到进程22347没有打印before,为什么呢?如下图所示:

所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定

问1:fork之后,是否只有fork之后的代码是被父子进程共享的?

  • fork之后,父子共享所有的代码!!!子进程执行的后续代码 != 共享的所有代码,只不过子进程只能从这里开始执行!理由如下:在CPU内部有一个叫eip程序计数器的寄存器,其是用来保存当前正在执行指令的下一条指令,eip会拷贝给子进程,子进程便从该eip所指向的代码处开始执行!!

问2:fork之后,操作系统做了什么?

  • 因为进程 = 内核数据结构+进程的代码和数据,所以fork之后,创建子进程的内核数据结构(struct task_struct + struct mm_struct + 页表)代码继承父进程,数据以写时拷贝的方式,来进行共享或者独立。

fork函数返回值

  • 子进程返回0
  • 父进程返回的是子进程的pid

关于fork函数返回值的相关问题我在进程概念那已经讲解清楚,可以移步至那查询。


写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自产生一份副本。具体见下图:

写时拷贝本身是由OS的内存管理模块帮助我们完成的,接下来有如下几个问题:

问:操作系统为什么要完成写时拷贝?

  • 其实我在创建父子进程的时候完全可以把父子进程的数据分开,没必要共享一个,这样刚好满足父子进程的独立性,这样子可以吗?其实也是可以的,不过会有如下几个问题,我们从如下几个方面进行分析:
  1. 父进程的数据,子进程不一定全用,即便使用,也不一定全部写入——会有浪费空间的风险。
  2. 最理想的情况,只有会被父子修改的数据,进行分离拷贝,不需要修改的共享即可——但是从技术的角度实现复杂
  3. 如果fork的时候,就无脑拷贝数据给子进程,无疑会增加fork的成本(内存和时间)
  • 所以最终我们采用写时拷贝。写时拷贝的好处很明显,它只会拷贝父子修改的,变相的就是拷贝数据的最小成本。可是拷贝的成本依旧存在啊。写时拷贝本质是一种延迟拷贝策略,只有真正使用的时候,才给你,也就是你想要,但是不立马使用的空间,先不给你,那么也就意味着可以先给别人。继而变相的提高内存使用效率。

fork常规用法

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

fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

2、进程终止

2.1、进程退出常见场景

有如下三种退出的方式:

  • 1、代码跑完,结果正确
  • 2、代码跑完,结果不正确
  • 3、代码没跑完,程序异常了

2.2、进程退出码

我们先前写C/C++代码的时候,都会在入口函数main函数开始写,我们总是喜欢在结尾的时候给上一个return 0,继而引发出了如下的两个问题:

  1. return 0,给谁return?
  2. 为何是0?其它值可以吗?

下面一次解决:

  • 1、return 0,给谁return?

给父进程,具体理由在下面会有讲解。

  • 2、为何是0?其它值可以吗?

返回值代表的是进程代码跑完,结果是否正确,如果是0,则成功非零则失败。所以我们在写一个程序的时候,如果测试结果正确,这里我们可以给上return返回值0,可如果不正确,我们return的应该是其他值以此表示结果失败,只不过我们平时都无脑return 0了,准确说是不太正确的。

此外,失败虽是用非零值表示,可也是有讲究的,结果成功都是用0表示结果失败反倒用不同的数字来表示,以此表示失败的不同原因

所以我们把main函数的return返回值称之为进程退出码!!进程退出码表征了进程推出的信息,此信息是要给父进程去读取的。

示例:

我们可以通过如下的指令查看退出码:


   
  1. echo $?
  2. //$?表示在bash中,最近一次执行完毕时,对应进程的退出码!

再比如我们平时在命令行输入的指令,诸如ls、cd……类的,其退出码均为0,表示结果正确,可是当你随便输入一条错误指令的时候,其退出码则是某一数字表示结果错误:

问:一般而言,失败的的非零值我该如何设置呢?以及默认表达的含义?

  • C语言当中的strerror函数可以通过错误码,获取该错误码在C语言当中对应的错误信息:

  • 总结:错误码退出码可以对应不同的错误原因,方便定位问题!

2.3、进程常见退出方法

正常终止:

  • 1、在main函数中return代表进程退出,非main函数return代表函数调用结束,这两点要注意。
  • 2、在自己的代码任意地点中,调用exit()。
  • 3、_exit

异常终止:

  • ctrl + c,信号终止

示例:

下面我们来查看此进程的退出码,如果为10说明是return终止,如果是111则说明是通过exit退出的。

很显然是通过exit退出的。


_exit函数


   
  1. #include <unistd.h>
  2. void _exit( int status);
  3. 参数:status 定义了进程的终止状态,父进程通过wait来获取该值

_exit函数也是用来终止进程的,如下:

说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255

_exit和exit的作用都是终止进程,不过还是有所区别的,在下面会有讲解


exit函数


   
  1. #include <unistd.h>
  2. void exit(int status);

exit最后也会调用exit, 但在调用exit之前,还做了其他工作:

  1. 执行用户通过 at_exit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

下图很清晰的显示了exit和_exit在终止进程的区别之处:

假设我限制想用printf语句输出一段话,但是不加\n,对于这样一份代码,用exit和_exit处理后的结果是不同的:

根据这里的结果,也能充分正式上图的结论:

  • exit终止进程,刷新缓冲区
  • _exit直接终止进程,不会有任何刷新操作 

return退出

return是一种更常见的退出进程方法。执行return n等同于执行exit(n)因为调用main的运行时函数会将main的返回值当做 exit的参数。这里我们上文已经做过示例,不再赘述。


2.4、关于终止,内核做了什么

进程 = 内核结构 + 进程代码 和 数据

  • 当进程终止了,首先进入Z状态,等待父进程回收子进程的信息,读取它的退出码,随后将进程设置为X状态,释放内核结构,释放曾经加载的进程代码和数据。

这里详细讲解下释放内核结构

  • 当进程终止的时候,代码和数据是必定要被释放的,因为没有什么价值,可是对于内核结构(task_struct && mm_struct),操作系统可能并不会释放该进程的内核数据结构,当我们创建进程的时候,它需要从0开始构建对象,要完成开辟空间 + 初始化的操作,均需要花时间,为了避免开销过大,Linux维护了一张废弃的数据结构链表,头节点称为obj,当进程释放的时候,进程的相关数据结构被维护到了此废弃链表中,这里并未释放其空间,只是设置其为无效,当你下次再创建进程的时候,他会从此废弃队列中拿到相应的task_struct和mm_struct,以此节省开辟空间所花费的时间,此时只需要给新进程初始化即可,此举大大减少了时间上的开销。这种举措就叫做内核的数据结构缓冲池,在操作系统里面叫做slab分派器

3、进程等待

3.1、进程等待必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程需要获得子进程的退出状态。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3.2、进程等待的方法

wait方法


   
  1. #include<sys/types.h>
  2. #include<sys/wait.h>
  3. pid_t wait(int*status);
  4. 返回值:
  5. 等待成功返回被等待进程pid,失败返回 -1
  6. 参数:
  7. 输出型参数,获取子进程退出状态,不关心则可以设置成为 NULL
  8. 作用:
  9. 等待任意子进程

示例:

我们可以使用以下监控脚本对进程进行实时监控:

while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep;echo "—————————————————————————————————————————————————————————————————";sleep 1;done

运行结果如下:

这时我们可以看到,当子进程退出后,父进程读取了子进程的退出信息,子进程也就不会变成僵尸进程了。 由此得知我们可以通过wait()的方案解决回收子进程Z状态,让子进程进入X


waitpid方法

注意wait和waitpid均是系统调用,下面详细说明waitpid:


   
  1. pid_ t waitpid(pid_t pid, int *status, int options);
  2. 返回值:
  3. > 0 等待子进程成功,当正常返回的时候waitpid返回收集到的子进程的进程ID;
  4. 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回 0
  5. 如果调用中出错,则返回 -1,这时errno会被设置成相应的值以指示错误所在;
  6. 参数:
  7. pid:
  8. Pid= -1,等待任一个子进程。与wait等效。
  9. Pid> 0.等待其进程ID与pid相等的子进程。
  10. status:
  11. WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  12. WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
  13. options:
  14. WNOHANG: 若pid指定的子进程没有结束,则 waitpid()函数返回 0(阻塞等待),不予以等待。若正常结束,则返回该子进程的ID。
  15. 作用:等待指定子进程或任意子进程
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

下面再来强调下第二个参数:status

status是一个输出型参数,通过调用该函数,从函数内部拿出来特定的数据,也就是从操作系统拿到特定数据。图示:

子进程退出的时候会将自己的退出信息写入自己的task_struct,随后变成Z状态,随后父进程调用wait / waitpid接口,通过status把子进程的退出码拿到。 


3.3、获取子进程status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数由操作系统填充
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

解释上图:

  • 在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。

我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。


   
  1. exitCode = (status >> 8) & 0xFF; //退出码
  2. exitSignal = status & 0x7F; //退出信号

对于此,系统中提供了两个宏来获取退出码和推出信号。

  • WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
  • WEXITSTATUS(status):用于获取进程的退出码

   
  1. exitNormal = WIFEXITED(status); //是否正常退出
  2. exitCode = WEXITSTATUS(status); //获取退出码
  • 需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。

示例:

结果如下:

status的最低7位表示进程退出时收到的信号,进程如果异常退出,是因为这个进程收到了特定的信号,我们先前kill -9 pid就是在进程异常时退出而发出的信号。下面来模拟下进程的异常退出:

这里子进程是在无线循环的,父进程只能阻塞等待,现在我们把子进程kill掉,结果如下:

如果kil -3 pid,退出信号就是3……当进程收到信号的时候,就代表进程异常了。 综上:退出信号代表进程是否异常,退出码代表程序跑完后的结果正确与否。

问:一个进程退出的时候,父进程会拿到退出码和退出信号,那到底先看谁呢?

  • 一旦进程出现异常,只关心退出信号,退出码没有任何意义

强调系统中的两个宏:


   
  1. status:
  2. WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  3. WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)


3.4、阻塞等待和非阻塞等待

再来强调下waitpid函数中的最后一个参数options,当其值为0时,就是阻塞等待,当为WNOHANG时,就是非阻塞等待。下面展开讨论:

阻塞等待:

  • 如果子进程就是不退出(如死循环),怎么办呢?我的父进程只能阻塞等待。
  • 当我们调用某些函数的时候,因为条件不就绪(可能是任意的软硬件条件),需要我们阻塞等待,本质就是当前进程自己变成阻塞状态,当条件就绪的时候再被唤醒。

详细说明:

  • 就是一个进程在系统层面上因为要等待某种事件发生,如果这个事件并没有发生,那么当前进程的代码和数据将无法运行,此时就要进入阻塞状态,也就是将父进程的task_struct的状态由R->S,从运行队列投入到等待队列,等待子进程退出,当子进程退出了,本质就是条件就绪,那么就会逆向执行上述操作,将进程从等待队列搬到运行队列,并将状态由S->R。

举例:

  • 假设马上就要考高数了,为了能够及格,我打电话给了我的一位学霸朋友张三来让他教我,整个过程我就是一个进程,打电话的过程就是在调用接口,张三就是所谓的OS操作系统,当电话接通了,但是张三说他在忙,于是我让张三别挂电话,我在电话这头一直等待你,此时这个等待过程中我什么也没干,只是等待张三,也就是说父进程在等待期间不做任何事情,这个过程就是阻塞等待。

非阻塞等待:

  • 此时重复上述场景,当电话接通后,张三表示还在忙,那么我直接挂电话,此时我做些自己的事情,忙了一会后又给张三打了个电话,张三还在忙,那么我又挂电话继续做自己的事情,while(1)重复循环直至张三说自己ok了。
  • 整个过程我依旧是用户,张三是OS操作系统,打电话就等价于调用waitpid函数,相当于是用户问操作系统子进程是否退出,当OS回应没有时,此时waitpid直接返回,此时用户不会调用wait而将自己阻塞住,此时用户在空闲时间段内做自己的事情,做一会之后再去问OS操作系统好了没,这个过程就叫做非阻塞等待。这种多次调用非阻塞接口,就是轮询检测

代码示例:


   
  1. #include<stdio.h>
  2. #include<unistd.h>
  3. #include<stdlib.h>
  4. #include<sys/types.h>
  5. #include<sys/wait.h>
  6. int main()
  7. {
  8. pid_t id = fork();
  9. if (id == 0)
  10. {
  11. //子进程
  12. while ( 1)
  13. {
  14. printf( "我是子进程,我的PID:%d, 我的PPID:%d\n", getpid(), getppid());
  15. sleep( 3);
  16. }
  17. exit( 104);
  18. }
  19. else if (id > 0)
  20. {
  21. //父进程
  22. //基于非阻塞的轮询等待方案
  23. int status = 0;
  24. while ( 1)
  25. {
  26. pid_t ret = waitpid( -1, &status, WNOHANG);
  27. if (ret > 0)
  28. {
  29. printf( "等待成功,%d,退出信号是:%d,退出码是:%d\n", ret, status & 0x7F, (status >> 8) & 0xFF);
  30. break;
  31. }
  32. else if (ret == 0)
  33. {
  34. //等待成功了,但是子进程没有退出
  35. printf( "子进程好了没,奥,还没,那么我父进程就做其他事情啦...\n");
  36. sleep( 1);
  37. }
  38. else
  39. {
  40. //出错了,暂时不处理
  41. }
  42. }
  43. }
  44. else
  45. {
  46. //do nothing
  47. }
  48. return 0;
  49. }

运行结果就是,父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程推出后读取子进程的退出信息。

现在增加点设计,模拟让父进程在等待的过程中做些自己的事情,如下的代码:


   
  1. #include<iostream>
  2. #include<vector>
  3. #include<stdio.h>
  4. #include<unistd.h>
  5. #include<stdlib.h>
  6. #include<sys/types.h>
  7. #include<sys/wait.h>
  8. typedef void (*handlder_t)();
  9. //方法集
  10. std::vector< handlder_t> handlers;
  11. void fun1()
  12. {
  13. printf( "hello, 我是方法1\n");
  14. }
  15. void fun2()
  16. {
  17. printf( "hello, 我是方法2\n");
  18. }
  19. void Load()
  20. {
  21. //加载方法
  22. handlers. push_back(fun1);
  23. handlers. push_back(fun2);
  24. }
  25. int main()
  26. {
  27. pid_t id = fork();
  28. if (id == 0)
  29. {
  30. //子进程
  31. while ( 1)
  32. {
  33. printf( "我是子进程,我的PID:%d, 我的PPID:%d\n", getpid(), getppid());
  34. sleep( 3);
  35. }
  36. exit( 104);
  37. }
  38. else if (id > 0)
  39. {
  40. //父进程
  41. //基于非阻塞的轮询等待方案
  42. int status = 0;
  43. while ( 1)
  44. {
  45. pid_t ret = waitpid( -1, &status, WNOHANG);
  46. if (ret > 0)
  47. {
  48. printf( "等待成功,%d,退出信号是:%d,退出码是:%d\n", ret, status & 0x7F, (status >> 8) & 0xFF);
  49. break;
  50. }
  51. else if (ret == 0)
  52. {
  53. //等待成功了,但是子进程没有退出
  54. printf( "子进程好了没,奥,还没,那么我父进程就做其他事情啦...\n");
  55. if (handlers. empty()) Load();
  56. for ( auto f : handlers)
  57. {
  58. f(); //回调处理对应的任务
  59. }
  60. sleep( 1);
  61. }
  62. else
  63. {
  64. //出错了,暂时不处理
  65. }
  66. }
  67. }
  68. else
  69. {
  70. //do nothing
  71. }
  72. return 0;
  73. }


4、进程程序替换

替换原理

程序替换的概念

  • 用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,这就需要用到程序替换,而完成程序替换需要用到exec函数。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动李成开始执行。

为什么要完成程序替换

我们一般在服务器设计(linux变成)的时候,往往需要子进程干两件种类的事情:

  1. 让子进程执行父进程的代码片段(服务器代码)
  2. 让子进程执行磁盘中一个全新的程序(shell,想让客户端执行对应的程序,通过我们的进程。执行其他人写的进程代码等待),如C/C++(自己写的) -> C/C++/Python/Shell/Php/java……(别人写的)

上述的第二点需求就是用我自己写的C/C++代码调用别人不同语言的代码程序,完成这一项需求就需要用到程序替换。

程序替换的原理

如下图所示:

  • 前面我们学习到,当fork创建子进程的时候,子进程的PCB、虚拟地址空间都以父进程为模板,页表中的代码段指向的是父进程中的代码段,数据也以写时拷贝的方式来和父进程进行共享,如果现在有一个全新的程序b.exe,并且我现在不想让子进程执行任何父进程相关的代码以及访问父进程的数据,并执行的是a.exe程序,此时把b.exe的程序加载到物理内存上,让子进程重新调整自己的页表映射,使其指向新的b程序的代码和数据,这种过程就叫做程序替换。

总结程序替换的原理:

  1. 将磁盘中的程序,加载入内存结构
  2. 重新建立页表映射,谁执行程序替换,就重新建立谁的映射,最终达到的效果就是让父进程和子进程彻底分离,并让子进程执行一个全新的程序!!!

问1:当进行程序替换时,有没有创建新的进程?

  • 进程程序替换后,该进程对应的PCB、进程地址空间以及页表等数据结构均没有发生改变,只是重新建立了一下物理内存中的映射关系罢了,它的内核对应的数据结构没有发生变化,他的pid也没有发生变化,也就没有创建新的进程,只不过是让进程执行不同的程序罢了!!!

问2:子进程进行进程程序替换后,会影响父进程的代码和数据吗?

  • 子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。

替换函数

六种替换函数(实际上七种,换汤不换药):

  • 上述过程中的程序替换是我们通过调用接口(函数)来让OS操作系统帮助我们完成的,此接口就是替换函数,替换函数有六种以exec开头的函数,他们统称为exec函数:

   
  1. #include <unistd.h>`
  2. int execl(const char *path, const char *arg, ...);
  3. int execlp(const char *file, const char *arg, ...);
  4. int execle(const char *path, const char *arg, ...,char *const envp[]);
  5. int execv(const char *path, char *const argv[]);
  6. int execvp(const char *file, char *const argv[]);
  7. int execve(const char *path, char *const argv[], char *const envp[]);

函数解释:

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值

命名理解:

这六个exec系列函数的函数名都以exec开头,其后缀的含义如下:

  • l(list):表示参数可用列表的形式,一一列出
  • v(vector):表示参数采用数组的形式
  • p(path):表示能自动搜索环境变量PATH,进行程序查找
  • e(env):表示可以传入自己设置的环境变量
函数名 参数格式 是否带路径 是否使用当前环境变量
execl 列表 不是
execlp 列表
execle 列表 不是 不是,需自己组装环境变量
execv 数组 不是
execvp 数组
execve 数组 不是 不是,需自己组装环境变量

事实上,只有execve是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,其它五个函数在man手册的第3节,也就是说其它五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的需求。

下图为exec系列函数族之间的关系:


替换函数示例

我们如果要执行一个全新的程序,需要做2件事情:

  1. 先找到这个程序在哪里
  2. 程序可能携带选项进行执行(也可以不携带),要明确告诉OS操作系统,我想怎么执行这个程序?要不要带选项?

下面几个替换函数均是按照上述逻辑执行的。

①:execl

int execl(const char *path, const char *arg, ...);
  • 第一个参数是要执行程序的路径
  • 第二个参数是可变参数列表(我们可以按照用户的意愿传入数量大小不等的参数),表示你要如何执行这个此程序,命令行怎么写(ls -l -a),这个参数就怎么填 "ls","-l","-a",并以NULL结尾。

示例:

假设我执行的是ls -l -a程序:

当我make之后,会生成myexec可执行程序,现在运行此程序: 

当我执行top、pwd命令也亦是同样的操作:

  • 此时一个现象就产生了,当我调用execl替换程序函数成功后,后面的printf语句并没有执行,原因就在于一旦execl替换成功,是将当前进程的代码和数据全部替换了,后面的printf自然就被替换了,即该代码就不存在了。

问:程序替换用不用判断返回值?

  • 程序替换不用判断返回值,这就是我们前面提到的,因为一旦程序替换成功了,就不会有返回值,而失败的时候,必然会向后执行,最多通过返回值得到什么原因导致的替换失败。

下面演示进程替换失败的场景(只需随便执行一个不存在的程序即可)

我们的上述操作是一个单进程程序,我没有创建子进程,相当于是把父进程的代码给替换了,现在想让子进程完成替换操作,如下:

上述操作就完成了让子进程执行全新的程序,以前是执行父进程的代码片段,运行结果如下:

问:子进程执行程序替换,会不会影响父进程呢?

  • 答案是不会的,这个我上面详细讲解过,总结就是进程具有独立性,当程序替换的时候,代码和数据都发生了写时拷贝完成父子的分离。

②:execv

int execv(const char *path, char *const argv[]);
  • 第一个参数是要执行程序的路径
  • 第二个参数是一个字符指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾

例如,要执行的是ls -l -a -i


   
  1. char * const argv_[] = { ( char*) "ls", ( char*) "-a", ( char*) "-l", ( char*) "-i", NULL };
  2. execv( "/usr/bin/ls", argv_);

前面的execl是一个一个参数传过去的,这里execv是直接传一个指针数组,示例如下:

补充:vim批量化注释、取消注释小技巧

  • 注释:小写,ctrl+v,hjkl选中区域,切换大写,输入I,//,esc
  • 取消注释:小写,ctrl+v,hjkl选中区域(注释区域),输入d

③:execlp

int execlp(const char *file, const char *arg, ...);
  • 第一个参数是要执行程序的名字
  • 第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾

例如,要执行的是ls -a -l

execlp("ls", "ls", "-a", "-l", NULL );

它和execl唯一的区别就是多了一个p,在我们执行指令的时候,默认的搜索路径是在环境变量PATH搜索的,而execl中的p就是此PATH环境变量,前面我们的execl和execv都是要指明路径的,而这里execlp命名带p了,因此就可以不带路径,只说出你要执行哪一个程序即可。示例:

  • 这里出现了两个ls,含义是不一样的,第一个参数是供系统去找你要执行谁的,第二个是你想怎么执行它。

④:execvp

int execvp(const char *file, char *const argv[]);
  • 第一个参数是要执行程序的名字
  • 第二个参数是一个字符指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾

例如,要执行的是ls -a -l -i


   
  1. char * const argv_[] = { ( char*) "ls", ( char*) "-a", ( char*) "-l", ( char*) "-i", NULL };
  2. execvp( "ls", argv_);

此替换函数同样是带了p,所以它是从PATH路径里头找,我们只需要程序名即可,还带了v,我们就可以将命令行参数字符串统一放入数组中即可完成调用。示例:

⑤:execle

在正式讲解此替换函数execle前,先来看这样一个问题:

  • 目前我们执行的程序,全部都是系统命令,如果我们要执行自己写的C/C++程序呢?如果我们要执行其它语言写的程序呢?

首先,一次makefile默认只能生产一个可执行程序,但是按如下修改一次可生产多个可执行程序:

现在有两个可执行程序,我现在的目标是让myexec把mycmd调起来,代码如下:


   
  1. execl( "/home/xzy/dir/date16/mycmd", "mycmd", NULL); //绝对路径
  2. //execl("./mycmd", "mycmd", NULL);相对路径

详情如下:

上述实现了C可执行程序调C++可执行程序,现在来实现C可执行程序调用python可执行程序,如下一个python小脚本:

现在想让C可执行程序myexec来调用python,只需如下操作:

execl("/usr/bin/python3", "python3", "test.py", NULL); 

 解决了刚刚那个问题,现在再回过头来看execle替换函数:

int execle(const char *path, const char *arg, ..., char * const envp[]);
  • 第一个参数是要执行程序的路径
  • 第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾
  • 第三个参数是你自己设置的环境变量

以我设置的"MYPATH"环境变量为例:


   
  1. char* const env_[] = { ( char*) "MYPATH=YouCanSeeMe!!!", NULL };
  2. execle( "./mycmd", "mycmd", NULL, env_);

execle的第三个参数表示的是如果你想给你的程序传入对应的环境变量信息,也是一个字符指针数组,那就可以传入对应的环境变量参数,示例:

如下我们的mycmd.cpp文件:

如上我输出的是系统PATH中的环境变量,现在我想自定义环境变量MYPATH并交给子进程,我的myexec.c程序如下:

当我make后并运行myexec可执行程序会发现有个问题:

当我自定义一个环境变量时,运行myexec,我PATH环境变量就输出不了了,当我把mycmd.cpp中的PATH语句那块给注释掉,再运行myexec,此时就能看到我自定义的环境变量MYPATH了:

为什么我把PATH的注释取消了,却输出不了我的PATH环境变量?相反的,把PATH注释掉才能打印我的MYPATH? 

  • 根本原因:自己添加环境变量给目标进程,是覆盖式的!!!

解决办法如下:

使用environ(系统环境变量的指针声明),利用export将MYPATH添加到系统环境变量中:

⑥:execve

int execve(const char *filename, char *const argv[], char *const envp[]);
  • 第一个参数是要执行程序的路径
  • 第二个参数是一个指针数组,表示你要如何执行这个程序,数组以NULL结尾
  • 第三个参数是你自己设置的环境变量 

例如,你设置了MYPATH环境变量,在myexec程序内部就可以使用该环境变量:


   
  1. char* my_argv[] = { "myexec", NULL };
  2. char* my_envp[] = { "MYPATH=helloWorld", NULL };
  3. execve( "./myexec", my_argv, my_envp);

⑦:execvpe

int execvpe(const char *file, char *const argv[], char *const envp[]);
  • 第一个参数是要执行程序的路径
  • 第二个参数是一个指针数组,表示你要如何执行这个程序,数组以NULL结尾
  • 第三个参数是你自己设置的环境变量

此替换函数在我们前面讲解的基础上,已经不难理解了,换汤不换药,就不给出测试用例了。

问:为什么会有这么多接口?execve为什么是单独的?

  • 其实替换函数一共有7个,只不过execve是单独的:

有多个接口的原因在于调用替换函数的场景各不相同,所以有不同的接口,execve是单独的原因其实我在命名理解那也说明了,这里再强调下:

  • 事实上,只有execve是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,其它五个函数在man手册的第3节,也就是说其它五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的需求。

转载:https://blog.csdn.net/bit_zyx/article/details/126582805
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场