目录
1 进程创建
1.1 fork()创建子进程
- fork创建子进程,操作系统做了什么?
新增一个进程,分配新的内核数据结构和内存块给子进程,将父进程部分数据结构内容拷贝给子进程,将子进程添加到系统进程列表,fork返回,调度器开始调度
- 子进程执行那些代码语句
int main(){
printf("Before: pid is %d\n", getpid());
pid_t pid = fork();
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运行结果如下:
- 子进程未执行fork()前语句,原因如下:
首先,父子进程共享的代码是所有的,不是只共享fork()后的代码。但是在创建子进程时,父进程的上下文数据也拷贝给了子进程,此时,父进程已经执行完成fork()了,因此子进程运行时,读取上下文数据后,默认从fork()后的代码开始执行
- fork常规用法(两个)
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数(进程替换)
- fork调用失败原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
1.2 写时拷贝
- 父子进程是共享代码,独立数据
理论上,进程具有独立性,拥有自己的内核结构,自己的代码和数据。但在一般情况下,子进程没有加载过程,即子进程没有自己的代码和数据,因此子进程只能“使用”父进程的代码和数据。
但是在一般情况下,代码是不可写的,因此父子共享可行。而数据可能被修改,因此必须有各自的数据。
- 数据独立实现手段
为了节省空间,不会对数据进行全部拷贝,只拷贝将来会被父子进程写入的数据,但是由于无法提取预知哪些数据要分离,因此,操作系统采用写时拷贝技术,将父子进程的数据进行分离。
写时拷贝(使用时再分配)使得父子进程完成彻底分离,保证了进程的独立性,是一种延时申请技术,能提高整机内存的使用率
2 进程终止
- 进程终止时,操作系统释放进程申请的相关内核数据结构和对于的数据和代码;本质是释放系统资源
- 进程终止的常见方式
a.代码运行结束,结果正确
b.代码运行结束,结果错误
c.代码未运行结束,程序崩溃
2.1 退出码
main()的返回值是该进程的退出码,是返回给上级进程用来判断该进程的执行结果。0表示运行结果正确,非0表示运行结果错误,不同数字可标识不同的错误原因,从而方便在运行结果错误时,定位错误原因。
程序崩溃时,退出码无意义,因为退出码对应的return语句没有被执行
strerror 打印相应退出码对应的错误
#include <stdio.h>
#include <string.h>
int main(){
int number;
for( number = 0 ; number < 20; number++){
printf("%d: %s\n",number, strerror(number));
}
return 0;
}
运行结果如下
echo $? 可查看最近一个进程执行完毕的退出码
2.2 _exit()和exit()
- 代码如何终止进程
- return,在main()里表示进程退出,执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数
- exit(),与return区别是,在任何地方调用,都直接终止整个进程
- _exit()
- _exit()
#include <unistd.h>
void _exit(int status);
status 定义了进程的终止状态,父进程通过wait来获取该值;虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255
- exit()
#include <unistd.h>
void exit(int status);
调用exit(),最终仍会调用_exit(),不同之处在于exit()会执行用户通过 atexit或on_exit定义的清理函数,关闭所有打开的流,所有的缓存数据均被写入;通俗点讲,exit()会冲刷缓冲区,然后才结束,而_exit()则是直接结束
- 观察如下代码,感受_exit()和exit()区别
int main(){
printf("hello");
exit(0);
}
int main(){
printf("hello");
_exit(0);
}
int main(){
printf("hello\n");
_exit(0);
}
运行结果分别为
exit()会打印缓冲区数据再结束,因此第一个代码会打印"hello"
_Exit()不会打印缓冲区的数据,直接结束,因此第二个代码不会打印
printf输出的数据不是直接输出到屏幕上,而是保存在缓冲区,后加\n,\n相当于刷新缓存的作用,因此,第三个"hello\n"会直接输出到屏幕上
- 总结
- exit()是C/C++接口,exit()会刷新缓冲区,缓冲区是由C标准库维护的;_exit()是操作系统提供的接口
- 库函数 exit 可以在任意位置调用,用于退出进程, 并且退出前会刷新文件缓冲区中的数据到文件中
- 系统调用 _exit 可以在任意位置调用,用于退出进程,但是退出时直接释放所有资源,并不会刷新缓冲区
3 进程等待
- 为什么要进程等待:
父进程通过进程等待,回收子进程资源,获取子进程退出信息(退出码),根据退出码判断子进程运行结果
3.1 如何等待
- 通过wait()和waitpid()进行等待
- wait()
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);
pid_t ret = wait(NULL);
返回值:成功则返回被等待进程的pid,失败则返回-1;
参数:status存放子进程退出状态,如果不关心子进程退出状态则填NULL
- waitpid()
pid_t waitpid(pid_t pid, int *status, int options);
pid_t ret = waitpid(pid, NULL, 0)
||pid_t ret = waitpid(pid, &status, 0);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
参数:
pid = -1:等待任一个子进程; pid > 0:等待其进程ID与pid相等的子进程
option 默认为0,表示阻塞等待;WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
status 输出型参数 不是按照整数来整体使用的,是按照比特位的方式,将32位比特位进行划分正常终止,次第八位表示子进程退出的退出码
异常退出,本质是操作系统杀掉进程,通过发送信号的方式,最低7位表示进程收到的信号
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
- wait/waitpid的本质是读取子进程的task_struct结构,使用系统调用让操作系统将进程内核数据结构对象的数据传给wait/waitpid
3.2 阻塞等待和非阻塞等待
- 进程阻塞等待
- 阻塞意味着进程的PCB被放入等待队列中,并将进程状态由R改为S状态;子进程退出后,父进程的PCB从等待队列中拿回,进入就绪队列等待CPU调度
- 实例
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(){
// 创建子进程
pid_t id = fork();
if(id < 0 ){
perror("fork");
exit(1); // 标识进程运行完毕且结果错误
}
// 子进程运行代码
else if(id == 0){
int cnt = 5;
while(cnt--){
printf("cnt: %d, 我是子进程, pid: %d, ppid:%d\n", cnt, getpid(), getppid());
sleep(1);
}
exit(222); // 设置子进程退出码
}
// 父进程运行代码
else{
int status = 0;
// 子进程退出后,父进程才执行waitpid
// 保持进程退出顺序性,子进程退出,父进程才能退出,让父进程进行更多收尾工作
pid_t ret = waitpid(id, &status, 0);
// 默认在阻塞状态下等待子进程退出
// id > 0: 等待指定进程;id = -1:等待任意一个子进程退出(与wait()等价)
if(ret > 0){
// 宏处理
if(WIFEXITED(status)){
printf("子进程执行完成,正常退出,退出码: %d\n", WEXITSTATUS(status));
}
else{
printf("子进程异常退出: %d\n", WIFEXITED(status));
}
}
}
return 0;
}
waitpid(pid, NULL, 0)
与wait(NULL)
等效,均为父进程阻塞等待子进程退出
运行结果如下
- 进程非阻塞等待
- 非阻塞等待意味着父进程在子进程退出前,能够运行其他任务,而不是只等待子进程
父进程在等待子进程返回结果,情况有如下:
- 等待成功,子进程退出
- 等待成功,子进程还未退出
- 等待失败
- 实例
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <vector>
typedef void (*handler_t)(); //函数指针类型
std::vector<handler_t> handlers; //函数指针数组
void fun_one(){
printf("这是一个临时任务1\n");
}
void fun_two(){
printf("这是一个临时任务2\n");
}
// 设置对应的方法回调
// 以后想让父进程执行任何方法的时候,只要向Load里面注册,就可以让父进程执行对应的方法喽!
void Load(){
handlers.push_back(fun_one);
handlers.push_back(fun_two);
}
int main(){
pid_t id = fork();
if(id < 0 ){
perror("fork");
exit(1); // 标识进程运行完毕且结果错误
}
else if(id == 0){
int cnt = 5;
while(cnt--){
printf("cnt: %d, 我是子进程, pid: %d, ppid:%d\n", cnt, getpid(), getppid());
sleep(1);
}
exit(222);
}
else{
int quit = 0;
while(!quit){
int status = 0;
pid_t res = waitpid(-1, &status, WNOHANG); //以非阻塞方式等待
if(res > 0){
//等待成功 && 子进程退出
printf("等待子进程退出成功, 退出码: %d\n", WEXITSTATUS(status));
quit = 1;
}
else if( res == 0 ) {
//等待成功 && 但子进程并未退出
printf("子进程还在运行中,暂时还没有退出,父进程可以在等一等, 处理一下其他事情??\n");
if(handlers.empty()) {
Load();
}
for(auto func: handlers) {
//执行处理其他任务
func();
}
}
else{
//等待失败
printf("wait失败!\n");
quit = 1;
}
sleep(1);
}
}
return 0;
}
waitpid(-1, &status, WNOHANG)
WNOHANG表示父进程非阻塞等待
运行结果如下
4 进程替换
- 进程替换是通过调用一种exec函数,将该进程的用户空间代码和数据被新程序替换
- 进程替换的目的是,通过fork创建子进程后,将子进程进行进程替换,从而让子进程执行其他程序
- 需要注意的是:
1.程序替换是在当前进程pcb并不退出的情况下,替换当前进程正在运行的程序为新的程序(加载另一个程序在内存中,更新页表信息,初始化虚拟地址空间),因此进程替换的过程不会创建新进程,调用exec前后该进程的id并未改变
2. 并且当前进程在运行完替换后的程序后就会退出,并不会继续运行原先的程序,即原程序exec函数的后续代码失效
4.1 进程替换的使用场景
- 不创建子进程,原进程进行进程替换
int main(){
printf("当前进程开始运行!\n");
execl("/usr/bin/ls", "ls", "-l", "-a", "-i", NULL);
printf("当前进程运行结束!\n");
return 0;
}
运行结果如下:
进程替换完成,调用打印当前文件夹的目录信息命令,但并不打印 “当前进程运行结束!” 这条语句
原因在于,进程替换会将当前进程的所有代码和数据全部替换,一旦调用完成,execl函数的后续所有代码都不会执行
因此,execl()不需要进行返回值判定,因为一旦替换完成,后续所有代码不会执行,故无返回值。只有当替换失败,返回-1
- 创建子进程,让子进程进行进程替换
- 创建子进程目的:为了不影响父进程,使用进程替换,替换子进程的代码和数据,执行新进程,不影响父进程的代码和数据;让父进程聚焦在读取和解析数据上,指派子进程执行代码的功能
- 未进行进程替换、加载新程序之前,父子进程代码共享,数据写时拷贝;当子进程加载新程序后,代码也写时拷贝,父子进程代码分离
int main(){
pid_t id = fork();
if(id == 0){
// 子进程
printf("子进程开始运行,pid:%d\n", getpid());
sleep(2);
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
exit(1);
}
else{
// 父进程
printf("父进程开始运行,pid:%d\n", getpid());
int status = 0;
pid_t id = waitpid(-1, &status, 0); // 阻塞等待
if(id > 0){
printf("wait success, exit code: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
运行结果如下:
4.2 其他exec函数使用
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
char* const _argv[NUM] = {"ls", "-a", "-l", NULL};
execv("/usr/bin/ls", _argv);
l——list 意味着execl的参数采用列表; v——vector 表示execv的参数采用数组
int execlp(const char *file, const char *arg, ...);
execlp("ls", "ls", "-a", "-l", NULL);
p —— 在环境变量PATH中进行查找,无需执行程序的路径,有p自动搜索环境变量PATH,无需手动写路径
int execvp(const char *file, char *const argv[]);
execvp("ls", _argv);
vp —— 参数采用数组,无需手动写路径
int execle(const char *path, const char *arg, ..., char * const envp[]);
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execle("ls", "ls", "-a", "-l", NULL, envp);
le——参数采用列表自己维护环境变量
int execve(const char *file, char *const argv[], char *const envp[]);
execve("/bin/ls", _argv, envp);
- 上述1-5是系统提供的基本封装,6execve才是系统调用,其它五个函数最终都调用execve
- 进程替换自己写的C、C++二进制程序
mycmd.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[]){
if(argc != 2){
printf("can not execute!\n");
exit(1);
}
printf("获得环境变量 : %s\n", getenv("env17"));
if(strcmp(argv[1], "-a") == 0){
printf("hello a!\n");
}
else if(strcmp(argv[1], "-b") == 0){
printf("hello b!\n");
}
else{
printf("hello NULL!\n");
}
return 0;
}
main函数部分
char* const _env[NUM] = {(char*)"env17=111222", NULL};
execle(./mycmd, "mycmd", "-a", NULL, _env);
运行结果如下:
- 执行其他语言的程序
- exec* 功能其实是加载器的底层接口
execlp("python" , "test.py", NULL);
execlp("bash", "test.sh", NULL);
execlp("./test.py" test.py" NULL);
5 简易Shell
- 基础
- 首先shell是常驻内存的进程,因此需要while(1),保证不退出
- 其次需要打印提示信息,如[yjp@localhost myshell]#
- 然后需要从键盘获取用户输入,使用
fgets函数
- 解析用户输入,保存用户命令参数,使用
strtok函数
对用户输入进行拆分,并存放到g_argv数组 - fork函数创建子进程,使用execvp函数进行进程替换,父进程阻塞等待
int main(){
// 0.shell是常驻内存的进程,即不退出
while(1){
// 1.打印提示信息
printf("[yjp@localhost myshell]# ");
fflush(stdout); // 刷新缓冲区
memset(cmd_line, '\0', sizeof cmd_line);
// 2.获取用户输入,即各种指令
if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL){
continue;
}
// 清理'\n'
cmd_line[strlen(cmd_line) - 1] = '\0';
// 3.命令行字符串解析
g_argv[0] = strtok(cmd_line, SEP); // 首次使用需要传入原始字符串,返回SEP前的字串,并且指针指向子串的下一个位置
int index = 1;
while(g_argv[index++] = strtok(NULL, SEP)); // 第二次使用如果仍需使用原始字符串,传NULL,并且指针指向首次使用后,返回的子串的下一个位置
// 4. fork()
pid_t id = fork();
if(id == 0){
// 子进程
printf("子进程执行以下功能\n");
execvp(g_argv[0], g_argv);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0){
printf("exit code: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
- ls和ll命令
在Shell输入whilch ls
和whilch ll
发现ls与’ls --color=auto’等同、ll与’ls -l --color=auto’等同,因此可对这两个命令进行特殊修改
if(strcmp(g_argv[0], "ls") == 0){
g_argv[index++] = "--color=auto";
}
if(strcmp(g_argv[0], "ll") == 0){
g_argv[0] = "ls";
g_argv[index++] = "-l";
g_argv[index++] = "--color=auto";
}
- 内置命令cd
实际使用时发现cd命令失效,原因是cd是内置命令,内置命令和shell是为一体的,是shell的一部分,不需要单独去读取某个文件,系统启动后,就执行在内存中了,内置命令不会产生子进程去执行,因此需要让父进程自己执行cd命令
if(strcmp(g_argv[0], "cd") == 0 ){
// 父进程cd 而不是子进程cd
if(g_argv[1] != NULL){
chdir(g_argv[1]); // 改变当前工作目录到指定的路径g_argv[1]
}
continue;
}
- 综上,总体代码如下
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define NUM 1024
#define SIZE 31
#define SEP " "
char cmd_line[NUM];
char* g_argv[SIZE];
// shell运行原理 -- 子进程执行命令,父进程等待和解析命令
int main(){
// 0.shell是常驻内存的进程,即不退出
while(1){
// 1.打印提示信息
// [yjp@localhost myshell]#
printf("[yjp@localhost myshell]# ");
fflush(stdout); // 刷新缓冲区
memset(cmd_line, '\0', sizeof cmd_line);
// 2.获取用户输入,即各种指令
if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL){
continue;
}
// 清理'\n'
cmd_line[strlen(cmd_line) - 1] = '\0';
// 3.命令行字符串解析
g_argv[0] = strtok(cmd_line, SEP); // 首次使用需要传入原始字符串,返回SEP前的字串,并且指针指向子串的下一个位置
int index = 1;
if(strcmp(g_argv[0], "ls") == 0){
g_argv[index++] = "--color=auto";
}
if(strcmp(g_argv[0], "ll") == 0){
g_argv[0] = "ls";
g_argv[index++] = "-l";
g_argv[index++] = "--color=auto";
}
while(g_argv[index++] = strtok(NULL, SEP)); // 第二次使用如果仍需使用原始字符串,传NULL,并且指针指向首次使用后,返回的子串的下一个位置
// 4. TODP 内置命令 让父进程自己执行的命令,称为内置命令,有些命令一定要父进程执行,如cd命令
if(strcmp(g_argv[0], "cd") == 0 ){
// 父进程cd 而不是子进程cd
if(g_argv[1] != NULL){
chdir(g_argv[1]); // 改变当前工作目录到指定的路径g_argv[1]
}
continue;
}
// 5. fork()
pid_t id = fork();
if(id == 0){
// 子进程
printf("子进程执行以下功能\n");
execvp(g_argv[0], g_argv);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0){
printf("exit code: %d\n", WEXITSTATUS(status));
}
}
return 0;
}