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

2023-12-15 21:37:04

在这里插入图片描述

一、进程替换是什么?

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

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

将新的磁盘上的程序加载加载到内存
并和当前进程页表重新建立映射
用操作系统相关接口即可完成
在这里插入图片描述

二、execl系列函数

man execl 查看exec系列函数

在这里插入图片描述

一共有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;
}

测试结果
在这里插入图片描述
本来要执行后面的
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;
}

测试结果
在这里插入图片描述
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)里定义的
环境变量并打印

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

三、execv系列函数

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

在这里插入图片描述
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;
}

测试结果
在这里插入图片描述
execvp函数
我们学习上面几个函数
其他函数就很简单了
用法都是类似的

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

运行结果: 可以看到是能正常运行的
在这里插入图片描述
这些函数原型看起来很容易混
但只要掌握了规律就很好记

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

在这里插入图片描述
所有的接口底层都是调用
execle系统接口
在这里插入图片描述

四、如何执行其他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 <stdio.h>
#include <string.h>
#include <stdlib.h>

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;
}

运行结果
在这里插入图片描述
用一个可执行程序执行
其他语言的可执行程序

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

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

五、自实现shell命令解析器

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

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

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

// shell 运行原理:通过让子进程执行命令,父进程等待 && 解析命令
int main()                                                                                        
{
    // 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"
        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
		// 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");
    		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;
 }

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

文章来源:https://blog.csdn.net/2302_76421042/article/details/134985452
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。