一、梗概
本章讨论了 Unix/Linux 中的进程管理;阐述了多任务处理原则;介绍了进程概念;以一个编程示例来说明多任务处理、上下文切换和进程处理的各种原则和方法。多任务处系统支持动态进程创建、进程终止,以及通过休眠与唤醒实现进程同步、进程关系,以及二叉树的形式实现进程家族树,从而允许父进程等待子进程终止;提供了一个具体示例来阐释进程管理函数在操作系统内核中是如何工作的;然后,解释了 Unix/Linux 中各进程的来源,包括系统启动期间的初始进程、INIT 进程、守护进程、登录进程以及可供用户执行命令的 sh 进程;接着,对进程的执行模式进行了讲解,以及如何通过中断、异常和系统调从用户模式转换到内核模式;再接着,描述了用于进程管理的 Unix/Linux 系统调用,包括 fork、wait、exec 和 exit ;阐明了父进程与子进程之间的关系,包括进程终止和父进程等待操作之间关系的详细描述;解释了如何通过 INIT 进程处理孤儿进程,包括当前 Linux 中的 subreaper 进程,并通过示例演示了 subreaper 进程;接着,详细介绍了如何通过 exec 更改进程执行映像,包括 execve 系统调用、命令行参数和环境变量;解释了 1/0 重定向和管道的原则及方法,并通过示例展示了管道编程的方法。
二、知识点总结
1、多任务处理
32位windows操作系统中,多任务处理是指系统可同时运行多个进程,而每个进程也可同时执行多个线程。一个线程是指程序的一条执行路径,它在系统指定的时间片中完成特定的功能。系统不停地在多个线程之间切换,由于时间很短,看上去多个线程在同时运行。或者对于在线程序可并行执行同时服务于多个用户称为多任务处理。
引入多任务处理是为了提高CPU的利用率。Windows、IOS、Android等操作系统都支持多任务处理。Windows 中的前台与后台任务都能分配到CPU的使用权。
2、进程的概念
狭义定义:进程是正在运行的程序的实例。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
操作系统引入进程的概念的原因:
从理论角度看,是对正在运行的程序过程的抽象;
从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。
特征
动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
并发性:任何进程都可以同其他进程一起并发执行
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
结构特征:进程由程序、数据和进程控制块三部分组成。
多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。
3、进程同步
一组进程为了协调其推进速度,在某些地方需要相互等待或者唤醒,这种进程间的相互制约就被称作是进程同步。这种合作现象在操作系统和并发式编程中属于经常性事件。
进程同步机制
信号量与PV操作
这个进程同步机制应该算的上是古董级的了,这种机制的主要思想就是——通过将资源数量化,将申请资源和释放资源的动作具体化,从而达到对资源的操作及结果可视化的程度。
信号量和PV操作的具体定义如下:
struct semaphore
{
int value; //初始化值必须>=0
pointer_to_PCB queue; //指向PCB的队列
} //信号量,其变量必须也只能初始化一次,只能执行PV操作
void P(semaphore *s)
{
s->value--;
if (s->value < 0)
asleep(s->queue);
} //P操作
void V(semaphore *s)
{
s->value++;
if (s->value <= 0)
wakeup(s->squeue);
} //V操作```
解释几个内容:
queue是指向一个由PCB所构成的队列头部,当这个队列中不存在任何等待进程时,其指向为空(初始也为空)。
asleep和wakeup可以按照字面意理解——即休眠和唤醒。
执行asleep(s->queue)的进程的PCB会进入queue这个等待队列的尾部,其由运行态 —> 等待态,系统转处理器调度程序。
执行wakeup(s->queue)时,统一将queue队列头部的PCB取出(遵循队列先入先出原则)放入就绪队列,等待态 —> 就绪态。
4、进程终止
在操作系统中,进程可能终止或死亡,这是进程终止的通俗说法。如第2章所述,进程能以两种方式终止:
·正常终止:进程调用 exit(value),发出_exit(value)系统调用来执行在操作系统内核中的 kexit(value),这就是我们本节要讨论的情况。
·异常终止:进程因某个信号而异常终止。信号和信号处理将在后面第 6 章讨论。
在这两种情况下,当进程终止时,最终都会在操作系统内核中调用 kexit()。
5、进程管理的系统调用
1)创建(fork)
为什么需要这么一个系统调用呢,因为我们的系统在最开始启动后并不会有太多的进程,需要有一个系统调用能够在接下来的使用中创建更多的进程。例如我们的shell,可以在shell中使用命令创建一个进程。
fork就像一把叉子,主要完成
1、做一份进程完整的复制(内存、寄存器运行现场)
2、父进程返回子进程的pid,子进程返回0
那么创建一个进程为什么要叫做叉子呢?还是以我们刚才的qq进程为例,如果它调用了fork系统调用就会在当前系统中多出一个进程,而这个进程是之前qq进程的拷贝,它包括几乎所有的进程当前的状态(除了返回值pid),包括内存、寄存器。原来的进程也叫父进程和新创建的进程也叫子进程都可以继续向下执行,所以说它像一个叉子,走着走着就分了个叉。父进程返回值是子进程的pid,子进程的返回值是0。
2)execve系统调用
除了能够对进程进行创建,还需要能够执行别的程序,execve系统调用就是这样一个功能,将当前运行的进程“替换”成另一个程序,从头开始执行。fork以后得到的进程是与父进程相同的,但是跟多的时候我们创建子进程是为了让他执行不同的程序,去做别的工作。execve就是启动一个新程序的系统调用。exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。
execve(filename,argv,enpv)
//执行名称为filename的程序
//分别传入参数v,环境变量e
一个进程调用execve以后,它没有创建新的进程,而是从传入的可执行程序中加载代码和静态数据,并用它覆写自己的代码段、堆、栈,以及其他内存空间也会被重新初始化。然后操作系统就执行该程序。所以对execve的成功调用永远不会返回,只有在发生错误的时候才会返回-1,从原程序的调用点接着往下执行。
6、I/O重定向
1)I/O重定向是一个过程,这个过程捕捉一个文件、命令、程序或脚本,甚至代码块的输出,然后把捕捉到的输出作为输入发送给另外一个文件、命令、程序或脚本。
2)文件描述符
文件描述符是从0开始到9的结束的整数,指明了与进程相关的特定数据流的源。当Linux系统启动一个进程(该进程可能用于执行shell命令)时,将自动为该进程打开三个文件:标准输入(文件标识符为0)、标准输出(1标识)和标准错误输出(2标识),若要打开其他的输入或输出文件则从整数3开始标识。默认情况下,标准输入与键盘输入相关联,标准输出与标准错误输出与显示器相关联。
Shell从标准输入读取输入数据,将输出送到标准输出,如果该命令在执行过程中发生错误,则将错误信息输出到标准错误输出。
tee命令将shell的输出从标准输出复制一份到文件中,tee命令加-a表示追加到文件的末尾。
3)I/O重定向符号
I/O重定向符号分为:基本I/O重定向符号和高级I/O重定向符号(与exec命令有关)。
|符号是强制覆盖文件的符号,如果noclobber选项开启(set -o noclobber),表示不允许覆盖任何文件,此时>|可强制将文件覆盖。n>> file、n>|file与n>file都是将FD为n的文件重定向到file文件中。
<是I/O重定向的输入符号,它可将文件内容写到标准输入之中。wc -l < newfile,其中shell从命令行会“吞掉”<newfile并启动wc命令。
<<delimiter(delimiter为分界符),该符号表明:shell将分界符delimiter之前的所有内容作为输入,cat > file << FIN,输入FIN后按回车键结束编辑,输入内容重定向到file文件中。其另一种形式:-<<delimiter,在<<前加一个负号,这样输入文本行所有开头的"Tab"键都会被删除,但开头的空格键却不会被删除,如cat > file -<< FIN。