操作系统之进程与进程控制
一、进程概念
引子 程序运行在并发环境中的问题
(1)运行过程不确定
(2)结果不可再现
1.进程定义
进程是程序在某个数据集合上的一次运行活动。数据集合是指软硬件环境,多个进程共存或共享的环境。
2.进程的特征
(1)动态性
进程是程序的一次执行过程,动态产生且动态消亡;
(2)并发性
进程同其他进程一起向前推进;
(3)异步性
进程按照各自的速度向前推进(每一个进程按照自定逻辑,不考虑其他进程的运行,各自占用CPU);
(4)独立性
进程是系统分配资源和调度CPU的单位(但是有了线程后,操作系统调度CPU的单位就变成了线程)。
3.进程与程序的区别
(1)进程是动态的:程序的一次执行过程;
(2)程序是静态的:一组指令的有序集合;
(3)进程是暂存的:在内存中短期驻留;
(4)程序是长存的:可以在存储介质上长期保存;
(5)一个程序可能有多个进程。
4.进程的分类
(1)按照使用资源的权限进行分类
①系统进程:系统内核相关的进程;
②用户进程:运行于用户态的进程。
(2)按照对CPU的依赖性进行分类
①CPU型进程:主要用于计算;
②I/O型进程:主要用于I/O操作。
二、进程状态
1.进程的状态
(1)运行状态(Running)
进程已经占用CPU,在CPU上运行。
(2)就绪状态(Ready)
具备运行条件但是由于没有CPU可用,所以暂时不能运行。
(3)阻塞状态(Block)也叫等待状态(Wait)
由于等待某项服务完成或者等待某个信号而不能运行的状态,比如等待系统调用,I/O操作等等。
2.进程的三态模型
(1)就绪->运行:进程调度;
(2)运行->就绪:时间片到或者被强占;
(3)运行->阻塞:请求服务后等待响应,或者等待某个信号的到来;
(4)阻塞->就绪:请求的服务已经完成,或者等待的信号已经到来。
3.进程的五态模型
(1)新建状态
用户向系统提交程序后,在进程建立之前的过程。
(2)终止状态
进程撤出系统的过程。
4.Linux中的状态定义
(1)可运行态
①就绪:在就绪队列中等待调度;
②运行:正在运行。
(2)阻塞(等待)态
①浅度(可中断)阻塞:能被其他进程的信号或者时钟唤醒;
②深度(不可中断)阻塞:不能被其他进程通过信号和时钟唤醒,一般是用来请求文件服务,I/O服务,系统服务。
(3)僵死态
进程终止运行,释放大部分资源。
(4)挂起态
进程被挂起,暂停。可用于调试进程。
三、进程控制块(Process Control Block,PCB)
1.进程控制块的定义
(1)描述进程状态、资源、与相关进程关系的数据结构;
(2)PCB是进程的标志。对于操作系统来说,它通过PCB来感知和管理进程;
(3)进程创建时会建立PCB,进程撤出时会销毁PCB。
2.PCB中的基本成员
(1)name(ID):进程名称(标识符,0~32768的正整数);
(2)status:状态;
(3)next_pcb:指向下一个PCB的指针;
(4)start_addr:程序地址;
(5)p_priority:优先级;
(6)cpu status:现场保留区(堆栈);
(7)comminfo:进程通信;
(8)processfamily:家族;
(9)own resource:资源。
不同的操作系统PCB成员会有所区别,但是一些基本成员都是相同的,可能只是命名上的区别而已。进程控制块应包含以下三类信息:标识信息、现场信息、控制信息。
3.Linux中的进程控制块
Linux的进程控制块一般包含如下信息:
(1)进程状态;
(2)调度信息;
(3)标识符;
(4)内部进程通信信息;
(5)链接信息;
(6)时间和计时器;
(7)文件系统;
(8)虚拟内存信息;
(9)处理器信息。
4.进程的切换
(1)进程的上下文
Context,指进程运行环境,CPU环境(比如各个寄存器的取值)等等。进程的上下文由三部分组成:用户级上下文(程序、数据、共享存储区、用户栈,它们占用进程的虚拟地址空间)、寄存器上下文(由各个寄存器组成)、系统级上下文(PCB、核心栈等)。
(2)进程切换过程
①进程被从堆栈调度到CPU运行;
②进程被从CPU调度到堆栈暂停运行。
四、进程控制的概念
1.进程控制的概念
在进程生存期间,对其全部行为的控制。典型的控制行为有创建进程、阻塞进程、唤醒进程、撤销进程。
2.进程控制行为之一——进程创建
(1)功能
创建一个具有指定标识的进程。
(2)参数
进程标识、优先级、进程起始地址、CPU初始状态、资源需求等等。
(3)创建进程的过程
①创建一个空白PCB;
②获得并赋予进程标识符ID;
③为进程分配空间;
④初始化PCB;
⑤把进程插入到相应的进程队列,一般放到就绪队列中。
3.进程控制行为之二——进程撤销
(1)功能
撤销一个指定的进程,收回进程所占用的资源,撤销该进程的PCB。
(2)进程撤销的时机/时间
①正常结束;
②异常结束;
③外界因素。
(3)参数
被撤销的进程名(ID)。
(4)进程撤销的过程
①在PCB队列中找到要撤销进程的PCB;
②获取该进程的状态;
③若该进程处于运行态,则立即终止该进程(先检查该进程是否有子进程,若有子进程,则先撤销子进程,递归执行此操作);
④释放进程占有的资源;
⑤将进程的PCB从PCB队列中移除。
4.进程控制行为之三——进程阻塞
(1)功能
阻塞进程的执行。
(2)阻塞的时机/事件
①请求系统服务:操作系统不能立即满足进程的请求,导致进程不能满足运行条件;
②启动某种操作:进程启动某操作,阻塞等待该操作完成(比如进行I/O操作);
③新数据尚未到达:该进程要获得另外一个进程的中间结果,该进程需要等待另一个进程运算结束;
④进程正常结束:进程完成任务后,自我阻塞,等待新任务到达。
(3)参数
阻塞原因。
(4)进程阻塞的实现
①停止运行;
②将PCB“运行态”改为“阻塞态”;
③出入相应原因的阻塞队列;
④转到调度程序(把系统控制权交给调度模块)。
5.进程控制行为之四——进程唤醒
(1)功能
唤醒处于阻塞队列中的某个进程。
(2)引起唤醒的时机/事件
①系统服务满足进程要求;
②I/O事件完成;
③新数据到达;
④进程向系统或I/O提出新请求(服务)。
(3)参数
进程ID。
6.进程控制原语
由若干指令构成的具有特定功能的函数,具有原子性,其操作不可分割。以上四种进程控制行为(创建、撤销、阻塞、唤醒)都被封装为原语,以保证其运行的完整性。
五、Linux下的进程控制
1.创建进程
创建进程的函数原型是pid_t fork(void);
pid_t是一个整数类型,即fork()函数会返回新进程的ID号(0~32768的整数)。
例如:pid_t pid = fork();
注意:
(1)新进程是当前进程的子进程。
(2)父进程和子进程
①父进程:fork()的调用者;
②子进程:新建的进程。
(3)子进程是父进程的复制(相同的代码,相同的数据,相同的堆栈),除了ID号和时间信息外,两者完全相同。
(4)子进程和父进程可以并发运行。
请问下面的程序会输出什么结果?
//文件名为test.c int main(void) { fork(); printf("Hello World!\n"); return 0; }
答案:
一个是子进程输出的,一个是父进程输出的。
请问下面的程序会输出什么结果?
//文件名为fork_2.c int main(void) { pid_t pid; pid = fork(); if(pid == 0) { printf("pid == 0\n"); } else { printf("pid != 0\n"); } return 0; }
答案:
为什么if和else两个分支都被执行了呢?
因为fork()函数执行后,创建了一个新的进程,子进程是父进程的复制,所以相同的代码会执行两次。在子进程中fork()函数会返回0,在父进程中,fork()函数会返回子进程的id号。所以在子进程中,会执行if分支,在父进程中,会执行else分支。但谁先谁后不确定。
2.fork()函数的执行步骤
由于子进程是父进程的复制,所以子进程中也会有创建子进程的语句,如果不加以限制,就会形成递归创建,但实际上并不是这样的。
实际流程是:父进程创建了子进程后,子进程中“创建进程”语句及其前面的语句都不再执行。并发运行的是fork()语句后的语句。
在linux的源码中我们可以找到fork函数:
//linux内核中的fork函数 ... copy_files(clone_flags,p); //克隆文件 copy_fs(clone_flags,p); //克隆文件系统 copy_mm(clone_flags,p); //克隆内存信息 ...
我们可以看到有三条语句,用于拷贝进程的所有信息,这也解释了为什么说子进程是父进程的复制。
3.子进程如何执行与父进程不同的功能。
Linux中 ,init进程(初始化进程)是所有其他进程的父进程,那么是不是就说所有的进程都执行与init进程相同的功能呢?并不是。Linux中某些子进程和父进程的执行并不是完全相同的。它是如何做到的呢?这是因为exec函数簇的存在,它是若干函数的集合。其功能是让子进程具有和父进程完全不同的新功能。
4.一个进程控制的实例
编写一个代码,创建两个子进程,第一个子进程打印“I am the 1st subprocess!”,第二个子进程打印“I am the 2rd subprocess”,父进程打印“I am the parent process”。要求实现先打印第一个子进程的内容,再打印第二个子进程的内容,最后打印父进程的内容。
1 int main() 2 { 3 int p1, p2; //进程ID 4 5 p1 = fork(); 6 switch(p1) 7 { 8 case -1: //进程创建失败时执行下列语句,因为进程创建失败fork函数会返回-1 9 printf("Error\n"); 10 exit(1); 11 case 0: //子进程中执行下列语句,因为在子进程中fork函数会返回0 12 execl("/bin/echo","echo", "I am the 1st subprocess!", NULL); //使用exec函数簇中的execl函数将子进程一的功能进行改造 13 exit(1); 14 default: //父进程中执行下列语句,因为在父进程中fork函数会返回子进程的ID号(大于0,小于32768) 15 wait(NULL); //用于进程同步,wait函数的作用是:父进程在此处暂停运行,等待一个子进程结束后,再从此处继续向下运行 16 break; 17 } 18 19 p2 = fork(); 20 switch(p2) 21 { 22 case -1: //进程创建失败时执行下列语句,因为进程创建失败fork函数会返回-1 23 printf("Error\n"); 24 exit(1); 25 case 0: //子进程中执行下列语句,因为在子进程中fork函数会返回0 26 execl("/bin/echo", "echo", "I am the 2rd subprocess", NULL); //使用exec函数簇中的execl函数将子进程二的功能进行改造 27 exit(1); 28 default: //父进程中执行下列语句,因为在父进程中fork函数会返回子进程的ID号(大于0,小于32768) 29 wait(NULL); //用于进程同步,wait函数的作用是:父进程在此处暂停运行,等待一个子进程结束后,再从此处继续向下运行 30 break; 31 } 32 33 printf("I am the parent process\n"); 34 35 return 0; 36 }
运行结果: