在操作系统中,进程之间怎么通信是如何通信的

Researcher on Stochastic Process, Variational Inference, Computer Vision and Machine Learning.
操作系统 -- 进程间通信机制
基本概念与分类
之前的内容中我们分析了进程的相关概念,接下来我们考虑一下进程之间的交互,也就是本文中主要谈到的进程间通信(IPC – Interprocess Communication)。
进程间的通信可以分为两种模型,分别为:
共享内存(Shared Memory)
消息传递(Message Passing)
以下可以用一幅图将上述两者的异同进行直观地展现:
我们可以看到,左边的是消息传递的基本模型,进程A如果想要与进程B建立通信,那么首先将相关的信息传给内核,然后内核将其转发给进程B; 而通过共享内存的方式进行的进程间通信,则是开辟一段新的贡献资源段,然后两个进程之间进行数据信息等的交互,该过程不受CPU的控制。
实践中遇到的基本问题
上述讲解的进程通信的两种方式,主要是宏观上的内容。那么其微观上可能存在怎样的问题呢?
譬如通信传输的数据的格式如何定义,通信是否采取广播机制等等。该部分内容与计算机网络通信有很多类似的地方,之后进行分析。
下面着重分析几个典型的通信分类方式。
直接通信与间接通信
直接通信,顾名思义,就是两个进程之间的通信不需要任何媒介,在两个进程之间建立一条通路,或者采用匿名的方式。相对而言较为简单,但是当进程较多时,进程间通信的管理较为复杂。
另外的一种通信方式是间接通信,间接通信此时需要一个媒介。而这个媒介我们在计算机系统中称之为mailboxes。每个mailbox的信息输入与输出,我们称之为port(端口)。
某一进程通过mailbox发送信息,对应同端口的进程才能接受到此信息。
同步通信与异步通信
同步通信包含两个方面的内容,一方面是指消息发出的同步性,也就是说当某个进程无论采取直接还是间接的方式发出消息时,都会进入挂起状态,等待消息被接收;另外一方面消息的接收方一直处于等待接收有效消息的状态,而不会进行其他处理。
异步通信同样包含类似于上述两个维度的内容。一方面是指消息发出的异步性,也就是通信发起进程发出消息之后,不进入挂起状态,继续执行后面的操作;而对消息接受进程而言,并不用等待接收到有效的消息。
进程间通信(IPC) 是操作系统中极为重要的概念,其两种模型都有着广泛的应用。关于其应用的实例可以看到在各大操作系统中普遍存在;另外关于其实现的具体细节,将在之后的试验中进行具体分析。
没有更多推荐了,进程的概念
进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放。可以认为进程是一个程序的一次执行过程。
进程通信的概念
进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。
进程通信的应用场景
数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程通信的方式
管道( pipe ):
管道包括三种:
普通管道PIPE: 通常有两种限制,一是单工,只能单向传输;二是只能在父子或者兄弟进程间使用.
流管道s_pipe: 去除了第一种限制,为半双工,只能在父子或兄弟进程间使用,可以双向传输.
命名管道:name_pipe:去除了第二种限制,可以在许多并不相关的进程之间进行通讯.
信号量( semophore ) :
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列( message queue ) :
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号 ( sinal ) :
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存( shared memory ) :
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
套接字( socket ) :
套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
各进程间通信的原理及实现
管道是如何通信的
管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。
管道是如何创建的
从原理上,管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。最开始的时候,上面的两个箭头都连接在同一个进程Process 1上(连接在Process 1上的两个箭头)。当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。随后,每个进程关闭自己不需要的一个连接 (两个黑色的箭头被关闭; Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了如上图的PIPE。
管道通信的实现细节&在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。如下图
有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。
关于管道的读写
管道实现的源代码在fs/pipe.c中,在pipe.c中有很多函数,其中有两个函数比较重要,即管道读函数pipe_read()和管道写函数pipe_wrtie()。管道写函数通过将字节复制到 VFS 索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。
当写进程向管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的 file 结构。file 结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查 VFS 索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:
内存中有足够的空间可容纳所有要写入的数据;
内存没有被读程序锁定。
如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 VFS 索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。
管道的读取过程和写入过程类似。但是,进程可以在没有数据或内存被锁定时立即返回错误信息,而不是阻塞该进程,这依赖于文件或管道的打开模式。反之,进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放。
Linux函数原型
#include &unistd.h&
int pipe(int filedes[2]);
filedes[0]用于读出数据,读取时必须关闭写入端,即close(filedes[1]);
filedes[1]用于写入数据,写入时必须关闭读取端,即close(filedes[0])。
程序实例:
int main(void)
int fd[2];
char line[MAXLINE];
if(pipe(fd)
由于基于fork机制,所以管道只能用于父进程和子进程之间,或者拥有相同祖先的两个子进程之间 (有亲缘关系的进程之间)。为了解决这一问题,Linux提供了FIFO方式连接进程。FIFO又叫做命名管道(named PIPE)。
FIFO (First in, First out)为一种特殊的文件类型,它在文件系统中有对应的路径。当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统(file system,命名管道是一种特殊类型的文??,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在。)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。FIFO的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接
函数原型:
#include &sys/types.h&
#include &sys/stat.h&
int mkfifo(const char *filename, mode_t mode);
int mknode(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0 );
其中filename是被创建的文件名称,mode表示将在该文件上设置的权限位和将被创建的文件类型(在此情况下为S_IFIFO),dev是当创建设备特殊文件时使用的一个值。因此,对于先进先出文件它的值为0。
程序实例:
#include &stdio.h&
#include &stdlib.h&
#include &sys/types.h&
#include &sys/stat.h&
int main()
int res = mkfifo("/tmp/my_fifo", 0777);
if (res == 0)
printf("FIFO created/n");
exit(EXIT_SUCCESS);
Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)
什么是信号量
为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法。比如在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。
信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。
信号量的工作原理
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。
Linux的信号量机制
Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。
semget函数
它的作用是创建一个新信号量或取得一个已有信号量,原型为:
int semget(key_t key, int num_sems, int sem_flags);
第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。
第二个参数num_sems指定需要的信号量数目,它的值几乎总是1。
第三个参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
semget函数成功返回一个相应信号标识符(非零),失败返回-1.
它的作用是改变信号量的值,原型为:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
sem_id是由semget返回的信号量标识符,sembuf结构的定义如下:
struct sembuf{
short sem_
semctl函数
int semctl(int sem_id, int sem_num, int command, ...);
如果有第四个参数,它通常是一个union semum结构,定义如下:
union semun{
struct semid_ds *
unsigned short *
前两个参数与前面一个函数中的一样,command通常是下面两个值中的其中一个&SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。&IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
阅读(...) 评论()【构建操作系统】进程间通信有问题,上知乎。知乎作为中文互联网最大的知识分享平台,以「知识连接一切」为愿景,致力于构建一个人人都可以便捷接入的知识分享网络,让人们便捷地与世界分享知识、经验和见解,发现更大的世界。在本系统中发现了两个BUG:,限于自己水平比较渣,没法解决这两个BUG,那么OS系列就告一段落了,纵观整个过程,还是对IPC机制的理解帮助比较大,这种思想可以运用于实践中,如管道、套接字、流的实现。---------------------------------------------------------------------------有童鞋问如何从零开始学习代码,我写了点总结:首先要了解操作系统的知识,从零开始指的是代码从零开始,所以相关知识的基础也是不可或缺的。要了解系统的启动过程, 。从启动过程来分析代码,首先是BIOS。(1)然后是boot文件夹下的三个asm文件,它们完成读软盘、设置GDT、加载内核数据至0x100000,并跳转到0x100000处执行内核代码;(2)随后根据main.c中初始化的顺序学习:(I)vga显示输出、(II)gdt分段管理、(III)idt中断向量设置、(IV)isr中断处理程序、(V)pmm获取可用内存并按4KB记录并保存至栈中,完成物理页框的申请与释放、(VI)vmm分页管理、(VII)sys初始化系统调用函数、(VIII)proc多进程管理,进程切换。(3)从零开始添加功能,如先写好boot.asm,能够开机并用中断打印一个A,没问题之后,再设置gdt,vga以及中断管理,这时运行一个int 3看看有没有打印结果出来;这样每次写的代码都能够独立运行。如果是把所有代码下载下来运行的话,效果很不错,但是会不知道从读起。现在要做的就是精简代码,只保留核心功能,摒弃一切杂念。(4)那么写代码过程中有几个情况要处理:(I)不明所以,这时可以看注释或是上网查资料(II)这里代码是不是有问题?先放着不管(III)运行出错了,那就debug吧。其中debug的时间比较久,我做到IPC这一部分起码编程时间100小时以上,不包括日常吃饭时想的时间,所以不要慌,操作系统代码需要细嚼慢咽,急也急不得。debug可能用时比较久,这时比较纠结、想放弃、头脑混乱,因为好多bug真不知道哪冒出来的,用qemu源码级调试好处也有限。其实这一关过不了,对操作系统的理解水平也就触到天花板了,也就是只是理解了书上的思想,而没有将代码和思想结合起来。写代码、山寨别人的代码、东拼西凑,不管用什么方式,只要把bug除了就皆大欢喜。(5)这样做有好处:代码、思路,所有的细节全部load到了脑子里,要啥有啥,也就是真正理解了内核,可以举一反三,并自己更改代码,添加功能。(6)要有毅力、有恒心,能吃苦,成功不是一蹴而就,别看我写的代码运行效果挺好,我起码debug了100h以上,每天打底调试6小时,最后才能bug弄好。真正自己花时间花工夫写的代码,才会长久的留自己的脑海里。--------------------------------------------------------------------写在前面Release:总结下当前的进度:引导、GDT、中断、虚页:花了两三天多进程:花了一周进程间通信:花了两三天90%的时间花在了debug上,除完所有bug,已经对整个实现机制了如指掌。所以,debug的过程也是一个学习的过程,虽然效率不高,但是效果好(就是单词抄写百遍的效果啦)。90%操作系统的内容 = 枯燥乏味,而10%(余下的内容) = 精妙绝伦目前感受设计精妙的地方:进程切换机制(时钟分派),进程的父子关系,进程的状态。代表:fork,wait,sleep,exit,kill,如wait和exit的搭配为例,进程的销毁是惰性的(销毁操作集中于父进程中的wait)。进程间通信机制。通信分异步和同步,那么这里实现的是同步IPC,比较简单,用不着写队列。这是微内核的基础,它将某些同类别的系统调用转化为唯一一个系统调用0x80。如何区分不同功能的调用呢?就是在调用int 0x80前将参数入栈,参数有通信方式(SEND/RECEIVE),通信对象(正数表示pid,-1表示任意对象),消息结构。这些精妙的地方只能通过代码去体会。用户代码static void halt() {
while (1);
static int delay() {
volatile int i;
for (i = 0; i & 0x1000000; i++);
void user_main() {
i = call(SYS_FORK);
if (i != 0) {
sys_tasks0();
call(SYS_WAIT);
while (1) {
i = sys_ticks();
printk("!! proc#%d received tick '%d'\n", proc2pid(proc), i);
printk("halt...\n");
void sys_tasks0() {
extern uint32_t tick;
MESSAGE msg;
while (1) {
send_recv(RECEIVE, TASK_ANY, &msg);
int src = msg.source;
switch (msg.type) {
case SYS_TICKS:
msg.RETVAL = tick;
printk("!! proc #%d sent tick '%d'\n", proc2pid(proc), tick);
send_recv(SEND, src, &msg);
assert(!"unknown msg type");
static int sys_ipc_call(int type) {
MESSAGE msg;
reset_msg(&msg);
msg.type = type;
send_recv(BOTH, TASK_SYS, &msg);
return msg.RETVAL;
int sys_ticks() {
return sys_ipc_call(SYS_TICKS);
进程的结构说到IPC,可能会想到pipe、clipboard、windows message、socket、shared memory、file等方式,然而没法实现那么多(括弧笑,所以跟着书上走吧~目前只是抄了书上的代码(希望尽快看到结果),还没时间去分析IPC机制的代码。首先,我们创建的进程proc的结构有:struct proc {
/* KERNEL */
struct interrupt_frame *fi;
// 中断现场
volatile uint8_t pid;
uint32_t size;
// 用户空间大小
uint8_t state;
// 进程状态
char name[PN_MAX_LEN];
// 进程名称
pde_t *pgdir;
// 虚页目录(一级页表)
char *stack;
// 进程内核堆栈
struct proc *parent;
int8_t ticks;
int8_t priority;
int p_flags;
MESSAGE *p_msg;
int p_recvfrom;
// 接收消息的进程ID
int p_sendto;
// 发送消息的进程ID
int has_int_msg;
// nonzero if an INTERRUPT occurred when the task is not ready to deal with it.
struct proc *q_sending;
// queue of procs sending messages to this proc
struct proc *next_sending;
// next proc in the sending queue (q_sending)
结构很复杂吧?不过,如果是一步步实现功能,往里添加的话,其实也不算多。PCB结构:进程相关信息:名称,ID,状态,父进程调度信息:中断现场,时间片,优先级内存信息:代码空间大小,页表,内核堆栈IPC:消息收发状态flags,消息msg,收发进程ID,消息队列(链表)进程的切换:主进程死循环,通过时钟中断,进行调度中断时保存现场(即proc-&fi),如果是最外层中断,那么起调度作用(此时k_reenter=0),内层中断k_reenter&0,中断结束后iret返回,从proc-&fi中恢复现场,此时修改相应特权级微内核架构原版的linux中有一堆的系统调用,那么微内核架构与此不同,它将系统调用按功能划分开来,如分成内存管理、文件管理等,建立专门的系统级进程来负责系统调用。那么,也就是 “ring1级系统服务进程” 与 系统 打交道(通过系统调用),而我们的“ring3级用户进程” 只要与 “ring1级系统服务进程” 通信就可以了。结论:ring3用户级 &=& ring1服务级 &=& ring0系统级,ring1就像中介一样,而ring0与ring3可以素不相识。这样,微内核架构(相当于微服务)抽象出一个服务层sys_task,降低了耦合度。ring1与ring0打交道:通过系统调用即可ring1与ring3打交道:维护一个消息等待队列进程间通信主要分两个函数msg_send和msg_receive。收/发消息有几种情况:系统服务监听消息:没消息时休眠,来消息时唤醒系统服务发送消息:仅当系统服务收到用户的SEND消息后,被唤醒,随后用户再发送RECV消息,系统服务收到后设置msg用户进程发送消息:系统服务不可用,用户进程堵塞,直到系统服务处理完其他任务,从等待队列中取出用户进程,并唤醒用户进程用户进程接收消息:当用户进程发送SEND消息收到回应后,会再发送一个RECV消息,等待系统服务响应并设置msg,最后用户进程拿到设置后的msg这里的操作挺像TCP的握手操作的,归纳起来的同步通信模型:系统服务:监听消息(就像web服务器一样),单线程: while(1){recv(), send()}用户进程:像web客户端一样,单线程:{send() recv()}消息队列统一调用接口:int send_recv(int function, int src_dest, MESSAGE* msg)
int ret = 0, caller;
caller = proc2pid(proc);
if (function == RECEIVE)
memset(msg, 0, sizeof(MESSAGE));
switch (function) {
case BOTH: // 先发送再接收
ret = _sendrec(SEND, src_dest, msg, caller);
if (ret == 0)
ret = _sendrec(RECEIVE, src_dest, msg, caller);
case SEND:
case RECEIVE:
ret = _sendrec(function, src_dest, msg, caller);
assert((function == BOTH) ||
(function == SEND) || (function == RECEIVE));
return ret;
对于系统服务service:send_recv(RECV, TASK_ANY, msg) 监听消息处理msgsend_recv(RECV, msg.source, msg) 发送消息给客户端对于客户端程序client:初始化msgsend_recv(SEND, SYSTASK_ID, msg) 发送消息给系统服务send_recv(RECV, SYSTASK_ID, msg) 堵塞并接收消息处理msg一、发送消息int msg_send(struct proc* current, int dest, MESSAGE* m)
struct proc* sender = current;
struct proc* p_dest = npid(dest); /* proc dest */
/* check for deadlock here */
if (deadlock(proc2pid(sender), dest)) {
printk("DEADLOCK! %d --& %d\n", sender-&pid, p_dest-&pid);
assert(!"DEADLOCK");
if ((p_dest-&p_flags & RECEIVING) && /* dest is waiting for the msg */
(p_dest-&p_recvfrom == proc2pid(sender) ||
p_dest-&p_recvfrom == TASK_ANY)) {
memcpy(va2la(dest, p_dest-&p_msg),
va2la(proc2pid(sender), m),
sizeof(MESSAGE));
p_dest-&p_msg = 0;
p_dest-&p_flags &= ~RECEIVING; /* dest has received the msg */
p_dest-&p_recvfrom = TASK_NONE;
unblock(p_dest);
else { /* dest is not waiting for the msg */
sender-&p_flags |= SENDING;
sender-&p_sendto = dest;
sender-&p_msg = m;
/* append to the sending queue */
struct proc * p;
if (p_dest-&q_sending) {
p = p_dest-&q_sending;
while (p-&next_sending)
p = p-&next_sending;
p-&next_sending = sender;
p_dest-&q_sending = sender;
sender-&next_sending = 0;
block(sender);
解释:判断是否死锁,即A-&send-&B,同时B-&send-&A若对方正在监听消息,则将msg拷贝到对方的p_msg中,并消除对方的监听与堵塞状态若对方不在监听消息(可能在处理其他事务),则将发送方PCB指针插入到对方的q_sending队列中,并将发送方堵塞以等待接收方的回应二、接收消息int msg_receive(struct proc* current, int src, MESSAGE* m)
struct proc* p_who_wanna_recv = current;
struct proc* p_from = 0; /* from which the message will be fetched */
struct proc* prev = 0;
int copyok = 0;
if ((p_who_wanna_recv-&has_int_msg) &&
((src == TASK_ANY) || (src == INTERRUPT))) {
/* There is an interrupt needs p_who_wanna_recv's handling and
* p_who_wanna_recv is ready to handle it.
MESSAGE msg;
reset_msg(&msg);
msg.source = INTERRUPT;
msg.type = HARD_INT;
assert(m);
memcpy(va2la(proc2pid(p_who_wanna_recv), m), &msg,
sizeof(MESSAGE));
p_who_wanna_recv-&has_int_msg = 0;
/* Arrives here if no interrupt for p_who_wanna_recv. */
if (src == TASK_ANY) {
/* p_who_wanna_recv is ready to receive messages from
* TASK_ANY proc, we'll check the sending queue and pick the
* first proc in it.
if (p_who_wanna_recv-&q_sending) {
p_from = p_who_wanna_recv-&q_sending;
copyok = 1;
/* p_who_wanna_recv wants to receive a message from
* a certain proc: src.
p_from = npid(src);
if ((p_from-&p_flags & SENDING) &&
(p_from-&p_sendto == proc2pid(p_who_wanna_recv))) {
/* Perfect, src is sending a message to
* p_who_wanna_recv.
copyok = 1;
struct proc* p = p_who_wanna_recv-&q_sending;
while (p) {
assert(p_from-&p_flags & SENDING);
if (proc2pid(p) == proc2pid(npid(src))) { /* if p is the one */
p_from = p;
p = p-&next_sending;
if (copyok) {
/* It's determined from which proc the message will
* be copied. Note that this proc must have been
* waiting for this moment in the queue, so we should
* remove it from the queue.
if (p_from == p_who_wanna_recv-&q_sending) { /* the 1st one */
assert(prev == 0);
p_who_wanna_recv-&q_sending = p_from-&next_sending;
p_from-&next_sending = 0;
prev-&next_sending = p_from-&next_sending;
p_from-&next_sending = 0;
/* copy the message */
memcpy(va2la(proc2pid(p_who_wanna_recv), m),
va2la(proc2pid(p_from), p_from-&p_msg),
sizeof(MESSAGE));
p_from-&p_msg = 0;
p_from-&p_sendto = TASK_NONE;
p_from-&p_flags &= ~SENDING;
unblock(p_from);
/* nobody's sending TASK_ANY msg */
/* Set p_flags so that p_who_wanna_recv will not
* be scheduled until it is unblocked.
p_who_wanna_recv-&p_flags |= RECEIVING;
p_who_wanna_recv-&p_msg = m;
if (src == TASK_ANY)
p_who_wanna_recv-&p_recvfrom = TASK_ANY;
p_who_wanna_recv-&p_recvfrom = proc2pid(p_from);
block(p_who_wanna_recv);
解释:若接收方发生中断,则处理中断,函数立即返回若接收方可以接收一切消息TASK_ANY,那么此时判断q_sending发送队列中是否有消息,是的话,则从队列中取消息,清除发送方的SENDING状态;如果此时q_sending中没有消息,则接收方堵塞,置RECEIVING状态若接收方只接收某一种消息,则当消息不匹配时,接收方堵塞;若消息匹配,进行第2步中的取消息操作死锁的简单判断:由于q_sending队列表示等待队列,只要遍历它,看是否可以遍历到当前进程本身即可。堵塞的简单实现:堵塞意味着要暂停当前进程并切换到其他进程,然而本系统的实现有限,只能强行触发时钟中断进行进程切换,由此可能导致BUG。阶段性总结如果说debug是负反馈,那么proc和ipc的实现就是大大的正反馈,先前用java实现了解释器并构建操作系统(),提供lambda、coroutine、multi-process等机制,但效率极低,求个一百内素数都要半天,还是没法完成做一个操作系统的愿望。本来用C/C++/ java/C# 也造了好多好多轮子,那么这次实现操作系统只用到了ASM和C,但是!!!难度非同小可!因为:资料贫乏、机制复杂、陷阱众多、难以调试、理解困难等等……但我没有放弃!!但看来IPC运行良好没有panic的时候,我的内心是非常喜悦的!这大概就是编程的美吧!参考于渊:Orange'S:一个操作系统的实现92分享收藏文章被以下专栏收录主要研究操作系统、编译原理、图形学,以及其他有趣的东西

我要回帖

更多关于 进程之间通信方式 的文章

 

随机推荐