西部数码网站备份,面备案网站建设,我想花钱做网站,广州电力建设有限公司网站文章目录 3. 进程程序替换3.1 单进程版 -- 看看程序替换3.2 替换原理3.3 替换函数函数解释命名理解 3.4 多进程版 -- 验证各种程序替换接口3.5 自定义shell 3. 进程程序替换
3.1 单进程版 – 看看程序替换
makefile
mycommand:mycommand.cgcc -o $ $^ -stdc99
.PHONY:clean
… 文章目录 3. 进程程序替换3.1 单进程版 -- 看看程序替换3.2 替换原理3.3 替换函数函数解释命名理解 3.4 多进程版 -- 验证各种程序替换接口3.5 自定义shell 3. 进程程序替换
3.1 单进程版 – 看看程序替换
makefile
mycommand:mycommand.cgcc -o $ $^ -stdc99
.PHONY:clean
clean:rm -f mycommandmycommand.c
#include stdio.h
#include unistd.h// 提供execl, getpid等函数
#include stdlib.hint main(){// 打印exec调用前的进程信息// getpid(): 获取当前进程ID// getppid(): 获取父进程IDprintf(before: I am a process, pid:%d, ppid:%d\n,getpid(),getppid());//这类方法的标准写法// execl函数执行新程序// 参数1 /usr/bin/ls: 要执行的程序的完整路径// 参数2 ls: 程序名称argv[0]// 参数3 -a: 显示所有文件包括隐藏文件// 参数4 -l: 使用长格式显示// 参数5 NULL: 参数列表结束标志execl(/usr/bin/ls,ls, -a, -l, NULL);// 如果exec执行成功这行代码永远不会被执行// 因为原程序的内容已被ls程序替换printf(after: I am a process, pid:%d, ppid:%d\n,getpid(),getppid());return 0;
}打印结果
ydk_108iZuf68hz06p6s2809gl3i1Z:~/108/lesson17$ ./mycommand
before: I am a process, pid:261495, ppid:261085
total 36
drwxrwxr-x 2 ydk_108 ydk_108 4096 Jan 20 22:26 .
drwxrwxr-x 13 ydk_108 ydk_108 4096 Jan 20 22:14 ..
-rw-rw-r-- 1 ydk_108 ydk_108 82 Jan 20 22:16 makefile
-rwxrwxr-x 1 ydk_108 ydk_108 16832 Jan 20 22:26 mycommand
-rw-rw-r-- 1 ydk_108 ydk_108 895 Jan 20 22:26 mycommand.c
ydk_108iZuf68hz06p6s2809gl3i1Z:~/108/lesson17$ 这个程序被ls替换了。 一开始这个进程有自己的数据段和代码段ls有自己的数据段和代码段但是这里ls直接把mycommand的进程的代码段和数据段在内存中覆盖了进而把页表中的虚拟地址也更改了。
这个就叫做进程替换也就是进程替换的基本原理。 3.2 替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。 3.3 替换函数
其实有六种以exec开头的函数,统称exec函数这六个都是库函数调用接口他们的区别只是传参的不同
#include unistd.hint execl(const char *path, const char *arg, ...);// 第一个参数是完整路径第二个参数通常是程序名
int execlp(const char *file, const char *arg, ...);//execlp自己会在默认的PATH系统的环境变量里面找所以可以不带路径
int execle(const char *path, const char *arg, ...,char *const envp[]);// e 表示可以传递环境变量最后一个参数是环境变量数组
int execv(const char *path, char *const argv[]);// v 表示参数以数组形式传递比execl更适合参数数量不确定的情况
int execvp(const char *file, char *const argv[]);// 结合了v(数组)和p(PATH搜索)的特点
int execvpe(const char *file, char *const argv[], char *const envp[]);// 结合了v(数组)、p(PATH搜索)和e(环境变量)的特点这个和上面6个不一样这个是系统调用接口上面6个是库函数调用接口。 int execve(const char *path, char *const argv[], char *const envp[]); 函数解释 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。 如果调用出错则返回-1 所以exec函数只有出错的返回值而没有成功的返回值。 要执行一个程序首先就要找到这个程序所以这里面所有的exec函数的第一个参数都是帮我们绝对如何找到这个程序的。 找到这个程序后要告诉系统如何执行这个程序。要不要涵盖选项涵盖哪些 如果exec*能够实现系统程序那么可以实现我们自己的程序吗 可以的。 命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记。 l和v不会同时出现 l(list) : 表示参数采用列表一个一个传参 v(vector) : 参数用数组 p(path) : 有p自动搜索环境变量PATH e(env) : 表示自己维护环境变量 3.4 多进程版 – 验证各种程序替换接口
#include stdio.h
#include unistd.h// 提供execl, getpid等函数
#include stdlib.h
#include sys/types.h
#include sys/wait.hint main(){pid_t id fork();if(id 0){//childprintf(before: I am a process, pid:%d, ppid:%d\n,getpid(),getppid());sleep(5);execl(/usr/bin/ls,ls, -a, -l, NULL);printf(after: I am a process, pid:%d, ppid:%d\n,getpid(),getppid());exit(0);}//fatherpid_t ret waitpid(id, NULL, 0);if(ret 0){printf(wait success, father pid:%d, ret id:%d\n,getpid(), ret);}sleep(5);return 0;
}运行结果
ydk_108iZuf68hz06p6s2809gl3i1Z:~/108/lesson17$ ./mycommand
before: I am a process, pid:261679, ppid:261678
total 36
drwxrwxr-x 2 ydk_108 ydk_108 4096 Jan 20 22:54 .
drwxrwxr-x 13 ydk_108 ydk_108 4096 Jan 20 22:14 ..
-rw-rw-r-- 1 ydk_108 ydk_108 82 Jan 20 22:16 makefile
-rwxrwxr-x 1 ydk_108 ydk_108 17008 Jan 20 22:54 mycommand
-rw-rw-r-- 1 ydk_108 ydk_108 628 Jan 20 22:54 mycommand.c
wait success, father pid:261678, ret id:261679
ydk_108iZuf68hz06p6s2809gl3i1Z:~/108/lesson17$ 通过命令查看
ydk_108iZuf68hz06p6s2809gl3i1Z:~/108/lesson17$ while :; do ps ajx |head -1 ps ajx |grep mycommand |grep -v grep; sleep 1;echo----------;donePPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
echo----------: command not foundPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
echo----------: command not foundPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND261085 261678 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand261678 261679 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand
echo----------: command not foundPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND261085 261678 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand261678 261679 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand
echo----------: command not foundPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND261085 261678 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand261678 261679 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand
echo----------: command not foundPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND261085 261678 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand261678 261679 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand
echo----------: command not foundPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND261085 261678 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand261678 261679 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand
echo----------: command not foundPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND261085 261678 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand
echo----------: command not foundPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND261085 261678 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand
echo----------: command not foundPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND261085 261678 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand
echo----------: command not foundPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND261085 261678 261678 261085 pts/0 261678 S 1001 0:00 ./mycommand
echo----------: command not foundPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
echo----------: command not foundPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
echo----------: command not found
^C程序替换有没有创建新的子进程 没有子进程的pid没有变。不创建新进程只进行进程的程序代码和数据的替换结构。 补充知识 为什么after后面的代码没有执行呢 因为程序替换后after的printf的代码已经是老程序的代码了被ls覆盖掉了。 程序替换成功后exit之后的代码不会执行那么如果替换失败呢例如路径写错命令不存在 那么程序就会继续往后走。所以exit*函数只有失败有返回值没有成功的返回值。 我们的CPU怎么知道新程序应该进入的入口地址呢 Linux中形成的可执行程序是有格式的EIF是有可执行程序的表头的可执行程序的入口就在表中。 实际上不只是可以调用ls命令还可以调用自己写的C程序python程序shell脚本Java程序。但是无论是我们的可执行程序还是脚本为什么能够跨语言调用呢? 因为所有的语言运行起来本质上都是进程。 环境变量是什么时候给进程的呢 环境变量实际上也是数据。当我们创建子进程的时候环境变量就已经被子进程继承下去了。所以程序替换中环境变量信息不会被子进程替换。 如果我想给子进程传递环境变量应该怎么传递呢 新增环境变量 直接给bash添加环境变量 在父进程中使用putenv()添加环境变量 彻底替换环境变量 使用execle的时候把环境变量参数换成自己的 3.5 自定义shell
touch.sh
#!/usr/bin/bashecho hello 1
echo hello 1
echo hello 1
echo hello 1
echo hello 1
ls -a -l运行bash touch.sh
ydk_108iZuf68hz06p6s2809gl3i1Z:~/108/lesson17$ bash touch.sh
hello 1
hello 1
hello 1
hello 1
hello 1
total 40
drwxrwxr-x 2 ydk_108 ydk_108 4096 Jan 21 00:00 .
drwxrwxr-x 13 ydk_108 ydk_108 4096 Jan 20 22:14 ..
-rw-rw-r-- 1 ydk_108 ydk_108 82 Jan 20 22:16 makefile
-rwxrwxr-x 1 ydk_108 ydk_108 17008 Jan 20 22:54 mycommand
-rw-rw-r-- 1 ydk_108 ydk_108 628 Jan 20 22:54 mycommand.c
-rw-rw-r-- 1 ydk_108 ydk_108 101 Jan 21 00:00 touch.sh
ydk_108iZuf68hz06p6s2809gl3i1Z:~/108/lesson17$ 命令行交互的shell代码
// 包含必要的头文件
#include stdio.h // 标准输入输出
#include stdlib.h // 标准库函数
#include assert.h // 断言
#include string.h // 字符串操作
#include unistd.h // UNIX标准函数
#include sys/types.h // 系统数据类型
#include sys/wait.h // 进程等待// 定义shell提示符的格式
#define LEFT [ // 左边界符号
#define RIGHT ] // 右边界符号
#define LABLE # // 提示符标签
#define DELIM \t // 命令分隔符(空格和制表符)
#define LINE_SIZE 1024 // 命令行最大长度
#define ARGC_SIZE 32 // 命令参数最大个数
#define EXIT_CODE 44 // 子进程退出码// 全局变量定义
int lastcode 0; // 上一条命令的返回值
int quit 0; // 退出标志
extern char **environ; // 环境变量数组
char commandline[LINE_SIZE]; // 存储命令行
char *argv[ARGC_SIZE]; // 存储解析后的命令参数
char pwd[LINE_SIZE]; // 存储当前工作目录// 自定义环境变量存储空间
char myenv[LINE_SIZE];// 获取当前用户名
const char *getusername(){return getenv(USER);
}// 获取主机名
const char *gethostname(){return getenv(HOSTNAME);
}// 获取当前工作目录
void getpwd(){getcwd(pwd, sizeof(pwd));
}// 实现shell交互界面
void Interact(char *cline, int size){getpwd();// 打印提示符格式为 [用户名主机名 当前目录]#printf(LEFT%s%s %sRIGHTLABLE , getusername(), gethostname(), pwd);// 读取用户输入char *s fgets(cline, size, stdin);assert(s);(void)s;// 去掉输入字符串末尾的换行符cline[strlen(cline)-1] \0;
}// 解析命令行字符串为参数数组
int splistring(char cline[], char *_argv[]){int i 0;// 获取第一个参数(命令名)argv[i] strtok(cline, DELIM);// 获取后续参数while(_argv[i] strtok(NULL, DELIM));return i-1; // 返回参数个数
}// 执行外部命令
void NormalExcute(char *_argv[]){pid_t id fork();if(id 0){perror(fork);return;}else if(id 0){ // 子进程// 使用execvp执行命令execvp(_argv[0], _argv);exit(EXIT_CODE);}else{ // 父进程int status 0;// 等待子进程结束pid_t rid waitpid(id, status, 0);if(rid id){// 保存命令执行结果lastcode WEXITSTATUS(status);}}
}// 处理内建命令
int buildCommand(char *_argv[], int _argc){// cd命令if(_argc 2 strcmp(_argv[0],cd)0){chdir(argv[1]); // 改变当前目录getpwd(); // 更新pwdsprintf(getenv(PWD),%s, pwd); // 更新PWD环境变量return 1;}// export命令设置环境变量else if(_argc 2 strcmp(_argv[0],export)0){strcpy(myenv, _argv[1]);putenv(myenv);return 1;}// echo命令显示文本或变量值else if(_argc 2 strcmp(_argv[0],echo)0){if(strcmp(_argv[1],$?)0){ // 显示上一条命令的返回值printf(%d\n,lastcode);lastcode0;}else if(*_argv[1]$){ // 显示环境变量的值char *val getenv(_argv[1]1);if(val) printf(%s\n,val);}else{ // 显示普通文本printf(%s\n,_argv[1]);}return 1;}// 为ls命令添加颜色支持if(strcmp(_argv[0],ls)0){_argv[_argc] --color;_argv[_argc] NULL;}return 0; // 不是内建命令
}int main(){while(!quit){//1.//2.交互问题获取命令行Interact(commandline, sizeof(commandline));//3.子串分割的问题解析命令行int argc splistring(commandline, argv);if(argc 0) continue;//4.指令的判断,判断是不是内建命令//debug//for(int i0;argv[i];i){// printf([%d]: %s\n,i,argv[i]);//}//内键命令本质上就是shell内部的一个函数。int n buildCommand(argv, argc);//5.普通命令的执行// 如果不是内建命令则作为外部命令执行if(!n) NormalExcute(argv);}return 0;
}打印结果
ydk_108iZuf68hz06p6s2809gl3i1Z:~/108/lesson17$ ./myshell
[ydk_108(null) /home/ydk_108/108/lesson17]# ls -a
. .. makefile mycommand mycommand.c myshell myshell.c touch.sh
[ydk_108(null) /home/ydk_108/108/lesson17]# pwd
/home/ydk_108/108/lesson17
[ydk_108(null) /home/ydk_108/108/lesson17]#
Segmentation fault
ydk_108iZuf68hz06p6s2809gl3i1Z:~/108/lesson17$ 所以当我们进行登陆的时候系统就是要启动一个shell进程。
我们shell本身的环境变量是从哪里来的
当用户登录的时候shell会读取目录用户下的.bash_profile文件里面保存了导入环境变量的方式。