目录
1. 初识fork()
操作系统以父进程为模板创建子进程,父子进程代码段相同,但是有各自独有的数据
- 进程调用fork()后
- 给子进程分配新的内存及内核数据结构
- 拷贝父进程部分数据结构内容至子进程
- 将子进程添加至系统进程列表
- fork放回,调度器调度
- 父进程和子进程执行的先后顺序不定
-
1.1 fork的返回值
主要根据返回值来进行父子进程的代码分流
- 用户通过fork的返回值来判断,到底哪个进程是父进程,哪个是子进程
- 父进程:返回创建的子进程的pid
- 子进程:返回值是0
- 出现错误:返回值是-1
-
1.2 写时拷贝技术
通常情况下,父子代码共享;当任意一方试图写入时,使用写时拷贝技术。
示例:
//演示父进程创建子进程之后,父子进程数据独有的例子
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//全局变量
int val=100;
int main(){
int pid=fork();
//子进程
if(pid == 0){
val=200;
printf("child val:%d %p\n",val,&val);
}else if(pid>0){ //父进程
sleep(1);
printf("parent val:%d\n %p\n",val,&val);
}else{
return -1;
}
}
- 结果:
- child val:200 0x6009a4
- parent val:100 0x6009a4
- 100和200没有在同一块内存区域中,不是同一个变量;但是查看的地址是相同的,这就出现了矛盾
- 实际上,进程中访问到的这些地址都是虚拟地址 -> 程序地址空间实际应该称为进程的虚拟地址空间。
- 解释示例
- 子进程复制父进程,为子进程创建pcb,将父进程pcb中的数据全部拷贝。这时父子进程的虚拟地址空间、页表完全一样,因此其代码与数据也都看似一样 -> 图1
- 进程具有独立性,任意进程修改数据不应该影响其他进程。因此,子进程需要重新分配内存,存储新的数据,并且更新页表的映射关系。
- 但是子进程可能不用这些数据,那么直接开辟内存、更新页表以及拷贝数据都毫无意义。因此,操作系统不会直接开辟内存考内数据,而是等任意一个进程修改数据时,重新给子进程开辟物理内存,拷贝数据。 -> 写时拷贝技术 图2
-
1.3 常规用法
- 希望子进程复制父进程后,父子进程同时执行不同的代码段。
- 一个进程要执行一个不同的程序
-
1.4 fork调用失败情况
- 系统中进程过多
- 进程数超过限制
-
1.5 vfork()
- 父子进程公用同一块虚拟地址空间,由于同时运行会造成调用栈混乱。因此,子进程先运行,等到子进程exit()退出或程序替换后,父进程再开始运行。
- vfork创建的子进程不能使用return终止 -> return后,子进程将资源释放,父进程调用栈混乱。
- 存在意义
- 快速创建子进程,并且子进程专门用来运行其他程序。
- 速度快:共用地址空间可以减少子进程数据拷贝父进程的消耗
- 但是因为fork()实现了写时拷贝技术,vfork()就淘汰了。
- 快速创建子进程,并且子进程专门用来运行其他程序。
2. 进程终止
-
2.1 进程退出情况
- 正常退出,结果正确
- 正常退出,结果错误
- 异常退出(返回值不能作为判断标准)
-
2.2 进程的退出方式
- 正常终止
- 在main()中return
- 库函数 void exit(int status);
- 系统调用 void _exit(int status);
- 异常退出
- ctrl+c 信号终止
- 正常终止
-
返回值
- 通过 echo $? 查看进程返回值
- 错误编号
- 每个系统调用执行完毕都会重置进程中errno这个全局变量,其中存储的就是本次调用的系统调用接口错误编号。当系统调用接口出错,用户就可以通过这个errno获取系统调用的错误原因。
- void perror(const char *s);
- char *strerror(int errnum);
/*
*演示进程退出方式
* 1.main中return 效果等同于调用exit
* 直接退出,不进入死循环
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
//全局变量,存放上一条函数执行结果的错误编号(针对系统调用错误原因)
int errno;
int main(){
int i=0;
for(i=0;i<255;i++){
//char *strerror(int errnum);
//返回上一条系统调用的错误原因描述
//errno每次系统调用完毕都会重置的一个全局变量
//用于存储这个系统调用的错误原因编号
printf("%s\n",strerror(i)); //错误原因
}
//进程的退出数据只能是一个小于256的数据,因为进程的退出码只能使用一个字节来存储
return -1;
while(1){
printf("----------\n");
sleep(1);
}
return 0;
}
/*
* 2.void exit(int status);
* 3s后缓冲区刷新,打印
* exit()退出一个进程,退出时刷新缓冲区,还有其他释放操作
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int errno;
int main(){
printf("-------");
sleep(3);
exit(0);
int i=0;
for(i=0;i<255;i++){
printf("%s\n",strerror(i));
}
return -1;
while(1){
printf("----------\n");
sleep(1);
}
return 0;
}
/*
* 3. void _exit(int status);
* 直接退出
* _exit()粗鲁退出,直接释放所有资源
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int errno;
int main(){
printf("-------");
sleep(3);
exit(0);
int i=0;
for(i=0;i<255;i++){
printf("%s\n",strerror(i));
}
return -1;
while(1){
printf("----------\n");
sleep(1);
}
return 0;
}
3. 进程等待
父进程通过进程等待的方式,等待子进程退出,回收子进程资源,获取子进程退出信息,避免产生僵尸进程。
-
3.1 进程等待方法
- wait()
- 阻塞函数,等待任意一个子进程退出
- 阻塞与非阻塞:调用功能当前不具备完成条件,是否立即返回
- 阻塞:为了完成功能发起调用,若当前不具备完成条件,则一直等待,直到完成后返回
- 非阻塞:为了完成功能发起调用,若当前不具备完成条件,则立即报错返回
- 阻塞与非阻塞:调用功能当前不具备完成条件,是否立即返回
- 返回值
- 等待进程pid:成功
- -1:错误
- 阻塞函数,等待任意一个子进程退出
- pid_t waitpid(int pid,int *status,int opt)
- 等待任意一个子进程退出 / 等待指定子进程退出
- 通过opt将操作设置为非阻塞(WNOHANG)
- pid
- -1:等待任意子进程退出,等效于wait()
- >0:等待指定的子进程退出
- 返回值
- -1:错误
- 0:没有子进程退出
- >0:退出子进程的pid
- status:用于获取退出原因
- 将status当做位图来看
- 子进程的返回值只有一个字节,剩下的字节存储的是其他数据,直接打印statu将无法获取返回值。
- wait()
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
int pid=fork();
if(pid<0){
perrpr("fork error");
exit(-1);
}else if(pid==0){
sleep(5);
exit(0);
}
int statu;
//int ret=wait(&statu);
int ret=waitpid(pid,&statu,WNOHANG);
printf("%d-%d\n",ret,pid);
while(1){
printf("----------\n");
sleep(1);
}
return 0;
}
4. 程序替换
一个进程运行什么程序,取决于虚拟地址空间中的代码段映射在物理地址中哪一个真实代码区域。若将虚拟地址代码段所映射的物理内存的位置替换为另一个程序的位置,则实际运行另一个程序。
- 本质
- 用另一个内存区域代码的位置 替换 代码段映射的代码位置,并且重新初始化数据段。
- 存在意义
- 大多数情况下,创建子进程都是为了让子进程运行另一个程序,执行其他任务。
- 让子进程处理特定功能的好处
- 为了保持父进程的稳定性。当出现问题时,子进程是背锅侠。
- 实现方式
- 操作系统提供一套接口都能实现成需替换,统称为exec函数族。
- 只有execve是真正的系统调用,其它五个函数终都调用 execve
-
所替换的程序名称 参数赋予方式 环境变量 execl 需要全路径文件名 列表参数平铺 继承父进程环境变量 execlp 不需要文件路径,只需要文件名,借助环境变量去指定路径下找文件
列表参数平铺 继承父进程环境变量 execle 需要全路径文件名 列表参数平铺 自定义环境变量 execv 需要全路径文件名 组织参数数组 继承父进程环境变量 execvp 不需要文件路径,只需要文件名,借助环境变量去指定路径下找文件
组织参数数组 继承父进程环境变量 execve 需要全路径文件名 组织参数数组 自定义环境变量 - exec函数族
- 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 execvpe(const char *file, char *const argv[],
char *const envp[]);
- 操作系统提供一套接口都能实现成需替换,统称为exec函数族。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
//execl("./bin/ls","ls","-l","-a",NULL);
//execlp("ls","ls","-l","-a",NULL);
char *env[5]={NULL};
env[0]="MYVAL=100";
env[1]="home=/home";
//execle("./bin/test","test","-l",NULL,env);
char *argv[5]={NULL};
argv[0]="hello";
argv[1]="bonjour";
argv[2]="vie";
execv("./bin/test",argv);
printf("----------\n");
return 0;
}
5. 实现一个简单的shell
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
while(1){
//1.等待标准输入
printf("[qqy@localhost]$ ");
fflush(stdout);
char buf[1024]={0};
//%[^\n] 从标准输入缓冲区中获取一个字符串,遇到换行为止
//%*c 将一个字符从缓冲区中取出(丢弃缓冲区中的/n)
//!=1代表buf中没有获取到数据
if(scanf("%[^\n]%*c",buf)!=1){
getchar(); //将回车取出
continue;
}
//2.解析输入的数据
//buf[ ls -a]如何解析
//ls是程序名称,也是第0个参数
//-a是第1个参数
//检测空白字符
char *argv[32];
int argc=0;
char *ptr=buf;
//直到字符串结束
while(*ptr!='\0'){
//当前位置是非空白字符
if(!isspace(*ptr)){
argv[argc++]=ptr;
//不是空白符号,也没有结束
while(!isspace(*ptr) && *ptr!='\0'){
ptr++;
}
continue;
}
//设置字符串结束标志
*ptr='\0';
ptr++;
}
//参数需要用NULL结尾
argv[argc]=NULL;
//3.判断是否为内建命令
//内建命令,shell内部实现其功能,无需创建子进程
if(strcmp(argv[0],"cd")==0){
//int chdir(const char *path);
//改变当前路径
chdir(argv[1]);
continue;
}
//创建子进程,执行任务
int pid=fork();
//创建子进程失败
if(pid<0){
continue;
}else if(pid==0){
//子进程,执行其他任务 -> 程序替换
if(execvp(argv[0],argv)<0){
perror("");
}
//替换失败 -> 退出
exit(0);
}
//父进程等待子进程退出,避免出现僵尸进程
wait(NULL);
}
return 0;
}
- 下列程序中一共创建了几个进程?
fork()
if(fork()&&fork()||fork()){
fork();
}
16个
- 下列程序中打印几条_
//1
for(int i=0;i<2;i++){
fork();
printf("_");
}
//2
for(int i=0;i<2;i++){
fork();
printf("_\n");
}
- 没有/n,_存储在缓冲区 -> 8条
- p创建子进程c1
- 两者都有一条_
- 创建子进程c2时,c2复制p及p的_;c3复制c1及c1的_
- p、c1、c2、c3再各自有一条_
- 有\n,直接打印_ -> 6条
- p创建子进程c1
- 两者都直接打印一条_
- c2、c3只复制p和c1
- 4者相继打印一条_