文章目录
一、进程程序替换
1.替换原理
2.替换函数
3.函数解释
4.命名理解
二、手写一个mini Shell
一、进程程序替换
创建子进程的目的就是为了让子进程执行特定的任务,比如:1.让子进程执行父进程的一部分代码;2.让子进程指向一个全新的程序代码,就是进行程序替换
1.替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。(执行程序替换,是整体替换,不能局部替换)。调用exec并不创建新进程,所以调用exec前后该进行的id并未改变。站在程序的角度,就是这个程序被加载到内存。exec函数就相当于一个加载器。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("begin...");
printf("begin...");
printf("begin...");
//程序的路径 ; 程序的名称 ;要执行程序的选项
execl("/bin/ls","ls","-a","-l",NULL);
//执行程序替换,新的代码和数据被加载,后续的代码属于老代码,直接被替换了,没机会执行后续的原代码
printf("end...");
printf("end...");
}
这个函数就是实现让一个进程执行另一个在磁盘中的程序。既然我自己写的代码可以加载新的程序,os实现的功能:创建进程的时候,先创建进程数据结构,再加载代码和数据。
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
printf("我是子进程:%d \n",getpid());
execl("/bin/ls","ls","-a","-l",NULL);
//execl 返回有值的时候,就是执行失败 比如 加载一个不存在的指令 lsssss
//执行成功的时候,不会有返回值
//不用对该函数返回值判断,只要继续向后运行一定是失败的
//替换成功,则由执行替换程序的时候,退出码由新进程去决定
执行错误的时候exit(1);
}
sleep(5);
//father
int status = 0;
printf("我是父进程:%d\n",getpid());
waitpid(id,&status,0);
return 0;
}
程序替换只会影响调用的进程。因为进程具有独立性。os会在子进程执行新程序的时候,需要程序替换,发生写时拷贝。写时拷贝在代码区也可以发生。如果程序替换失败,就执行原来的代码。
2.替换函数
有六种以exec开头的函数,统称exec函数:
1.int execl(const char * path , const char * arg, ...) //list
//path :要执行程序的路径 执行一个程序需要先找到它,再加载执行它
//arg :要如何执行 想在命令行如何执行这个命令,将参数逐个传递给它
execl("/bin/ls","ls","-a","-l",NULL)
2.int execv(const char * path,char * const argv[ ]); // vector
//path:路径
//agrv[ ] :按照数组的方式传参
char * const myargv[ ] = {"ls" ,"-a", "-l","-n",NULL};
execv("/bin/ls",myargv);
3.int execlp(const char * file,const char * arg ...); //在环境变量path中
带p只需要指定程序名即可,系统会自动在环境变量path中进行查找
execlp("ls","ls","-a","-l",NULL)
//第一个ls是指定程序名
//第二个往后是在命令行怎么执行它 ls -a -l
4.int execvp(const char * file,char * const argv[ ]); //vector path
execvp("ls",myargv);
以上都是执行命令,以下执行我自己写的程序:c -- > c++
5.int execle(const char * path,const char * arg,... char * const envp[ ]);
//char * const envp[ ] 自定义环境变量
5.int execvpe(const char * file, char * const argv[ ],char * const envp[ ]);
6.int execve(const char * filename,char * const argv[ ] ,char * const envp[ ] );
1-5号最终都调用6号,6号是os提供的系统调用,1-5号是对6号做的封装。
//myproc.c
int main()
{
pid_t id = fork();
if(id == 0)
{
execl("./exec/otherproc","otherproc",NULL);
exit(1);
}
//father
int status = 0;
waitpid(id,&status,0);
return 0;
}
//otherproc.cc c++程序
#include<iostream>
using namespace std;
int main()
{
for(int i = 0; i<5 ;i++)
{
cout<<"I am otherproc,my pid is " <<getpid() <<endl;
sleep(1);
}
return 0;
}
//先编译otherproc.cc
g++ -o otherproc otherproc.cc
实验结果:子进程运行调用.cc程序,然后父进程获取status
//myproc.c
int main()
{
pid_t id = fork();
if(id ==0)
{
//child
//1.传入新定义的环境变量
char * const myenv[] = { "MYENV = youcanseeme",NULL};
execle("./exec/otherproc","otherproc",NULL,myenv);
//2.传入原来存在的环境变量
// extern char ** environ;
// execle(" ....."."....",NULL,environ);
// 3.两个都传入
putenv("MYENV = youcanseeme");
execle("....","....",NULL,environ);
exit(1);
}
//father
...
return 0;
}
//otherproc.cc
int main()
{
for(int i = 0; i<5 ;i++)
{
cout<<"I am otherproc,my pid is: ,MYENV : "<<getpid()<<" "<<(getenv("MYENV") == NULL?"NULL:getenv("MYENV")<<endl;
}
return 0;
}
此时传入环境变量,env变成了全新自定义的环境变量。使用execle的时候会覆盖式传入新的环境变量。环境变量具有全局属性,可以被子进程继承。bash是父进程,然后执行execle,以传参的方式传给子进程。
3.函数解释
- 这些函数如果调用成功,则加载新的程序,从启动代码开始执行,不再返回
- 如果调用出错返回-1
- exec函数只有出错的返回值,没有成功的返回值
4.命名理解
- l(list):参数采用列表
- v(vector):参数采用数组
- p(path):有p自动搜索环境变量path
- e(env):自己维护环境变量
二、手写一个mini Shell
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell从用户读入字符串ls,shell建立一个新的进程,然后在那个进程中运行ls程序并等待进程结束。
要写一个shell,需要循环以下过程:
1.获取命令行
2.解析命令行
3.建立一个子进程(fork)
4.替换子进程(execvp)
根据这些思路,接下来实现一个shell,①需要先从命令行获取字符串fgets ②覆盖掉\n ③切割字符串 ④进行进程替换
代码和注释如下
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include<unistd.h>
4 #include<sys/wait.h>
5 #include<string.h>
6 #include<assert.h>
7 #include<sys/types.h>
8
9 #define MAX 1024
10 #define ARGC 64
11 #define SEP " "
12
int split(char * commandstr,char * argv[])
16 {
17 assert(commandstr);
18 assert(argv);
19
20 argv[0] = strtok(command,SEP);
21 if(argv[0] == NULL)
22 return -1;
23 int i = 1;
24 while(argv[i++] = strtok(NULL,SEP));
25 return 0;
26 }
27 int main()
28 {
29 while(1)
30 {
//获取的字符串 均初始化为0
31 char commandstr[MAX] = {0};
//切割之后保存到数组中
char *argv[ARGC] = {NULL};
33 printf("[linux@mymachine currpath]# ");
34 fflush(stdout);
35 char * s = fgets(commandstr,sizeof(commandstr),stdin);
36 assert(s);
37 (void)s;//在release方式发布的时候,去掉了assert,所以s没有被使用,带来了编译警告 void强制转换之后,什么都没做,但是充当一次使用
//覆盖掉\n
38 commandstr[strlen(commandstr)-1] = '\0';
//切割字符串
39 int n = split(commandstr,argv);
40 if(n!=0) continue;
pid_t id = fork();
44 if(id == 0)
45 {
//进行程序替换
46 execvp(argv[0],argv);
47 exit(1);
48 }
49 int status = 0;
50 waitpid(id,&status,0);
51 }
52 }
其中涉及到了一些c中的库函数,复习笔记如下:
char * strtok(char * str, const char * sep);
- sep是字符串,定义了用作分隔符的字符集合
- 第一个参数是要分割的字符串
- strtok函数找到str中的第一个sep,将其用\0作为结尾,返回指向这个标记的指针
- strtok中第一个参数为null,函数将在同一个字符串中被保存的位置开始,找到下一个sep
- 如果找不到sep,就返回null