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传入全部的环境变量,自己定义的环境变量要自己手动添加

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

042b5959402f4f728dc508855c6e2186.png

总览代码:

mycmd.cpp:

#include <iostream>
#include <stdlib.h>

int main()
{
    std::cout << "PATH:" << getenv("PATH") << std::endl;
    std::cout << "-------------------------------------------\n";
    std::cout << "MYPATH:" << getenv("MYPATH") << std::endl;
    std::cout << "-------------------------------------------\n";

    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    return 0;
}
myexec.c:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>


int main()
{
    //环境变量的指针声明
    extern char**environ;

    printf("我是父进程,我的pid是: %d\n", getpid());
    pid_t id = fork();
    if(id == 0){
        //child
        //我们想让子进程执行全新的程序,以前是执行父进程的代码片段
        
        printf("我是子进程,我的pid是: %d\n", getpid());
        char *const env_[] = {
            (char*)"MYPATH=YouCanSeeMe!!",
            NULL
        };
        //env_: 添加环境变量给目标进程,是覆盖式的
        //execle("./mycmd", "mycmd", NULL, env_);
可利用extern新增式添加环境变量:
        execle("./mycmd", "mycmd", NULL, environ);
      
        exit(1); //只要执行了exit,意味着,execl系列的函数失败了
    }
    // 一定是父进程
    int status = 0;
    int ret = waitpid(id, &status, 0);
    if(ret == id)
    {
        sleep(2);
        printf("父进程等待成功!\n");
    }
    return 0;
}

861411a85968406b99a76a93d23ce03d.png

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

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

f1e9555620c44a5197c3a92ec8569e87.png

275a091b81ee4720a9ecec35888a678c.png

   // 一定是父进程
    int status = 0;
    int ret = waitpid(id, &status, 0);
    if(ret == id)
    {
        sleep(2);
        printf("父进程等待成功!\n");
    }
    return 0;
}

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

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

e1d7cd5dc710416c8f18399739849877.png

6.模拟实现shell

hello.c

#include <stdio.h>

int main()
{
    printf("hello my shell\n");
    return 0;
}

makefile

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

myshell.c

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

#define SEP " "    //可以是多个,比如" ,." ——空格,逗号,句号 隔开
#define NUM 1024
#define SIZE 128

char command_line[NUM];
char *command_args[SIZE];

int main()
{
    //shell 本质上就是一个死循环
    while(1)
    {
        //不关心获取这些属性的接口, 搜索一下
        //1. 显示提示符
        printf("[张三@我的主机名 当前目录]# ");
        fflush(stdout);
        //2. 获取用户输入
        memset(command_line, '\0', sizeof(command_line)*sizeof(char));
        fgets(command_line, NUM, stdin); //键盘,标准输入,stdin, 获取
//到的是c风格的字符串, 尾部会加上'\0',从stdin获取NUM个字节放入地址command_line
        command_line[strlen(command_line) - 1] = '\0';// fgets时,最后敲回车也会
//输入进command_line中,所以要清空这个\n,把\n设置成\0
        //3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分
        command_args[0] = strtok(command_line, SEP);
        int index = 1;
        // (1)= 是故意这么写的
        // strtok 截取成功,返回字符串起始地址;截取失败,返回NULL
        // (2)上面strtok已截取ls,想继续截取,参数1应给NULL
        while(command_args[index++] = strtok(NULL, SEP));

        //for debug为了打印看一下我们输入的字符串是否都保存到command_args中了——————————
        //for(int i = 0 ; i < index; i++)
        //{
        //    printf("%d : %s\n", i, command_args[i]);
        //}
        //——————————————————————————————————————————————————————————————
        // 4. TODO, 编写后面的逻辑, 内建命令
        // 5. 创建进程,执行
        pid_t id = fork();
        if(id == 0)
        {
            //child
            // 6. 程序替换
            //exec*?
execvp(command_args[0]/*不就是保存的是我们要执行的程序名字吗?*/, command_args);
            exit(1); //执行到这里,子进程一定替换失败
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
            printf("等待子进程成功: sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF);
        }
    }// end while
}

二.内建命令

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

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

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

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

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

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

//对应上层的内建命令
int ChangeDir(const char * new_path)
{
    chdir(new_path);    

    return 0; // 调用成功
}

while(1)
{
……
 // 4. TODO, 编写后面的逻辑, 内建命令
        if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
        {
            ChangeDir(command_args[1]); //让调用方进行路径切换, 父进程
            continue;
        }
}

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

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

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

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

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

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

3fbad74f509a4a79a3c0a5c0d6a2bee2.png

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

void PutEnvInMyShell(char * new_env)
{
    putenv(new_env);
}

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

补充模拟shell给ls上色:

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

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

        //3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分
        command_args[0] = strtok(command_line, SEP);
        int index = 1;
        // 给ls命令添加颜色
        if(strcmp(command_args[0]/*程序名*/, "ls") == 0 ) //如果是ls命令,就加色
            command_args[index++] = (char*)"--color=auto";

        // = 是故意这么写的
        // strtok 截取成功,返回字符串其实地址
        // 截取失败,返回NULL
        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完整代码:

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

#define SEP " "
#define NUM 1024
#define SIZE 128

char command_line[NUM];
char *command_args[SIZE];

char env_buffer[NUM]; //for test

extern char**environ;

//对应上层的内建命令
int ChangeDir(const char * new_path)
{
    chdir(new_path);

    return 0; // 调用成功
}

void PutEnvInMyShell(char * new_env)
{
    putenv(new_env);
}

int main()
{
    //shell 本质上就是一个死循环
    while(1)
    {
        //不关心获取这些属性的接口, 搜索一下
        //1. 显示提示符
        printf("[张三@我的主机名 当前目录]# ");
        fflush(stdout);

        //2. 获取用户输入
        memset(command_line, '\0', sizeof(command_line)*sizeof(char));
        fgets(command_line, NUM, stdin); //键盘,标准输入,stdin, 获取到的是c风格的字符串, '\0'
        command_line[strlen(command_line) - 1] = '\0';// 清空\n

        //3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分
        command_args[0] = strtok(command_line, SEP);
        int index = 1;
        // 给ls命令添加颜色
        if(strcmp(command_args[0]/*程序名*/, "ls") == 0 ) //如果是ls命令,就加色
            command_args[index++] = (char*)"--color=auto";

        // = 是故意这么写的
        // strtok 截取成功,返回字符串其实地址
        // 截取失败,返回NULL
        while(command_args[index++] = strtok(NULL, SEP));

        //for debug
        //for(int i = 0 ; i < index; i++)
        //{
        //    printf("%d : %s\n", i, command_args[i]);
        //}
    
        // 4. TODO, 编写后面的逻辑, 内建命令
        if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
        {
            ChangeDir(command_args[1]); //让调用方进行路径切换, 父进程
            continue;    //内建命令走完直接continue就不会创建子进程
        }
        if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
        {
            // 目前,环境变量信息在command_line,会被清空
            // 此处我们需要自己保存一下环境变量内容
            strcpy(env_buffer, command_args[1]);
            PutEnvInMyShell(env_buffer); //export myval=100, BUG?
            continue;    //内建命令走完直接continue就不会创建子进程
        }

        // 5. 创建进程,执行
        pid_t id = fork();
        if(id == 0)
        {
            //child
            // 6. 程序替换
            //exec*?
            execvp(command_args[0]/*不就是保存的是我们要执行的程序名字吗?*/, command_args);
            exit(1); //执行到这里,子进程一定替换失败
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
            printf("等待子进程成功: sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF);
        }
    }// end while
}

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

a8c12675341c43509df42fe30df1c55c.png

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

cc60a1557517492fbfe0bb69cfb271c4.png

猜你喜欢

转载自blog.csdn.net/zhang_si_hang/article/details/127401753