一、进程替换是什么?
fork()之后,父子各自执行父进程代码
的一部分。如果子进程想执行全新程序
就会用到进程的程序替换来完成这个功能
程序替换:通过特定接口,加载磁盘
上的一个权限程序(代码和数据)
加载到调用进程的地址空间中
以达到让子进程执行其他程序的目的
将新的磁盘上的程序加载加载到内存
并和当前进程页表重新建立映射
用操作系统相关接口即可完成
二、execl系列函数
man execl 查看exec系列函数
一共有6个函数,这些函数都是
成功无返回值
失败返回 -1
以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;
}
✨✨✨✨✨✨✨✨
本篇博客完,感谢阅读🌹
如有错误之处可评论指出
博主会耐心听取每条意见
✨✨✨✨✨✨✨✨