目录
(2)所以这个程序替换函数execl,用不用判断返回值?为什么?
(3)引入进程创建——子进程执行程序替换,会不会影响父进程呢? ?
子进程执行的是父进程的代码片段,如果我们想让创建出来的子进程,执行全新的程序呢?
需要用到:进程的程序替换
我们一般在服务器设计(linux编程)的时候,往往需要子进程干两件种类事情
1.让子进程执行父进程的代码片段(服务器代码)
2.让子进程执行磁盘中一个全新的程序(shell,想让客户端执行对应的程序,通过我们的进程,执行其他人写的进程代码等等),c/c++ -> c/c++/Python/Shell/Php/Java...
1.将磁盘中的程序,加载入内存结构
2.重新建立页表映射,谁执行程序替换,就重新建立谁的映射(子进程)
效果:让我们的父进程和子进程彻底分离,并让子进程执行一个全新的程序!
这个过程有没有创建新的进程呢?
没有! 子进程的PCB等结构并未改变,只是改变的页表映射关系。
程序替换成功后,运行完新程序,则程序直接退出;程序替换成功后,原进程没有退出,使用原进程运行新程序
我们只能调用接口,为什么呢?
因为这个过程实际上是把数据从一个硬件搬到另一个硬件的操作,这个操作只能由OS操作系统完成
man execl 查看进行程序替换的函数:
具体例子:execl("usr/bin/pwd","ls","-l","-a",NULL);
我们如果想执行一个全新的程序(本质就是磁盘上的文件),我们需要做几件事情:
1.先找到这个程序在哪里? ——程序在哪?(举例:可以通过which "pwd",查pwd的路径)
2. 程序可能携带选项进行执行(也可以不携带) ——怎么执行?
所以要明确告诉OS,我想怎么执行这个程序?要不要带选项
红线部分执行第一个问题,绿线部分执行第二个问题
命令行怎么写(ls -l -a), 这个参数就怎么填"ls","-l","-a",最后必须是NULL,标识参数传递完毕[如何执行程序的]
后面的printf是代码吗? 为什么没执行?
因为execl一旦替换成功,是将当前进程的所有代码和数据全部替换了!
后面的printf 实际上已经早就被替换了!该代码不存在了
int ret= execl(...);
答:不用判断返回值(但是还是需要返回值),因为一旦替换成功,就不会有返回值,也不会执行返回语句,因为int ret 这个返回值也是当前进程的代码和数据,execl一旦替换成功,是将当前进程的所有代码和数据全部替换了,execl就直接执行ls命令的代码去了。如果有返回值,必然是程序替换失败,也必然会继续向后执行! ! 最多通过返回值得到什么原因导致的替换失败!
子进程执行程序替换,会不会影响父进程呢? ?
不会,因为进程具有独立性。
为什么,如何做到的? ?数据层面发生写时拷贝!当程序替换的时候,我们可以理解成为:代码和数据都发生了写时拷贝完成父子的分离!
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
execl结尾 l 为list,列表传参——>可变参数包,一个一个传。
execv结尾 v 为vector,数组传参——>传的是指针数组。
带e的都是可以传环境变量的(execle,execvpe)但是会覆盖系统原有的环境变量,把自己传的环境变量交给进程;不带e是默认继承系统的环境变量;带p的都是可以自带路径的,直接传命令名称即可(execlp,execvp,execvpe)
int execv(const char *path, char *const argv[]);
path 依然是程序的路径,参数 argv[] 是存着要实现指令的指针数组
execv VS execl 只有传参方式的区别! ! execl是传可变参数,execv是传指针数组
int execlp(const char *file, const char *arg, ...); 带p的就传程序名即可
file:要执行的程序。执行指令的时候,默认的搜索路径,在哪里搜索呢?在环境变量PATH
命名带p的,可以不带路径,只说出你要执行哪一个程序即可!
execlp("ls","ls", "-a", "-1 ",NULL)
里出现了两个Is,含义不一样:第一个ls告诉你要执行的程序,后面的ls已经-a等是执行方式
int execvp(const char *file, char *const argv[]); 与上面同理
int execle(const char *path, const char *arg, ..., char * const envp[]);
char * const envp[]: 添加环境变量给目标进程,是覆盖式的。如果传 execle("./mycmd", "mycmd", NULL, env_); 会导致原本的环境变量全部被覆盖而失效,所以要利用全局变量environ传入全部的环境变量,自己定义的环境变量要自己手动添加
- 环境变量的指针声明
- extern char**environ;
- ……
- execle("./mycmd", "mycmd", NULL, environ);
总览代码:
- mycmd.cpp:
-
- #include
- #include
-
- int main()
- {
- std::cout << "PATH:" << getenv("PATH") << std::endl;
- std::cout << "-------------------------------------------\n";
- std::cout << "MYPATH:" << getenv("MYPATH") << std::endl;
- std::cout << "-------------------------------------------\n";
-
- std::cout << "hello c++" << std::endl;
- std::cout << "hello c++" << std::endl;
- std::cout << "hello c++" << std::endl;
- std::cout << "hello c++" << std::endl;
- std::cout << "hello c++" << std::endl;
- std::cout << "hello c++" << std::endl;
- std::cout << "hello c++" << std::endl;
- std::cout << "hello c++" << std::endl;
- return 0;
- }
- myexec.c:
- #include
- #include
- #include
- #include
-
-
- int main()
- {
- //环境变量的指针声明
- extern char**environ;
-
- printf("我是父进程,我的pid是: %d\n", getpid());
- pid_t id = fork();
- if(id == 0){
- //child
- //我们想让子进程执行全新的程序,以前是执行父进程的代码片段
-
- printf("我是子进程,我的pid是: %d\n", getpid());
- char *const env_[] = {
- (char*)"MYPATH=YouCanSeeMe!!",
- NULL
- };
- //env_: 添加环境变量给目标进程,是覆盖式的
- //execle("./mycmd", "mycmd", NULL, env_);
- 可利用extern新增式添加环境变量:
- execle("./mycmd", "mycmd", NULL, environ);
-
- exit(1); //只要执行了exit,意味着,execl系列的函数失败了
- }
- // 一定是父进程
- int status = 0;
- int ret = waitpid(id, &status, 0);
- if(ret == id)
- {
- sleep(2);
- printf("父进程等待成功!\n");
- }
- return 0;
- }
目前我们执行的程序,全部都是系统命令,如果我们要执行自己写的C/C++程序呢? ?
如何我们要执行其他语言写的程序?
- // 一定是父进程
- int status = 0;
- int ret = waitpid(id, &status, 0);
- if(ret == id)
- {
- sleep(2);
- printf("父进程等待成功!\n");
- }
- return 0;
- }
为什么会有这么多接口?——因为要适配应用场景。
execve为什么是单独的?——实际上,只有 execve是系统调用,其他都是对系统接口的封装,最后都要调用到execve
- #include
-
- int main()
- {
- printf("hello my shell\n");
- return 0;
- }
- myshell:myshell.c
- gcc -o $@ $^ -std=c99 //编不过就加-std=c99
- .PHONY:clean
- clean:
- rm -f myshell
- #include
- #include
- #include
- #include
- #include
- #include
-
- #define SEP " " //可以是多个,比如" ,." ——空格,逗号,句号 隔开
- #define NUM 1024
- #define SIZE 128
-
- char command_line[NUM];
- char *command_args[SIZE];
-
- int main()
- {
- //shell 本质上就是一个死循环
- while(1)
- {
- //不关心获取这些属性的接口, 搜索一下
- //1. 显示提示符
- printf("[张三@我的主机名 当前目录]# ");
- fflush(stdout);
- //2. 获取用户输入
- memset(command_line, '\0', sizeof(command_line)*sizeof(char));
- fgets(command_line, NUM, stdin); //键盘,标准输入,stdin, 获取
- //到的是c风格的字符串, 尾部会加上'\0',从stdin获取NUM个字节放入地址command_line
- command_line[strlen(command_line) - 1] = '\0';// fgets时,最后敲回车也会
- //输入进command_line中,所以要清空这个\n,把\n设置成\0
- //3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分
- command_args[0] = strtok(command_line, SEP);
- int index = 1;
- // (1)= 是故意这么写的
- // strtok 截取成功,返回字符串起始地址;截取失败,返回NULL
- // (2)上面strtok已截取ls,想继续截取,参数1应给NULL
- while(command_args[index++] = strtok(NULL, SEP));
-
- //for debug为了打印看一下我们输入的字符串是否都保存到command_args中了——————————
- //for(int i = 0 ; i < index; i++)
- //{
- // printf("%d : %s\n", i, command_args[i]);
- //}
- //——————————————————————————————————————————————————————————————
- // 4. TODO, 编写后面的逻辑, 内建命令
- // 5. 创建进程,执行
- pid_t id = fork();
- if(id == 0)
- {
- //child
- // 6. 程序替换
- //exec*?
- execvp(command_args[0]/*不就是保存的是我们要执行的程序名字吗?*/, command_args);
- exit(1); //执行到这里,子进程一定替换失败
- }
- int status = 0;
- pid_t ret = waitpid(id, &status, 0);
- if(ret > 0)
- {
- printf("等待子进程成功: sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF);
- }
- }// end while
- }
如果直接exec*执行cd,最多只是让子进程进行路径切换,子进程是一运行就完毕的进程!切换子进程路径是没有意义的,我们在shell中,更希望父进程-shell本身的路径发生变化。只要父进程路径变化,后面的子进程就会继承父进程路径
如果有些行为,是必须让父进程shell执行的, 不想让子进程执行,此时就绝对不能创建子进程!
只能是父进程自己实现对应的代码! 由父进程shell自己执行的命令,我们称之为内建命令/内置bind-in 命令
内建命令 相当于 shell内部的一个函数!
(在上面myshell的基础上在第4部TODO做添加)
父进程自己执行的,对应上层的内建命令
chdir:想去哪个路径就传哪个路径
- //对应上层的内建命令
- int ChangeDir(const char * new_path)
- {
- chdir(new_path);
-
- return 0; // 调用成功
- }
-
- while(1)
- {
- ……
- // 4. TODO, 编写后面的逻辑, 内建命令
- if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
- {
- ChangeDir(command_args[1]); //让调用方进行路径切换, 父进程
- continue;
- }
- }
内建命令举例:cd命令,export,echo
环境变量的数据,在进程的上下文中
1.环境变量会被子进程继承下去,所以他会有全局属性
2.当我们进行程序替换的时候,当前进程的环境变量非但不会被替换,而且是继承父进程的! !因为环境变量是系统的数据。
带e的都是可以传环境变量的(execle,execvpe)但是会覆盖系统原有的环境变量。执行到子进程的execle,execvpe时,把自己传的环境变量传入就会覆盖系统原有的环境变量;不带e是默认继承系统的环境变量。如果我们不想覆盖原有的(就不能执行到子进程的execle,execvpe),只想新增环境变量,就要父进程新增环境变量,父进程执行putenv这种内建命令来增加我们的环境变量,子进程就可以继承并获取了(环境变量会被其之下的所有子进程默认继承下去)
如何在shell内部新增自己的环境变量- putenv 注意:需要是一个独立的空间
putenv:把传入的环境变量导入自己的上下文中
函数说明:getenv()用来取得参数 name 环境变量的内容(linux命令env可以用来查看环境变量). 参数name 为环境变量的名称, 如果该变量存在则会返回指向该内容的指针。
- void PutEnvInMyShell(char * new_env)
- {
- putenv(new_env);
- }
-
- // 4. TODO, 编写后面的逻辑, 内建命令
- if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
- {
- // 目前,环境变量信息在command_line,每次memset时command_line都会被清空
- // 所以我们需要自己用全局的env_buffer保存一下环境变量内容
- strcpy(env_buffer, command_args[1]);
- PutEnvInMyShell(env_buffer); //export myval=100, BUG?
- continue;
- }
- [zsh@ecs-78471 ~]$ which ls
- alias ls='ls --color=auto'
- /usr/bin/ls
解释:alias 起别名,alias ls='ls --color=auto' 系统中是给指令'ls --color=auto'起别名为ls,所以平常我们的ls实际就是 'ls --color=auto' 这个命令。给ls上色就需要加上这个命令
- //3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分
- command_args[0] = strtok(command_line, SEP);
- int index = 1;
- // 给ls命令添加颜色
- if(strcmp(command_args[0]/*程序名*/, "ls") == 0 ) //如果是ls命令,就加色
- command_args[index++] = (char*)"--color=auto";
-
- // = 是故意这么写的
- // strtok 截取成功,返回字符串其实地址
- // 截取失败,返回NULL
- while(command_args[index++] = strtok(NULL, SEP));
解释: command_args[0] = strtok(command_line, SEP); 已经把ls放进数组 command_args[0]了, if(strcmp(command_args[0]/*程序名*/, "ls") == 0 ) 判断如果是ls命令,就加色,执行下面命令:command_args[index++] = (char*)"--color=auto";(不是ls就不执行)给ls加上--color=auto就是上色,后面就是"ls" "--color=auto" "-a" "-l" "-i"
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- #define SEP " "
- #define NUM 1024
- #define SIZE 128
-
- char command_line[NUM];
- char *command_args[SIZE];
-
- char env_buffer[NUM]; //for test
-
- extern char**environ;
-
- //对应上层的内建命令
- int ChangeDir(const char * new_path)
- {
- chdir(new_path);
-
- return 0; // 调用成功
- }
-
- void PutEnvInMyShell(char * new_env)
- {
- putenv(new_env);
- }
-
- int main()
- {
- //shell 本质上就是一个死循环
- while(1)
- {
- //不关心获取这些属性的接口, 搜索一下
- //1. 显示提示符
- printf("[张三@我的主机名 当前目录]# ");
- fflush(stdout);
-
- //2. 获取用户输入
- memset(command_line, '\0', sizeof(command_line)*sizeof(char));
- fgets(command_line, NUM, stdin); //键盘,标准输入,stdin, 获取到的是c风格的字符串, '\0'
- command_line[strlen(command_line) - 1] = '\0';// 清空\n
-
- //3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分
- command_args[0] = strtok(command_line, SEP);
- int index = 1;
- // 给ls命令添加颜色
- if(strcmp(command_args[0]/*程序名*/, "ls") == 0 ) //如果是ls命令,就加色
- command_args[index++] = (char*)"--color=auto";
-
- // = 是故意这么写的
- // strtok 截取成功,返回字符串其实地址
- // 截取失败,返回NULL
- while(command_args[index++] = strtok(NULL, SEP));
-
- //for debug
- //for(int i = 0 ; i < index; i++)
- //{
- // printf("%d : %s\n", i, command_args[i]);
- //}
-
- // 4. TODO, 编写后面的逻辑, 内建命令
- if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
- {
- ChangeDir(command_args[1]); //让调用方进行路径切换, 父进程
- continue; //内建命令走完直接continue就不会创建子进程
- }
- if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
- {
- // 目前,环境变量信息在command_line,会被清空
- // 此处我们需要自己保存一下环境变量内容
- strcpy(env_buffer, command_args[1]);
- PutEnvInMyShell(env_buffer); //export myval=100, BUG?
- continue; //内建命令走完直接continue就不会创建子进程
- }
-
- // 5. 创建进程,执行
- pid_t id = fork();
- if(id == 0)
- {
- //child
- // 6. 程序替换
- //exec*?
- execvp(command_args[0]/*不就是保存的是我们要执行的程序名字吗?*/, command_args);
- exit(1); //执行到这里,子进程一定替换失败
- }
- int status = 0;
- pid_t ret = waitpid(id, &status, 0);
- if(ret > 0)
- {
- printf("等待子进程成功: sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF);
- }
- }// end while
- }
测试 cd .. 发现可以正常使父进程返回
测试export也可以正常添加环境变量