前言
此文章为读书——《操作系统设计与实现》(Andrew S. Tanenbaum)的摘抄与个人注解。
为方便日后再次阅读。
一 引言
小结
我们可以从两种观点来看待操作系统:资源管理器的观点和扩展机的观点。
从资源管理器的观点来看,操作系统的任务是高效地管理整个系统的各个部分;
从扩展机的观点来看,其任务是为用户提供一台比物理计算机更易于使用的虚拟计算机;
操作系统的历史
代替操作员手动操作的系统
多道程序系统
操作系统的核心
一组系统调用,界定了操作系统能完成的功能。
可分为6大类:
进程的创建与终止
处理信号
针对文件读写
进行目录管理
对信息进行保护
用于时间管理
操作系统的构造方式
整体结构
分层结构
虚拟机
外核
CS模型
操作系统定义/描述
负责管理计算机的所有资源并提供一个可以在其上编写应用程序的平台。在裸机上引入一层软件,让它来管理系统的各个部件,并给上层的用户提供一个易于理解和编程的接口。
作为资源管理器的操作系统
按照自底向上的观点,可以把操作系统作为一个复杂系统的管理者。
操作系统的任务就是在相互竞争的程序之间,如何有序地控制这些硬件设备的分配。
作为扩展机的操作系统
从自顶向下的观点看,操作系统就是为用户提供一台等价的扩展计算机。
它比底层硬件更容易编程。操作系统会通过提供各式各样的服务,用户程序通过系统调用来使用这些服务。
操作系统的历史
1945-1955 真空管和插接板
由专门小组负责设计,制造,编程,操作和维护。
没有OS
1955-1965 晶体管和批处理系统
批处理操作系统:在作业输入室收集满满一盘子的作业,然后用一台比较小、比较便宜的计算机将它们读到磁带上,然后再用一台比较昂贵的计算机来完成真正的计算。
在收集到一批作业后,输入磁带被送到机房里,并装到磁带机上。然偶操作员会装入一个特殊的程序,它将把磁带上的第一个作业读入并运行,输出结果被写入到另一盘磁带上,而不是打印出来。每当一个作业结束,操作系统就会自动读入下一个作业并运行。当这一批作业全部结束后,操作员会取下输入和输出磁带,把输入磁带换成下一批作业,然后把输出磁带拿到一台1401机器上进行脱机处理。
1、程序员将卡片拿到1401处
2、1401将一批作业读到磁带上
3、操作员将输入磁带送至7094处
4、7094进行计算
5、操作员将输出磁带送至1401处
6、1401打印输出结果
典型操作系统有:FMS(FORTRAN Monitor System) IBSYS(IBM 为7094机配备的操作系统)
1965-1980 集成电路和多道程序
多道程序:将内存划分为几个分区,每个分区存放不同的作业,当一个作业正在等待IO操作完成时,另一个作业就可以去使用CPU。
按照这个思路,如果在内存中存放足够多的作业,那么CPU的利用率就可以接近100%。(第三代计算机配备有能保护内存中的各个作业,使它们不会相互攻击和妨碍)
假脱机技术:当一张卡片被拿到机房后,能够很快把其中的作业读入磁盘。
这样当一个作业运行结束后,操作系统就能将一个新作业从磁盘中读出,并装入刚刚空出的内存分区去运行。
分时系统:是多道程序的变种。每个用户都有一个联机的终端。CPU可轮流分配给剩下那些需要得到服务的命令。
MULTICS:设计目的:一台能同时支持数百个分时用户的计算机。
UNIX:Ken Thompson
IEEE制定了一个标准:POSIX。其定义了一组最小的系统调用接口,所有兼容的UNIX版本都必须支持这组函数接口。
1980 个人计算机
CP/M,MS-DOS, Apple-DOS命令行系统。用户通过在键盘上键入命令,来控制操作系统的运行。
Mac OS X,在berkeley UNIX的基础上,构造了一个全新的,Macintosh GUI版本。
UNIX主要用于工作站和其他高档计算机。
操作系统核心概念
操作系统与用户程序之间的接口有操作系统提供的扩展指令集来定义。这些扩展指令系统上被称为系统调用(system call)
进程
一个进程就是一个正在执行的程序,每个进程都有自己的地址空间,也就是一组内存地址,从某个最小值到某个最大值,进程可以读写其中的内容。地址空间中包括可执行程序、程序的数据和它的栈。
在许多操作系统中,一个进程的所有信息均存放在操作系统的一张表中,该表称为进程表。它实际上是一个结构数组或链表,系统中的每个进程都要占用其中的一项。
shell负责从终端读入命令,执行。
一个进程能够创建一个或多个其他的进程(子进程),这些子进程还能继续创建子进程,这样就得到了一棵进程树。
有时,一组相关的进程需要相互工作,共同完成某项任务,这样它们之间就需要相互通信以协调各自的进展,这种通信称为进程间通信。
文件
创建文件,删除文件,读文件,写文件。
目录(directory)
路径名规范 windows \ UNIX /
工作目录(working directory)
文件描述符(file descriptor) 访问权限不够 返回错误码(-1)
文件系统的挂装(mount)
设备文件(special file) : 使I/O设备使用起来更类似于文件。对设备的读写操作就可以使用与普通文件相同的系统调用。设备文件分为块设备文件(block special files)和字符设备文件(charactper special files)。
块设备文件描述的是以随机访问的数据块为单元的设备,如磁盘。在打开一个块设备文件后,可以直接去访问它的某一个数据块。而不用考虑其文件系统的内部结构。
字符设备文件指的是那些以字符流方式进行操作的设备,如打印机,调制解调器等。
管道(pipe):一种用来连接两个进程的虚拟文件。
熟模式(cooked): 字符的删除和终止能正常的工作。
生模式(raw):所有上述功能被取消,每一个字符都是在未加处理的情况下直接发送给程序。用户输入的任何字符都会立刻发送给程序。而不是像熟模式那样,等到输入了一行才发送。全屏幕编辑器常常使用这种模式
cbreak:介于二者之间,但是禁用删除键和终止键。单字符不等一行就发送
正规模式(canonical mode):对应熟模式
非正规模式(noncanonical mode):字符的读操作由两个因素决定,可接受的最小字符的个数,指定的时间。
操作系统的构造方式
整体结构
它的结构实际上就是无结构,整个操作系统是一组函数的集合,其中每个函数在需要的时候可以去调用任何其他的函数。
当使用这种技术时,系统中的每个函数都有一个定义完好的接口,包括它的入口参数和返回值,而相互之间的调用不受任何约束的影响。
操作系统提供的服务的请求过程是这样的:先将参数放入预先确定的地方,如寄存器和栈,然后执行一条特殊的陷阱指令,即访管程序调用(supervisor call) 指令 或 内核调用(kernel call) 指令。这条指令把CPU从用户态切换到内核态,并将控制权交给操作系统。
大部分操作系统有两种状态:内核态供操作系统使用,在该状态下可以执行所有的指令’用户态供用户使用,在此状态下不能执行I/O操作和其他的一些操作。
1.一个主程序,用来调用被请求的服务例程
2.一组服务例程,用来实现相应的系统调用
3.一组工具函数,用来帮助服务例程的实现
分层结构
THE 操作系统的结构
5 操作员
4 用户程序
3 输入 / 输出 管理
2 操作员 - 进程间通信
1 内存和磁鼓管理
0 处理器分配和多道程序
MULTICS 对层次化概念进行了进一步的推广,采取了同心环结构。
内层环比外层环有更高的权限。
虚拟机
系统的核心是一个虚拟机监控程序
在裸机上运行 并提供多道程序功能。
向上层提供若干台虚拟机。
虚拟机仅仅是裸机硬件的精确拷贝,包含内核态/用户态,I/O功能,中断以及真实硬件所具有的所有内容。
由于每台虚拟机都与裸机一样,所以在每台虚拟机上都可以运行任何一种操作系统。
某些运行着的是OS/360, CMS系统
当一个CMS程序执行一条系统调用时,该调用将陷入到其虚拟机的操作系统。然后CMS发出正常的硬件I/O指令来读取它的虚拟磁盘或者做其他的事情来完成这次系统调用。
外核
每个用户都可以获得真实计算机的一个副本,但只能占用部分系统资源。
例如,一台虚拟机可能占用第0个到第1023个磁盘块,而另一台虚拟机可能占用第1024到2047个磁盘块。
在内核态下运行的最底层软件是一个称为外核的程序(exokernel)其任务是为虚拟机分配资源并确保资源的使用不会发送冲突。
外核方案的优点是省掉了一个映射,不再像之前的方案。每台虚拟机认为它们自己有独立的磁盘,从0到最大。
外核只记录虚拟机被分配到了那些资源。
以较少的开销将多道程序与用户操作系统代码分离开来,因为外核需要做的工作仅是使各个虚拟机互不干扰。
C/S
从操作系统中去掉尽可能多的东西,只留下一个最小的内核。
一般采用:将大多数的操作系统功能由用户进程来实现
为了获得某项服务,如读取文件的某个数据块,客户进程将此请求发送给服务器进程,服务器进程随后完成此次操作并将应答信息送回。
某些操作系统功能仅靠用户空间的程序是很难完成的。
解决问题的方法有两个
一个是设立一些运行于内核态的专用服务器进程,它们能访问所有的硬件,但是仍然通过平常的消息机制与其他进程通信。
驱动程序被编译进核心,但是作为一个单独的进程运行。
另一种方法是在内核中建立一套最小的机制(mechanism),而将策略(policy)留给用户空间中的服务器进程。
驱动程序运行在用户空间,然后通过特殊的内核调用来请求读、写I/O寄存器。
二 进程
一个进程是某种类型的活动,它有程序,输入,输出及状态。单个处理机被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转为为另一个进程提供服务。
进程创建的四个主要原因
系统初始化
正在运行的一个进程执行了创建进程的系统调用
用户请求创建一个新进程
批处理作业的初始化
守护进程(daemon):处于后台用来处理网页、打印之类的活动的进程。
新进程都是由于一个已经存在的进程执行了进程创建的系统调用而创建的。
进程终止的原因
1 正常退出 自愿
2 出错退出 自愿
3 严重错误 非自愿
4 被其他进程杀死 非自愿
进程的状态
运行态 (Running) 在该时刻实际占用处理机
就绪态 (Ready) 可运行 因为其他进程正在运行而暂时被挂起
阻塞态 (Blocked) 除非某种外部事件发生,否则不能运行
当进程发现自己无法继续运行下去时则发生转换1 从运行态转换为阻塞态。进程需要执行一个系统调用block 或 pause ,来进入阻塞态。
转换关系2,3 (调度器选择另一个进程_运行态到就绪态_,调度器选择该进程—就绪态到运行态—是由进程调度器引起的)调度器的主要内容是决定哪个进程应当运行,以及它应运行多长时间。
当进程等待的一个外部事件发生时,发生转换4,(从阻塞态进入就绪态)。
进程的实现
为了实现进程模型,操作系统维持着一张表格。
进程表 process table
一个进程占用一个进程表项(pcb process control block 进程控制块)
进程的状态:程序计数器,栈指针,内存分配情况,打开文件状态,统计和调度信息,定时器和其他信号,以及进程由运行态到就绪态切换时所必须保存的其他信息。
硬件压栈程序计数器等
硬件按中断向量下载新的程序计数器
汇编语言程序存储寄存器值
汇编语言程序设置新的栈
C语言中断服务例程运行
消息传递代码对等待的就绪任务进行标识
调度器决定哪个进程是下一个将运行的进程
C程序段返回汇编代码
汇编语言程序开始运行当前进程
线程
在相同地址空间中有多个控制流并行地运行,就像它们是单独的进程一样。这些控制流被称为线程(thread),有时也叫轻量线程(lightweight process);
当同一个地址空间有多个线程后,需要一张线程表,每个线程占用一项。针对每个线程的信息——程序计数器,寄存器值及状态。
将进程表放在内核态管理还是放在用户态管理很复杂。现实中各种方式都被采取了。
进程间通信
竞争条件
两个多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序。race condition。
临界区
互斥(mutual exclusion)以某种手段确保当一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作。
在一部分时间内,进程忙于做内部计算或其他一些不会引发竞争条件的操作。在某些时候进程可能会访问共享内存或共享文件。这里把对共享内存进行访问的程序片段称为临界区或临界段(critical region / critical section)
解决竞争的好的解决方案 应当满足以下4个条件
任何两个进程不能同时处于临界区
不应对CPU的速度和数目做任何假设
临界区外的进程不得阻塞其他进程
不得使进程在临界区外无休止地等待
解决竞争的方案
关闭中断
把关闭中断的权力交给用户程序不是很明智,而且关闭中断更容易造成竞争。且这种解决方案对于运行在多核处理器的程序没有效果。
锁变量
采取加锁的办法没法避免竞争。因为锁本质上也是一个内存空间,同样可能存在竞争。
严格交替法
/*
process 0
*/
while(TRUE) {
while(turn != 0); /*loop*/
critical_region();
turn = 1;
noncritical_region();
}
/*
process 1
*/
while(TRUE) {
while(turn != 1); /*loop*/
critical_region();
turn = 0;
noncritical_region();
}
采取忙等待的方式(busy waiting)。尽可能避免忙等待,因为这样实际上是不断地做死循环。会浪费CPU时间。只有在有理由预期等待时间很短时才使用忙等待。
适用于忙等待的锁称为自旋锁(spin lock)
但是这样同样会出现问题,不再是竞争的问题。而是无法进入的问题。上述方式其实就是要求进程0 1 严格交替进入临界区。但是如果不按照这样的方式,则会被阻塞。比如进程0进入后置为turn置为1,进程1再进入,turn置为0.但我这个时候0不再需要进入了,那么1就会被锁在外面。而且对于有更多个进程共享该区域,此种算法会过于复杂。
Peterson解决方案
/*
下面这份代码仅对n = 2 有效
如果有更多的N 请修改other
这段代码的核心在于在循环时 要求了请求仍然存在
*/
#define FALSE 0
#define TRUE 1
#define N 2 /*进程数*/
int turn; /*轮到谁了*/
int interested[N]; /*所有值初始为0*/
void enter_region(int process) { /*进程号为0或1*/
int other; /*另一个进程的进程号*/
other = 1 - process; /*另一个进程*/
interested[process] = TRUE;/*标识 希望进入 临界区*/
turn = process; /*设置标志位*/
while(turn == process
&& interested[other] == TRUE); /*空语句*/
}
void leave_region(int process) { /*process : 即将离开临界区的进程*/
interested[process] = FALSE; /*标识即将离开临界区*/
}
TSL指令
从硬件上进行上锁。规定写操作是原子操作,不能被打断。(直接锁住总线)
enter_region:
TSL REGISTER, LOCK ;复制LOCK到寄存器 并将LOCK 置为1
CMP REGISTER, #0
JNE enter_region
RET
leave_region:
MOVE LOCK, #0
RET
睡眠和唤醒
上述Peterson解法和TSL都是正确的,但是它们都是忙等待的。会无端消耗CPU的时间。而且有可能产生优先级反转的问题(priority inversion problem),比如有两个进程,H,L,h的优先级高于l。但是h要执行必须等待l向某块写入,修改数据,可是h一来,l就没办法被调度,也就无法离开临界区。
这些解法的本质就是当一个进程想进入临界区时,先检查是否允许进入,若不允许,则进程将忙等待,直到许可为止。
sleep, wakeup 进程间通信原语。它们在无法进入临界区的时候先阻塞,而不是忙等待。
sleep 系统调用 将引起调用进程阻塞,即被挂起,直到另一进程将其唤醒。wakeup调用有一个参数,即要被唤醒的进程。
另外一种方法是sleep和wakeup都有一个参数,用来匹配sleep和wakeup的内存地址。
生产者消费者问题 有界缓冲问题
两个进程共享一个公共的固定大小的缓冲区。其中一个是生产者,负责将信息放入缓冲区;另一个是消费者,负责从缓冲区取出信息。
#define N 100
int count = 0;
void producer(void) {
int item;
while(TRUE) {/*无限循环*/
item = produce_item();/*进行生产*/
if(count == N) sleep();/*缓冲区满 进入睡眠*/
insert_item(item);/*将数据放入缓冲区*/
count = count + 1;/*缓冲区数据项加1*/
if(count == 1) wakeup(consumer);/*缓冲区有数据了 唤醒消费者*/
}
}
void consumer(void) {
int item;
while(TRUE) {
if(count == 0) sleep(); /*缓冲区为空 不能再消费 睡眠*/
item = remove_item(); /*取走数据*/
count = count - 1; /*缓冲区数据项减1*/
if(count == N - 1) wakeup(producer); /*缓冲区不再满 唤醒生产者*/
consume_item(item); /*消费数据*/
}
}
但是上述还是可能会出现问题,即当消费者发现count为0时,准备执行sleep的时候,调度器选择执行生产者,然后生产者一直执行到满,中间也去唤醒过消费者,但是消费者这个时候还没有sleep,所以唤醒没有效果。然后生产者生产到缓冲区满了,它也睡眠了。结果调度器这个时候再执行消费者,消费者就睡眠了。两个都睡眠了,无法再度运行。
可以添加唤醒等待位去改善两个进程的问题。但是如果有多个,就会失效。这种方式只能是避免问题,并没有解决问题。
信号量
引入新的变量类型,信号量(semaphore)。一个信号量的值可以为0,表示没有积累下来的唤醒操作;或者正值,表示有一个或多个被积累下来的唤醒操作。
down,和up(一般化的sleep, wakeup)(最开始为p v),保证整体操作为 原子操作(atomic action)即保证一旦一个信号量操作开始,则在操作完成或阻塞之前其他的进程均不允许访问该信号量。
#define N 100 // 缓冲区内槽数
typedef int semaphore; // 定义信号量
semaphore mutex = 1; // 控制对临界区的访问
semaphore empty = N; // 记录缓冲区内空的草书
semaphore full = 0; // 记录缓冲区内满的槽数
void producer(void) {
int item;
while(TRUE) {
item = produce_item();
//递减空槽数 开mutex
down(&empty);
down(&mutex);
insert_item(item);
//关 mutex 递增full
up(&mutex);
up(&full);
}
}
void consumer(void) {
int item;
while(TRUE) {
//取走 递减full 开mutex
down(&full);
down(&mutex);
item = remove_item();
//取完 递增empty 关mutex
up(&mutex);
up(&empty);
consume_item(item);
}
}
互斥
如果不需要计数的信号量,则可以只使用mutex,互斥。0表示解锁,其他表示加锁。
管程
//死锁 deadlock 信号量的缺点 两行顺序反了就死锁
#define N 100 // 缓冲区内槽数
typedef int semaphore; // 定义信号量
semaphore mutex = 1; // 控制对临界区的访问
semaphore empty = N; // 记录缓冲区内空的草书
semaphore full = 0; // 记录缓冲区内满的槽数
void producer(void) {
int item;
while(TRUE) {
item = produce_item();
//递减空槽数 开mutex
//注意这里的顺序
//如果先锁上 再去判断能不能放 会出现死锁的问题
down(&mutex);
down(&empty);
/*
因为锁上之后 发现缓冲区满了 放不下 只能 先休眠
但是这个时候没有解锁
消费者由于缓冲区被锁住了 只能等待
也就没办法再给拿走缓冲区的东西 给生产者解锁了
*/
insert_item(item);
//关 mutex 递增full
up(&mutex);
up(&full);
}
}
void consumer(void) {
int item;
while(TRUE) {
//取走 递减full 开mutex
down(&full);
down(&mutex);
item = remove_item();
//取完 递增empty 关mutex
up(&mutex);
up(&empty);
consume_item(item);
}
}
typedef struct {
int value;
ListOfProcess *L;
}semaphore;
void wait(static semaphore s) {
s.value--;
if(s.value < 0) {
block(s.L);
}
}
void signal(static semaphore s) {
s.value++;
if(s.value <= 0) {
wakeup(s.L);
}
}
管程不能被管程外的任何过程访问管程内的数据结构。
任意时刻管程中只能有一个活跃的进程。
需要让进程在无法进行时被阻塞。采取了条件变量(condition variable) 和相关的两个操作 wait 和 signal.
当管程过程发现它无法继续时,它在某些条件变量上执行wait,如full, 这个动作引起调用进程阻塞。此后,它允许之前被阻塞的一个进程现在进入管程。
消费者可以通过对其伙伴正在其上等待的一个条件变量执行signal操作唤醒伙伴进程。但是为了防止管程中通过signal出现多个进程。在此规定:
signal只能作为一条管程过程的最后一条语句。
(还有其他两种方法——让新唤醒的进程运行——让发送信号者继续运行,只有在发送信号者退出管程后,才允许等待的进程开始执行)
//使用伪 pascal 语言描述的 管程
monitor ProducerConsumer
condition full, empty;
integer count;
procedure insert(item : integer);
begin
if count = N then
wait(full);
insert_item(item);
count := count + 1;
if(count = 1) then
signal(empty);
end;
function remove : integer;
begin
if count = 0 then
wait(empty);
remove = remove_item;
count := count - 1;
if count = N - 1 then
signal(full);
end;
count := 0;
end monitor;
procedure producer;
begin
while true do
begin
item = produce_item;
ProducerConsumer.insert(item);
end
end;
procedure consumer;
begin
while true do
begin
item = ProducerConsumer.remove;
consume_item(item);
end
end;
wait ,signal 和之前的sleep , wakeup 很类似;但是存在一个关键的区别, 由于管程内部是互斥的,所以不用担心一个进程想wait时,另外一个进程去signal它,但是漏掉了这个signal信号。
当将关键字 synchronized 加入到方法声明中, java 保证一旦某个线程 执行该方法, 就不允许其他线程执行该类中的任何其他方法。
但是java实现的管程与经典的管程相比,没有条件变量,java提供了两个过程 wait, notify.分别和 sleep, wakeup等价。但是它们在同步方法内部使用,不受到竞争态条件的影响。
缺点
除了少数几个编程语言实现,没有编程语言实现。实现太过于困难。编译器如何知道哪些过程属于管程内部,哪些不属于管程内部,这也是一个问题。
消息传递 message passing
进程间通信的方法使用两条原语。send 和 receive.
像信号量一样,是系统调用,不像管程是语言组件。容易被添加到库里面。
send(destination, &message); receive(source, &message)
前一个调用向一个给定的目标发送一条消息。
后一个调用从给定的源(或者任意源 如果接收方不介意的)接收一条信息。
如果没有消息可用,则接收方可能一直阻塞到一条消息到达。或者立即返回,带回一个错误码。
消息传递系统的设计要点
为防止消息丢失,发送方和接收方需要达成一致。一旦信息被收到,接收方马上发回一条特殊的应答消息。如果发送方在一段时间内未受到应答。则进行重发。(采取应答的方式 acknowledgement)
当信息被正常接收,但是应答信息丢失。为了让接收者区分新消息和老消息,可以采取为消息嵌入一个连续的序号来解决,如果序号一致,就直接接收即可。
消息系统还需要解决进程命名的问题,这样才能明确在send和receive调用中所指定的进程。
身份认证也是一个问题,客户如何知道它是在与一个真正的文件服务器通信,而不是一个冒充者。
如何使消息传递变得高效。即使是在同一台机器上,将消息从一个进程发送到另一个进程比信号量和管程操作慢。
#define N 100 /*缓冲区槽数*/
void producer(void) {
int item;
message m; /*消息缓冲区*/
while(TRUE) {
item = produce_item(); /*生产*/
receive(consumer, &m); /*等待空消息到达*/
build_message(&m, item); /*构造消息发送*/
send(consumer, &m); /*向消费者发送一数据项*/
}
}
void consumer(void) {
int item, i;
message m;
for(int i = 0; i < N; ++i) send(producer, &m); /*发送N条空消息*/
while(TRUE) {
receive(producer, &m); /*收到一条包含数据的消息*/
item = extract_item(&m); /*从消息中取出数据*/
send(producer, &m); /*应答*/
consume_item(item); /*消费*/
}
}
如何对消息编址?
1.为每个进程分配一个唯一的地址,按进程为消息指定地址。
2.引入新的数据结构,信箱(mailbox)。信箱就是用来对一定数量的消息进行缓冲的地方,典型的情况是在信箱创建时确定消息的数量。当使用信箱时,send和receive调用的地址参数使用信箱,而不是进程。当一个进程试图向一个满的信箱发消息时,它将被挂起,直至信箱内有消息被取走而为新消息腾出空间。 对于生产者消费者问题,生产者和消费者都创建足够容纳N条消息的信箱。生产者向消费者信箱发送包含数据的信箱,消费者向生产者发送应答信息。。使用信箱时,缓冲机制很清楚:目标信箱容纳那些被发送但尚未被目标进程接收的信息
同样可以去掉缓冲,执行send后,就阻塞发送进程,直到receive发生。同样的,当receive先被执行,接收方一直阻塞到send发生。这种策略叫做聚合(rendezvous)。实现容易,但是降低灵活度,因为要发生和接收步步紧接。
MINIX3和UNIX中用户进程间采用管道进行通信。采用信箱的消息系统和管道机制的实际区别是 管道没有预先确定边界。可能你向管道中放入了10条100字的信息,用户一次读了1000字,就会把这些信息全读了。当然如果进程能够一致,读取固定大小的信息,或者加入特定的结束标识符,就不会出现多读的问题。
个人总结
进程通信出现问题的关键点在于操作系统为了提高CPU利用率,会去挂起一些进程,也就是说某些指令的执行并不是一致的。而这无法由用户程序控制。对于没有访问相同存储的程序,这没问题,但是对于那些访问的,就会出现大问题。这些程序本该按照一定顺序去访问,但是由于这些进程可能会被挂起来,所以会导致按照我们给定的顺序运行的程序,实际上访问的情况则不是这般。所以最开始就考虑直接把中断给关了,但是这样太过于危险,而且容易导致其他的问题。采取加锁的办法同样没有解决,因为锁也是一个共享量,并没有改变问题的本质。采取严格交替法,会导致程序跑不下去。采取Peterson方法可以解决,但是由于是忙等待,会浪费CPU的性能。TSL是直接从硬件上确保读取操作是原子的,(直接锁总线)。如果采取睡眠的方式,可以不浪费CPU的性能,但是可能会让两个进程都进入睡眠,一睡不起了。采取信号量的方式可以比较好的解决这个问题,但是只要把两行代码的位置写反了,就会出现死锁的问题。互斥只是信号量的简化版本。管程也能比较好地解决这个问题,但是没几个编程语言实现。利用消息传递同样会有一堆问题,比如信息丢失,无限重发,信息过大,信息接收。实际上采取的管道,把要传输的信息放入管道,但是管道和信箱还不一样,管道没有限制读取大小,为了去区分每条信息,要么每次读取固定大小的信息,要么加特殊的标识符,或者其他的什么方式。