进程切换
进程切换也叫上下文切换
所谓的其他状态,可能是由于IO操作或等待事件而进入等待状态,或者由于被抢先、时间片运行完转回到就绪状态
现在的计算机,进程切换是很频繁的,通常10ms左右就有一次进程切换,所以要求快速切换,这通常由汇编来实现。
进程切换时需要保存的上下文
上下文切换图示如下,假定有P0和P1两个进程。开始P0在用户态执行,P1处于空闲状态(就绪或等待)
在P0运行过程中,出现中断或系统调用,保存P0的进程状态,恢复P1的进程状态,P1开始执行
P1运行一段时间,又出现中断或系统调用,保存P1,由处理机选择进程。
至此一个完成的进程切换过程就展现出来了。
PCB:内核的进程状态记录
内核为每个进程维护了对应的进程控制块。
下面这张图回答了上一讲的一个选择题,内核把相同状态的PCB放到同一个队列中去,有就绪队列、多种等待序列、僵尸队列
用ucore举例说明,ucore的进程控制块结构proc_struct
中包含
一类是进程的标识信息:
char name[PROC_NAME_LEN+1]
进程的标识信息,
int runs
执行哪个可执行文件,
int pid ID
是多少,
struct proc_struct *parent
父进程是谁
另一类是进程的状态信息:
CPU状态寄存器的相关信息,地址空间起始地址,第一级页表的起始地址,
进程的状态和是否允许调度
还有一类是
进程所占用的资源(如存储资源),占用的内核堆栈
还有一类保护现场用的,两个进程需要做复用的部分在这里保存
还有一类用于描述当前进程在哪个队列当中
实际Windows和Linux的数据结构要比这多得多
来看一下ucore中proc_struct的定义
首先是进程状态,进程ID(pid),线程ID(tid),组ID(gid),进程运行次数
然后是进程执行的相关信息
进程核心堆栈,是否需要调度,父进程是谁,进程的内存管理数据结构,上下文现场和中断保护现场,CR3(页表的起始地址),进程标志位
可执行文件进程的名字,进程的哈希表、链表
重点说明ucore的内存地址空间结构mm_struct。首先,它到底有哪些内存块,内存逻辑地址空间中映射是在哪个地方,对应的地址空间是什么样的。
如下代码所示,pgdir
是第一级页表的起始地址,map_count
是指如果有共享共享了几次,sm_priv
表示跟外存之间置换相关的数据结构。
12.2 进程创建
不同的系统,创建新进程的接口不同
使用fork创建子进程
为了明白fork()和exec()的执行过程,下面来详细说说。fork()创建一个继承的子进程,这个子进程和原来的进程基本上是一模一样,不一样的是其中一个寄存器保留着可以区分子进程和父进程的ID
子进程和父进程额fork后的返回值不同
则创建进程的示例如下
fork()的地址空间复制
如下面的代码所示,对于父进程fork()返回childID,对于子进程返回0;根据不同的返回值,执行不同的代码。
在执行fork()时,会创建一个子进程,它的childPID(即fork返回值)为0
下面来看在操作系统中的变化,fork()之前只有一个进程,并且有一个进程控制块来维护相关信息
fork()时,子进程为右边的程序,内核态创建一个新的进程,对应的pid与原来的程序不同,其他都相同。
在执行exec()后,就会导致子进程的地址空间内的代码都被换掉,加载的文件页发生变化。
上面是从代码的角度来看,下面从地址空间来说,执行fork和exec后,子进程地址空间的内容都被换掉
稍微复杂一点的例子,下面这个例子在for循环中使用了fork,这样就不仅仅是一个进程被复制三次了。
最初始时,假定最开始是1166,
fork之后,产生id为1167的子进程,并打印循环次数,子进程id,父进程id
父进程和子进程都要在原来的位置运行程序,即第二次循环,这时两个进程都进行一次复制,此时共有四个进程
再循环一次,四个进程都fork一次,此时共8个进程
fork()的开销很大
空闲进程的创建
系统没有新的任务执行了,这时CPU并没有停下来,而是去执行空闲进程了
创建第一个内核线程
进程加载
使用exec()来完成一个新的可执行文件的加载
允许进程“加载”一个完全不同的程序,并从main开始执行(即_start)
允许进程加载时指定启动参数(argc,argv)
exec调用成功时,与之前ID相同的进程,但是运行了不同的程序
代码段、堆栈、堆被完全重写
ucore中的exec()实现
从外存上把可执行文件加载进来,并且跳转到上面去执行就可以了。这和引导扇区不同,引导扇区是读一块内容,对格式是没有任何理解的,但是执行一个应用程序时,里面的应用程序时非常复杂的。
因此主要的识别工作就是可执行文件的识别。具体说来,主要用到下面这三个函数。核心功能是do_execve,最核心的是load_icode,用于识别可执行文件的格式。
sys_exec()就是为了获取相关的参数,do_execve主要的工作是创建新进程所需要的内存相关的段结构
进程等待与退出
完成父子进程之间的交互,完成子进程的回收。
wait()系统调用可以用于父进程等待子进程的结束:子进程结束时通过exit()向父进程返回一个值,父进程通过wait()接受并处理这个返回值,
如果一个子进程还有它的子进程,那么父进程进入等待状态,等待子进程访问结果。所以此时exit()是在wait()之后执行的,
如果有僵尸子进程,子进程先执行exit(),这时子进程还处于等待状态,wait()立即返回其中一个值
exit()
其他进程控制系统调用:
优先级控制
进程调试支持,这是操作系统必须提供的一项功能
定时
进程控制vs进程状态
fork()会导致一个新的系统进程的创建,父进程exit()则退出,子进程exit()则父进程就绪,