把进程管理放在操作系统中较后的位置,不是没有原因的。想一想,如果没有那么多的设备又何必设计存储管理,如果没有存储设备中内容的复杂性,又何来文件系统管理。如果没有文件系统提供那么多的资源,又何必设备多个程序,让它们按照可被调度的对象运行? 由此可见。在底层,在操作系统设计的时候,按照一个一个的模块来设计的。有了前面的准备,进程的运行才有一个良好的环境,才方便我们设计调度。
进程管理中的进程被提供了一个抽象的体例。在进程中,也许应该首先使用虚拟地址空间,区分内核态与用户态,再联系处理器的权限管理的机制。这样对于为何进程有必要存在给了一个比较好的解释。
在存储管理的层次上,可以按照所谓单一连续分配,固定分配,动态分配,分页或者分段的方法。但是这只是一个方面。这是进程的静态的结构,这时进程被看成是存储区域。但是还可以更高级地,把进程看成是运行着的文件。
多用户情况下,进程的权限可以看成是由文件的权限演变而来,而且它访问的设备,都是这样的一种抽象。所以如果把进程管理放在最后的话,我们会更能看出进程技术的复杂性。在学习路线上,先设备后进程的方法也许是很适合的。
在进程与编译技术上,刚开始的编译,只是按照实际地址空间的编译,自从有了进程与抽象之后,便可以使用可重定位的代码了。这样编译的技术也就可以更进一步。有了这些,我们才可能进一步讲解在操作系统之下的虚拟机,调试,安全优化这些内容。
进程与子进程的同步
- 使用
_exit()系统调用或者exit()库函数终止进程,或者等待进程代码执行完毕的时候退出,或者使用return()语句。通常并不直接使用系统调用。因为与_exit()相比,exit()还执行了更多的清除工作。
void _exit(int status);
void exit(int status);当一个进程创建一个子进程的时候,大多数情况下,紧跟其后的是调用exec语句。exec的用法这里不讲。
等待进程死亡的系统调用是wait()。它的效果是在父进程执行它的地方被阻塞,直到有一个子进程死亡,wait得到它的状态返回,父进程因此继续执行。wait的不好之处是在这之前父进程可能创建了多个子进程,wait返回时,父进程并不知道是哪个子进程结束了。
子进程先死亡的时候,子进程的进程表不会立即被释放。除非是init进程,有定时清理defunct进程的功能。
wait(int *exitstatus);为了改进这一状态,可以选择使用waipid()函数代替。
pid_t waitpid(pid_t pid, int *stat_loc, int options);第二个参数保存退出状态。options参数取WNOHANG,WUNTRACED,WCONTINUED之一或者其中的组合。有NOHANG的时候立即返回,不会阻塞。pid是所等待的进程。但是有些有特殊的意义。比如-1代表进程一直阻塞;0代表等待进程组中任何一个进程的死亡;\(pid>0\)时表示所等待的进程;\(pid<0\)时绝对值表示等待PGID等于pid绝对值的进程(组领导进程)死亡。
进程组是Berkeley小组提出的一个概念,利用它可以控制一组具有共同特性的进程。组中每个进程有进程组标识号PGID,其值等于进程组领导者的PID
不同shell中的特性也不一样,比如在Bourne Shell里,其中运行的命令与shell的PGID相同。C Shell, Korn Shell, Bash Shell中,处于同一管道的命令组成一个独立的进程组。具体来说。支持作业控制的Shell才会像后者一样。
一个用户可以有多个进程组,只有一个进程组在前台,它连接到会话的控制终端。在按下CTRL-C的时候,我们实际上是向前台进程组里的所有进程发送SIGINT信号。因此在一堆管道命令当中,我们也可以使用一个CTRL-C.
当后台进程组试图从终端读取数据的时候,终端驱动程序会发现这种行为,发送一个信号,挂起这个进程组。
使用环境变量
环境变量是进程地址空间的一部分。在创建子进程时父进程的环境变量会传递给子进程。保存在一个外部变量里。使用如下的语句声明它:
extern char **environ;这个指针数组的每一分量是name=value的形式。
POSIX规范定义了如下的两个函数分别用于求取和设置环境变量的值:
char *getenv(const char *name);
int setenv(const char*envname, const char * envval, int overwrite);其中overwrite的含义是:非零值代表如果已有环境变量,其值被覆盖。
和C语言非零值代表true的约定是一样的。
程序设计中的进程控制
进程控制块是进程存在的根据,在进程控制块中有许多的内容可以使用。内容在上个笔记中已经有介绍了。
- 创建进程使用fork()函数,返回
pid_t类型。向父进程返回子进程PID,向子进程返回0.父进程从从那时创建fork()调用的下一行开始。
使用 pid_t 类型的一个好处是,我们不知道 pid_t 的真正类型是什么,因而也就无需考虑这个类型的加法减法有什么意义了,反而落得清静。
进程号相关的方法getpid()与getppid()分别获取当前进程ID和其父进程ID.
进程用户属性getuid(),getgid(),geteuid()与getegid()。
设计父进程和子进程的机制可以稍微从POSIX的规定中看到一些,比如说,因为多用户的环境下所有的进程如果彼此独立的话,对于系统程序设计是一个挑战。
进程控制的确是一个技术思路的问题,而非一个科学上的问题,不仅如此,很多在操作系统原理上学的也是几个技术实现,并不能称上是计算机科学。
进程创建的机制
一个进程的创建明显有三个阶段,需要调用三个重要的系统调用或系统函数。它们分别是fork,exec,wait.当我们在shell里执行一个命令${CMD}的时候,shell会另外创建一个shell进程,新创建的进程映象载入${CMD}映象,${CMD}进程开始运行。SHELL父进程等待${CMD}的执行结束,并获取${CMD}的退出状态值。
对于进程之间的关系来说,以下的属性比较重要:
- 进程的真实UID和GID
- 进程的有效UID和GID
- 进程启动时所在的目录
- 所有由父进程打开的文件描述符
- 环境变量
其中有效UID与有效GID和SUID和SGID有关。对于有些设置了SUID或SGID属性的程序有用,用于一个程序以它的UID和GID启动,而不是当前用户。
操作系统原理讲的很简略的,没有讲过进程之间父子关系这些东西。但是我们可以自己把它理解成人为加上去的属性,不影响进程的调度。
当一个进程死亡的时候,会立即转入僵尸状态,直到它的父进程从进程表中获取它的状态值。然后内核从进程表中删除这个记录。处于僵尸状态的进程是一个无害的子进程,但是我们不能终止它。
倘若在子进程之前,父进程已经死亡,此时这个子进程成为一个孤儿,内核将指派init成为所有孤儿进程的父进程。而特殊之处是中有所有子进程都死亡之后,init才会死亡。
由此我们补充进程的几个阶段当中,一旦执行完毕,并不会直接就从PCB中消失,而是还要经过一系列的处理过程。
僵尸进程和孤儿进程的不同定义。
关于进程有很多的主题,在操作系统当中学习的是进程管理,并发控制,进程调度,在这里学习的是进程控制,虽然也是进程管理的一部分但是没有包含在操作系统原理当中。
对于进程的理解
学习的进程实际上并不是最深层的原理。进程本身的机制还是我们不能了解的。所以就当操作系统为我们提供了进程这样的一个机制。在其中我们进行进程的创建和管理等等工作。
计算机程序运行状态分为内核态和用户态。我们把进程想象成什么比较好呢?在原来的环境下,正如天地始于混沌一样,代码是不分彼此的。后来为了独立出来问题才有了这样的分别。计算机之所以成为计算机而不是计算器,本质在于其理论上的通用性。在原来的操作系统理论当中,引入多道程序设计的目的是提高运行的效率。这个方面的因素不能说没有吧。但相对来说,计算机的功能整合其实发生了很重要的作用。
我们这样反思在计算机初期的发展原理。当时具有中央机与外围机,中央机的作用只是进行所谓的运算。这样一来就有了“单道”的程序环境。随着输入速度的提高,在实践中开始暴露出许许多多的问题。计算机当时没有什么理论性,或者说其理论还是作为业余人员的兴趣在被谈论,并未指导用于计算机的工业制造。工业上真正的变化是因为计算机速度的提高,使输入和输出跟不上计算性能,所以当时的首要问题和现在相反,是计算能力不能满足科学的需求,但这是因为人们不能在输入和输出上跟上计算机。
在这种情况下首要的任务自然是对外围机进行整合,由更加高速的设备和控制逻辑代替某些人工操作。其中蕴含着可以实现通用计算的机会。因此计算机才开始进入整合的时期,才进行输入,运算,存储和输出的整合。在这种情况下当然一个紧密相关的系统的产生就是不可避免的了。既然如此,就不可能不让众多的功能聚在一起。在这种融合下,首先发生的变化就是和运算器,数理逻辑结合在一起。两个发现方向,其一,实用科学的计算需求,其二,人们对于可计算性的思考,之间的结合,是促使计算机原理化的第一动力。
其次才是多道程序设计的产生。这个现象并不是遇然的,前面的理论已经为它的产生做好了准备。多道程序发生的第一因素是中央机和外围机对应不同功能需要使用不同的程序逻辑实现。在实际发展当中,我们还看到60年代的机器,越来越显得中央机太快,因此在我们的输入进行提高的同时,针对计算机空闲的现象,还试图在同一中央机当中添加多道程序,以提高运算器的使用效率。所以多道程序设计的原理在这个时候已经需要成熟。
所以多道程序设计的必要性至少在计算机实体与计算性理论相结合的时候,多道程序就已经实现了。甚至在90年代,我们从WINDOWS作为DOS上的一层外壳,也能看出多道程序设计在萌发阶段的表现。典型的就是操作系统还只是一个软件中间件,程序员和使用者能够清楚地看到硬件结构。
因此我们把进程想象成一群有交往关系的个体。这些个体要生活在同样的一个环境当中,我们需要合理安排,否则因为它们非常笨,会导致什么事情都做不了。它们做不了事情,我们的生命也就失去了意义了。
所以原始的方法是我们在一个较弱的界面上实现一个较强的功能,但是我们拥有了相当程度的知识之后,就要想办法使整个系统有很高的整合度,特别是需要很简单。
以我们的观点来看,计算机是我们认识世界的窗口,其中的有些道理和人类世界是相通的。
进程最基本的一面
我们先讲的是进程机制。所以整个事情有显得没有什么必然的联系。实际上却是还有一些思想在其中的。
首先要看到进程的结构为什么是这样的。底部的细节已经是是为我们屏蔽了的,比如分页机制与分段机制,还有虚拟内存。所以现在我们以编程人员探寻进程的实质为理由学习操作系统。然后我们就可以进行抽象了。
那么程序执行的实质过程是什么呢?编译器封装了很多的细节。所幸可以根据汇编语言来进行推测。其结果就是所谓的数据段,文本段,BSS段。
学习汇编语言的恰当时机个的认为是在学习了一门不太高级的高级语言如C之后,因为C语言的实用性最高,和汇编语言比较容易联系起来,技术性比较强,反观JAVA等就比较复杂,还得学习编译原理之后才会有一个比较明确的理解。所以学习一个面向过程,甚至直接学习汇编,皆是面向机器编程所需,这样以来,可以很好了解操作系统提供特性,执行的过程。更因为学习嵌入式实际上也算其中有操作系统,而其中的代码就和机器比较紧密,所以我们知道操作系统原理应该在裸机上学,在一个屏蔽了机器细节的机器上学习。这个和编译器生成的中间代码是处在同一个层次。
所以整个计算机课程学习的过程就是中间代码可以算是一个分水岭。在其上生长了丰富的思想,在其下生长着各种性能的器件。
数据结构在一门编程语言和操作系统之间。
所以操作系统的进程一开始涉及的是进程段的分配。而内存我们很容易就可以想象成一个线性的地址序列,所以也没有什么困难的地方,即使我们将内存想象成一个一个的空格组成的空间也可以。
接下来就涉及进程空间的分配。内存只有一条,但是我们在编程的时候由于都要转化成操作符和操作数的络式,所以地址就成了一个重要的问题。操作系统的解决手段相当于说,自己当成一个中介,代表每个进程进行分配。这就像一群人有东西要分,但是没有人出来做协调,于是就有一个比较聪明的人站出来给大家出谋划策,当然不是无条件的,它本身也要占据一些资源,不过结果在家都能满意。这说明这人确实有才能,不过目的却并不怎么光明磊落,就像微软在DOS上加了一系列WINDOWS的库让大家用一样。
操作系统利用的是硬件提供的机制,形成的是进程空间这样的一个抽象。背后利用的当然是分段或者分页。我们就当操作系统在执行程序代码的时候自动对程序的地址进行变换,就可以了。或者干脆说,被体系拦住了。
在硬件上我们知道有MMU等机制。
进程因此被操作系统所管理。操作系统使用的数据结构称为进程控制块PCB.
操作系统完成对进程的抽象
但是进程不需要知道它的存在,完成自己的事情就可以了,所以如果把计算机系统看成一个人的生命周期,开关机和保存状态都和生命的存在有关,但是一个进程的灭亡,只是相当于一个细胞的死亡。重要的器官还都存在着。
对于一个人而言,就知道,会玩游戏只是会一个程序,它终究是会死亡的,我们使用计算机的意义不在于那个程序,而在于我们使用那些功能,留下了那些重要的数据。或者说,我们的思想有什么收获。
所以现在我们开始讨论一个有限生命,这就不得不拿出生理学的淡定态度来。
在操作系统看来进程有四个部分,进程控制块,进程程序块,进程内核栈,进程数据块。
一个一个解释这些内容显得好琐碎,这样说吧,一个是进程控制块为操作系统所用,相当于进程的身份信息。其余的内容都相当于进程告诉操作系统哪些在执行时用到,哪些是先注册好的资源。这样系统知道在执行切换的时候如何安排,因为就相当于一次旅游,操作系统要把人送到目的地,人总要说,自己有哪些行李是要带到那个地方使用的。
在unix看来,后三个具有RWX类的权限,各自为R-X RW-RW-。这是因为在unix看来,写不只是向硬盘中去写,内存,甚至寄存器也有这样的概念。所以unix操作系统的权限设定是从头到尾的,任何可能的位置都赋予了相应的权限。
缺少其中任何之一就不称为进程,如果只有前三项就称为线程,如果用户空间共享,则称为用户线程。如果没有用户空间,就称为内核线程。
所以线程的定义是非常不严谨的。
在实现的时候,由于进程程序块里的内容不会被改变,所以系统也可以透明地让其它的进程使用,只是开始的位置可能不同而已,没有必要记录下所有的内容。
与操作系统原理的对应是,程序段代表其中的内容会被取指令所操作。数据段则是进程地应程序加工处理的原始数据或者中间结果。进程控制块是系统统一管理调度进程的一个数据结构。
所以说系统与进程的地位就有些混乱了。有时候操作系统好像过分控制了进程。对此我只能说这种不公正对特进程的现象只能是操作系统自身的原因,一个好的操作系统,应当能够让用户保证对自己来说有意义的程序得到最好最恰当的调度。
先不考虑读写磁盘,只考虑程序执行过程中涉及运算的那个部分。