Linux学习笔记(十)--进程替换与创建一个自己的简易版shell
进程替换:进程替换是指在当前进程的上下文中完全替换为执行另一个程序的过程。在Unix/Linux系统中,这通过exec
系列函数实现。
替换原理:用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
exec系列函数:(l表示list列表,p表示path,e表示environment,v可以理解成vector数组)
函数 | 描述 | 参数格式 | 是否使用PATH |
---|---|---|---|
| 参数列表 |
| 否 |
| 参数列表,使用PATH |
| 是 |
| 参数列表,自定义环境 |
| 否 |
| 参数数组 |
| 否 |
| 参数数组,使用PATH |
| 是 |
| 参数数组,自定义环境 |
| 是 |
在shell中,所有进程都是bash的子进程,所有命令的启动都依赖于exec*系列的函数,exec*承担的是一个加载器的效果。
execl函数:
函数原型:
#include <unistd.h>int execl(const char *path, const char *arg0, ..., (char *) NULL);
参数:
path: 要执行的可执行文件的完整路径
arg0: 第一个参数通常是程序名称本身
..: 后续参数列表,以 (char *) NULL
结束
返回值:(1)成功,不返回,进程已被替换(2)失败,返回-1,并设置error
execl函数从第二个参数开始,在命令行当中是怎么写的,就把参数依次传递给execl,只不过把空格替换成",",所有的exec系列的函数的第一个参数就是执行这个方法的位置,后面的参数用来如何执行程序(要不要涵盖选项,涵盖哪些选项)
#include <unistd.h>
#include <stdio.h>int main() {printf("准备执行ls命令...\n");// 参数列表必须以NULL结束execl("/bin/ls", "ls", "-l", "-a", NULL);// 如果exec成功,下面的代码不会执行perror("exec失败");return 1;
}
execlp函数:相比于execl多了个p,会在自己默认的path环境变量中查找。
#include <unistd.h>int execlp(const char *file, const char *arg0, ..., (char *) NULL);
参数:
ile: 要执行的文件名(不需要完整路径)
arg0: 第一个参数通常是程序名称本身
...: 后续参数列表,必须以 (char *) NULL
结束
返回值与execl相同。
execlp("ls", "ls", "-l", "-a", NULL);
execlp中的两个ls有什么区别?
第一个ls : 要执行的程序名这是 execlp 函数的第一个参数。它的作用是告诉系统 “我要执行哪个程序”。execlp 函数会根据这个名称,在系统的 PATH 环境变量所指定的目录中搜索名为 ls 的可执行文件。
第二个ls: 传递给程序的第一个命令行参数,这是 execlp 函数的第二个参数。它的作用是作为命令行参数传递给即将被执行的 ls 程序。对于 ls 命令来说,这通常是它要操作的对象(比如文件或目录名),或者是一个选项标志(如 -l , -a )。在您的截图中,这个参数就是 ls 命令本身要执行时接收到的第一个参数。所以第二个ls通常可以任意更改,因为它对应的是argv[0] 通常被设置为程序自身的名称。所以我们在例子中看到的是 "ls" 。execv函数:
#include <unistd.h>int execv(const char *path, char *const argv[]);
参数:
path: 要执行的可执行文件的完整路径
argv: 参数数组,其中:(1)argv[0]
通常是程序名称本身(2)最后一个元素必须是 NULL
指针
返回值与execl相同。
示例:
#include <unistd.h>
#include <stdio.h>int main() {char *args[] = {"ls", "-l", "-a", NULL};printf("准备执行ls命令...\n");execv("/bin/ls", args);perror("execv失败");return 1;
}
execve:(e表示环境变量)
#include <unistd.h>int execve(const char *pathname, char *const argv[], char *const envp[]);
参数说明:
pathname: 要执行的可执行文件的完整路径
argv: 参数数组,格式与 execv()
相同
envp(环境变量数组):(1)格式为 "VARNAME=value"
的字符串(2)必须以 NULL
指针结束
返回值与execl相同。
#include <unistd.h>
#include <stdio.h>int main() {char *args[] = {"ls", "-l", NULL};char *env[] = {"PATH=/bin:/usr/bin", "TERM=xterm-256color", NULL};printf("准备执行ls命令...\n");execve("/bin/ls", args, env);perror("execve失败");return 1;
}
我们知道,在makefile中在编译执行可执行文件时,哪个文件在前面就形成哪一个文件的可执行文件,那么我们如何用一个makefile一次形成两个可执行文件?我们这个时候就需要创立一个伪目标文件all。如下代码所示:
.PHONY: all
all: otherExe mycommandotherExe: otherExe.cppg++ -o $@ $^ -std=c++11mycommand: mycommand.cgcc -o $@ $^ -std=c99.PHONY: clean
clean:rm -f mycommand otherExe
作用:在一个可执行程序中调用另一个可执行程序。
所以结合我们所学知识,我们可创建一个属于自己的简易版shell。
要写一个shell,需要遵守以下循环:
1. 获取命令行
2. 解析命令行
3. 建立一个子进程(fork)
4. 替换子进程(execvp)
5. 父进程等待子进程退出(wait)
根据这些思路,和前面所学到的知识,我们就可以自己创建一个简易版的shell
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <ctype.h>#define MAX_CMD 1024
char command[MAX_CMD];int do_face()
{memset(command, 0x00, MAX_CMD);printf("minishell$ ");fflush(stdout);if (scanf("%[^\n]%*c", command) == 0) {getchar();return -1;}return 0;
}char **do_parse(char *buff)
{int argc = 0;static char *argv[32];char *ptr = buff;while (*ptr != '\0') {if (!isspace(*ptr)) {argv[argc++] = ptr;while ((!isspace(*ptr)) && (*ptr != '\0')) {ptr++;}if (*ptr == '\0') {break;}*ptr = '\0';ptr++;} else {ptr++;}}argv[argc] = NULL;return argv;
}int do_exec(char *buff)
{char **argv = NULL;pid_t pid = fork();if (pid == 0) {argv = do_parse(buff);if (argv[0] == NULL) {exit(-1);}execvp(argv[0], argv);perror("execvp");exit(-1);} else if (pid > 0) {waitpid(pid, NULL, 0);} else {perror("fork");return -1;}return 0;
}int main(int argc, char *argv[])
{while (1) {if (do_face() < 0) {continue;}do_exec(command);}return 0;
}