【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器

【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器_第1张图片

一、进程替换是什么?

fork()之后,父子各自执行父进程代码
的一部分。如果子进程想执行全新程序
就会用到进程的程序替换来完成这个功能

程序替换:通过特定接口,加载磁盘
上的一个权限程序(代码和数据)
加载到调用进程的地址空间中
以达到让子进程执行其他程序的目的

将新的磁盘上的程序加载加载到内存
并和当前进程页表重新建立映射
用操作系统相关接口即可完成
【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器_第2张图片

二、execl系列函数

man execl 查看exec系列函数

【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器_第3张图片

一共有6个函数,这些函数都是

  1. 成功无返回值
  2. 失败返回 -1
  3. 以NULL作为结束标识符

且都属于exec系列函数

我们先演示最简单的execl函数

execl = exec + l
表示它属于exec系列里的execl系列函数
l 表示list,即后面传参时一个个往后跟
像链表的结点一样去传参

execl 是程序替换,该函数调用成功之后
会将当前进程所有代码和数据都进行替换
包括已执行的和没执行的

代码测试

int main()
{
	
    printf("当前进程的开始代码\n");
    execl("/usr/bin/ls", "ls", "-l", NULL);
    
    printf("当前进程的结束代码\n");
    
    return 0;
}

测试结果
【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器_第4张图片
本来要执行后面的
printf(“当前进程的结束代码\n”);
被替换成 ls -l 命令

execlp函数

execlp函数名中p表示PATH
即这个函数会自己在环境变量(PATH)
中进行查找,不用自己传路径

在这里插入图片描述
代码测试

#define NUM 16
 
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        // ls -a -l
        printf("子进程开始运行,pid: %d\n", getpid());
        sleep(3);                                                                                  
        // execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        execlp("ls", "ls", "-a", "-l", NULL);
        exit(1);
    }
    else
    {
        // 父进程
        printf("父进程开始运行,pid: %d\n", getpid());
        int status = 0;
        pid_t id = waitpid(-1, &status, 0); // 阻塞等待
        if (id > 0) // 等待成功
        {
            // 打印子进程退出码
            printf("wait success exit code: %d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

测试结果
【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器_第5张图片
execle函数

execle函数比execl函数多个一个参数
这个参数就是环境变量

在这里插入图片描述
在这里插入图片描述

// mycmd.c
int main()                                                                                                            {   
	printf("获取环境变量: my_l: %s\n", getenv("my_l"));
	return 0;
}
// myproc.c
#define NUM 16
const char* myfile = "./mycmd";

int main()
{	
    // 定义一个环境变量,环境变量名my_l后面的=不要空格
    char* const _env[NUM] = { (char*)"my_l=999777888666", NULL }; 
    pid_t id = fork();
    if (id == 0)
    {	
        // 子进程
        printf("子进程开始运行,pid: %d\n", getpid());
        sleep(3);
        char* const argv[NUM] = { (char*)"ls", (char*)"-a", (char*)"-l", NULL };
        execle(myfile, "mycmd", "-a", NULL, _env);
        exit(1);                                                                                                      
    }
    else
    {
        // 父进程
        printf("父进程开始运行,pid: %d\n", getpid());
        int status = 0;
        pid_t id = waitpid(-1, &status, 0); // 阻塞等待
        if (id > 0) // 等待成功
        {
            // 打印子进程退出码
            printf("wait success exit code: %d\n", WEXITSTATUS(status)); 
        }
    }
return 0;
}

通过execle函数
用一个可执行文件(myproc.c)
执行另一个可执行文件(mycmd.c)
并通过另一个可执行文件(mycmd.c)
获取可执行文件(myproc.c)里定义的
环境变量并打印

执行结果
【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器_第6张图片

三、execv系列函数

execv = exec + v
表示它属于exec系列里的execv系列函数
v 表示vector,即后面传参时把你要传的
参数构建成一个指针数组传过去

【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器_第7张图片
execv函数
可以看到execl和execv除了
传参的方式不一样其他都一样

代码测试

#define NUM 16
 
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        // ls -a -l
        printf("子进程开始运行,pid: %d\n", getpid());
        sleep(3);
        char* const argv[NUM] = { (char*)"ls", (char*)"-a", (char*)"-l", NULL };
        execv("/usr/bin/ls", argv);                                                                                     
        // execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        exit(1);
    }
    else
    {
        // 父进程
        printf("父进程开始运行,pid: %d\n", getpid());
        int status = 0;
        pid_t id = waitpid(-1, &status, 0); // 阻塞等待
        if (id > 0) // 等待成功
        {
            // 打印子进程退出码
            printf("wait success exit code: %d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

测试结果
【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器_第8张图片
execvp函数
我们学习上面几个函数
其他函数就很简单了
用法都是类似的

// 还是上面的代码,函数改成execvp函数
char* const argv[NUM] = { (char*)"ls", (char*)"-a", (char*)"-l", NULL };
execvp("ls", argv);

运行结果: 可以看到是能正常运行的
【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器_第9张图片
这些函数原型看起来很容易混
但只要掌握了规律就很好记

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量

【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器_第10张图片
所有的接口底层都是调用
execle系统接口
【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器_第11张图片

四、如何执行其他C、C++二进制程序

形成两个可执行程序
用一个可执行程序
调用另一个可执行程序

代码实现

// 另一个可执行程序地址
const char* myfile = "/home/ff/Gogh/tempfile/2023/mycmd";
// const char* myfile = "./mycmd"; // 绝对路径和相对路径都是可以的

int main()
{

    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        printf("子进程开始运行,pid: %d\n", getpid());
        sleep(3);
        execl(myfile, "mycmd", "-a", NULL);
        // execlp("./mycmd", "./mycmd", NULL); 用execlp调用另一个可执行程序
        exit(1);
    }
    else
    {
        // 父进程
        printf("父进程开始运行,pid: %d\n", getpid());
        int status = 0;
        pid_t id = waitpid(-1, &status, 0); // 阻塞等待
        if (id > 0) // 等待成功
        {
            // 打印子进程退出码
            printf("wait success exit code: %d\n", WEXITSTATUS(status)); 
        }
    }

    return 0;
}

另一个可执行程序代码

#include 
#include 
#include 

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        printf("can not execute\n");
        exit(1);
    }
    if (strcmp(argv[1], "-a") == 0)
    {
        printf("hello a\n");
    }
    else if (strcmp(argv[1], "-b") == 0)
    {
        printf("hello b\n");                                                                                            
    }
    else
    {
        printf("default\n");
    }
    return 0;
}

运行结果
【Linux进程控制(二)】进程程序替换(exec系列函数) and 自实现shell命令解释器_第12张图片
用一个可执行程序执行
其他语言的可执行程序

// 还是上面的代码
execlp("python", "python", "test.py", NULL);

通过上面的代码实现可以发现
exec*函数:功能就是加载器的底层接口

五、自实现shell命令解析器

shell执行命令通常有两种

  1. 第三方提供的对应在磁盘中有具体
    二进制文件的可执行程序(由子进程执行)
  2. shell内部自己实现的方法,由父进程执行
    shell代表用户,有些命令会影响shell本身
    比如cd,export等
#include 
#include 
#include 
#include 
#include 
#include 

#define NUM 1024
#define SIZE 32
#define SEP " "

// 保存完整的命令行字符串
char cmd_line[NUM];
// 保存打散之后的命令行字符串
char* g_argv[SIZE];

// 写一个环境变量buffer,用来测试
char g_myval[64];

// shell 运行原理:通过让子进程执行命令,父进程等待 && 解析命令
int main()
{
    extern char**environ; // 获取全局环境变量
    // 0.命令行解释器,一定是一个不退出的常驻内存的进程
    while (1)
    {
        // 1.打印提示信息 
        printf("[ff@localhost myshell]#"); // 内容在缓冲区,没有\n无法刷新,有\n光标无法定位当前行
        fflush(stdout); // 用fflush函数刷新
        memset(cmd_line, '\0', sizeof cmd_line); // 初始化
        // 2.获取用户的键盘输入[输入的是各种指令和选项:"ls -a -l -i"]
        if (fgets(cmd_line, sizeof cmd_line, stdin) == NULL) 
        {
            // 读取出错
            continue;
        }
        cmd_line[strlen(cmd_line) - 1] = '\0'; // 清除输入的换行符
        // printf("echo: %s\n", cmd_line);
        // 3.命令行字符串解析:"ls -a -l -i" ->  "ls" "-a" "-l" "-i"
        // export myval=111
        g_argv[0] = strtok(cmd_line, SEP); // 第一次调用,要传入原始字符串
        int index = 1;
        if (strcmp(g_argv[0], "ls") == 0) // 让目录带颜色
        {                                                                 
            g_argv[index++] = "--color=auto";
        }                                                
        if (strcmp(g_argv[0], "ll") == 0)
        {                                                                                                             
            g_argv[0] = "ls";
            g_argv[index++] = "-l";      
            g_argv[index++] = "--color=auto";
        }                    
        while (g_argv[index++] = strtok(NULL, SEP)); // 第二次如果还要解析原始字符串,传入NULL
        if (strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
        {
            strcpy(g_myval, g_argv[1]); // 把自己要导的环境变量不要和cmd_line共用空间,把g_myval拷到全新空间防止被覆盖
            int ret = putenv(g_myval);                            
            if (ret == 0) printf("%s export success\n", g_argv[1]);
            int i = 0;                                                                                                
            for (i = 0; environ[i]; i++)
                printf("%d: %s\n", i, environ[i]);                 
                      
            continue;                   
        }                                         
        // for (index = 0; g_argv[index]; index++ )
        //     printf("g_argv[%d]: %s\n", index, g_argv[index]); // g_argv解析完毕最后以NULL结尾
        // 4.TODO,内置命令,让父进程(shell)自己执行的命令叫做内置命令
        // 内置命令本质shell中的一个函数调用
        if (strcmp(g_argv[0], "cd") == 0)
        {
            if (g_argv[1] != NULL) chdir(g_argv[1]);
            continue;
        }
        // 5.fork
        pid_t id = fork();
        if (id == 0) // child
        {
            printf("下面功能让子进程进行的\n");
            printf("child, MYVAL: %s\n", getenv("MYVAL"));
            printf("child, PATH: %s\n", getenv("PATH"));
            // execvpe(g_argv[0], g_argv, environ);
            execvp(g_argv[0], g_argv);
            exit(1);
        }
        // father 阻塞等待
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if (ret > 0) printf("exit code: %d\n", WEXITSTATUS(status)); 
    }
    return 0;
}

扩展

程序替换会替换代码和数据
但环境变量相关的数据不会被替换
这也体现了环境变量具有全局属性

✨✨✨✨✨✨✨✨
本篇博客完,感谢阅读
如有错误之处可评论指出
博主会耐心听取每条意见
✨✨✨✨✨✨✨✨

你可能感兴趣的:(Linux,linux,windows,运维)