第四章 进程管理和通信
进程概念
进程定义
- 进程是程序在某个数据集合上的一次运行活动
- 数据集合:软/硬件环境,多个进程共存/共享的环境
进程的特征
- 动态性:进程是程序的一次执行过程,动态产生/消亡
- 并发性:进程可以同其他进程一起向前推进
- 异步性:进程按各自速度向前推进
- 独立性:进程是系统分配资源和调度CPU的单位
进程与程序的区别
-
动态与静态
- 进程是动态的:程序的一次执行过程
- 程序是静态的:一组指令的有序集合
-
暂存与长存
- 进程是暂存的:在内存驻留
- 程序是长存的:在介质上长期保存
-
程序和进程的对应
- 一个程序可能有多个进程
进程的状态
- 运行状态:进程已经占有CPU,在CPU上运行
- 就绪状态:具备运行条件但由于无CPU,暂时不能运行
- 阻塞状态:因为等待某项服务完成或信号来到而不能运行的状态
例如等待:系统调用,I/O操作,合作进程的服务或信号…
进程状态的变迁
- 服务:系统调用、I/O操作
- 信号:事件
- 就绪运行:进程调度
- 运行就绪:时间片到;被抢占
- 运行阻塞:服务请求;等待信号
- 阻塞就绪:服务完成;信号来到
Linux进程的状态
- 可运行态
- 就绪:在就绪队列中等待调度
- 运行:正在运行
- 睡眠态/阻塞态/等待态
- 深度睡眠(不可中断):不能被其他进程通过信号和时钟中断唤醒
- 浅度睡眠(可中断):可被其他进程的信号或时钟中断唤醒
- 僵死态:进程终止执行,释放大部分资源
- 挂起态:进程被挂起
ps aux命令显示进程状态
进程控制块(Process Control Block, PCB)
描述进程的状态、资源和相关进程的关系
- PCB是进程的标志
- 创建进程时创建PCB;进程撤销后PCB同时撤销
- 进程=程序+PCB
Linux的进程控制块:task_struct
-
Linux进程的标识
- PID:进程ID
- PPID:父进程ID
- PGID:进程组ID
-
Linux进程的用户标识
- UID:用户ID
- GID:用户组ID
-
进程的上下文PCB
- Context,进程的运行环境
-
分时系统的进程切换过程
- 进程的上下文在CPU中交换
- 换入进程的上下文进入CPU(从栈+PCB上来)
- 换出进程的上下文离开CPU(到栈+PCB上去)
进程控制
在进程生存全期间,对其全部行为的控制
- 创建进程
- 撤销进程
- 阻塞进程
- 唤醒进程
进程创建
创建一个具有指定标识(ID)的进程
过程:
- 创建一个空白PCB
- 赋予进程标识符ID
- 为进程分配空间
- 初始化PCB
- 默认值(CPU的状态、内存、优先级等)
- 插入相应的进程队列
- 新进程插入就绪队列
进程撤销
-
功能
-
撤销一个指定的进程
-
收回进程所占有的资源,撤消该进程的PCB
-
-
进程撤销的时机/事件
- 正常结束
- 异常结束
- 外界干预
-
参数:被撤销的进程名(ID)
-
进程撤销的实现
- 在PCB队列中检索出该PCB
- 获取该进程的状态
- 若该进程处在运行态,立即终止该进程
- 递归地撤销子进程(是否需要撤销子进程?是)
- 子进程挂接到init进程下(是否需要撤销子进程?否)
- 释放进程占有的资源
- 将进程从PCB队列中移除
进程阻塞
- 功能:停止进程执行,变为阻塞
- 阻塞的时机/事件
- 请求系统服务
- (由于某种原因,OS不能立即满足进程的要求)
- 启动某种操作
- (进程启动某操作,阻塞等待该操作完成)
- 新数据尚未到达
- (A进程要获得B进程的中间结果,A进程等待)
- 无新工作可做/idle进程/pause
- (进程完成任务后,自我阻塞,等待新任务到达)
- 请求系统服务
- 参数
- 阻塞原因
- 不同原因构建有不同的阻塞队列
- 进程阻塞的实现
- 停止运行
- 将PCB从“运行态”改为“阻塞态”
- 插入对应的阻塞队列
- 转调度程序
进程唤醒
- 功能:唤醒处于阻塞队列当中的某个进程
- 引起唤醒的时机/事件
- 系统服务由不满足到满足
- I/O完成
- 新数据到达
- 进程提出新请求(服务)
- 参数:被唤醒进程的标识
进程控制原语
-
原语
- 由若干指令构成的具有特定功能的函数
- 具有原子性,其操作不可分割
-
进程控制原语
- 创建原语
- 撤消原语
- 阻塞原语
- 唤醒原语
Windows进程控制
- CreateProcess创建新进程
- ExitProcess结束自身进程
- TerminateProcess结束目标进程
Linux进程控制
- fork()创建进程
- wait()阻塞进程
- exit()结束进程
- sleep()休眠进程
fork()创建进程
pid_t fork(void);
例:pid_t pid = fork();
-
父进程和子进程
- 子进程:新建的进程
- 父进程:fork()的调用者
- 子进程是父进程的复制
- 父进程和子进程并发运行
-
fork的返回值:pid
- 在子进程中:pid=0
- 在父进程中:pid>0(子进程ID)
- pid=-1(出错)
fork 为什么在子进程中没有再建立新的进程呢?新的进程的CS:IP 指
向在fork 函数后的第一个指令。
-
fork()执行流程
- 分配task_struct结构
- 拷贝父进程
- 复制正文段、用户数据段及系统数据段
- 复制task_struct的大部分内容
- 修改task_struct的小部分内容
- 把新进程的task_struct保存在task队列中
- 新进程置于就绪状态
-
fork()的实际开销
- 复制父进程页表
- 给子进程创建PCB
父进程的资源被设置为只读,当父进程或子进程试图修改某些内容时,内核才在修改前将该部分进行拷贝——写时复制
-
Linux启动后第一个进程是init进程,其他进程都是init子孙进程
-
exec函数族(包含若干函数):在子进程空间运行指定的“可执行程序”
- exec调用成功:进入新进程且不再返回
- exec调用失败:继续从调用点向下执行
wait()阻塞进程
进程调用wait(int &status)阻塞自己
- 检测有无子进程结束?
- 没有:继续阻塞,等待子进程结束
- 有:收集该子进程信息并彻底销毁它,返回
- Status接收子进程退出时的退出代码
- status:按位处理
- 若忽略子进程的退出信息:pid=wait(NULL)
exit()结束进程
调用void exit(int status)终结进程,进程终结时要释放资源并向父进程报告
- 利用status向父进程报告结束时的退出代码
- 变为僵尸状态,保留部分PCB信息供wait收集
- 正常结束还是异常结束
- 占用总系统cpu时间
- 缺页中断次数
- 调用schedule()函数,选择新进程运行
Sleep()休眠进程
Sleep(int nSecond)
- 进程暂停执行nSecond秒
- 系统暂停调度该进程
- 相当于windows挂起操作resume( ),挂起指定秒
线程
- 线程是进程内的一个执行路径
- 一个进程可以创建和包含多个线程
- 线程之间共享CPU可以实现并发运行
- 创建线程比创建进程开销要小
- 线程间通信十分方便
线程的应用:如果把程序中某些函数创建为线程,那么这些函数将可以并发运行!
进程=资源集+线程组
线程的状态变迁
- 单线程程序:整个进程只有一个线程。Windows程序缺省创建一个线程(主线程,main线程)。
- 多线程程序:整个进程至少有2个线程。主线程和至少1个用户线程。
多线程的应用
- 多个功能需要并发的地方
- 需要改善窗口交互性的地方
- 需要改善程序结构的地方
- 多核CPU上的应用,充分发挥多核性能
进程相互制约关系
进程的互斥关系
多个进程共享具有独占性的资源时,必须确保各进程互斥地存取资源,即确保没有任何两个进程同时存取资源。
- 临界资源:一次只允许一个进程独占访问(使用)的资源
- 临界区:进程中访问临界资源的程序段
进程的同步关系
若干合作进程为了共同完成一个任务,需要相互协调运行步伐:一个进程A开始某个操作之前要求另一个进程B必须已经完成另一个操作,否则进程A只能等待。
同步机制的功能
- 当进程不能执行,即将要执行的某个操作的运行条件不满足时能让该进程立即暂停执行该操作
- 当被暂停的操作在运行条件一旦满足时,相应进程能被尽快唤醒以便继续运行
- 同步进制在实现上也需要满足原子性
访问临界区的方法
- 硬件方法
- 中断屏蔽:进入临界区前执行“关中断”指令(IF=0),离开临界区后执行“开中断”指令(IF=1)
- 测试并设置
- 交换指令
- 软件方法
- 锁
- 信号量
锁机制
- 设置一个“标志”S:表明临界资源“可用”还是“不可用”?1:0
- 在进入临界区之前检查标志是否“可用”?
- 若为“不可用”状态:进程在临界区之外等待
- 若为“可用”状态:
- 进入临界区,并将标志修改为“不可用”
- 在临界区内访问临界资源……
- 退出临界区,并将标志修改为“可用”状态
上锁操作
- 第1步:检测锁S的状态(0或1?)
- 第2步:如果S=0,则返回第1步
- 第3步:如果S=1,则置其为0
上锁原语
开锁操作
- 把锁S的状态置1
开锁原语
用锁机制访问临界区
目标:确保临界区中任何时候最多只有1个进程运行于其中
- 初始化锁的状态S = 1 (可用)
- 进入临界区之前执行上锁Lock(s)操作
- 离开临界区之后执行开锁unLock(s)操作
设计临界区访问机制的四个原则
- 忙则等待:当临界区忙时,其他进程必须在临界区外等待
- 空闲让进:当无进程处于临界区时,任何有权进程可进入临界区
- 有限等待:进程进入临界区的请求应在有限时间内得到满足
- 让权等待:等待进程放弃CPU(让其它进程有机会得到CPU)
锁机制满足前三条,不满足让权等待原则
信号灯与P-V操作
基本思想:进程在运行过程受信号灯控制,并能改变信号灯。
- 进程受控制:进程因信号灯的状态被阻塞或被唤醒
- 改变信号灯:信号灯的状态可以被进程改变
信号灯数据结构
信号灯定义为一个二元矢量(S, q)。
- S:整数,初值非负(S又称信号量)
- q:队列(进程PCB集合),初值为空集
P操作的原理(P(S))
- 第1步:S值减1;
- 第2步:判断S<0
- 若S大于或等于零,该进程继续;
- 若S小于零,该进程阻塞并加入到q中,转调度函数。
V操作的原理(V(S))
- 第1步:S值加1;
- 第2步:判断S <= 0
- 若S大于零,该进程继续;
- 若S小于或等于零,该进程继续同时从q中唤醒一个进程。
P-V操作应用过程
实现进程互斥
-
先设定合适的S初值(S初值为临界资源的数量)
s=m:允许最多m个进程同时处于临界区 -
进入临界区之前先执行P操作(可能阻塞当前进程)
-
离开临界区之后再执行V操作(可能唤醒某个进程)
实现进程同步
实质:
- 运行条件不满足时,能让进程暂停
- 运行条件满足时,能让进程立即继续
基本思路:
- 暂停当前进程:在关键操作之前执行P操作
- 继续进程:在关键操作之后执行V操作
- 定义有意义的信号量S,并设置合适的初值(一般设为0或初始状态值)
不合理的初值不仅达不到同步的目的,还会发生死锁
进程通信
Linux信号通信
- 信号是Linux进程间一种重要的通信机制
- 信号是向进程发送的一个通知,通知某个事件已发生
- 收到信号的进程可以立即执行指定的操作
- 信号的发出可以是进程,也可以是系统(含硬件)
信号的产生
- 键盘输入特殊组合键产生信号,例:“Ctrl+C”
- 执行终端命令产生信号,例:kill系列命令
- 程序中调用函数产生信号,例:kill()、abort()
- 硬件异常或内核产生相应信号,例:内存访问错
Linux定义了64种信号,用整数1~64表示
常用函数
- signal():注册信号处理函数的函数
- kill():发送信号的函数
- SIGUSR1:用户自定义信号1
kill(childpid,SIGUSR1):向childpid进程发送信号SIGUSR1
signal(SIGUSR1,handler):当收到SIGUSR1信号时,执行handler函数(自己定义)