20201322陈俊池学习笔记6
3.1 多任务处理
-
多任务处理指的是同时执行几个独立的任务。在单处理器(单CPU)系统中,一次只能执行一个任务。
-
多任务处理是通过在不同任务之间多路复用CPU的执行时间来实现的,即将CPU执行操作从一个任务切换到另一个任务。如果切换速度足够快,就会给人一种同时执行所有任务的错觉。这种逻辑并行性成为“并发”。
3.2 进程的概念
进程是对映像的执行。
操作系统内核将一系列执行视为使用系统资源的单一实体。系统资源包括内存空间、I/O设备以及最重要的CPU时间。在操作系统内核中,每个进程用一个独特的数据结构表示,叫作进程控制块(PCB)或任务控制块(线程控制块)(TCB)等。在本书中,我们直接称它为PROC结构体。与包含某个人所有信息的个人纪录一样,PROC结构体包含某个进程的所有信息。在实际操作系统中,PROC结构体可能包含许多字段,而且数量可能很庞大。首先,我们来定义一个非常简单的PROC结构体来表示进程。
3.3 多任务处理系统
多任务处理系统,简称MT。
-
type.h文件
定义了系统常熟和表示进程的简单PROC结构体
-
ts.s文件
在32位GCC汇编代码中可实现进程上下文切换
-
queue.c文件
可实现队列和链表操作函数。
enqueue()函数按优先级将PROC输入队列中。在优先级队列中,具有相同优先级的进程按照FIFO的顺序排序。
dequeue()函数可返回从队列或链表中删除的第一个元素。
printList()函数可打印链表元素。
-
t.c文件
定义MT系统数据结构、系统初始化代码和进程管理函数
3.4 进程同步
一个操作系统包含许多并发进程,这些进程可以彼此交互。进程同步是指控制和协调进程交互以确保其正确执行所需的各项规则和机制。最简单的进程同步工具是休眠和唤醒操作。
3.4.1 睡眠模式
当某进程需要某些当前没有的东西时,例如申请独占一个存储区域、等待用户通过标准输入来输入字符等,它就会在某个事件值上进入休眠状态,该事件值表示休眠的原因
3.4.2 唤醒模式
多个进程可能会进入休眠状态等待同一个事件,这是很自然的,因为这些进程可能都需要同一个资源,例如一台当前正处于繁忙状态的打印机。在这种情况下,所有这些进程都将休眠等待同一个事件值。当某个等待时间发生时,另一个执行实体(可能是某个进程或中断处理程序)将会调用kwakeup(event),唤醒正处于休眠状态等待该事件值的所有程序。如果没有任何程序休眠等待该程序,kwakeup()就不工作,即不执行任何操作。
3.5 进程终止
-
在操作系统中,进程可能终止或死亡,这是进程终止的通俗说法。如第2章所述,进程能以两种方式终止:
-
正常终止:进程调用exit(value),发出_exit(value)系统调用来执行在操作系统内核中的kexit(value),这就是我们本节要讨论的情况。
-
异常终止:进程因某个信号而异常终止。信号和信号处理将在第六章讨论。
在这两种情况下,当进程终止时,最终都会在操作系统内核中调用kexit()。
3.5.1 kexit()的算法
/****************** Algorithm of kexit(int exitValue) **********************/
1. Erase process user-mode context, e.g. close file descriptors,
release resources, deallocate user-mode image memory, etc.
2. Dispose of children processes, if any
3. Record exitValue in PROC.exitCode for parent to get
4. Become a ZOMBIE (but do not free the PROC)
5. Wakeup parent and, if needed, also the INIT process P13.5.2 进程家族树
通常进程家族树通过一个PROC结构中的一对子进程和兄弟进程指针以二叉树的形式实现,如:
PROC *child, *sibling, *parent
其中,child指向进程的第一个子进程,sibling指向同一个父进程的其他子进程。为方便起见,每个PROC还使用一个parent指针指向其父进程。
使用进程树,更容易找到进程的子进程。首先,跟随child指针到第一个子进程。然后,跟随sibling指针遍历兄弟进程。要想把所有子进程都送到P1中,只需要把子进程链表分出来,然后把它附加到P1的子进程链表中(还要修改它们的ppid和parent指针)。
每个PROC都有一个退出代码(exitCode)字段,是进程终止时的进程退出值(exitValue)。在PROC.exitCode中记录exitValue之后,进程状态更改为ZOMBIE,但不释放PROC结构体。然后,进程调用kwakeup(event)来唤醒其父进程,其中事件必须是父进程和子进程使用的相同唯一值,例如父进程的PROC结构体地址或父进程的pid。如果它将任何孤儿进程送到P1中,也会唤醒P1。濒死进程的最后操作是进程最后一次调用tswitch()。在这之后,进程基本上死亡了,但还有一个空壳,以僵尸进程的形式存在,它通过等待操作被父进程埋葬(释放)。
3.5.3 等待子进程终止
在任何时候,进程都可以调用内核函数
pid = kwait(int *status);
等待僵尸子进程。如果成功,则返回的pid是僵尸子进程的pid,而status包含僵尸子进程的退出代码。此外,kwait()还会将僵尸子进程释放回freeList以便重用。kwait的算法是:
/**************** Algorithm of kwait() *****************/
int kwait(int *status)
{
if (caller has no child) return -1 for error;
while(1) { // caller has children
search for a (any) ZOMBIE child;
if (found a ZOMBIE child) {
get ZOMBIE child pid;
copy ZOMBIE child exitCode to *status;
bury the ZOMBIE child (put its PROC back to freeList);
return ZOMBIE child pid;
}
// ***** has children but none dead yet *****
ksleep(running); // sleep on its PROC address
}
}在kwait算法中,如果没有子进程,则会返回-1,表示错误。否则,它将搜索僵尸子进程。如果它找到僵尸子进程,就会收集僵尸子进程的pid和退出代码,将僵尸进程释放到freeList并返回僵尸子进程的pid。否则,它将在自己的PROC地址上休眠,等待子进程终止。由于每个PROC地址都是一个唯一值,所有子进程也都知道这个值,所以等待的父进程可以在自己的PROC地址上休眠,等待子进程稍后唤醒它。相应地,当进程终止时,它必须发出:
kwakeup(running->parent);
以唤醒父进程。若不用父进程地址,读者也可使用父进程pid进行验证。在kwait()算法中,进程唤醒后,当它再次执行while循环时,将会找到死亡的子进程。注意,每个kwait调用只处理一个僵尸子进程(如有)。如果某个进程有多个子进程,那么它可能需要多次调用kwait()来处理所有死亡的子进程。或者,某进程可以先终止,而不需要等待任何死亡子进程。当某进程死亡时,它所有的子进程都成了P1的子进程。在真实系统中,P1在无限循环中执行,多次等待死亡的子进程,包括接收的孤儿进程。因此,在类Unix系统中,INIT进程P1扮演着许多角色。
-
它是除了P0之外所有进程的祖先。具体来说,它是所有用户进程的始祖,因为所有登录进程都是P1的子进程。
-
它就像孤儿院的院长,所有孤儿都会送到它这里,并叫它爸爸。
-
它又像是太平间管理员,因为它要不不停地寻找僵尸进程,以埋葬它们死亡的空壳。 所以,在类Unix系统中,如果INIT进程P1死亡或被卡住,系统将停止工作,因为用户无法再次登录,系统内很快就会堆满腐烂的尸体。
-
3.7 Unix/Linux中的进程
3.7.1 进程来源
当操作系统启动时,操作系统内核的启动代码会强行创建一个PID=0的初始进程,即通过分配PROC结构体(通常是proc[0])进行创建,初始化PROC内容,并让运行指向proc[0]。然后,系统执行初始进程P0。大多数操作系统都以这种方式开始运行第一个进程。P0继续初始化系统,包括系统硬件和内核数据结构。然后,它挂载一个根文件系统,使得系统可以使用文件。在未初始化系统之后,P0复刻出一个子进程P1,并把进程切换为以用户模式运行P1。
3.7.2 INIT和守护进程
P1运行时,将执行映像更改为init程序,P1通常被称为init进程,P1的大部分子进程都是用来提供系统服务的,称为守护进程。
3.7.3 登录进程
除了守护进程之外,P1还复刻了许多LOGIN进程,每个终端上一个,用于用户登录。 每个LOGIN进程打开三个与自己的终端相关联的文件流。这三个文件流是用于标准输入的stdin、用于标准输出的stdout和用于标准错误消息的stderr。每个文件流都是指向进程堆区中FILE结构体的指针。每个FILE结构体记录一个文件描述符(数字),stdin的文件描述符是0,stdout是1,stderr是2。
3.7.4 sh进程
当用户成功登录时,LOGIN进程会获取用户的gid和uid,从而成为用户的进程。它将目录更改为用户的主目录并执行列出的程序,通常是命令解释程序sh。现在,用户进程执行sh,因此用户进程通常称为sh进程。它提示用户执行命令。一些特殊命令,如cd(更改目录)、退出、注销等,由sh自己直接执行。其他大多数命令是各种bin目录(如/bin、/sbin、/usr/bin、/usr/local/bin等)中的可执行文件。对于每个(可执行文件)命令,sh会复刻一个子进程,并等待子进程终止。子进程将其执行映像更改为命令文件并执行命令程序。子进程在终止时会唤醒父进程sh,父进程会收集子进程终止状态、释放子进程PROC结构体并提示执行另一个命令等。除简单的命令之外,sh还支持I/O重定向和通过管道连接的多个命令。
3.7.5 进程的执行模式
在Unix/Linux中,进程以两种不同的模式执行,即内核模式和用户模式,简称Kmode和Umode。在每种执行模式下,一个进程有一个执行映像,如图:
在进程的生命周期中,会在Kmode和Umode之间发生多次迁移。每个进程都在Kmode下产生并开始执行。事实上,它在Kmode下执行所有相关操作,包括终止。在Kmode模式下,通过将CPU的状态寄存器从K模式更改为U模式,可轻松切换到Umode。但是,一旦进入Umode,就不能随意更改CPU的状态了,原因很明显。Umode进程只能通过以下三种方式进入Kmode:
-
中断:中断是外部设备发送给CPU的信号,请求CPU的服务。当在Umode下执行时,CPU中断是启用的,因此它将响应任何中断。在中断发生时,CPU将进入Kmode来处理中断,这将导致进程进入Kmode。
-
陷阱:陷阱是错误条件,例如无效地址、非法指令、除以0等,这些错误条件被CPU识别为异常,使得CPU进入Kmode来处理错误。在Unix/Linux中,内核陷阱处理程序将陷阱原因转换为信号编号,并将信号传递给进程。对于大多数信号,进程的默认操作是终止。
-
系统调用:系统调用(简称syscall)是一种允许Umode进程进入Kmode以执行内核函数的机制。当某进程执行完内核函数后,它将期望结果和一个返回值返回到Umode,该值通常为0(表示成功)或-1(表示错误)。
3.8 进程管理的系统调用
3.8.1 fork()
-
int pid = fork()
-
fork()创建子进程并返回子进程的pid
3.8.3 进程终止
-
正常终止:当内核中的某个进程终止时,他会将_exit(value)系统调用中的值记录为进程PROC结构体中的退出状态。并通知他的二父进程并使该进程成为僵尸进程。父进程课通过系统调用找到僵尸子进程,获得其pid和退出状态
pid=wait(int *status)
-
异常终止:当某进程遇到异常时,他会陷入操作系统内核。内核的异常处理程序将陷阱错位类型转换为一个幻数,称为信号,将信号传递给进程,时进程终止。用户可以使用命令
kill -s signal_numeber pid
向通过pid识别的目标发送信号。
3.8.4 等待子进程终止
-
在任何时候,一个进程都可以使用
int pid = wait(int *status);
系统调用,等待僵尸子进程。
3.8.7 环境变量
-
各环境变量定义为:关键字=字符串
-
重要环境变量:
SHELL=/bin/bash
TERM=xterm
USER=kcw
PATH=/usr/1oca1/bin:/usr/bin:/bin:/usr/local/games:/usr/games:./
HOME= / home /kcw
-
SHELL:指定将解释任何用户命令的sh。
-
TERM:指定运行sh时要模拟的终端类型。
-
USER:当前登录用户。
-
PATH:系统在查找命令时将检查的目录列表。
-
HOME:用户的主目录。在 Linux 中,所有用户主目录都在/home中。
-
在sh会话中,可以将环境变量设置为新的(字符串)值,如:
HOME= / home / newhome
-
可通过EXPORT命令传递给后代sh,如
expoert HOME
3.10 管道
管道是用于进程交换数据的单向进程间通信通道,有一个读取端和一个写入端。 (1)管道编程
int
(2)管道命令处理
在Unix/Linux中,命令行cmd1 | cmd2
,sh将通过一个进程运行cmd1,并通过另一个进程运行cmd2,他们通过一个管道连接在一起,因此cmd1的输出变为cmd2的输入
(3)命名管道—FIFO 命令管道又叫FIFO
-
在sh中,通过mknod命令创建一个命令管道:
mknod mypipe p
-
或在c语言中发出mknod()系统调用
int r = mknod("mypipe",s_IFIFP,0);
-
进程可像访问普通文件一样发个文命名管道。
实践内容与截图
fork函数
代码:
#include <stdio.h>
int main(){
int pid;
printf("(1)我是 %d ,我的父亲是 %d\n",getpid(),getppid());
pid = fork();//复刻一个子进程
if (pid){
printf("这是 %d 孩子 PID= %d\n",getpid(),pid);
}
else{
printf("(2)我是 %d ,我的父亲是 %d\n",getpid(),getppid());
}
}
结果: