• linux篇【7】:进程程序替换


    目录

    一.进程程序替换

    1.概念(原理在下面)

    2.为什么程序替换

    3.程序替换的原理:

    4.如何进行程序替换

    我们先用第一个execl做示范:

    例子:

    (1) execl执行成功后面的代码全部不执行

    (2)所以这个程序替换函数execl,用不用判断返回值?为什么?

    返回例子失败1:路径是错误的

     返回失败例子2:选项是错误的

    (3)引入进程创建——子进程执行程序替换,会不会影响父进程呢? ?

    5.大量的测试各种不同的接口

    命名理解 (带v和带l的)

    记忆技巧:

    带e和带p

    (1)execv

    (2)execlp

    ​编辑  (3)execvp

    (4)execle

    用系统接口在c程序中调用cpp程序

    6.模拟实现shell

    hello.c

    makefile

    myshell.c

    二.内建命令

    1.内建命令——以chdir为例

    三.进程替换中的环境变量

    补充模拟shell给ls上色:

    模拟shell完整代码:


    一.进程程序替换

    1.概念(原理在下面)

    子进程执行的是父进程的代码片段,如果我们想让创建出来的子进程,执行全新的程序呢?
    需要用到:进程的程序替换
     

    2.为什么程序替换

    我们一般在服务器设计(linux编程)的时候,往往需要子进程干两件种类事情
    1.让子进程执行父进程的代码片段(服务器代码)
    2.让子进程执行磁盘中一个全新的程序(shell,想让客户端执行对应的程序,通过我们的进程,执行其他人写的进程代码等等),c/c++ -> c/c++/Python/Shell/Php/Java...

    3.程序替换的原理:

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

     

    这个过程有没有创建新的进程呢?
    没有! 子进程的PCB等结构并未改变,只是改变的页表映射关系。

    程序替换成功后,运行完新程序,则程序直接退出;程序替换成功后,原进程没有退出,使用原进程运行新程序

    我们只能调用接口,为什么呢?
    因为这个过程实际上是把数据从一个硬件搬到另一个硬件的操作,这个操作只能由OS操作系统完成

    4.如何进行程序替换

    man execl 查看进行程序替换的函数:

    18f55bb0eb2b496ab9b2ba4019dd723d.png

    我们先用第一个execl做示范:

    ae86d3781266486f9c5187757178b16f.png

    具体例子:execl("usr/bin/pwd","ls","-l","-a",NULL); 
    我们如果想执行一个全新的程序(本质就是磁盘上的文件),我们需要做几件事情:
    1.先找到这个程序在哪里?        ——程序在哪?(举例:可以通过which "pwd",查pwd的路径)
    2. 程序可能携带选项进行执行(也可以不携带)        ——怎么执行?
            所以要明确告诉OS,我想怎么执行这个程序?要不要带选项

    红线部分执行第一个问题,绿线部分执行第二个问题

    命令行怎么写(ls -l -a), 这个参数就怎么填"ls","-l","-a",最后必须是NULL,标识参数传递完毕[如何执行程序的]

    例子:

    327e672cdcb340948f5d97fc69d29fed.png

    (1) execl执行成功后面的代码全部不执行

    4956d8944de84cc59e4e73d71f1bd175.png

    后面的printf是代码吗? 为什么没执行?

    因为execl一旦替换成功,是将当前进程的所有代码和数据全部替换了!
    后面的printf 实际上已经早就被替换了!该代码不存在了

    (2)所以这个程序替换函数execl,用不用判断返回值?为什么?

    int ret= execl(...);

    答:不用判断返回值(但是还是需要返回值),因为一旦替换成功,就不会有返回值,也不会执行返回语句,因为int ret 这个返回值也是当前进程的代码和数据,execl一旦替换成功,是将当前进程的所有代码和数据全部替换了execl就直接执行ls命令的代码去了。如果有返回值,必然是程序替换失败,也必然会继续向后执行! ! 最多通过返回值得到什么原因导致的替换失败!

    返回例子失败1:路径是错误的

    b76db24524954fb3af3fe63ae3332ce0.png

     返回失败例子2:选项是错误的

    1e3b7015951744f3997a89bbbf49043e.png

    (3)引入进程创建——子进程执行程序替换,会不会影响父进程呢? ?

    37fb8e3fed9c477898a78fd854b1d72c.png

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

    不会,因为进程具有独立性。
    为什么,如何做到的? ?数据层面发生写时拷贝!当程序替换的时候,我们可以理解成为:代码和数据都发生了写时拷贝完成父子的分离!

    5.大量的测试各种不同的接口

    命名理解 (带v和带l的)

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

    l(list) : 表示参数采用列表

    v(vector) : 参数用数组

    p(path) : 有p自动搜索环境变量PATH

    e(env) : 表示自己维护环境变量

    记忆技巧:

    execl结尾 l 为list,列表传参——>可变参数包,一个一个传。

    execv结尾 v 为vector,数组传参——>传的是指针数组。

    8a3abef935e14dc9911ce63a5beae742.png

    带e和带p

    带e的都是可以传环境变量的(execle,execvpe)但是会覆盖系统原有的环境变量,把自己传的环境变量交给进程;不带e是默认继承系统的环境变量;带p的都是可以自带路径的,直接传命令名称即可(execlp,execvp,execvpe)

    18f55bb0eb2b496ab9b2ba4019dd723d.png

    (1)execv

    int execv(const char *path, char *const argv[]);        

    path 依然是程序的路径,参数 argv[] 是存着要实现指令的指针数组

    4b2b1eff01d34b1b9713c06a40dd71cc.png

     execv VS execl        只有传参方式的区别! ! execl是传可变参数,execv是传指针数组

    f74fcc9112954b268110a1b08e832a22.png

    (2)execlp

    int execlp(const char *file, const char *arg, ...);        带p的就传程序名即可

    file:要执行的程序。执行指令的时候,默认的搜索路径,在哪里搜索呢?在环境变量PATH
    命名带p的,可以不带路径,只说出你要执行哪一个程序即可!
    execlp("ls","ls", "-a", "-1 ",NULL)

    里出现了两个Is,含义不一样:第一个ls告诉你要执行的程序,后面的ls已经-a等是执行方式

    f27b940a28bd404eb8aa58826d36259f.png  (3)execvp

    int execvp(const char *file, char *const argv[]); 与上面同理

    5b579586b62442b7898514a90f1b9755.png

    (4)execle

     int execle(const char *path, const char *arg, ..., char * const envp[]);

    char * const envp[]: 添加环境变量给目标进程,是覆盖式的。如果传 execle("./mycmd", "mycmd", NULL, env_); 会导致原本的环境变量全部被覆盖而失效,所以要利用全局变量environ传入全部的环境变量,自己定义的环境变量要自己手动添加

    1. 环境变量的指针声明
    2. extern char**environ;
    3. ……
    4. execle("./mycmd", "mycmd", NULL, environ);

    042b5959402f4f728dc508855c6e2186.png

    总览代码:

    1. mycmd.cpp:
    2. #include
    3. #include
    4. int main()
    5. {
    6. std::cout << "PATH:" << getenv("PATH") << std::endl;
    7. std::cout << "-------------------------------------------\n";
    8. std::cout << "MYPATH:" << getenv("MYPATH") << std::endl;
    9. std::cout << "-------------------------------------------\n";
    10. std::cout << "hello c++" << std::endl;
    11. std::cout << "hello c++" << std::endl;
    12. std::cout << "hello c++" << std::endl;
    13. std::cout << "hello c++" << std::endl;
    14. std::cout << "hello c++" << std::endl;
    15. std::cout << "hello c++" << std::endl;
    16. std::cout << "hello c++" << std::endl;
    17. std::cout << "hello c++" << std::endl;
    18. return 0;
    19. }
    1. myexec.c:
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. //环境变量的指针声明
    9. extern char**environ;
    10. printf("我是父进程,我的pid是: %d\n", getpid());
    11. pid_t id = fork();
    12. if(id == 0){
    13. //child
    14. //我们想让子进程执行全新的程序,以前是执行父进程的代码片段
    15. printf("我是子进程,我的pid是: %d\n", getpid());
    16. char *const env_[] = {
    17. (char*)"MYPATH=YouCanSeeMe!!",
    18. NULL
    19. };
    20. //env_: 添加环境变量给目标进程,是覆盖式的
    21. //execle("./mycmd", "mycmd", NULL, env_);
    22. 可利用extern新增式添加环境变量:
    23. execle("./mycmd", "mycmd", NULL, environ);
    24. exit(1); //只要执行了exit,意味着,execl系列的函数失败了
    25. }
    26. // 一定是父进程
    27. int status = 0;
    28. int ret = waitpid(id, &status, 0);
    29. if(ret == id)
    30. {
    31. sleep(2);
    32. printf("父进程等待成功!\n");
    33. }
    34. return 0;
    35. }

    861411a85968406b99a76a93d23ce03d.png

    用系统接口在c程序中调用cpp程序

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

    f1e9555620c44a5197c3a92ec8569e87.png

    275a091b81ee4720a9ecec35888a678c.png

    1. // 一定是父进程
    2. int status = 0;
    3. int ret = waitpid(id, &status, 0);
    4. if(ret == id)
    5. {
    6. sleep(2);
    7. printf("父进程等待成功!\n");
    8. }
    9. return 0;
    10. }

     为什么会有这么多接口?——因为要适配应用场景。

    execve为什么是单独的?——实际上,只有 execve是系统调用,其他都是对系统接口的封装,最后都要调用到execve

    e1d7cd5dc710416c8f18399739849877.png

    6.模拟实现shell

    hello.c

    1. #include
    2. int main()
    3. {
    4. printf("hello my shell\n");
    5. return 0;
    6. }

    makefile

    1. myshell:myshell.c
    2. gcc -o $@ $^ -std=c99 //编不过就加-std=c99
    3. .PHONY:clean
    4. clean:
    5. rm -f myshell

    myshell.c

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #define SEP " " //可以是多个,比如" ,." ——空格,逗号,句号 隔开
    8. #define NUM 1024
    9. #define SIZE 128
    10. char command_line[NUM];
    11. char *command_args[SIZE];
    12. int main()
    13. {
    14. //shell 本质上就是一个死循环
    15. while(1)
    16. {
    17. //不关心获取这些属性的接口, 搜索一下
    18. //1. 显示提示符
    19. printf("[张三@我的主机名 当前目录]# ");
    20. fflush(stdout);
    21. //2. 获取用户输入
    22. memset(command_line, '\0', sizeof(command_line)*sizeof(char));
    23. fgets(command_line, NUM, stdin); //键盘,标准输入,stdin, 获取
    24. //到的是c风格的字符串, 尾部会加上'\0',从stdin获取NUM个字节放入地址command_line
    25. command_line[strlen(command_line) - 1] = '\0';// fgets时,最后敲回车也会
    26. //输入进command_line中,所以要清空这个\n,把\n设置成\0
    27. //3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分
    28. command_args[0] = strtok(command_line, SEP);
    29. int index = 1;
    30. // (1)= 是故意这么写的
    31. // strtok 截取成功,返回字符串起始地址;截取失败,返回NULL
    32. // (2)上面strtok已截取ls,想继续截取,参数1应给NULL
    33. while(command_args[index++] = strtok(NULL, SEP));
    34. //for debug为了打印看一下我们输入的字符串是否都保存到command_args中了——————————
    35. //for(int i = 0 ; i < index; i++)
    36. //{
    37. // printf("%d : %s\n", i, command_args[i]);
    38. //}
    39. //——————————————————————————————————————————————————————————————
    40. // 4. TODO, 编写后面的逻辑, 内建命令
    41. // 5. 创建进程,执行
    42. pid_t id = fork();
    43. if(id == 0)
    44. {
    45. //child
    46. // 6. 程序替换
    47. //exec*?
    48. execvp(command_args[0]/*不就是保存的是我们要执行的程序名字吗?*/, command_args);
    49. exit(1); //执行到这里,子进程一定替换失败
    50. }
    51. int status = 0;
    52. pid_t ret = waitpid(id, &status, 0);
    53. if(ret > 0)
    54. {
    55. printf("等待子进程成功: sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF);
    56. }
    57. }// end while
    58. }

    二.内建命令

    1.内建命令——以chdir为例

    如果直接exec*执行cd,最多只是让子进程进行路径切换,子进程是一运行就完毕的进程!切换子进程路径是没有意义的,我们在shell中,更希望父进程-shell本身的路径发生变化。只要父进程路径变化,后面的子进程就会继承父进程路径

    如果有些行为,是必须让父进程shell执行的, 不想让子进程执行,此时就绝对不能创建子进程!
    只能是父进程自己实现对应的代码! 由父进程shell自己执行的命令,我们称之为内建命令/内置bind-in 命令
    内建命令 相当于 shell内部的一个函数!

    (在上面myshell的基础上在第4部TODO做添加)

    父进程自己执行的,对应上层的内建命令

    chdir:想去哪个路径就传哪个路径

    1. //对应上层的内建命令
    2. int ChangeDir(const char * new_path)
    3. {
    4. chdir(new_path);
    5. return 0; // 调用成功
    6. }
    7. while(1)
    8. {
    9. ……
    10. // 4. TODO, 编写后面的逻辑, 内建命令
    11. if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
    12. {
    13. ChangeDir(command_args[1]); //让调用方进行路径切换, 父进程
    14. continue;
    15. }
    16. }

    内建命令举例:cd命令,export,echo

    三.进程替换中的环境变量

    环境变量的数据,在进程的上下文中
    1.环境变量会被子进程继承下去,所以他会有全局属性
    2.当我们进行程序替换的时候,当前进程的环境变量非但不会被替换,而且是继承父进程的! !因为环境变量是系统的数据。

    带e的都是可以传环境变量的(execle,execvpe)但是会覆盖系统原有的环境变量。执行到子进程的execle,execvpe时,把自己传的环境变量传入就会覆盖系统原有的环境变量;不带e是默认继承系统的环境变量。如果我们不想覆盖原有的(就不能执行到子进程的execle,execvpe),只想新增环境变量,就要父进程新增环境变量,父进程执行putenv这种内建命令来增加我们的环境变量,子进程就可以继承并获取了(环境变量会被其之下的所有子进程默认继承下去

    如何在shell内部新增自己的环境变量- putenv 注意:需要是一个独立的空间

    putenv:把传入的环境变量导入自己的上下文中

    3fbad74f509a4a79a3c0a5c0d6a2bee2.png

    函数说明:getenv()用来取得参数 name 环境变量的内容(linux命令env可以用来查看环境变量). 参数name 为环境变量的名称, 如果该变量存在则会返回指向该内容的指针。

    1. void PutEnvInMyShell(char * new_env)
    2. {
    3. putenv(new_env);
    4. }
    5. // 4. TODO, 编写后面的逻辑, 内建命令
    6. if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
    7. {
    8. // 目前,环境变量信息在command_line,每次memset时command_line都会被清空
    9. // 所以我们需要自己用全局的env_buffer保存一下环境变量内容
    10. strcpy(env_buffer, command_args[1]);
    11. PutEnvInMyShell(env_buffer); //export myval=100, BUG?
    12. continue;
    13. }

    补充模拟shell给ls上色:

    1. [zsh@ecs-78471 ~]$ which ls
    2. alias ls='ls --color=auto'
    3. /usr/bin/ls

    解释:alias 起别名,alias ls='ls --color=auto' 系统中是给指令'ls --color=auto'起别名为ls,所以平常我们的ls实际就是 'ls --color=auto' 这个命令。给ls上色就需要加上这个命令

    1. //3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分
    2. command_args[0] = strtok(command_line, SEP);
    3. int index = 1;
    4. // 给ls命令添加颜色
    5. if(strcmp(command_args[0]/*程序名*/, "ls") == 0 ) //如果是ls命令,就加色
    6. command_args[index++] = (char*)"--color=auto";
    7. // = 是故意这么写的
    8. // strtok 截取成功,返回字符串其实地址
    9. // 截取失败,返回NULL
    10. while(command_args[index++] = strtok(NULL, SEP));

    解释: command_args[0] = strtok(command_line, SEP); 已经把ls放进数组 command_args[0]了, if(strcmp(command_args[0]/*程序名*/, "ls") == 0 ) 判断如果是ls命令,就加色,执行下面命令:command_args[index++] = (char*)"--color=auto";(不是ls就不执行)给ls加上--color=auto就是上色,后面就是"ls" "--color=auto" "-a" "-l" "-i"

    模拟shell完整代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #define SEP " "
    9. #define NUM 1024
    10. #define SIZE 128
    11. char command_line[NUM];
    12. char *command_args[SIZE];
    13. char env_buffer[NUM]; //for test
    14. extern char**environ;
    15. //对应上层的内建命令
    16. int ChangeDir(const char * new_path)
    17. {
    18. chdir(new_path);
    19. return 0; // 调用成功
    20. }
    21. void PutEnvInMyShell(char * new_env)
    22. {
    23. putenv(new_env);
    24. }
    25. int main()
    26. {
    27. //shell 本质上就是一个死循环
    28. while(1)
    29. {
    30. //不关心获取这些属性的接口, 搜索一下
    31. //1. 显示提示符
    32. printf("[张三@我的主机名 当前目录]# ");
    33. fflush(stdout);
    34. //2. 获取用户输入
    35. memset(command_line, '\0', sizeof(command_line)*sizeof(char));
    36. fgets(command_line, NUM, stdin); //键盘,标准输入,stdin, 获取到的是c风格的字符串, '\0'
    37. command_line[strlen(command_line) - 1] = '\0';// 清空\n
    38. //3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分
    39. command_args[0] = strtok(command_line, SEP);
    40. int index = 1;
    41. // 给ls命令添加颜色
    42. if(strcmp(command_args[0]/*程序名*/, "ls") == 0 ) //如果是ls命令,就加色
    43. command_args[index++] = (char*)"--color=auto";
    44. // = 是故意这么写的
    45. // strtok 截取成功,返回字符串其实地址
    46. // 截取失败,返回NULL
    47. while(command_args[index++] = strtok(NULL, SEP));
    48. //for debug
    49. //for(int i = 0 ; i < index; i++)
    50. //{
    51. // printf("%d : %s\n", i, command_args[i]);
    52. //}
    53. // 4. TODO, 编写后面的逻辑, 内建命令
    54. if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
    55. {
    56. ChangeDir(command_args[1]); //让调用方进行路径切换, 父进程
    57. continue; //内建命令走完直接continue就不会创建子进程
    58. }
    59. if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
    60. {
    61. // 目前,环境变量信息在command_line,会被清空
    62. // 此处我们需要自己保存一下环境变量内容
    63. strcpy(env_buffer, command_args[1]);
    64. PutEnvInMyShell(env_buffer); //export myval=100, BUG?
    65. continue; //内建命令走完直接continue就不会创建子进程
    66. }
    67. // 5. 创建进程,执行
    68. pid_t id = fork();
    69. if(id == 0)
    70. {
    71. //child
    72. // 6. 程序替换
    73. //exec*?
    74. execvp(command_args[0]/*不就是保存的是我们要执行的程序名字吗?*/, command_args);
    75. exit(1); //执行到这里,子进程一定替换失败
    76. }
    77. int status = 0;
    78. pid_t ret = waitpid(id, &status, 0);
    79. if(ret > 0)
    80. {
    81. printf("等待子进程成功: sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF);
    82. }
    83. }// end while
    84. }

    测试 cd .. 发现可以正常使父进程返回

    a8c12675341c43509df42fe30df1c55c.png

    测试export也可以正常添加环境变量

    cc60a1557517492fbfe0bb69cfb271c4.png

  • 相关阅读:
    [附源码]Python计算机毕业设计Django基于Java的图书购物商城
    API对接需求如何做需求调研,需要注意什么?
    DAY9-力扣刷题
    es elasticsearch 九 索引index 定制分词器 type结构后期弃用原因 定制动态映射 动态映射模板 零停机重建索引
    【自然语言处理】利用python创建简单的聊天系统
    [附源码]Python计算机毕业设计Django房屋租赁信息系统
    Vue非父子组件之间的通信
    基于二次近似(BLEAQ)的双层优化进化算法_matlab程序
    java-net-php-python-springboot学校在线作业考试系统计算机毕业设计程序
    基于Huffman码实现的编码译码系统
  • 原文地址:https://blog.csdn.net/zhang_si_hang/article/details/127401753