进程管理
文章目录
1. 进程描述
1.1 进程定义
一个程序在一个数据集合上的一次动态执行过程。
程序=算法+数据结构
与程序的区别:
- 程序是静态的,进程是程序的动态执行,有核心态和用户态。
- 进程是暂时的,程序可长久保存。
- 进程包括数据,进程控制块和(多个)程序
1.2 进程组成
- 代码;
- 处理的数据;
- 计数器的值,指示下一条指令;
- 寄存器的值,堆,栈;
- 一组系统资源,如文件。
1.3 进程特点
- 动态
- 并发性:可以被独立调度并占用cpu运行
- 独立性:以页表为基础,在独立空间运行,不同进程互不影响。
- 制约性:因访问共享数据及资源,或者进程间同步而产生制约
并发Concurrency:一段时间内有多个进程执行;
并行Parallelism:一个时刻有多个进程执行。
1.4 进程控制结构
刚刚说过,程序=算法+数据结构,描述进程的数据结构用到了进程数据块PCB
,os为每个进程维护了一个PCB(os concept 9th P170)。
PCB
:os管理控制进程所用的信息集合,它是进程存在的唯一标识。
- 进程创建:生成pcb
- 进程终止L:回收pcb
- 进程组织管理:管理pcb
pcb包含3类信息:
- pid,ppid,用户标识
- cpu状态信息,主要是寄存器:用户可见寄存器、程序计数器pc、程序状态字psw,栈指针
- 进程控制信息,包括调度和状态信息、ipc信息、存储管理信息、所用资源、与其它pcb的连接信息。
pcb的组织形式:
- 链表:同一状态(就绪、阻塞)进程的pcb组成链表,
- 索引表:同一状态(就绪、阻塞)进程的pcb组成index表,
以下为进程切换(或者说上下文切换)图示。
1.6 调度
上下文切换和cpu调度不同,后者是从就绪队列中挑选cpu下一个要运行的进程或线程,调度程序负责挑选进程或线程的内核函数。
内核满足调度程序条件之一即可:
- 进程从运行状态切换到等待状态
- 进程终结
调度策略有抢占式Preemptive和非抢占式non-preemptive。抢占式更灵活高效。
调度算法:
- FCFS,先来先服务
- SPN(SJF),SRT,短进程(作业)优先,短剩余时间优先
- HRRN,最高响应比优先
- Round Robin,轮询
- Multilevel Feedback Queues,多级反馈队列
- Fair Share Scheduling,公平共享调度
实时调度,多处理器调度,优先级反转暂不整理。
2. 进程状态
2.1 进程的生命周期
- 进程创建
- 进程运行
- 进程等待
- 进程唤醒
- 进程结束
进程创建
引起进程创建的3个事件:
- os初始化
- 用户请求
- 正在运行的进程执行了创建进程的系统调用
进程运行
选择一个就绪进程占用cpu。如何选择要用到调度算法.
进程等待
也叫阻塞,有以下触发条件:
- 请求系统服务,无法马上完成
- 启动某种操作,无法马上完成
- 数据未到达
进程只能自己阻塞自己。
进程唤醒
唤醒原因:
- 需要的资源被满足
- 等待的事件到达
- 该进程pcb插入就绪队列
进程只能被别的进程或os唤醒。
进程结束
有4种情况:
- 正常退出,自愿
- 错误退出,自愿
- 致命退出,os强制
- 被其它进程强制杀死
2.2 进程状态变化模型
ready,running,waiting或blocked
是3种基本状态。
2.3 进程挂起模型
挂起(suspend)的进程没有占用内存,进程映像在磁盘中。
- 阻塞挂起blocked-suspend:进程在外存并等待事件出现。
- 就绪挂起ready-suspend:进程在外存,只要进入内存即可运行。
进程挂到外存分为这3类:
- 阻塞->阻塞挂起:就绪进程需要更多内存资源
- 就绪->就绪挂起:有高优先级进程存在
- 运行->就绪挂起:抢先式分时系统中,有高优先级阻塞挂起进程因事件出现而就绪挂起(下面要说的这种情况)
外存中的状态转换:
- 阻塞挂起->就绪挂起
激活activate:把进程从外存转到内存。有以下2种情况:
- 就绪挂起->就绪
- 阻塞挂起->阻塞
状态队列:表示当前os所有进程的状态。不同状态用不同队列表示。
每个进程的pcb根据当前状态加入相应队列。
- 就绪队列
- I/O队列
- 僵尸队列
2.4 进程控制
加载和执行进程
exec()
调用允许进程加载一个不同的程序,可指定argc
和argv
。调用成功后,仍是相同进程,但运行了不同的程序,pcb会变化,堆栈会重写。
fork()
对子进程分配内存,子进程复制父进程的内存和cpuj寄存器,开销大。多数情况下,使用fork()
后调用exec()
再次覆盖内存,所以前面的复制工作是多余的。优化后的函数有vfork()
。
vfork()
又称为虚拟fork()
、轻量级fork()
,它创建一个子进程,并共享父进程的内存数据,而不是复制,之后立即d调用exec()
。
结束子进程要用
exit()
而不是return
,如果你在vfork()
中return
了,那么,这就意味main()
函数return
了,注意因为函数栈父子进程共享,所以整个程序的栈就跪了。
等待和终止进程
wait()
被父进程用来接收子进程返回的值,并进行处理。它会使父进程睡眠并等待返回值。子进程调用exit()
后,os解锁父进程,并通过exit()
的返回值作为wait()
的结果。
exit()
会以返回值为参数,释放资源,并检查父进程是否活着,如果活着,则保留结果知道父进程使用,进入僵尸zombie/defunct
状态;如果父进程没有或者,则该进程死亡。
如果父进程先退出 ,子进程被
init
接管,子进程退出后init
会回收其占用的相关资源,清理僵尸进程。
3. 线程
3.1 为什么使用线程
并发时,进程之间要通信,还要共享数据。但维护进程的开销很大:
- 创建进程时,分配资源,建立pcb
- 撤销进程时,回收资源,撤销pcb
- 进程切换时,保存当前进程状态信息。
线程thread这种新的实体,满足以下特性:
- 并发
- 共享资源,如地址空间、代码、内存和文件等
3.2 线程定义
thread:进程当中的一条执行流程。
对比进程:
- 资源组合方面:进程就是用来管理资源,构成一个资源平台,是资源分配单位。
- 进程的执行功能交给线程,线程是cpu调度单位。
- 线程的创建、撤销、切换时间短
- 线程间共享资源,可进行不通过内核的通信
相同点:都有[5种状态](#2.2 进程状态变化模型)。
线程有自己的组织结构TCB
.
线程优点:
- 一个进程可存在多个线程
- 并发
- 共享资源
缺点:
- 一个线程崩溃,进程所有线程崩溃。
现代浏览器打开网页是用进程,崩溃后不影响其它进程。
早期ms-dos是单进程、单线程的;unix是多进程、单线程的。
3.3 线程实现
3种实现:
- 用户线程,在用户空间实现。posix,mach,solaris
- 内核线程,在内核中实现。windows,linux,solaris
- 轻量级进程,在内核中。solaris
用户线程与内核线程的对应关系(os concept 9th P170):
- 多对一
- 一对一
- 多对多
用户线程
不依赖内核,由用户级线程库管理,包括创建、终止、同步和调度,可用于不支持线程技术的多进程os。
每个进程都有TCB列表,由线程库函数维护。
用户线程切换也由线程库函数完成,无需用户态和核心态切换,所以快。
允许每个进程有自定义的线程调度算法。
缺点:
- 如果一个线程发起系统调用而阻塞,则整个进程等待。
- 线程间是并发的。
- 若每个线程得到的时间片较少,则慢。
内核线程
pcb和tcb都由内核维护。创建、终止和切换都通过系统调用或内核函数进行,所以开销大。
一个进程中,若某线程发起系统调用而阻塞,不会影响其它线程。
获得更多cpu时间。
轻量级进程
一个进程可有多个轻量级进程,每个轻量级进程由一个单独的内核线程支持。
4.IPC
IPC两个原语:
send(msg)
,消息长度固定或可变recv(msg)
两种方式:
- 建立通信链路
send(),recv()
间接通信:通过内核
直接通信:共享内存
消息传递可以阻塞或非阻塞。阻塞被认为是同步的,非阻塞被认为是异步的,
-
信号:复习
kill
命令。 -
管道:其实就是父进程建立好通道,设置stdout为管道写端,stdin为管道读端。
-
消息队列:fifo
-
共享内存:在每个进程的私有地址k空间内,设置共享内存段。
5. 进程互斥与同步
5.1 竞争条件
race condition,具有不确定性,难以重现。
避免方法:原子操作atomic operation,使指令不被打断。
5.2 饥饿
starvation,一个可执行的进程,被调度器持续忽略,导致虽处于可执行状态却不被执行。
5.3 临界区
critical section,指进程中一段需要访问共享资源并且当另一个进程处于相应代码区域时便不会被执行的的代码区域。
至此,我们引入锁的概念。
lock.acquire() //在锁被释放前一直等待,然后获得锁。
//critical section
lock.release() //释放锁并唤醒等待中的进程
临界区的一些属性:
- 互斥,同一时间临界区中最多存在一个线程。
- progress,可前进
- 有限等待
- 无忙等待,可选属性,即进入临界区前,可以挂起,避免消耗cpu时间。
互斥,mutual exclusion,当一个进程处于临界区并访问共享资源时,没有其它进程会处于临界区并且访问任何相同的共享资源。
临界区只能用于对象在同一进程里线程间的互斥访问;互斥量是可以命名的,可以跨越进程使用
mutex是内核对象,慢;critical section非内核对象,快。
临界区代码保护的方法:
- 禁用硬件中断,但可能导致其它线程饥饿,应该慎用。
- 软件方法,Peterson算法等
- 更高级的抽象,锁,信号量。
5.5 死锁
两个或以上的进程,互相等待完成特定任务,没法将自身任务进行下去。
一组阻塞的进程各持有一种资源,并彼此等待获取另一个进程所占有的一个资源。本质是并发共享问题。
死锁4个必要条件:
- 互斥,Mutual exclusion
- 持有并等待,Hold and wait
- 非抢占,No preemption
- 循环等待,Circular wait
处理方法:
- Deadlock prevention,预防,打破4个条件之一;
- Deadlock avoidance,避免,动态检查资源分配情况;
- Deadlock detection,基于死锁检测算法,很难应用;
- Recovery from Deadlock,恢复,可以按特定顺序终止死锁进程
资源分配图、银行家算法等内容查阅《os concept》死锁一章。
5.6 信号量
semaphare,对于一个整型数据sem,有两个原子操作:
P()
:prolaag,尝试减少(荷兰语),sem–,如果sem<0则等待,否则继续V()
:verhoog,增加(荷兰语),sem++,如果sem<=0,唤醒一个等待的P()
.
这两个操作是早期unix主要的同步原语,现在很少用。
两种信号量:
- 二进制信号量
- 计数信号量,任何非负值
信号量可用在两个方面:
- 互斥
- 条件同步
5.7 管程
monitor
,也称监视器
管程的目的:分离互斥和条件同步的关注。
管程包含:
- 一个锁,用于指定临界区
- 0或多个条件变量,等待或通知信号量用于管理并发访问共享数据
管程把共享变量和对它进行操作的若干个操作封装起来。在管程入口处可能有多个等待队列,而进程只能互斥使用管程。
管程使用了条件变量这种同步机制。条件变量是在管程内部的数据结构,且只有在管程内才能被访问,它对管程内所有过程是全局的,只能通过wait(),signal()
两个操作访问。
wait()
:挂起调用进程并释放管程,直至另一进程在条件变量上执行signal()
。signal()
:如果有其他进程因对条件变量执行wait()
而被挂起,便释放之,如果没有进程等待,则信号被忽略,不保存。
几个经典同步问题:
- 读者写者问题
- 哲学家就餐问题
- 生产者-消费者问题
- 理发师问题
- 三个烟鬼问题