送给大家一句话:
我并不期待人生可以一直过得很顺利,但我希望碰到人生难关的时候,自己可以是它的对手。—— 加缪
开始学习进程控制
1 前言
通过对进程的学习,我们对虚拟地址,页表,物理地址有了大概认识。我们平时使用的地址都是虚拟地址,通过页表可以访问物理地址(统一的视角进行控制,保证数据安全)。也认识到写时拷贝。
也认识O(1)调度算法,通过两个队列(活跃队列,过期队列)完成进程的分时控制,通过优先级来放入不同位置,以时间复杂度O(1)快速寻找进程。
2 进程创建
2.1 fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。fork函数具有两个返回值,通过对返回值的判断(if else )可以进行父进程和子进程的不同书写。
注意:进程调用fork,当控制转移到内核中的fork代码后,内核做以下工作:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程(进程:内核的相关数据管理的数据结构(task_struct + mm_struct + 页表)+ 代码与数据)
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
这里是为了保证父进程和子进程的独立性。
2.2 fork函数返回值
- 子进程返回0
- 父进程返回的是子进程的pid
那为什么父进程返回子进程PID ,给子进程返回0呢???
很好理解:就像现实生活中,父母有了孩子,会给他或她起一个名字,父母知道了名字,就可以很好管理孩子。父进程与子进程同理,父进程为了便于管理子进程,所以fork函数会返回对应子进程的pid。
2.3 写时拷贝
通过图解可以很好理解写时拷贝。
在创建子进程的时候,子进程的页表映射与父进程一致(默认继承的),一旦子进程要进行修改数据,为了保证进程的独立性(保证父进程安全运行),不得不开辟一个新空间,并修改子进程页表的映射(虚拟地址不变!)。
2.4 fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
一般使用if else 分开书写,也可以通过系统调用打开新的进程。
2.5 fork调用失败的原因
- 系统中有太多的进程(数据空间是有限的)
- 实际用户的进程数超过了限制(必须是有限的)
2 进程终止
2.1 终止是在做什么
进程终止会进行:
- 释放曾经的代码和数据所占据的空间
- 释放内核数据结构
但是task_struct会延期处理,因为终止的进程处于Z状态(僵尸进程)
2.2 进程终止的情况
我们的main函数常常会有一个返回值 0 ,那为什么要返回零呢???
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<string.h> 4 5 6 int main() 7 { 8 9 printf("I am parent process,pid:%d,ppid:%d\n",getpid(),getppid()); 10 sleep(2); 11 12 return 100; 13 }
来看我们返回100(为了效果明显)时
echo 打印的是bash 的环境变量,这个100 就是刚才进程返回到父进程(bash)的退出码(环境变量 ?
表示最近一个进程的退出码),一般0表示正常运行,非零表示有问题。
父进程关心子进程的信息,想要知道子进程是否正常运行。不同的退出码表示不一样的失败原因,我们来获取一下:
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<string.h> 4 5 int g_val = 10000; 6 7 int main() 8 { 9 10 for(int errcode = 0 ;errcode <=255; errcode++) 11 { 12 printf("%d: %s\n",errcode,strerror(errcode)); 13 14 } 15 printf("I am parent process,pid:%d,ppid:%d\n",getpid(),getppid()); 16 sleep(1); 17 18 return 0; 19 }
这样就可以获取所有的退出码和对应的退出信息了:
通过退出码就能获取对应退出信息,来告知用户为什么退出了。其底层实现也好理解,通过数字与字符串的一一对应来做到。
常见进程退出场景:
- 代码运行完毕,结果正确(正常结束进程)
- 代码运行完毕,结果不正确
- 代码异常终止,出现异常提前退出
就像:VS编程运行的时候,如果崩溃了 — 操作系统发现你的进程做了不应该做的事情,OS就杀死进程!!!
一旦出现异常,退出码就没有意义了!!! 为什么出异常才是最重要的!!!
那为什么会出现异常呢??? 原因是:进程出现异常的本质是进程收到来自OS发给进程的信号!(kill -9 就是一个信号)
注意:
- 先确认是否异常
- 不是异常就是代码正常跑完,看退出码即可。
- 可以通过退出信号来判断出现了什么异常
2.3 如何终止
正常终止(可以通过 echo $? 查看进程退出码):
- 从main函数return,表示进程终止
- 调用exit
- _exit
异常退出:
ctrl + c,信号终止
来看手册中如何描述的:
调用exit 函数试试:
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<string.h> 4 #include<stdlib.h> 5 int g_val = 10000; 6 7 int main() 8 { 9 10 printf("I am parent process,pid:%d,ppid:%d\n",getpid(),getppid()); 11 sleep(1); 12 exit(123); 13 14 } 15
运行后是这样的效果:
exit比return 直接,调用一次就可以完全退出!
_exit 是一个系统调用(system call),参数与exit一致,使用与exit几乎一模一样。
**但是exit会冲刷缓冲区,而_exit 不会,**因为缓冲区在系统调用之上 ,而exit 是一个C语言库函数。图解:
exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
3 进程等待
3.1 进程等待必要性
- 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
也就是说,任何进程在退出时都要被父进程进行等待,不然子进程处于僵尸进程就会造成内存泄漏!!!
3.2 进程等待的方法
- wait方法
- waitpid方法
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回
wait方法
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int*status);
返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
我们测试一下:
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<string.h> 4 #include<stdlib.h> 5 #include<sys/types.h> 6 #include<sys/wait.h> 7 8 void Childrun() 9 { 10 int cnt = 5; 11 while(cnt) 12 { 13 printf("I am child ,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt); 14 sleep(1); 15 cnt--; 16 } 17 } 18 19 int main() 20 { 21 22 printf("I am father ,pid:%d,ppid:%d\n",getpid(),getppid()); 23 24 pid_t id = fork(); 25 if(id == 0) 26 { 27 //child 28 Childrun(); 29 printf("child quit...\n"); 30 exit(0); 31 } 32 //father 33 sleep(10); 34 pid_t rid = wait(NULL); 35 if(rid > 0) 36 { 37 printf("wait success,rid:%d\n",rid); 38 } 39 sleep(3); 40 return 0; 41 }
这个程序会在子进程运行结束前等待子进程,并且会存在一段时间的窗口期,此时子进程处于僵尸进程:
在这个父进程等待的过程中,父进程一直在等待子进程的退出,处于阻塞等待状态。父进程本质是等待某种软件条件就绪,那么如何理解阻塞等待子进程呢???
就是把自己列入等待队列,把状态列入不运行状态,等待子进程(类似scanf 的阻塞)。
waitpid方法
#include<sys/types.h> #include<sys/wait.h> pid_ t waitpid(pid_t pid, int *status, int options);
注意其中的三个参数!
返回值: 当正常返回的时候waitpid返回收集到的子进程的进程ID; 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0; 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在; 参数: pid: Pid = -1 , 等待任一个子进程。与wait等效。 Pid>0.等待其进程ID与pid相等的子进程。 status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
pid_t rid = waitpid(-1,NULL,0); 与刚才的wait等价!!!如果id不为-1,是一个对应的id,那么就会等待指定进程!!!,如果id错误(不存在该进程),就会发生等待错误!!!
status 是一个输出型参数,需要我们传入一个指针来获取。来测试一下(子进程退出码设置为1 )
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<string.h> 4 #include<stdlib.h> 5 #include<sys/types.h> 6 #include<sys/wait.h> 7 8 void Childrun() 9 { 10 int cnt = 5; 11 while(cnt) 12 { 13 printf("I am child ,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt); 14 sleep(1); 15 cnt--; 16 } 17 } 18 19 int main() 20 { 21 22 printf("I am father ,pid:%d,ppid:%d\n",getpid(),getppid()); 23 24 pid_t id = fork(); 25 if(id == 0) 26 { 27 //child 28 Childrun(); 29 printf("child quit..\n"); 30 exit(1); //退出码设置为1 31 } 32 //father 33 sleep(10); 34 int status = 0 ; 35 pid_t rid = waitpid(id , &status , 0); 36 if(rid > 0) 37 { 38 printf("wait success,rid:%d\n",rid); 39 } 40 printf("father quit...,status:%d\n",status); 41 sleep(3); 42 return 0; 43 }
这就成功获取了status!
我们需要的是 退出码 和 退出信号,那么我们如何通过status获取这两个数据呢???
也就通过位运算就可以成功获取了。
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/direct/2650b18879684c50981074b8e6bbdf7d.png)
这样就可以了:
通过两个信息就可以判断进程是否正常运行,如果异常,也能知道异常原因了。
当然,如果使用位运算就有点那啥了,我们可以使用宏:
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
对于第三个参数,就可以让父进程在等待的刚才中区做其他事情。也就是进行非阻塞等待:WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
- 阻塞等待就类似张三给李四打电话帮忙,李四正在忙,告诉张三等一会,然后张三这个电话就不挂了,等着李四完成工作,张三也不做其他事情。
- 非阻塞等待类似张三给李四打电话帮忙,李四正在忙,告诉张三等一会,然后张三说李四忙完打回来,张三就先去做其他事情。
下面写入了一段非阻塞轮询等待的代码,这样就能保证父进程在等待的过程中,可以去做其他事情!
34 while(1) 35 { 36 int status = 0 ; 37 pid_t rid = waitpid(id , &status , 0); 38 if(rid == 0) 39 { 40 sleep(1); 41 printf("child is running ,father check next time!\n"); 42 } 43 else if(rid > 0) 44 { 45 if(WIFEXITED(status)) 46 { 47 printf("child quit success,child exit code:%d\n",WEXITSTATUS(status)); 48 } 49 else 50 { 51 printf("child quit unnormal!\n"); 52 } 53 break; 54 } 55 else 56 { 57 printf("waitpid failed!\n"); 58 break; 59 } 60 }
来看运行效果(父进程一直在查):
这样就完成了。
4 总结
- 等待很容易理解,等待是必须进行的,回收子进程资源,获取子进程退出信息
- 进程等待常用waitpid,并常用非阻塞等待