前言
接上文继续谈谈对Linux下面进程的理解
五、使用系统系统调用创建进程
1)创建子进程
在C语言中,可以使用系统接口fork函数来创建子进程
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(){
printf("before Fork,PID is %d, PPID is %d\n",getpid(),getppid());
fork();
printf("after fork,PID is %d, PPID is %d\n",getpid(),getppid());
}
第二个语句打印了两边,因为创建出来的那个进程也打印了一遍,观察打印信息,创建出来的进程是该进程的子进程
2)fork返回值问题
现象
获取fork()的返回值后,并且打印返回值、PID、PPID,发现
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(){
pid_t return_value = fork();
printf("return value is %d,PID is %d,PPID is %d\n",return_value,getpid(),getppid());
sleep(2);
}
当前进程的返回值是子进程返回值的PID,当前进程的PID是子进程的PPID,子进程的返回值为0
结论
fork(()对父进程的返回值是子进程的PID,对子进程的返回值是0
通过查看man手册也可验证
如果成功创建,子进程的PID被返回给父进程,0被返回给子进程,如果失败,-1被返回给父进程,子进程不创建
目的
因为进程的task_struct对象中存有PPID,可以轻易找到父亲,但是不存有子进程的PID,这样做的目的是让父进程能够找到子进程,子进程能通过task_struct对象找到父进程。
Q:为什么fork函数会返回两次?
A:因为在fork函数返回之前,子进程就已经创建出来了,在创建出来子进程之后,后面的代码都会被父子进程分别执行。
3)父子进程工作分配
现象
使用if-else进行任务分流
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(){
pid_t return_value = fork();
unsigned int i=0;
if(return_value){
while(1){
printf("Parent process:return value is %d,PID is %d,PPID is %d,cnt is %d\n",return_value,getpid(),getppid(),i++);
sleep(1);
}
}else{
while(1){
printf("Child process:return value is %d,PID is %d,PPID is %d,cnt is %d\n",return_value,getpid(),getppid(),i++);
sleep(1);
}
}
sleep(2);
}
观察结果发现,有两个while循环在同时运行,他们的代码时共享的,但是数据i显然不是共享的
原理
在Linux下面,如果一个进程创建了一个子进程,子进程会以父进程为模板,创建task_struct对象,如果不给该子进程分配代码,则子进程会和父进程共享代码。但是数据在只读的情况下,是共享的,但是如果父子之间有一个进程对数据进行了修改,那么就会在对应的进程中拷贝一份数据。
通过以下代码验证
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(){
int a=1;
int *p = &a;
pid_t id = fork();
if(id){
(*p)++;
printf("Parent:PID is %d,&a = %p,a=%d\n",getpid(),p,*p);
}else{
printf("Child:PID is %d,&a = %p,a=%d\n",getpid(),p,*p);
}
sleep(2);
}
运行结果:
在Linux下面,为了避免用户直接对内存进行访问,存在虚拟地址的概念,即通过地址映射表来对物理地址进行修改,所以在此处,虽然父进程和子进程的虚拟内存相同,但是其对应的其实是不同的物理内存,因为同一块内存不可能会有两个值,说明两个进程的a值一定在不同物理内存里面。
六、僵尸进程和孤儿进程
僵尸进程
处于僵尸状态的进程就是僵尸进程。
僵尸进程的意义
进程是服务于用户,僵尸进程的作用就是给父进程响应,如果父进程不读取僵尸进程的响应,则僵尸进程会一直存在。
观察僵尸进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main(){
pid_t id = fork();
if(id == 0){
int cnt = 5;
while(cnt--){
printf("Child:PID is %d,PPID is %d\n",getpid(),getppid());
sleep(1);
}
exit(1);
}
while(1){
}
}
子进程在进行完5次循环后退出,而父进程会进入到死循环当中,观察子进程的状态,为僵尸状态,此时子进程就为僵尸进程。
孤儿进程
如果父进程比子进程先退出,那么子进程无法在结束时会处于一直保持僵尸状态,无法释放内存,就会导致内存泄漏,所以系统让子进程被1号进程,即系统进程领养。此时,该子进程称作为孤儿进程。
七、Linux对进程的调度与切换
概念
时间片
在现代操作系统中,操作系统都是基于时间片对于进程进行轮流执行的,时间片就是计算机规定一个进程一次最长占用CPU的时间,到了时间计算机的CPU会直接执行下一个进程。
竞争性
因为系统中进程有很多,但是CPU资源只有一个,所以进程之间必须对资源进行竞争,排队和优先级的本质就是在对有限的资源进行竞争。
独立性
各个进程之间独立运行,不会影响其他进程,在运行期间不会产生干扰
并行
多个进程在多个CPU下面分别同时运行
并发
多个进程在有一个CPU下采用高频的进程切换的方式,在一段时间内多个进程都得以推进
进程切换
在某个进程运行的时候,CPU的寄存器为该进程服务,CPU会产生大量临时数据,存放在寄存器中,在时间片结束后,PCB对象中会有一块单独的内存来存放该进程暂存在寄存器的临时数据,以便下次恢复运行,其中,储存在寄存器里面的临时数据,叫做硬件上下文。存放临时数据的过程,称为保护上下文,后面每次再切换到该进程,都会先恢复硬件上下文。
在CPU上,只有一套寄存器,所有进程共享一套寄存器,但是都拥有独立的寄存器里的数据。
进程调度算法
在Linux下面,每一个CPU会有一个运行队列,运行队列的结构如上,在运行的过程中,会按照优先级,下标0-99是按照实时优先级进行执行,不会被占用,而100-139按照顺序执行,40个位置刚好对应40个优先级。
进程增加或减少的解决方案
在操作系统运行过程中,会有进程增加和减少的过程,为了防止进程的增加或减少对进程运行造成影响,运行队列会有两份,一份叫做活跃队列,另外一份叫做过期队列,在运行的过程中,如果一个进程运行完以后,该进程会被存放到过期队列中去,而如果系统产生新的进程,也会将进程链接到过期进程中去,所以活跃队列中的进程会不断减少而过期进程队列中的进程会不断增加。在运行队列中,存在两个指针,active指针(指向活跃队列)和expried指针(指向过期队列)。当活跃队列中的进程全部执行完了之后,两个指针会交换,即活跃队列变为过期队列,过期队列变为活跃队列。
注:在进程的增加减少的过程中,bitmap和pr_active也会随着更新