目录
前言
一.进程替换的原理
二.六个进程替换的系统调用
1.execl
2.execv
3.execlp
4.execvp
5.execle
6.execvpe
三.用C语言执行python程序
四.实现简单的shell
1.shell与shell脚本的区别
2.模拟实现简单的shell
3.重点总结
想要深刻理解本章进程替换的知识, 需要有进程地址空间与进程创建/终止/等待的相关知识作为铺垫
详细了解进程地址空间入口: http://t.csdn.cn/oSQ0f
详细了解进程创建/终止/等待入口: http://t.csdn.cn/Gchpw
进程替换本质上不是新建一个进程, 而是用已有的可执行文件来替换掉当前进程, 一般不替换父进程, 都是替换父进程fork出来的子进程.
注意: 上面所说的是用可执行文件替换当前进程, 而并非是用进程替换进程
父进程fork之后产生子进程, 此时父进程与子进程是两个独立的进程
父进程与子进程的代码是共享的, 数据是分离的
数据分离的方式采用写时拷贝, 也就是子进程要对数据进行写入(修改)时, 才会给子进程在物理内存上重新开辟空间, 建立映射
由于代码是不可写入的, 所以代码的共享, 并不影响父子进程的独立性
从而引出本章讨论的重点: 进程替换
进程替换的原理是什么, 操作系统是如何做到进程替换的?
子进程发生进程替换, 本质是将新的进程的数据和代码加载到物理内存上
如何理解将新程序加载到内存中, 通过exec*系列系统调用(加载器), 来加载进程的数据和代码到内存.
断掉与之前物理内存的映射关系, 再将新加载到的和子进程重新建立映射关系
在进程替换时, 环境变量不会被替换, 在fork出子进程时, 子进程中就已经有了环境变量, 这个环境变量是继承的父进程的, 在进程替换之后, 环境变量并不会被替换, 因为如果一但环境变量也替换了, 而我替换之后, 加载的新的代码和数据到子进程中, 此时子进程的环境变量又从何而来呢?
整体来看: 进程替换中, 被替换的子进程就像是一个外壳, 而内部的代码和数据将会被指定的可执行文件替换掉, 替换的过程就是加载的过程
所以, 父进程与子进程的代码与数据一样, 都是以写时拷贝的形式
当子进程的数据发生写入时, 数据发生写时拷贝
当子进程发生进程替换时, 代码发生写时拷贝
以下这张图只是一个大概的描述, 仅仅为了便于初学者理解, 与实际情况其实略有不同
以下将展开exec*系列系统调用的具体使用方法
其中每一个的参数都是可变参数列表, 可以传入不定个数个参数
这些系统调用的名字的字母所代表的含义
首先这是六个exec系列系统调用
l(list): 列表
解释: 第二个参数以及后续参数, 以列表的形式传入, 以NULL结束
p(path): 环境变量
解释: 不用显示写出进程路径, 自动遍历当前bash中的环境变量中的路径来查找进程
v(vector): 指针数组
解释: 第二个参数是一个指针数组, 将list形式的列表放入指针数组
e(env): 环境变量
解释: 显式传入你自己定义的环境变量
关于返回值
这个int我们不用关心, 因为在用户看来, 一但exec系列系统调用替换成功, 整个进程就全部被替换掉了, 包括这个系统调用, 这时根本就不存在这个返回值了, 如果替换失败则返回-1
int execl(const char* path, const char *arg, ...);
第一个参数: 传入进程名与进程所在路径
第二个参数: 传入进程名
第三个参数以及后续参数: 传入选项或者NULL(你要如何执行这个进程), 后续参数以NULL结尾
#include
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
//-----------------------------------------------------------
//进程替换的主要逻辑
if(id == 0)
{
//执行子进程的代码
//使用execl替换掉当前子进程
execl("/usr/bin/ls", "ls", "-a", "-l", "-i", NULL);
//如果进程替换失败
exit(-3);//随意设置的退出码
}
//----------------------------------------------------------
else if(id > 0)
{
//执行父进程的代码
//阻塞等待回收子进程的退出状态
int status = 0;
pid_t res = waitpid(id, &status, 0);
if(res > 0)
{
//进程等待成功
printf("%s\n", strerror(WEXITSTATUS(status)));
}
else
{
//进程等待失败
printf("等待进程失败\n");
exit(-2);//随意设置的退出码
}
}
else
{
printf("fork失败\n");
exit(-1);//随意设置的退出码
}
return 0;
}
int execv(const char* path, char* const argv[]);
第一个参数: 传入进程名与进程所在路径
第二个参数: 传入一个指针数组
#include
#include
#include
#include
#include
#include
#define NUM 16
int main()
{
pid_t id = fork();
//-----------------------------------------------------------
//进程替换的主要逻辑
if(id == 0)
{
//执行子进程的代码
//使用execv替换掉当前子进程
char* const argv[NUM] = {
(char*)"ls",
(char*)"-a",
(char*)"-i",
(char*)"-l",
NULL};
execv("/usr/bin/ls", argv);
//如果进程替换失败
exit(-3);//随意设置的退出码
}
//----------------------------------------------------------
else if(id > 0)
{
//执行父进程的代码
//阻塞等待回收子进程的退出状态
int status = 0;
pid_t res = waitpid(id, &status, 0);
if(res > 0)
{
//进程等待成功
printf("%s\n", strerror(WEXITSTATUS(status)));
}
else
{
//进程等待失败
printf("等待进程失败\n");
exit(-2);//随意设置的退出码
}
}
else
{
printf("fork失败\n");
exit(-1);//随意设置的退出码
}
return 0;
}
int execlp(const char* file, const char *arg, ...);
第一个参数: 传入进程名, 进程根据此进程名自动去环境变量中遍历查找该进程的路径
第二个参数: 传入进程名
第三个参数以及后续参数: 传入选项或者NULL(你要如何执行这个进程), 后续参数以NULL结尾
注:
第一个参数与第二个参数都是传入的进程名, 有必要吗? 为什么要传入两次进程名?
第一个参数是根据进程名查找进程路径. 第二个参数是执行进程, 这两个参数是有本质上的区别的!
只有找到了进程所在路径, 才可以执行这个进程
#include
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
//-----------------------------------------------------------
//进程替换的主要逻辑
if(id == 0)
{
//执行子进程的代码
//使用execlp替换掉当前子进程
execlp("ls", "ls", "-a", "-l", "-i", NULL);
//如果进程替换失败
exit(-3);//随意设置的退出码
}
//----------------------------------------------------------
else if(id > 0)
{
//执行父进程的代码
//阻塞等待回收子进程的退出状态
int status = 0;
pid_t res = waitpid(id, &status, 0);
if(res > 0)
{
//进程等待成功
printf("%s\n", strerror(WEXITSTATUS(status)));
}
else
{
//进程等待失败
printf("等待进程失败\n");
exit(-2);//随意设置的退出码
}
}
else
{
printf("fork失败\n");
exit(-1);//随意设置的退出码
}
return 0;
}
int execvp(const char* file, char* const argv[]);
第一个参数: 传入进程名, 进程根据此进程名自动去环境变量中遍历查找该进程的路径
第二个参数: 传入一个指针数组
#include
#include
#include
#include
#include
#include
#define NUM 16
int main()
{
pid_t id = fork();
//-----------------------------------------------------------
//进程替换的主要逻辑
if(id == 0)
{
//执行子进程的代码
//使用execvp替换掉当前子进程
char* const argv[NUM] = {
(char*)"ls",
(char*)"-a",
(char*)"-i",
(char*)"-l",
NULL};
execvp("ls", argv);
//如果进程替换失败
exit(-3);//随意设置的退出码
}
//----------------------------------------------------------
else if(id > 0)
{
//执行父进程的代码
//阻塞等待回收子进程的退出状态
int status = 0;
pid_t res = waitpid(id, &status, 0);
if(res > 0)
{
//进程等待成功
printf("%s\n", strerror(WEXITSTATUS(status)));
}
else
{
//进程等待失败
printf("等待进程失败\n");
exit(-2);//随意设置的退出码
}
}
else
{
printf("fork失败\n");
exit(-1);//随意设置的退出码
}
return 0;
}
int execle(const char* path, const char* arg, ..., char* const envp[]);
第一个参数: 传入进程名与进程所在路径
第二个参数: 传入进程名
第三个参数以及后续参数: 传入选项或者NULL(你要如何执行这个进程), 后续参数以NULL结尾
最后一个参数: 传入一个指针数组, 该数组内存放环境变量
注:
在演示这个系统接口的时候, 我们使用自己写的进程来进行进程替换, 不再使用ls, 来便于观察环境变量的接收与打印
execle.c
#include
#include
#include
#include
#include
#include
#define NUM 16
int main()
{
pid_t id = fork();
//-----------------------------------------------------------
//进程替换的主要逻辑
if(id == 0)
{
//执行子进程的代码
//使用execle替换掉当前子进程
//环境变量
char* const env[NUM] = {
(char*)"MY_ENV=2001",
(char*)"YOU_ENV=2002",
NULL
};
//这里路径可以是绝对路径,也可以是相对路径
execle("./myproc", "myproc", "-b", NULL, env);
//如果进程替换失败
exit(-3);//随意设置的退出码
}
//----------------------------------------------------------
else if(id > 0)
{
//执行父进程的代码
//阻塞等待回收子进程的退出状态
int status = 0;
pid_t res = waitpid(id, &status, 0);
if(res > 0)
{
//进程等待成功
printf("%s\n", strerror(WEXITSTATUS(status)));
}
else
{
//进程等待失败
printf("等待进程失败\n");
exit(-2);//随意设置的退出码
}
}
else
{
printf("fork失败\n");
exit(-1);//随意设置的退出码
}
return 0;
}
myproc.c
#include
#include
#include
int main(int argc, char* argv[], char* env[])
{
printf("这是我的myproc进程\n");
if(argc < 2)
{
printf("执行该进程必须带至少一个参数");
exit(-1);
}
if(strcmp(argv[1], "-a") == 0)
{
printf("这是-a选项的功能\n");
}
else if(strcmp(argv[1], "-b") == 0)
{
printf("这是-b选项的功能\n");
}
printf("进程替换时传入的环境变量:%s\n", env[0]);
printf("进程替换时传入的环境变量:%s\n", env[1]);
printf("环境变量MY_ENV: %s\n", getenv("MY_ENV"));
printf("环境变量YOU_ENV: %s\n", getenv("YOU_ENV"));
return 0;
}
int execvpe(const char* file, char* const argv[], char* const envp[]);
第一个参数: 传入进程名与进程所在路径
第二个参数: 传入一个指针数组
最后一个参数: 传入一个指针数组, 该数组内存放环境变量
注:
在演示这个系统接口的时候, 我们使用自己写的进程来进行进程替换, 不再使用ls, 来便于观察环境变量的接收与打印
我将当前路径加入到了环境变量PATH
execvpe.c
#include
#include
#include
#include
#include
#include
#define NUM 16
int main()
{
pid_t id = fork();
//-----------------------------------------------------------
//进程替换的主要逻辑
if(id == 0)
{
//执行子进程的代码
//使用execvpe替换掉当前子进程
//环境变量
char* const envp[NUM] = {
(char*)"MY_ENV=2001",
(char*)"YOU_ENV=2002",
NULL
};
//argv
char* const argv[NUM] = {
(char*)"myproc",
(char*)"-a",
NULL
};
//execvpe("myproc", argv, envp);
execvpe("myproc", argv, envp);
//如果进程替换失败
exit(-3);//随意设置的退出码
}
//----------------------------------------------------------
else if(id > 0)
{
//执行父进程的代码
//阻塞等待回收子进程的退出状态
int status = 0;
pid_t res = waitpid(id, &status, 0);
if(res > 0)
{
//进程等待成功
printf("%s\n", strerror(WEXITSTATUS(status)));
}
else
{
//进程等待失败
printf("等待进程失败\n");
exit(-2);//随意设置的退出码
}
}
else
{
printf("fork失败\n");
exit(-1);//随意设置的退出码
}
return 0;
}
myproc.c
#include
#include
#include
int main(int argc, char* argv[], char* env[])
{
printf("这是我的myproc进程\n");
if(argc < 2)
{
printf("执行该进程必须带至少一个参数");
exit(-1);
}
if(strcmp(argv[1], "-a") == 0)
{
printf("这是-a选项的功能\n");
}
else if(strcmp(argv[1], "-b") == 0)
{
printf("这是-b选项的功能\n");
}
else
{
printf("default!\n");
}
printf("进程替换时传入的环境变量:%s\n", env[0]);
printf("进程替换时传入的环境变量:%s\n", env[1]);
printf("环境变量MY_ENV: %s\n", getenv("MY_ENV"));
printf("环境变量YOU_ENV: %s\n", getenv("YOU_ENV"));
return 0;
}
在我们之前的认知, 一个C语言程序, 只能执行我写的代码, 那我写的这个程序是用C语言写的, 当然执行的就是我写的这个C语言程序!
在我们学习过进程替换之后, 可以很容易的用一个其它语言的程序, 替换掉我们写的程序, 这不就用C语言执行了不同语言的程序了!
例如: 用C语言执行python程序
myproc1.c
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程执行的代码
//让这个子进程去执行python代码
printf("我是子进程, 我将要替换为一个python进程\n");
execlp("python", "python", "myproc2.py", NULL);
printf("exec失败\n");
exit(-1);
}
else if(id > 0)
{
printf("我是父进程, 我正在阻塞等待回收子进程\n");
int status = 0;
pid_t res = waitpid(id, &status, 0);
if(res > 0)
{
printf("子进程已成功退出\n");
printf("接收子进程: %d成功, 退出状态为: %d\n", res, WEXITSTATUS(status));
}
else
{
printf("waitpid失败\n");
exit(-1);
}
}
else
{
printf("fork失败\n");
exit(-1);
}
}
myproc2.py
#! /usr/bin/python2.7.5
print("hello python")
print("hello python")
print("hello python")
print("hello python")
print("hello python")
print("hello python")
友情提示小知识:
只有C/C++是纯编译型的编程语言, 而其他大部分语言, 例如python是解释型语言, python语言用的不是编译器而是解释器, 我们写的python程序文件, 例如上述myproc2.py这个文件并不能被编译,行时运行的是python解释器而我们写的myproc2.py是作为参数传给python的.
所以, 我们在运行一个python代码时, 本质是运行的python解释器, 我们写的python文件是作为参数传给解释器的
在调用exec系列接口时, 传参表面上看起来与C/C++是不同的, 但是实际上本质是相同的
因为我运行的是python解释器, 所以要先找到python, 这是第一个参数
接下来其余的参数就是我要执行谁如何去执行, 也就是我要执行python, 并且以myproc2.py(本质上当作参数传给python)的方式去执行
shell的底层是用C语言实现的
shell既是一种命令语言, 又是一种程序设计语言
做为命令语言: shell
shell是一个应用程序, 在Linux下我们看到的命令行解释器就是shell的一个版本(bash), shell连接了内核与用户, 是属于内核中的最基本编写的应用程序, 它整合了所有命令(以替换子进程的方式)
做为程序设计语言: shell脚本
shell脚本(shell script)是一个用shell写的程序, 是针对shell所写的"剧本", 是利用shell的功能所写的一个程序, 是使用的纯文本文件
#include
#include
#include
#include
#include
#include
#define CMD_LINE_NUM 256
#define ARGV_NUM 128
#define DELIMITER " "
int main()
{
//模拟实现shell(bash)
while(1)//这是一个常驻进程, 一但启动就不退出
{
printf("[模式实现的shell进程, 请输入你的指令]$ ");
fflush(stdout);
//父进程接收用户输入的指令
//...
char _accept_cmd_line[CMD_LINE_NUM];//将接收到的cmd以字符串形式存储到accept_cmd_line
memset(_accept_cmd_line, 0, sizeof _accept_cmd_line);
//接收命令
if(fgets(_accept_cmd_line, sizeof _accept_cmd_line, stdin) == NULL)
continue;
_accept_cmd_line[strlen(_accept_cmd_line) - 1] = '\0';
//用strtok将accept_cmd_line进行分割
char* _argv[ARGV_NUM];
memset(_argv, 0, sizeof _argv);
int index = 0;
_argv[index++] = strtok(_accept_cmd_line, DELIMITER);
if(strcmp(_argv[0], "ll") == 0)
{
_argv[0] = (char*)"ls";
_argv[index++] = (char*)"-l";
_argv[index++] = (char*)"--color=auto";
}
else if(strcmp(_argv[0], "ls") == 0)
{
_argv[index++] = (char*)"--color=auto";
}
while(_argv[index++] = strtok(NULL, DELIMITER));
//内置命令, 本质就是shell中调用的一个函数, 这是需要让父进程执行的, 并不需要让子进程去做
if(strcmp(_argv[0], "cd") == 0)
{
//使用chdir系统调用
if(_argv[1] != NULL)//如果第一个参数存在
{
chdir(_argv[1]);//移动到第一个参数所在路径
continue;
}
}
/*测试
for(index = 0; _argv[index]!=NULL; index++)
{
printf("_argv[%d]: %s\n", index, _argv[index]);
}*/
pid_t id = fork();
//核心逻辑
//------------------------------------------------------------------
if(id == 0)
{
//子进程处理指令
//处理方式:进程替换(用指令进程替换子进程)
//exec*
execvp(_argv[0], _argv);
exit(-1);//替换失败, 此处的退出码是随意设置的
}
//------------------------------------------------------------------
else if(id < 0)
{
//fork失败
perror("fork");
exit(-1);//终止父进程, 此处的退出码是随意设置的
}
//下面这段虽然父子共享,但只让父进程执行这段逻辑,进程替换掉子进程即可实现
//父进程阻塞等待回收子进程
int status = 0;
pid_t res = waitpid(id, &status, 0);
if(res > 0)
{
//等待成功
if(WIFEXITED(status))
{
//子进程正常退出
//printf("等待子进程成功, 子进程正常退出, pid: %d, 退出码: %d\n", res, WEXITSTATUS(status));
if(WEXITSTATUS(status) == 255)
{
printf("command not found\n");
}
else if(WEXITSTATUS(status) == 0)
{
printf("%s\n", strerror(WEXITSTATUS(status)));
}
}
else
{
//子进程异常退出
printf("子进程异常退出, pid: %d, 退出信号: %d\n", res, status & 0x7F);
}
}
else
{
//等待失败
perror("waitpid");
exit(-1);//终止父进程, 此处的退出码是随意设置的
}
}
return 0;
}
模拟简单的shell, 对于fork创建父子进程需要有牢固的基础
fork创建父子进程, 父子进程是代码共享的, 所以父子都会执行while(1), 但我们用进程替换就把子进程替换掉了, 所以父进程在一直循环, 成为了一个常驻进程
对于内置命令的理解
例如cd这样的命令, 我们要想清楚是让子进程更改路径还是让父进程更改路径
我们创建子进程的目的是要让它以进程替换的方式来处理父进程接收到的指令, 从而达到父进程可以一直接收, 并且每接收到就fork子进程让子进程替换成指令进程去执行, 但对于cd这样的命令, 子进程仅仅是去执行指令进程而已, 执行结束后子进程就是放掉了, 我们更改子进程路径毫无意义
所以对于内置命令, 我们是要让父进程去执行的, cd命令需要一个chdir系统调用, 并且父进程执行完之后就不应继续执行后面的逻辑了, 直接跳到循环处, 准备下一次循环