[操作系统] - 进程切换&进程控制
2.1.6 进程切换
-
名称解析
- 进程的上下文(Context)
当一个进程在执行时,CPU的所有寄存器的值、进程的状态以及堆栈中的内容被称为进程的上下文Context
- 进程的切换(switch)
当内核需要切换(switch)至另一个进程时,它就需要保存当前进程的上下文,以便在再次执行该进程时,能够恢复到切换时的状态执行下去。
-
进程队列
系统中有许多进程。处于就绪状态和处于阻塞状态的进程可以分别有多个,而阻塞的原因又各不相同。为了对所有进程进行有效的管理,常将各个进程的PCB 用适当的方式组织起来,称为进程队列
- 线性方式
采用顺序存储结构,操作系统预先确定整个系统中同时存在的进程的最大数目如n,静态分配空间时,把所有进程的PCB 都放在这个队列表中。
缺点:限定系统中最大进程数目;CPU调度时需要对整个表进行遍历。
- 链接方式
采用链表结构,按照各进程的不同状态分别将它们的PCB 放在不同的链表队列中。
在单CPU 情况下,处于运行状态的进程只有一个,可用一个指针指向它的PCB。
处于就绪状态的进程队列通常是一个(也可能多个)。
阻塞队列对应不同的阻塞原因通常有多个。
- 索引方式
索引方式利用索引表记载不同状态进程的PCB 地址。即系统建立几张索引表,如就绪索引表、阻塞索引表等。状态相同的进程的PCB 组织在同一索引表中,每个索引表的表目中存放一个PCB 地址。各索引表在内存的起始地址放在专用的指针单元中。
-
举例:Linux进程队列设计
以Linux0.11为例:计算机对进程的感知是依靠PCB结构(即task_struct),Linux使用了一个线性表task来存放多个进程的PCB。
在<kernel/sched.c>
中定义:
struct task_struct* task[NR_TASKS] = {&(init_task.task), };
这样操作系统就可以通过task来找到任何一个进程了。表中设置了一个&(init_task.task)
指向初始进程的指针,表示初始进程存放在task[0]
中。
为了清晰的描述这个线性表的头和尾,Linux0.11又定义了两个宏。
<include/kernel/sched.h>
#define FIRST_TASK task[0]
#define LAST_TASK task[NR_TASKS-1]
2.1.7 进程控制
进程控制是进程管理中最基本的功能,主要包括创建新进程、终止已完成的进程、将因发生异常情况而无法继续运行的进程置于阻塞状态、负责进程运行中的状态转换等功能。
如当一个正在执行的进程因等待某事件而暂时不能继续执行时,将其转变为阻塞状态,而在该进程所期待的事件出现后,又将该进程转换为就绪状态等。进程控制一般是由OS的内核中的原语来实现的。
- 内核定义
通常将一些与硬件紧密相关的模块(如中断处理程序等)、各种常用设备的驱动程序以及运行频率较高的模块(如时钟管理、进程调度和许多模块所公用的一些基本操作),都安排在紧靠硬件的软件层次中,将它们常驻内存,即通常被称为的OS内核。
- 执行状态分类
为了防止OS本身及关键数据(如PCB等)遭受到应用程序有意或无意的破坏,通常也将处理机的执行状态分成系统态和用户态两种
-
系统态:又称为管态,也称为内核态。它具有较高的特权,能执行一切指令,访问所有寄存器和存储区,传统的OS都在系统态运行
-
用户态:又称为目态。它是具有较低特权的执行状态,仅能执行规定的指令,访问指定的寄存器和存储区。一般情况下,应用程序只能在用户态运行,不能去执行OS指令及访问OS区域,这样可以防止应用程序对OS的破坏
-
内核功能
-
支撑功能
- 中断处理:内核最基本的功能,是整个操作系统赖以活动的基础,OS中许多重要的活动,如各种类型的系统调用、键盘命令的输入、进程调度、设备驱动等,无不依赖于中断。
- 时钟管理:内核的一项基本功能,在OS中的许多活动都需要得到它的支撑,如在时间片轮转调度中,每当时间片用完时,便由时钟管理产生一个中断信号,促使调度程序重新进行调度
- 原语操作:原语在执行过程中不允许被中断,原语不可分割。原子操作在系统态下执行,常驻内存。在内核中可能有许多原语,如用于对链表进行操作的原语、用于实现进程同步的原语等。
-
资源管理功能
- 进程管理:在进程管理中,或者由于各个功能模块的运行频率较高,如进程的调度与分派、进程的创建与撤消等;或者由于它们为多种功能模块所需要,如用于实现进程同步的原语、常用的进程通信原语等。通常都将它们放在内核中,以提高OS的性能。
- 存储器管理:存储器管理软件的运行频率也比较高,如用于实现将用户空间的逻辑地址变换为内存空间的物理地址的地址转换机构、内存分配与回收的功能模块以及实现内存保护和对换功能的模块等。通常也将它们放在内核中,以保证存储器管理具有较高的运行速度。
- 设备管理:由于设备管理与硬件(设备)紧密相关,因此其中很大部分也都设置在内核中。如各类设备的驱动程序、用于缓和CPU与I/O速度不匹配矛盾的缓冲管理、用于实现设备分配和设备独立性功能的模块等
-
-
进程控制-创建
- 先导知识1:进程的层次结构
在OS中,允许一个进程创建另一个进程,通常把创建进程的进程称为父进程,而把被创建的进程称为子进程。子进程可继续创建更多的孙进程,由此便形成了一个进程的层次结构。子进程可以继承父进程所拥有的资源,例如,继承父进程打开的文件,继承父进程所分配到的缓冲区等。当子进程被撤消时,应将其从父进程那里获得的资源归还给父进程。此外,在撤消父进程时,也必须同时撤消其所有的子进程。
注:在Windows中不存在进程层次结构的概念,所有进程都具有相同地位。Windows通过句柄来描述进程之间的关系。如果一个进程创建另外的进程时创建进程获得了一个句柄,其作用相当于一个令牌,可以用来控制被创建的进程。但是,这个句柄是可以进行传递的,也就是说,获得了句柄的进程就拥有控制其它进程的权力,因此,进程之间的关系不再是层次关系了,而是获得句柄与否、控制与被控制的简单关系。
- 先导知识2:进程图
进程图是用于描述进程间关系的一棵有向树
-
引起进程创建事件
- 系统内核创建
- 用户登录
- 作业调度
- 提供服务
- 用户创建
- 应用请求
- 系统内核创建
-
进程创建过程(Creation of Process)
- 申请空白PCB:为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白PCB
- 分配资源:各种物理和逻辑资源,如内存、文件、I/O设备和CPU时间等
- 初始化PCB:
- 初始化标识信息,将系统分配的标识符和父进程标识符填入新PCB中
- 初始化处理机信息,使程序计数器指向程序入口地址,使栈指针指向栈顶
- 初始化处理机控制信息,将进程的状态设置为就绪态或静止就绪态
- 若就绪队列未满,加入就绪队列
-
进程控制-终止
-
引起进程终止事件
- 正常结束
在任何计算机系统中,都应有一个用于表示进程已经运行完成的指示。例如,在批处理系统中,通常在程序的最后安排一条Halt指令或终止的系统调用。当程序运行到Halt指令时,将产生一个中断,去通知OS本进程已经完成。
-
异常结束
在进程运行期间,由于出现某些错误和故障而迫使进程终止。这类异常事件很多,常见的有:
① 越界错误;
② 保护错;
③ 非法指令;
④ 特权指令错;
⑤ 运行超时;
⑥ 等待超时;
⑦ 算术运算错;
⑧ I/O故障。 -
外界干预
外界干预并非指在本进程运行中出现了异常事件,而是指进程应外界的请求而终止运行。这些干预有:
① 操作员或操作系统干预;
② 父进程请求;
③ 父进程终止。
-
进程终止过程
- 读出进程此刻的状态:根据被终止进程的标识符,从PCB集合中检索出该进程的PCB,读出状态
- 终止执行:若进程处于执行状态,终止执行,置调度标志为真(指示该进程寄了后还应该被重新调度)
- 终止孙进程/分配养父:若进程存在孙进程,将孙进程也给予终止 或 安排init进程成为孤儿进程的养父(Linux)
- 归还资源:将被终止进程所拥有的全部资源或者归还给其父进程,或者归还给系统
- 移除进程所在队列:将被终止进程(PCB)从所在队列(或链表)中移出,等待其它程序来搜集信息
-
-
进程控制-阻塞
-
引起进程阻塞和唤醒事件
- 向系统请求共享资源失败
- 等待某种操作完成
- 新数据未到达
- 等待新任务到达
-
进程阻塞过程[1]
- 立刻停止执行该进程,更改PCB现行状态为“阻塞”
- 将PCB插入到具有相同事件的阻塞队列
- 调度程序将处理机状态分配给下一就绪程序,切换进程,按新进程的PCB处理机状态设置CPU状态
-
-
进程控制-唤醒
- 进程唤醒过程
- 移出等待该事件的阻塞队列
- PCB中的现行状态由阻塞改为就绪
- PCB插入到就绪队列中
- 进程唤醒过程
-
举例:Linux进程控制函数
fork()
[2]
作用:创建(克隆)一个新进程 系统调用格式:pid=fork( ) 参数定义:int fork( ) 返回值意义如下: 0:在子进程中,pid变量保存的fork( )返回值为0,表示当前进程是子进程。 >0:在父进程中,pid变量保存的fork( )返回值为子进程的id值(进程唯一标识符)。 -1:创建失败。 如果fork( )调用成功,它向父进程返回子进程的PID,并向子进程返回0,即fork( )被调用了一次,但返回了两次。此时OS在内存中建立一个新进程,所建的新进程是调用fork( )父进程的副本,称为子进程。子进程继承了父进程的许多特性,并具有与父进程完全相同的用户级上下文(即程序)。父进程与子进程并发执行。
exec()系列
[3]
exec( )系列函数,没有建立一个与调用进程并发的子进程,而是用新进程取代了原来进程,并将一个可执行的二进制文件覆盖在新进程的用户级上下文的存储空间上。因此,如果exec( )调用成功,调用进程将被覆盖,然后从新程序的入口开始执行。新进程的进程标识符id与调用进程相同。 exec( )系列有6个函数: execl、execlp、execle、execv、execvp、 execve,真正的系统调用只有execve,其他5个都是库函数,它们最终都会调用execve这个系统调用。 exec( )系列在系统库unistd.h中,其基本功能相同,只是以不同的方式来给出参数。主要参数包括路径、程序名、参数等。 一种是直接给出参数的指针,如: int execl(path,arg0[,arg1,...argn],0); char *path,*arg0,*arg1,...,*argn; 另一种是给出指向参数表的指针,如: int execv(path,argv); char *path,*argv[ ];
系统调用exec和fork( )联合使用能为程序开发提供有力支持。用fork( )建立子进程,然后在子进程中使用exec( ),这样就实现了父进程与一个与它完全不同子进程的并发执行。
-
wait()
-
exit()