目录
1.进程的创建
1.1fork函数
1.2fork创建子进程,OS做了什么?
1.3为什么要写实拷贝?
2.进程的终止
2.1进程终止,操作系统做了什么?
2.2进程常见的退出方式
2.3进程常见的退出方法
3.进程的等待
3.1为什么进行进程等待
3.2如何等待?
wait方法
waitpid方法
3.3测试代码
4.进程的替换
4.1概念以及原理
4.2 怎么做?
1.替换函数exec
2.测试代码
4.3为什么要有程序替换
1.场景需要
2.补充:为什么要创建子进程
学习目标:1.进程的创建 2.进程的终止 3.进程的等待 4.进程的替换
1.进程的创建
1.1fork函数
1.功能:在已有的进程下创建一个新的进程。新进程:子进程 , 原进程:父进程
2.引用的头文件:#include <unistd.h>
3.函数:pid_t fork(void); 使用:pid_t id = fork();
4.返回值:创建成功:a.把子进程的id返回给父进程,b.把0返回给子进程
创建失败:返回-1
5.特点:
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
1.2fork创建子进程,OS做了什么?
- 分配新的内存块和数据结构给子进程
- 将父进程部分数据结构内容拷贝给子进程(写时拷贝)
- 添加子进程到系统进程列表中
- fork返回,开始调度器调度
1.3为什么要写实拷贝?
--因为有写实拷贝技术得存在, 所以, 父子进程得以彻底分离! 完成了进程独立性得技术保证
--写实拷贝是一种延时申请技术, 可以提高整机内存得使用率
1.用的时候,再给你分配,是高效使用内存的一种表现
2.OS 无法再代码执行前, 预知哪些空间会被访问
2.进程的终止
2.1进程终止,操作系统做了什么?
释放 进程申请的相关内核数据结构和对应的数据和代码(本质:释放系统资源)
2.2进程常见的退出方式
a. 代码跑完, 结果正确
b. 代码跑完, 结果不正确 ---->main函数的返回值?有什么意义?
c. 代码没有跑完, 程序崩溃了
2.3进程常见的退出方法
--正常终止(可以通过 echo $? 查看进程退出码):1.return(main函数内) 2.exit函数
--非正常终止:ctrl + c ,信号终止
exit函数:头文件:#include <unistd.h> 函数: void exit(int status);
_exit函数:头文件:#include <unistd.h> 函数: void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
exit会执行用户定义的清理函数, 然后刷新缓冲, 关闭流等, 然后再退出
_exit直接退出
3.进程的等待
3.1为什么进行进程等待
1.处理僵尸进程,解决内存泄漏的问题
2.父进程可以通过等待知道派给子进程的任务完成的状况:正确与否,是否异常
3.父进程通过等待,回收子进程资源,获取子进程退出信息
3.2如何等待?
wait方法
1.头文件
#include<sys/types.h> #include<sys/wait.h>
函数:pid_t wait(int*status);
2.返回值:成功返回被等待进程pid,失败返回-1。
3.参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
1.返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
2.参数:--.pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
--.status:
获取状态码(即次第8位):(status>>8)&0xFF获取信号(即低7位):status & 0x7F
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
--.options:默认为0:阻塞等待
WNOHANG(非阻塞等待):若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
3.3测试代码
1.阻塞式等待
阻塞式等待,即把waitpid中的参数中options设为0
代码:
1 #include <stdio.h> 2 #include<sys/types.h>3 #include<sys/wait.h>//wait的头文件4 #include<unistd.h> //fork的头文件5 #include<stdlib.h>6 7 int main()8 { 9 pid_t id = fork(); //创建子进程10 if(id == 0)11 { 12 //子进程 13 int cnt = 5; 14 while(cnt) 15 { 16 printf("我是子进程: %d\n",cnt--);17 sleep(1);18 } 19 exit(11);20 } 21 else 22 { 23 //父进程 24 int status = 0;25 pid_t ret = waitpid(id,&status,0);//默认是阻塞式等待子进程26 if(ret >0) 27 { 28 if(WIFEXITED(status)) 29 printf("父进程等待成功,退出码: %d\n",WEXITSTATUS(status));30 else31 printf("子进程异常退出: %d\n",WIFEXITED(status));32 } 33 }34 return 0;35 }
效果:
2.非阻塞式等待
非阻塞式等待:即把waitpid中的参数中options设为WNOHANG
我们想要父进程在等待的过程中干点别的事情:
1.这里我们定义了一个类型为函数指针的vector
2.定义了两个函数
3.定义了一个load函数,用来往实例化的vector中填充函数指针
4.当我们waitpid返回0,表示等待成功,但子进程还没有退出,若vector不为空,往里加载函数指针,让父进程调用这些函数(用来模拟做其它的事情)
注意:这里使用了范围for去遍历vector,是C++11里面的,若是C98编译可能会出错
使用下面这段代码:g++ -std=c++11 -0 myproc myproc.cc
生成的执行文件 编译的文件
代码:
1 #include<iostream>2 #include<vector>3 #include<stdio.h>4 #include<sys/types.h>5 #include<sys/wait.h>//wait的头文件6 #include<unistd.h> //fork的头文件7 #include<stdlib.h>8 9 typedef void (*handler_t)();//函数指针类型10 std :: vector<handler_t> handlers;//函数指针数组11 12 void func_one()13 {14 printf("这是第一个临时任务\n");15 }16 17 void func_two()18 {19 printf("这是第二个临时任务\n");20 }21 22 void load()23 {24 handlers.push_back(func_one);25 handlers.push_back(func_two);26 }27 28 int main()29 {30 pid_t id = fork(); //创建子进程31 if(id == 0)32 { 33 //子进程34 int cnt = 5;35 while(cnt)36 {37 printf("我是子进程: %d\n",cnt--);38 sleep(1);39 }40 exit(11); 41 }42 else 43 {44 int quit = 0;45 while(!quit)46 {47 int status = 0;48 pid_t ret = waitpid(id,&status,WNOHANG);49 50 if(ret > 0)51 {52 quit = 1;53 printf("等待成功,退出码: %d\n",WEXITSTATUS(status));54 }55 else if(ret == 0)56 {57 printf("等待成功,但子进程还没有退出,父进程可以做其它的事情\n");58 if(handlers.empty()) load();59 for(auto& iter:handlers)60 {61 iter();62 }63 }64 else65 {66 //等待失败67 printf("等待失败\n");68 quit = 1;69 }70 sleep(1);71 }72 }73 74 return 0; 75 }
效果:
4.进程的替换
4.1概念以及原理
1.概念
程序替换:是通过特定的接口,加载磁盘上一个全新的程序(代码+数据)
2.原理
4.2 怎么做?
1.替换函数exec
#include <unistd.h>`//头文件
1.函数
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);int execve(const char *path, char *const argv[], char *const envp[]);
2.命名解释
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量3.返回值
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值(这是因为exec函数是进程的替换,调用该函数成功之后,会将当前进程的所有代码和数据都进行替换! 包括已执行的和没有执行的!)
加载 , 所谓的exec*函数, 本质就是如何加载程序的函数
2.测试代码
1.int execl(const char *path, const char *arg, ...);
代码:
1 #include<stdio.h>2 #include<unistd.h>3 #include<sys/wait.h>4 #include<sys/types.h>5 #include<stdlib.h>6 7 8 const char* path = "/usr/bin/ls";9 10 int main()11 {12 pid_t id = fork();//创建子进程13 if(id == 0)14 {15 printf("我是子进程\n\n");16 execl(path,"ls","-a","-l",NULL);//调用exec函数执行其它程序 17 18 exit(1);19 }20 else21 {22 int status = 0;23 pid_t ret = waitpid(id,&status,0);//阻塞式等待24 25 if(ret > 0)26 {27 printf("\n我是父进程,等待成功,退出码:%d\n",WEXITSTATUS(status));28 }29 }30 31 32 return 0;33 }
效果:
2.int execlp(const char *file, const char *arg, ...);
和上面基本一致,就是可以不用写绝对路径,path:环境变量,OS能直接找到
3.int execv(const char *path, char *const argv[]);
同上,只用把要执行的命令写入字符指针数组内,然后传递这个数组就行(最后一个参数必须是NULL)
4.int execvp(const char *file, char *const argv[]);
同上不用带绝对路径
5.int execle(const char *path, const char *arg, ...,char *const envp[]);
这个可以给其它程序传递环境变量
--我自己设置了一个环境变量val
--调用execle函数
--mycmd.c 可以获取val这个环境变量
--效果:
4.3为什么要有程序替换
1.场景需要
一定和应用场景有关, 我们有时候, 必须让子进程执行新的程序 !!!
例如:1.如何执行其他或我自己写的C. C++二进制程序2.如何执行其它语言的程序
1.如何执行其他或我自己写的C. C++二进制程序
--makefile:
--mycmd.c
--exec.c
--效果:
2.如何执行其它语言的程序
--效果:
2.补充:为什么要创建子进程
为什么要创建子进程
1.如果不创建, 那么进程替换的就是父进程,创建了,替换的就是子进程,而不影响父进程(进程的独立性,且加载属于改写了,由于写时拷贝的特点,会在物理内存上开辟一块新的内存,把新的代码加载到上面,并通过页表,改变映射关系)
2.我们想要父进程聚焦于读取数据,解析数据,指派进程进程执行代码的功能
(父进程读取分析数据,子进程执行代码的功能)
5.总结,实现简单的shell
链接:shell的模拟实现