20191305李天琦第三章学习笔记
20191305李天琦第三章学习笔记
摘要:本章讨论了Unix/Linux 中的进程管理;阐述了多任务处理原则;介绍了进程概念;并以一个编程示例来说明多任务处理、上下文切换和进程处理的各种原则和方法。多任务处理系统支持动态进程创建、进程终止,以及通过休眠与唤醒实现进程同步、进程关系,以及以二叉树的形式实现进程家族树,从而允许父进程等待子进程终止;提供了一个具体示例来阐释进程管理函数在操作系统内核中是如何工作的;然后,解释了Unix/Linux中各进程的来源,包括系统启动期间的初始进程、INIT进程、守护进程、登录进程以及可供用户执行命令的 sh 进程;接着,对进程的执行模式进行了讲解,以及如何通过中断、异常和系统调用从用户模式转换到内核模式;再接着,描述了用于进程管理的Unix/Linux 系统调用,包括fork、wait、exec 和 exit;阐明了父进程与子进程之间的关系,包括进程终止和父进程等待操作之间关系的详细描述;解释了如何通过INIT进程处理孤儿进程,包括当前Linux 中的subreaper 进程,并通过示例演示了subreaper 进程;接着,详细介绍了如何通过exec 更改进程执行映像,包括 execve 系统调用、命令行参数和环境变量;解释了I/O重定向和管道的原则及方法,并通过示例展示了管道编程的方法。
3.1多任务处理
一般来说,多任务处理指的是同时进行几项独立活的能力。比如,我们经常看到有人一边开车一边打电话。从某种意义上说,这些人正在进行多任务处理,尽管这样非常不好。在计算机技术中,多任务处理指的是同时执行几个独的任务。在单处理器(单CPU)系统中,一次只能执行一个任务。多任务处理是通过在不同任务之间多路复用CPU的执行时间来实现的,即将CPU执行操作从一个任务切换到另一个任务。不同任务之间的执行切换机制称为上下文切换,将一个任务的执行环境更改为另个任务的执行环境。如果切换速度足够快,就会给人一种同时执行所有任务的错觉这种逻辑并行性称为“并发”。在有多个CPU或处理器内核的多处理器系统中,可在不同CPU上实时,并行执行多项任务。此外,每个处理器也可以通过同时执行不同的任务来实现多任处理。多任务处理是所有操作系统的基础。总体上说,它也是并行编程的基础。
3.2进程的概念
操作系统是一个多任务处理系统。在操作系统中,任务也称为进程在实际应用中,任务和进程这两个术语可以互换使用。进程的正式定义:
进程是对映像的执行。
操作系统内核将一系列执行视为使用系统资源的单一实体。系统资源包括内存空间、I/O设备以及最重要的CPU时间。在操作系统内核中,每个进程用一个独特的数据结构表示,叫作进程控制块(PCB)或任务控制块(TCB)等。在本书中,我们直接称它为PROC结构体。与包含某个人所有信息的个人记录一样,PROC结构体包含某个进程的所有信息
在实际操作系统中,PROC结构体可能包含许多字段,而且数量可能很庞大。
3.3多任务处理系统
(1)type.h文件
type.h文件定义了系统常数和表示进程的简单PROC结构体
#define NPROC 9
#define SSIZE 1024
// PROC status
#define FREE 0
#define READY 1
#define SLEEP 2
#define ZOMBIE 3
typedef struct proc{
struct proc *next;
int *ksp;
int pid;
int status;
int priority;
int kstack [SSIZE];
}PROC;
后面,我们在扩展 MT系统时,应向PROC结构体中添加更多的字段。
(2)ts.s文件
ts.s在32位GCC汇编代码中可实现进程上下文切换。
.globl running,scheduler, tswitch
tSwitch:
SAVE:pushl %eax :
pushl %ebx
pushl %ecx
pushl %edx
pushl %ebp
pushl %esi
pushl %edi
pushf1
movl running, Sebx
mov1 # esp,4(%ebx)
FIND: call scheduler
RESUME: movl running,8ebx
Movl 4(%ebx),%esp
popf1
popl %edi
popl %esi
popl %ebp
popl %edx
popl %ecx
popl %ebx
popl %eax
ret
# stack contents=|retPC|eax|ebx|ecx|edx|ebp|esi|edi|eflag|
# -2 -3 -4 -5 -6 -7 -8 -9 -1
(3)queue.c文件
queue.c文件可实现队列和链表操作函数。
int enqueue(PROC **queue,PROC *p)
{
PROC *q = *queue;
if(q == 0 || p->priority> q->priority){
*queue = p;
p->next = q;
}
else{
while(g->next && p->priority <= q->next->priority)
q = q->next;
p->next = q->next;
q->next = p;
}
}
PROC *dequeue (PROC **queue)
{
PROC *p = *queue;
if (p)
*queue =(*queue)->next;
return p;
}
int printList(char *name,PROC *p)
{
printf("%s = ",name);
while(p){
printf("[8d %d]->",p->pid,p->priority);
p = p->next;
}
printf("NULL\n");
}
(4)t.c文件
t.c文件定义MT系统数据结构、系统初始化代码和进程管理函数。
(5)多任务处理系统代码介绍
通过以下步骤介绍MT系统的基本代码
(1)虚拟CPU:MT系统在Linux下编译链接为
gcc -m32 t.c ts.s
然后运行a.out。
(2)init():当MT系统启动时,main()函数调用init()以初始化系统。init()初始化PROC结构体,并将它们输入freeList中。
(3)P0调用kfork()来创建优先级为1的子进程P1,并将其输入就绪队列中。然后P0调用tswitch(),将会切换任务以运行P1。
(4)tswitich():tswitch()函数实现进程上下文切换。
(4).1 tswitch()中的SAVE函数:当正在执行的某个任务调用tswitch()时,它会把返回地址保存在堆栈上,并在汇编代码中进入tswitch()。
(4).2 scheduler():在执行了SAVE函数之后,任务调用scheduler()来选择下一个正在运行的任务。
(4).3 tswitch()中的RESUME函数执行从scheduler()返回时,运行指向的那个PROC就是当前正在运行的任务。
(5)kfork():kfork()函数创建一个子任务并将其输入readyQueue中。
(6)body():所有创建的任务都执行同一个body()函数。
(7)空闲任务 P0:P0的特殊之处在于它所在任务中具有最低的优先级
(8)运行多任务处理(MT)系统:在Linux下输入gcc -m32 t.c s.s 编译链接MT系统并运行所得到的a.out。
3.4进程同步
(1)睡眠模式
为实现休眠操作,我们可以在 PROC结构体中添加一个event字段,并实现ksleep(int event)函数,使进程进入休眠状态。接下来,我们将假设对 PROC结构体进行修改以包含加粗显示的添加字段。
struct proc *next;
int*ksp;
int pid;
int ppid;
int status;
int priority;
int event;
int exitCode;
struct proc *child;
struct proc *sibling;
struct proc *parent;
int kstack[1024];
}PROC;
(2)唤醒操作
当某个等待时间发生时,另一个执行实体(可能是某个进程或中断处理程序)将会调用 kwakeup(event)。唤醒正处于休眠状态等待该事件值的所有程序。如果没有任何程序休眠等待该程序,kwakeup()就不工作,即不执行任何操作。Kwakeup()的算法是:
/********** Algorithm of kwakeup(int event)*********/
// Assume SLEEPing proCs are in a global sleepiist
for each PROC *p in sleepList do {
if (p->event == event){
delete D from sleepLiBt;
p->8tatu8 = READY;
enqueue(EreadyQueue,p);
}
}
3.5进程终止
正常终止:进程调用exit(value),发出 exit(value)系统调用来执行在操作系统内核中的 kexit(value)。
异常终止:进程因某个信号而异常终止。
在这两种情况下,当进程终止时,最终都会在操作系统内核中调用kexit()。
在任何时候,进程都可以调用内核函数pid = kwait(int *status)等待僵尸子进程。如果成功,则返回的 pid是僵尸子进程的 pid,而 status包含僵尸子进程的退出代码。此外,kwait()还会将僵尸子进程释放回 freeList 以便重用。
3.6MT系统中的进程管理
完善基础MT系统,实现MT系统的进程管理函数,具体来说:
(1)用二叉树的形式实现进程家族树。
(2)实现 ksleep()和kwakeup()进程同步函数。
(3)实现kexit()和kwait()进程管理函数。
(4)添加"w"命令来测试和演示等待操作。
3.7Unix/Linux中的进程
进程的执行模式
1.中断:中断是外部设备发送给 CPU的信号,请求CPU服务。
2.陷阱:陷阱是错误条件,例如无效地址、非法指令、除以0等、这些错误条件被CPU识别为异常,使得CPU进入 Kmode 来处理错误。
3.系统调用:系统调用(简称syscall)是一种允许Umode 进程进入Kmode 以执行内核函数的机制。如果发生错误,外部全局变量 errno(在errno. h中)会包含一个ERROR代码,用于标识错误。用户可使用库函数 perror( "error message");
3.8进程管理的系统调用
fork()
Usage: int pid = fork()
进程终止
1.正常终止:回顾前面的内容,我们知道,每个C程序的 main()函数都是由C启动代码 crt0.o调用的。如果程序执行成功,main()最终会返回到 crt0.o,调用库函数 exit((0)来终止进程。首先,exit(value)函数会执行一些清理工作,如刷新 stdout、关闭I/O流等。然后,它发出一个_exit(value)系统调用,使进入操作系统内核的进程终止。
2.异常终止:在执行某程序时,进程可能会遇到错误,如非法指令、越权、除零等,这些错误会被 CPU识别为异常。当某进程遇到异常时,它会进入操作系统内核。内核的异常处理程序将陷阱错误类型转换为一个函数,称为信号,将信号传递给进程,使进程终止。
3.9I/O重定向
当进程执行库函数
printf("format=%s\n",items);
它试图将数据写入 stdout 文件FILE 结构体中的 fbuf[],这是缓冲行。如果 fbuf[]有一个完整的行,它会发出一个write系统调用,将数据从 fbuf[]写入文件描述符1,映射到终端屏幕上。要想将标准输出重定向到一个文件,需执行以下操作。
c1ose(1);
open("filename",O_WRONLY|O_CREAT,0644);
更改文件描述符1,指向打开的文件名。然后,stdout 的输出将会转到该文件而不是屏幕。同样,我们也可以将stderr重定向到一个文件。当某进程(在内核中)终止时,它会关闭所有打开的文件。
3.10管道
概念:
管道是用于进程交换数据的单向进程间通信通道。管道有一个读取端和一个写入端。可从管道的读取端读取写入管道写入端的数据。自从管道在最初的Unix 中首次出现以来,已经被用于几乎所有的操作系统中,有许多变体。一些系统允许双向管道,在双向管道上,数据可以双向传输。普通管道用于相关进程。命名管道是不相关进程之间的 FIFO通信通道。但是,如果管道不再有读进程,写进程必须将这种情况视为管道中断错误,并中止写入。
Unix/Linux中的管道编程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int pd[2],n,i;
char line[256];
int main()
{
pipe(pd);
printf("pd=[$d,%d]\n",pd[0],pd[1]);
if (fork(){
printf("parent $d close pd[0]\n",getpid());
close(pd[0]); // parent as pipe WRITER
while(i++ <10){ // parent writes to pipe 10 times
printf("parent 8d writing to pipe\n",getpid());
n = write(pd[1],"I AM YOUR PAPA",16);
printf("parent %d wrote %d bytes to pipe\n",getpid(),n);
}
printf("parent $d exit\n",getpid());
}
else{
printf("child $d close pd[1]\n",getpid());
close(pd[1]); // child as pipe READER
while(1) {
// child read from pipe
printf("child %d reading from pipe\n",getpid());
if((n = read(pd[0],line,128))){ // try to read 128 bytes
line[n]=0;
printf("child read $d bytes from pipe: 8s\n",n,line);
}
else // pipe has no data and no writer
exit(0);;
}
}
}