12.1进程切换
实际上为用户提供的系统调用服务,用户在执行它的应用的过程当中,有需求要创建一个新的进程,如何来创建一个新的进程,在这里头运行一个新的程序,那这个是进程加载,父进程创建子进程之后,它们俩之间需要有一些协调的关系,比如说子进程结束之后,父进程负责回收它所占用的资源,那这个时候它们之间有一个通讯关系这就是进程的等待和退出
进程切换
■进程切换(上下文切换)
暂停当前运行进程,从运行状态变成其他状态
调度另一个进程从就绪状态变成运行状态
■进程切换的要求
切换前,保存进程上下文
切换后,恢复进程上下文
快速切换
注:为了保证系统运行的效率,这个切换的速度必须非常快所以通常情况下它都是由汇编来实现的
■进程生命周期的信息
寄存器(PC, SP,…)
CPU状态
内存地址空间
精髓:
■进程切换的原因:中断,陷阱(异常)和系统调用
中断:
时钟中断:当前进程时间片用完了
I/O中断:操作系统确定是否发生了I/O活动。
内存失效:要访问的内存不在主存中,要调入内存。
进程控制块PCB:内核的进程状态记录
内核为每个进程维护了对应的进程控制块(PCB)
内核将相同状态的进程的PCB放置在同一队列
精髓:
进程状态的变化
进程切换步骤:
1)保存处理器上下文环境,包括程序计数器和其他寄存器。
2)更新当前处于运行态进程的进程控制块,包括将进程的状态改变到另一状态(就绪态、阻塞态、就绪/挂起态或退出态)。还必须更新其他相关域,包括离开运行态的原因和记账信息。
3)将进程的进程控制块移到相应的队列(就绪、在事件i处阻塞、就绪/挂起)。
4)选择另一个进程执行,这方面的内容将在本书的第四部分探讨。
5)更新所选择进程的进程控制块,包括将进程的状态变为运行态。
6)更新内存管理的数据结构,这取决于如何管理地址转换。
7)恢复处理器在被选择的进程最近一次切换出运行状态时的上下文环境,这可以通过载入程序计数器和其他寄存器以前的值来实现。
12.2进程创建
创建新进程
■Windows进程创建API:CreateProcess(filename)
创建时关闭所有在子进程里的文件描述符CreateProcess(filename, CLOSE_FD)
创建时改变子进程的环境CreateProcess(filename, CLOSE_FD, new_envp)
■Unix进程创建系统调用:fork/exec
·fork()把一个进程复制成二个进程
parent (old PID), child (new PID)
·exec()用新程序来重写当前进程
PID没有改变
■用fork和exec创建进程的示例
int pid = fork();//创建子进程
if(pid == 0) {//子进程在这里继续
// Do anything (unmap memory, close net connections…)
exec(“program”, argc, argv0, argv1,…);
}
■fork()创建一个继承的子进程
复制父进程的所有变量和内存
复制父进程的所有CPU寄存器(有一个寄存器例外)
■fork()的返回值
子进程的fork()返回0
父进程的fork()返回子进程标识符
fork()返回值可方便后续使用,子进程可使用getpid()获取PID
fork()的地址空间复制
■fork()执行过程对于子进程而言,是在调用时间对父进程地址空间的一次复制
对于父进程fork()返回child PID,对于子进程返回值为0
程序加载和执行
系统调用exec( )加载新程序取代当前运行进程
exec()示例代码
main()
…
int pid = fork();//创建子进程
if (pid == 0) {//子进程在这里继续
exec_status = exec(“calc”, argc, argv0, argv1,…);
printf(“Why would I execute?”);
} else {//父进程在这里继续
printf(“Whose your daddy?”);
…
child_status = wait(pid);
}
fork()使用示例(循环内fork()父子都同时产生新的子进程)
int main()
{
pid_t pid;
int i;
for (i=0; i
{
/* fork another process */
pid = fork();
if (pid < 0) { /*error occurred */
fprintf(stderr,“Fork Failed”);
exit(-1);
}
else if (pid == 0) { /* child process */
fprintf(stdout,“i=%d, pid=%d, parent pid=%d\n”,I,
getpid() ,getppid());
}
}
wait(NULL);
exit(0);
}
Fork()的开销?
■fork()的实现开销
对子进程分配内存
复制父进程的内存和CPU寄存器到子进程里
开销昂贵!!
■在99%的情况里,我们在调用fork()之后调用exec()
在fork()操作中内存复制是没有作用的
子进程将可能关闭打开的文件和连接
为什么不能结合它们在一个调用中?
■vfork()
创建进程时,不再创建一个同样的内存映像
一些时候称为轻量级fork()
子进程应该几乎立即调用exec()
现在使用Copy on Write (COW)技术
■父进程终止其子进程的原因有很多
子进程使用了超过它所分配到的一些资源。(为判定是否发生这种情况,要求父进程有一个检查其子进程状态的机制。)
分配给子进程的任务已不再需要
父进程退出,如果父进程终止,那么操作系统不允许子进程继续。
精髓:创建一个新进程,一般步骤:
1)给新进程分配一个唯一的进程标识符。此时,在主进程表中增加一个新表项,表中的每个新表项对应着一个进程。
2)给进程分配空间。这包括进程映像中的所有元素。因此,操作系统必须知道私有用户地址空间(程序和数据)和用户栈需要多少空间。可以根据进程的类型使用默认值,也可以在作业创建时根据用户请求设置。如果一个进程是由另一个进程生成的,则父进程可以把所需的值作为进程创建请求的一部分传递给操作系统。如果任何现有的地址空间被这个新进程共享,则必须建立正确的连接。最后,必须给进程控制块分配空间。
3)初始化进程控制块。进程标识符部分包括进程ID和其他相关的ID,如父进程的ID等;处理器状态信息部分的大多数项目通常初始化成0,除了程序计数器(被置为程序人口点)和系统栈指针(用来定义进程栈边界);进程控制信息部分的初始化基于标准默认值和为该进程所请求的属性。例如,进程状态在典型情况下被初始化成就绪或就绪/挂起;除非显式地请求更高的优先级,否则优先级的默认值为最低优先级;除非显式地请求或从父进程处继承,否则进程最初不拥有任何资源( I/O设备、文件)。
4)设置正确的连接。例如,如果操作系统把每个调度队列都保存成链表,则新进程必须放置在就绪或就绪/挂起链表中。
5)创建或扩充其他数据结构。例如,操作系统可能为每个进程保存着一个记账文件,可用于编制账单和/或进行性能评估。
12.3程序加载和执行系统调用exec()
程序加载和执行系统调用exec()
■允许进程“加载”一个完全不同的程序,并从main开始执行(即_start)
■允许进程加载时指定启动参数(argc, argv)
■exec调用成功时
它是相同的进程…
但是运行了不同的程序
■代码段、堆栈和堆(heap)等完全重写
12.4进程等待与退出
父进程等待子进程
■wait()系统调用用于父进程等待子进程的结束
子进程结束时通过exit()向父进程返回一个值
父进程通过wait()接受并处理返回值
■wait()系统调用的功能
有子进程存活时,父进程进入等待状态,等待子进程的返回结果
当某子进程调用exit()时,唤醒父进程,将exit()返回值作为父进程中wait的返回值
有僵尸子进程等待时,wait()立即返回其中一个值
无子进程存活时,wait()立刻返回
进程的有序终止exit()
■进程结束执行时调用exit(),完成进程资源回收
■exit()系统调用的功能
将调用参数作为进程的“结果”
关闭所有打开的文件等占用资源
释放内存
释放大部分进程相关的内核数据结构
检查是否父进程是存活着的
如存活,保留结果的值直到父进程需要它,进入僵尸(zombie/defunct)状态,等待父进程处理
如果没有,它释放所有的数据结构,进程结果(孤儿进程)
清理所有等待的僵尸进程
■进程终止是最终的垃圾收集(资源回收)
其他进程控制系统调用
■优先级控制
nice()指定进程的初始优先级
Unix系统中进程优先级会随执行时间而衰减
■进程调试支持
ptrace()允许一个进程控制另一个进程的执行
设置断点和查看寄存器等
■定时
sleep()可以让进程在定时器的等待队列中等待指定
进程控制v.s.进程状态