Linux下进程间通信概述

来源:互联网 发布:淘宝卖家如何退出村淘 编辑:程序博客网 时间:2024/05/18 13:45

1. Linux下进程间通信概述 P83-P84

将第一页和第二页合并起来讲了

引言:前面我们学习了一下进程,我们知道多,进程间的地址空间相对独立。进程与进程间不能像线程间通过全局变量通信。如果想进程间通信,就需要其他机制。

UNIX平台进程通信方式

} 早期进程间通信方式:

ü 无名管道(pipe):UNIX IPC的最古老的形式,所有的UNIX系统都支持,命令行里常用“|

ü 有名管道(fifo)

ü 信号(signal)

UNIX发展做出重要贡献的两大主力是AT&T的贝尔实验室和BSD(加州大学伯克利分校的伯克利软件发布中心),他们在进程间通信方面也有很大的贡献,但两者的研发侧重点不同导致了两种标准。

} AT&T的贝尔实验室,对Unix早期的进程间通信进行了改进和扩充,形成了“system V IPC”,其通信进程主要局限在单个计算机内:

ü 共享内存(share memory)

ü 消息队列(message queue)

ü 信号灯(semaphore) 或者叫信号量,严格意义上应该定义其为同步原语,而不是IPC,其存在常用于配合共享内存使用,用于同步和互斥。

} BSD(加州大学伯克利分校的伯克利软件发布中心),跳过了该限制,支持不同主机上各个进程间IPC

ü 形成了基于套接字(socket)的进程间通信机制:BSD并不是没有涉足单机内的进程间通信(socket本身就可以用于单机内的进程间通信)

由于UNIX版本的多样性,电子电气工程协会(IEEE)开发了一个独立的UNIX标准-POSIX,包含对IPC的支持部分。

Linux继承了上述所有的通信方式,在这里我们主要介绍

l 早期进程间通信方式下的:无名管道(pipe), 有名管道(fifo)和信号(signal)

重点讲解System V IPC, POSIX IPC不展开,(在讲解线程同步时已经涉猎过POSIX IPC的信号量部分)。在嵌入式领域,还是SystemV IPC比较多,由于历史的原因,POSIXSystemV出现的晚,很多嵌入式系统对POSIX IPC支持的还不是很好。

l BSD Socket,留在讲解网络时讲解。

参考:

1)深刻理解Linux进程间通信(IPChttps://www.ibm.com/developerworks/cn/linux/l-ipc/

2linux进程间通信(消息队列、信号量、共享内存等)http://www.docin.com/p-725076998.html

3)进程互斥锁 (比较了SystemVPOSIX的平台兼容性)http://blog.csdn.net/luansxx/article/details/7736618

提高: <<<< UNIX世界里IPC两套(标准)。

System V IPC and POSIX IPC: http://www.findfunaax.com/notes/file/213

这两套标准都包含并定义了消息队列,共享内存和信号量这三套机制。这两套标准孰优孰劣还可参考:

http://stackoverflow.com/questions/368322/differences-between-system-v-and-posix-semaphores

其中对POSIX 的信号量还分两种:

} 有名信号灯

} 基于内存的信号灯

两者的区别参考:http://blog.csdn.net/shanshanpt/article/details/7376311

注意看一下最后一段话讲得不错。

有名信号灯参考:http://blog.chinaunix.net/uid-25324849-id-207480.html

所以总结来看,信号灯种类可以分成三种:

} posix有名信号灯

} posix基于内存的信号灯(无名信号灯)

} System V信号灯(IPC对象)

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

2. 经典进程间通信方式

2.1无名管道

重点:

PIPE的特点

PIPE的创建典型流程

PIP的读写注意点

2.1.1概念介绍

2.1.1.1 概念:(P85

类似时空隧道的概念,建立两个进程之间的通讯桥梁。数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据

2.1.1.2 特点:(P86

} 只能用于具有亲缘关系的进程之间的通信:通过创建的流程来理解

} 半双工的通信模式,具有固定的读端和写端:传输方向同时只能是一个方向,强调固定。

管道可以看成是一种特殊的文件,对于它的读写可以使用文件IOreadwrite函数:但和FIFO不同的是在文件系统里并不存在pipe对应的文件。

} 不支持如lseek() 操作。

2.1.2无名管道创建的典型流程P87-P90

先讲看一下API

举例:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

samples\3.ProcessII\2.1-pipe\ pipe_open.c

半双工管道的典型创建流程,可以step by step地画图增加学生的理解。

强调一下:单独创建一个无名管道,并没有实际的意义。我们一般是在一个进程在由pipe()创建管道后,一般再由fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。这也是为何前面说只能用于具有亲缘关系的进程之间的通信的原因。

step1: 父进程创建一个pipe,其中fd[0]固定用于读管道,而fd[1]固定用于写管道。

 

Step2:父进程fork,子进程继承了父进程的管道

 

Step3:之后取决于我们想要的数据流方向来关闭相应的端。

 

 

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

2.1.3管道读写注意点(P91

参考ppthttp://blog.chinaunix.net/uid-26833883-id-3227144.html

man 7 pipe

当管道中无数据时,读操作会阻塞 - pipe_rw.c

向管道中写入数据时…- pipe_buf.c用该原理来测试管道内部缓冲区的大小

只有在管道的读端存在时,…SIGPIPE信号(通常Broken pipe错误)-信号未讲,暂不演示

2.1.4 作业:

实验5.1 <<<<<  labs\5.1

注意:

1)实验手册上的例子代码为实现先接收P1,后P2,采用了我们课程上没有讲过的函数来实现进程间同步。由于我们到目前课程为止还没有讲过进程间同步的方法,POSIX信号量可以支持,但我们截止目前课程也只讲了线程间同步,所以P1P2的顺序实现作为提高,需要和学生说一下。

2)因为n<PIPE_BUF,所以我们不需要担心破坏写的原子性。这里可以体现出来。

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

2.2 FIFO

重点:

无名管道和有名管道的区别

FIFO的创建方法

FIFO的读写注意点

总结

2.2.1 FIFO的概念(P92

又叫有名管道,为何要提出有名管道的说法,克服了无名管道的什么问题

} 无名管道只能用于具有亲缘关系的进程之间,这就限制了无名管道的使用范围

} 有名管道可以使互不相关的两个进程互相通信。有名管道可以通过路径名来指出,并且在文件系统中可见

FIFO不同于无名管道之处在于它提供了一个路径名与之关联,FIFO的文件形式存在于文件系统中这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信,因此,通过FIFO不相关的进程也能交换数据。

} 进程通过文件IO来操作有名管道:FIFO是一种文件类型,让学员回顾全面文件类型和stat结构中st_mode以及S_ISFIFO的宏

2.2.2 FIFO的创建mkfifo和打开openP93-P94

该函数的第一个参数是一个普通的路劲名,也就是创建后FIFO的名字。第二个参数与打开普通文件的open()函数中的mode参数相同。如果mkfifo的一个参数是一个已经存在路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数open就可以了。

mkfifo函数时可以让学员观察一下参数filename是一个文件路径,存在于文件系统中,mode则和openmode一样。注意,当多个进程通过FIFO交换数据时,内核通过内部的缓存,并不会往我们创建的文件里写入东西,所以该文件的内容是空的,有兴趣可以看看。这个文件只是在文件系统里提供了一个名字供多个进程访问pipe,所以我们称其为有名管道的由来。

注意mkfifo只是基于文件系统创建了一个文件。具体读写之前有名管道比无名管道多了一个打开操作:open调用mkfifo成功后,必须继续用open打开它,得到文件描述符,然后进一步用read/write/close等文件IO进行操作。

FIFOopen/打开规则:

在一个FIFO上打开一个读端,

写端是否存在

阻塞方式

非阻塞方式

成功返回

成功返回

阻塞一直到有其他相应进程在该FIFO上打开写端

成功返回

在一个FIFO上打开一个写端

读端是否存在

阻塞方式

非阻塞方式

成功返回

成功返回

阻塞一直到有相应进程在该FIFO上打开读端

返回ENXIO错误

举例<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

samples\3.ProcessII\2.2-FIFO\fifo_open.c

两个进程,一个在FIFO上只写,一个在FIFO上只读

演示重点:尝试先执行读进程后执行写进程,或者先写后读。探究发现,如果open时没有使用O_NONBLOCK参数,我们发现不论读端还是写端先打开,先打开者都会阻塞,一直阻塞到另一端打开。

如果open时使用了O_NONBLOCK参数,此时打开FIFO又会是什么情况?

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

2.2.3 有名管道的读写规则

有名管道的读写规则和无名管道是一致的。重点讲清楚无名管道的读写就好了

2.2.3.3 小结:

The only difference between pipes and FIFOs is the manner in which they are created and opened. Once these tasks have been accomplished, I/O on pipes and FIFOs has exactly the same semantics.

不同点

相同点

l PipeFIFO的区别只在于其创建和打开阶段。此后的操作两者的行为几乎是一致的。

ü 对于pipe,调用pipe()返回的两个fd,已经赋予了读端和写端的属性,不需要另外指定。

ü 对于FIFO,通过mkfifo创建,在文件系统中有了一个名字。然后要从该FIFO中读数据的进程以O_RDONLY 调用open()要从该FIFO中写数据的进程以O_WRONLY 调用open()。或者直接用O_RDWR

l 有名管道可以使互不相关,没有亲缘关系的的两个进程互相通信。

l 有名管道的读写需要通过打开文件,对于文件我们可以通过控制所有者和权限管理对管道的访问。

l 虽然管道,特别是有名管道可以很方便地在双向上打开读写,但其内核实现依然是单向的。严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。

l pipe, fifo都不支持诸如lseek()等文件定位操作。

l 对于pipe或者fifo,如果在读端或者写端打开了多个读写端(进程),之间的读写是不确定的,需要通过其他的同步机制实现多进程通讯的同步。

2.2.4 典型的FIFO模型(可忽略)

对照ppt以概念讲述为主。如果可以给些例子讲解。

2.2.5 实验:

实验5.2  <<<<<< labs\5.2

需要讲解一下编写方向再做

三个程序,程序1,输入参数为FIFO文件路径,创建fifo文件;程序2,输入参数为FIFO文件路径,以只读打开,打开后执行循环并读管道中的数据。如果读到文件尾则退出。程序3,输入参数为FIFO文件路径,以只写方式打开,从stdin上读取数据,读一行,写一行。>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

2.3 信号通信

2.3.1 信号基础

重点:

了解信号的概念

了解系统对信号的处理方式

2.3.1.1 信号的来源(P96

参考:linux系统编程之信号(三):信号的阻塞与未决:http://blog.csdn.net/jnu_simba/article/details/8944982

信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式

信号则是由内核(或其他进程)对某个进程的中断,不是软中断,更不是硬中断。

信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。

信号事件的发生主要有两个来源:

硬件来源

ü 用户在终端按下某些键时,终端驱动程序会发送信号给前台进程,例如ctr+c产生SIGINT,该信号会终止进程,  ctr + \产生SIGQUI信号,和Ctrl+C类似,ctr + z产生SIGTSTP,暂停进程并放到后台,即挂起。

ü 硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给当前进程 。

软件来源

ü 系统调用:一个进程调用int kill(pid_t pid,int sig)函数可以给另一个进程发送信号

ü 终端命令:可以用kill命令给某个进程发送信号,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。

ü 当内核检测到某种软件条件发生时也可以通过信号通知进程,例如子进程结束,内核给父进程发送SIGCHILD;闹钟超时产生SIGALRM信号;向读端已关闭的管道写数据时产生SIGPIPE信号。

如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程

实际执行信号的处理动作称为信号投递(Delivery),信号从发生到投递之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号。被阻塞的信号产生后将保持在未决状态,直到进程解除对此信号的阻塞,才执行投递的动作。

每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志(表示某信号是否发生过),直到信号投递完毕才清除该标志。在上图的例子中,

1. SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

2. SIGINT信号产生过,但正在被阻塞,所以暂时不能投递。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

3. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler

未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

2.3.1.2 信号的生存周期(P97

参考:signal.txt 。对着图讲一下

对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个阶段:

信号发生 (前面讲过主要分硬件和软件两个来源)

信号在进程中注册:进程的task_struct结构中有关于本进程中未决信号的数据成员,里面记录着哪些信号发生了以及对这些信号的处理策略(阻塞,忽略还是执行用户定义的处理函数),Linux的术语叫disposition(部署)信号发生时,通过调用系统调用陷入内核并在task_struct这个结构中打标记,如果发送给一个处于可运行状态的进程,则只置相应的域即可。如果信号发送给一个正在睡眠的进程,如果进程睡眠在可被信号中断的优先级上,则会因为信号临时唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。

注:信号的部署(disposition)信息是per-process的,这意味这对于多线程的进程,共享同一份信号部署。A child created via fork(2) inherits a copy of its parent's signal dispositions. During an execve(2), the dispositions of handled signals are reset to the default; the dispositions of ignored signals are left unchanged.

信号的投递,执行和注销:注册是先在进程的控制结构(task_struct)中记录下收到了某某信号,然后等到进程即将从内核态返回用户态的时候,流程才被中断handle函数才被调用。用户进程什么时候会从内核态返回用户态呢?那么首先要说进程何时会进入内核态,一般主要是种情况:

ü 系统调用(用户进程主动进入内核),信号的产生也会通过系统调用进入内核。

ü 中断(用户进程被动进入内核),调度也是由时间中断导致。

那么当进程由于以上原因进入内核态而需要返回用户态时,返回有可能是被信号唤醒或者是正常调度重新获得CPU在其从内核空间返回到用户空间时会检测是否有信号等待处理。如果存在未决信号等待处理且该信号没有被进程阻塞,则投递该信号并执行相应的信号处理函数,然后进程会把信号在未决信号结构中注销(删除)掉。

我们知道处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。如果进程收到一个要捕捉的信号, 

2.3.1.3 用户进程对信号的响应方式:

忽略信号(ignore the signal):对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。

为什么不能忽略:向超级用户提供一种使进程终止或停止的可靠方法。

捕捉信号(catch the signal with a signal handler):类似中断的处理程序,对于需要处理的信号,进程可以安装处理函数,由该函数来处理定义信号处理函数,进程从内核态返回用户态时执行用户定义的函数(为何不是直接在内核调用?当然不行。内核代码运行在高CPU特权级别下,如果直接调用handle函数,则handle函数也将在相同的CPU特权下被执行。那么用户将可以在handle函数里面为所欲为。)。执行完毕后又返回内核态继续检查是否还有其他未决信号,直至所有未决信号处理完,所以我们知道进程在用户态下是不会有未处理完的信号的.所有未决信号处理完毕后才会决定是否最终返回用户态执行其他的用户态程序代码。(最后是否会返回并执行后继的代码还不一定,比如syscallresume问题,但我们目前不讲这么深)

执行缺省操作(perform the default action):Linux对每种信号都规定了默认操作

对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。后面会讲到。 

2.3.1.4 使用信号的场合(P99)

} 后台进程需要使用信号,如xinetd

没有控制界面,或者守护进程连终端都没有,用信号来和他们通讯最好

} 如果两个进程没有亲缘关系,无法使用无名管道

} 如果两个通信进程之一只能使用标准输入和标准输出,则无法使用FIFO

由于某些应用限制不可以为FIFO创建文件

2.3.1.5 SIGNAL列表(P100

每个信号都有一个名字,名字以三个字符SIG开头。查看signal标准规范:Man 7 signal或者Kill –l

重点点一下SIGSTOPSIGKILL,不能被阻塞block,忽略ignore和处理caught。Others:

SIGSTOP & SIGTSTP

两个信号都会使进程挂起并可以通过另一个信号SIGCONT恢复,但SIGTSTP一般通过键盘产生CTRL+Z,而SIGSTOP可以通过kill命令发送。SIGSTOP不能被忽略和捕获,SIGTSTP可以。

SIGCHLD:

参考:UNIX系统中wait函数族和SIGCHLD信号的关系:http://blog.csdn.net/soloopin/article/details/8226748

简单的说,子进程退出时父进程会收到一个SIGCHLD信号,若不处理,默认方式则子进程会成为僵尸。而常规的做法是在这个信号处理函数中调用wait函数获取子进程的退出状态并回收僵尸。也可以注册直接忽略,则系统会将结束的子进程寄养给initinit会回收它。

SIGABORT

abort()  该函数产生SIGABRT信号并发送给自己,默认情况下导致程序终止

SIGALRM

alarm 函数产生,后面会介绍。

2.3.2 信号相关调用

重点:

掌握在程序中发送信号的方法

掌握信号的不同处理方式

signal函数原型

2.3.2.1 kill()和raise()

} kill函数同读者熟知的kill系统命令一样,可以发送信号给进程或进程组(实际上,kill系统命令只是kill函数的一个用户接口)。 可以对照man 2 kill 看看解释

} kill l命令查看系统支持的信号列表

} raise函数允许进程向自己发送信号

举例:<<<< P105例子

子进程的打印没有机会执行就被父进程杀死了 >>>>>>>>>>>

2.3.2.2 alarmpause

对照man手册讲解。

} alarm()也称为闹钟函数,它可以在进程中设置一个定时器。当定时器指定的时间到时,内核就向进程发送SIGALARM信号。

² 如果不忽略或者不捕捉此信号,默认动作是终止该进程

² 经过指定秒后,信号由内核产生,由于进程调度的延迟,进程得到控制能够处理该信号还需一段时间,所以该设置的时间不会非常准确。

² 每个进程只能有一个闹钟,新闹钟会替代老闹钟。

² 如果参数seconds0,则之前设置的闹钟会被取消 实践中常常会使用,防止多余的闹钟又来干扰。

} pause()函数是用于将调用进程挂起直到收到信号为止。

² 只有执行了一个信号处理程序并从其返回时,pause才返回;如果我们没有提供处理函数,pause缺省会终止进程,就不会返回了

² 对指定为忽略的信号,pause()不会返回。只有执行了一个信号处理函数,并从其返回,puase()才返回-1,并将errno设为EINTR

举例:<<<< 参考P109的例子,实现了进程等待sleep 5s

最后一句打印不会出来,为什么? 原因是前述pause的处理逻辑的第一条>>>>>>>

2.3.2.3 signal – 信号的安装

信号的安装的主要方法有两种

} 使用简单的signal()函数

使用信号集函数族

Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()我们主要介绍使用传统的signal函数的方式

这里 signal() 这个函数的原型我当时看了头有点大,还是先说明一下:首先该函数原型整体指向一个无返回值并且带一个整形参数的函数指针,也就是信号的原始配置函数;接着该原型又带有两个参数,其中第2个参数可以是用户自定义的信号处理函数的函数指针。不明白也没事,后边做实验就明白了,会用就行了。分解为如下比较容易理解:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

更多的signal介绍请看man 7 signal

举例:<<<<<P112例子

本质上是一个注册信号处理方式的过程, 参考课件的ppt page97

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

举例:<<<<<利用signal实现消除僵尸的两种方法

回顾消除僵尸的正确方式:1)父进程提前退出;2)真父委托继父;3)真父wait

l 1已经前面演示过;

l 2涉及signal:继父方式:如果真父进程的确不想回收。父进程可以通知内核忽略子进程的退出状态,让内核来处理,其基本原理是可让内核把僵尸子进程转交给init进程去处理,而init一般会主动回收结束的子进程。类似于交给福利院,自己不管了。samples\3.ProcessII\2.3-signal\z_sigign.c演示重点:子进程退出后父进程不会收到signal异步通知。子进程僵尸会被init自动回收。

对于3。由真父调用wait, waitpid来接收子进程退出状态。达到收尸的作用。但简单使用该方法需要父进程等待子进程结束,会导致父进程挂起。特别是在某些服务器实现上,一般由父进程fork很多子进程,如果需要父进程去长时间等待子进程,会很影响服务器进程的并发性能。其实为了避免父进程长时间等待还有一种方法是用signal函数为SIGCHLD安装handler,因为子进程结束后, 父进程会收到该信号,可以在handler中调用wait回收。samples\3.ProcessII\2.3-signal\z_sighdlr.c。演示重点:子进程退出后父进程收到signal异步通知。子进程僵尸会被回收。ps aux后面讲网络TCP服务器时还会用到该技术。

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

实验 <<<<<讲解实验5.3 使用信号实现同步,让学生自己先做再讲解

功能需求:

1) 同步过程 见实验手册

2) 利用两个进程(父子进程)来模拟司机和售票员

技术要点:

利用kill来发送信号,

信号的注册和捕获处理

父子进程在一个进程组内,如何避免同一个信号不同处理之间的干扰(IGN的作用)

Pause函数的使用

画一个流程图方便编写程序

>>>>>>>>>

3. SystemV IPC

1.1 IPC对象的创建 (P113

重点:

讲清楚IPC对象,IPC标识符,IPC键的概念以及之间的关系。

IPC对象的创建方法,包括KEY的创建方法和区别。

参考http://www.embedu.org/column/column616.htm

P113这张图主要是要说明以下几个概念并用文件系统对象对比讲解

内核对象

文件对象

IPC对象

内核标识符

文件描述符。进程范围内唯一分配,最小可用值

IPC标识符。系统全局的流水号

进程共享名

文件系统路径

IPC

创建API

open

Xget

3.1.1 IPC对象

linux中,可以使用IPC对象来进行进程间通信。IPC对象存在于内核中,作为桥梁供多进程操作进行数据通讯

需要注意的是IPC对象是系统范围内起作用的,创建后进程不删除它们就退出则会遗留在系统里。为此系统也提供了命令来维护:

查看IPC对象信息

删除IPC对象

命令:ipcs [-aqms]

参数说明:

1-a:查看全部IPC对象信息。

2-q:查看消息队列信息。

3-m:查看共享内存信息。

4-s:查看信号量信息。

命令1ipcrm -[qms] ID

命令2ipcrm -[QMS] key

参数说明:

1-q-Q:删除消息队列信息。

2-m-M:删除共享内存信息。

3-s-S:删除信号量信息。

注意事项:如果指定了qms,则用IPC对象的标识符(ID)作为输入;如果指定了QMS,则用IPC对象的键值(key)作为输入。

3.1.2 IPC标识符

消息队列,信号量或者共享存储段在内核中都有对应的数据结构,每个内核中的IPC结构在内核中都用一个非负的整数作为标识符来唯一标识并加以引用,类似内核中每个文件对应的文件描述符(与文件描述符不同点在于IPC标识符实现为一个系统全局的流水号(文件描述符是进程范围内唯一),当达到一个整型的最大值后归零)。

提高:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

IPC结构的访问采用了独立于文件系统的体系。不能像FIFO一样采用统一文件的方式来访问他们。比如不能用ls查看IPC对象,也不能用rm来删除它们也不能用chmod更改其访问权限。正如前面所说的对V5IPC我们需要用专用的ipcsipcrm。总之这让IPC看起来和Unix的其他对象不是很统一。

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

3.1.3 IPC

IPC标识符可以认为是IPC对象的内部名。-类比文件IO的文件描述符

外部名方案:进程访问IPC对象时使用的是“键”– 类比文件IOpathname

每个IPC的对象都有唯一的名字,称为""(key)。通过"",多个进程能够识别所用的同一个IPC对象。""IPC对象的关系就如同文件名称“/A/B/C”于文件,通过文件名,多个进程能够打开同一个内核文件对象并读写文件内的数据,甚至多个进程能够公用一个文件。而在IPC的通讯模式下,通过""的使用也使得一个IPC对象能为多个进程所共用。

关键字的数据类型由系统定义为ket_t, 通常在头文件<sys/types.h>被定义为是一个非负长整数。

简而言之,可以将key类比于文件的文件名称(文件系统中的路径)。

打开(创建)一个文件 int open(const char *path, int oflag, ... ); 输入文件路径,创建时还要指定文件访问权限,得到文件句柄。

打开(创建)一个IPC int shmget(key_t key, size_t size, int shmflg);输入keykey可以由文件路径和项目ID通过ftok产生), 指定shmflg(包含IPC对象访问权限),得到IPC句柄。

http://pages.ramapo.edu/~vmiller/UNIX/Lecture-IPC.htm

IPC structures have owners and groups. These permissions are similar to file permissions. For example, shared memory may be set up so that it has read/write access for owner processes and it has read only by any process that does not belong to the owner of the shared memory. (This is assuming that the operating system/hardware supports access control to memory.)

3.1.4 IPC结构的创建

在使用Xget过程中,当然可以由人工指定key值(直接使用立即数),但这么做很容易发生不同的应用之间可能会因为使用同一个key而产生冲突(文件路径没有这个问题,文件系统中的路径要存在必然唯一,要么不存在)。为此,Linux系统提供了如下机制产生惟一的关键字。

1) 普适方法,使用ftokFtok的入参有两个,一个是路径名,另一个是项目id。不同进程之间可以事先协商好这两个参数。然后都先后(必须的)调用ftok确保产生一个相同的唯一的key。然后某一方用该key调用Xget(需要指明IPC_CREAT参数)创建ID,而另一方则用相同的key调用Xget函数得到已经创建好的IDMan ftok: If the values for path and id are the same as a previous call to ftok() and the file named by path was not deleted and re-created in between calls to ftok(), ftok() will return the same key.

举例:<<<<<  samples\3.ProcessII\3.1-ipc\ftok.c

这是具体使用方法,在创建一个消息队列(其他ipc相同)时,需要先通过文件路径名和项目ID获取一个键值,然后通过此键值由内核生成标识符,在以后可通过此标识符来使用此消息队列。>>>>>>>>>>>

2) 如果两个进程之间有亲缘关系,则可以简化方法1,使用预定义的keyIPC_PRIVATE来直接创建ID然后通过fork或者文件共享的方式将ID传递给另外一个进程。。如果子进程还要exec新的程序,则子进程可以用exec的参数将ID传给新的程序。

3.2 共享内存

重点:

了解共享内存的特点

掌握使用共享内存的方法

3.2.1概述

SysV5IPC中最有价值的就是共享内存(+信号量为辅助)

共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝

最快的IPC,使用共享存储的唯一窍门是多个进程之间对一给定存储区的同步存储。后面要说的信号量就是用来实现serverclient对共享存储的同步。

为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间

4G进程空间中专门开辟了一块地址范围用于映射共享内存。见下附图。课件p115也是在说这个事情。

进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。

由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等

Memory layout on an Intel-based Linux system

 

3.2.2 共享内存实现(P116

函数

说明

int shmget(key_t key, int size, int shmflg);

l 功能说明:分配一块共享内存

l 返回值:调用成功返回一个shmid(类似打开一个或创建一个文件获得的文件描述符一样);调用失败返回-1

l 参数说明

² Key: 前面介绍过

² size是要建立共享内存的长度。所有的内存分配操作都是以页为单位的。所以如果一个进程只申请一块只有一个字节的内存,内存也会分配整整一页(i386机器中一页的缺省大小PACE_SIZE = 4096字节)

² shmflg有效的标志包括IPC_CREATIPC_EXCL,他们的功能与open()O_CREATO_EXCL相当。

ü IPC_CREAT:如果共享内存不存在,则创建一个共享内存,否则直接打开已存在的

ü IPC_EXCL:只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误

例子一:假设键值为key,创建一个共享内存大小为4k,访问权限为066,如果已经存在则返回其标识号

int shmid;

if( (shmid = shmget(key,4 * 1024,0666 | IPC_CREAT)) < 0) {

    perror("Fail to shmget");

    exit(EXIT_FAILURE)

}

例子二、假设键值为key,创建一个共享内存大小为1k,访问权限为0666,如果已经存在则报错

int shmid;

if((shmid  = shmget(key,1024,0666 | IPC_CREAT | IPC_EXCL)) < 0) {

    perror("Fail to shmget");

    exit(EXIT_FAILURE);

}

总结Xget的使用方法-如何创建一个IPC对象。Xget函数都有两个类似的参数,keyflag。如果满足下列两个条件之一,则在内核中创建一个新的IPC结构。:

KeyIPC_PRIVATE

Key不是IPC_PRIVATE,比如由ftok+ flag=IPC_CREAT创建并且该Key未与一个已存在的IPC对象绑定。

void *shmat(int shmid, const void *shmaddr, int shmflg);

 

l 功能说明:函数shmat将标识号为shmid共享内存映射到调用进程的地址空间中。

l 返回值 :调用成功放回映射后的地址,出错放回(void *)-1

l 参数说明:

² shmid  :  要映射的共享内存区标识符

² shmaddr  :  指定共享内存映射到的虚拟地址(若为NULL,则表示由系统自动完成映射)一般shmaddr填写为NULL/0,让系统内核替我们选择地址。除非你写的程序特定于某种需要手工指定共享地址(VM)。

² shmflg  :  SHM_RDONLY  共享内存只读。默认0:共享内存可读写。

int shmdt(const void *shmaddr);

 

l 功能说明:取消共享内存与用户进程之间的映射

l 参数说明: shmaddrshmat映射成功放回的地址。

注意:当一个进程不再需要共享内存段时,它将调用shmdt()系统调用取消这个段,但是,这并不是从内核真正地删除这个段,而是把相关shmid_ds结构的shm_nattch域的值减1,当这个值为0时,内核才从物理上删除这个共享段。

用完共享存储段后,将进程和该共享存储段脱离。这并不是从系统中删除其标识符以及其数据结构,直到某个进程(一般是创建者server)调用shmctlIPC_RMID)特地删除它。

int shmctl(int shmid,  int cmd,  struct shmid_ds  *buf);

 

l 功能说明:控制共享内存

l 参数说明:

² shmid  共享内存标识ID

² cmd

ü IPC_STAT得到共享内存的状态

ü IPC_SET改变共享内存的状态

ü IPC_RMID删除共享内存

² buf  是一个结构体指针。IPC_STAT的时候,取得的状态放在这个结构体中。如果要改变共享内存的状态,用这个结构体指定;

l 注意:

² IPC_RMID命令实际上不从内核删除一个段,而是仅仅把这个段标记为删除,实际的删除发生最后一个进程离开这个共享段时。

² 2.cmdIPC_RMID时,第三个参数应为NULL。呵呵,大部分我们都是这样做,用这个函数删除共享内存。

参考:LINUX共享内存使用常见陷阱与分析http://www.dcshi.com/?p=79

举例 <<<<<<课件(P121)  同实验5.4 实验5.4也是参考的P121的讲义

Demo时注意显示的下列列

shmid      owner      perms      bytes      nattch

实际的例子见信号量实验5.6>>>>>>>>>>>>>>>>>>

实验 <<<<<实验5.6POSIX信号量版本。因为还没有讲到SysV信号灯,所以这个实验要用到POSIX信号灯。

要求: samples\3.ProcessII\3.2-shm\shm.c

需要讲解的注意点:

l AB可以有亲缘关系,也可以没有。简单点用fork做。

利用两个POSIX无名信号灯实现同步A,B两个进程对共享内存的先写再读,先:A负责从stdin读一行后写入共享区,后:B负责从共享区读出并打印到屏幕上。提高:实现循环读和写,并且当A进程从stdin读到“quit”时,在将quit写入共享区后退出,B进程读到“quit”后也退出。

信号量也需要放在进程间的共享内存区中,因为回忆前面线程间同步的AB例子,这里是两个进程都要访问信号量。需要将结构的概念重复,很多学生对这个想不通。

l gcc 时要加上-lpthread 

更多有关共享内存同步的内容参考:http://www.embedu.org/Column/Column798.htm >>>>>>>>>>>>>>>>

3.3 信号灯(信号量)(P135-P136

重点:

信号灯提前到队列前讲,我认为共享内存和信号灯是两个关系很密切的IPC对象类型,消息队列次要一点。

理解信号灯集相关概念

掌握信号灯集使用方法

使用信号灯集实现对共享内存的访问控制

注意不要和信号(Signal)混淆

} 信号灯(semaphore),也叫信号量。它是不同进程间或一个给定进程内部不同线程间同步的机制。

} 信号灯种类:

根据目前的讲解给学生总结一下

} posix有名信号灯

主要用户进程间的同步

} posix基于内存的信号灯(无名信号灯)

可用于进程间和线程间的同步

} System V信号灯(IPC对象)

Page136是在讲解基本的信号量的概念

} 二值信号灯:值为0或1。与互斥锁类似,资源可用时值为1,不可用时值为0。

} 计数信号灯:值在0到n之间。用来统计资源,其值代表可用资源数

} 等待操作是等待信号灯的值变为大于0,然后将其减1;而释放操作则相反,用来唤醒等待资源的进程或者线程

回忆线程时的PV概念。为了获得共享资源,进程执行如下操作:

1)测试控制该资源的信号量

2)若此信号量的值为正,则进程可以使用该资源,然后进程将信号量减1

3)若此信号量的值为0,则进程进入休眠,直到信号量值大于0,进程被唤醒,返回第一步。

3.3.1 SystemV 信号灯

} System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。而Posix信号灯指的是单个计数信号灯

systemV的信号灯实现不是一个简单的非负整数,而是一个比较复杂的数据结构集合。详细见p138页的图。大概讲一下内核数据结构。

} System V 信号灯由内核维护

} 主要函数semget,semop,semctl

3.3.2 信号灯函数

函数

说明

int semget(key_t key, int nsems, int semflg);

 

创建一个信号灯集

功能:创建一个信号灯集并返回这个信号灯集的ID 或直接返回一个已经存在的信号灯集的ID

参数说明:

l key:如果key值为IPC_PRIVATEkey0并且semflg设置了IPC_CREAT,此时调用此函数总是创建一个新的信号灯集(我们在共享内存的时候验证过,还记的吗?其实system v ipc对象的创建机制都很类似。

l nsems:指定这个信号灯集中信号灯的个数(每个信号灯代表了某一类资源)

l semflg:可以指定为IPC_CREAT | 0666,其含义为,不存在则创建,访问权限为0666。我们也可以通过IPC _CREAT | IPC_EXCL一起使用的时候确定要创建的信号灯集是否存在,如果存在此时这个函数放回-1

int semop ( int semid, struct sembuf  *opsptr,  size_t  nops);

 

功能:semop系统调用可以实现对由semid标志的信号灯集中的某一个指定信号灯的一系列操作

参数说明:

l semid信号灯集的标识ID

l opsptr: 指向结构体sembuf的指针,指向一个结构体数组的指针。数组的每个成员是一个sembuf,用来设置信号灯集中某些个信号灯的工作方式。

struct sembuf {

short  sem_num;  //  要操作的信号灯的编号

short  sem_op;   //   0 :  调用者阻塞等待,直到信号灯的值等于0时返回。可以用来测试共享资源是否已用完。

//   1  :  释放资源,V操作

//   -1 :  分配资源,P操作                    

short  sem_flg; //

a.   0  代表阻塞调用(资源不满足阻塞)

b.   IPC_NOWAIT  代表非阻塞调用

c.   如果设置了SEM_UNDO标志,那么在进程结束时,相应的操作将被取消,这是一个比较重要的一个标志。

};

例子:

struct sembuf sops[2];

int semid;

/* Code to set semid omitted */

sops[0].sem_num = 0;        /* Operate on semaphore 0 */

sops[0].sem_op = 0;         /* Wait for value to equal 0 */

sops[0].sem_flg = 0;

sops[1].sem_num = 0;        /* Operate on semaphore 0 */

sops[1].sem_op = 1;         /* Increment value by one */

sops[1].sem_flg = 0;

if (semop(semid, sops, 2) == -1) {

perror("semop");

exit(EXIT_FAILURE);

}

l nops:  要操作的信号灯的个数

案例:封装一个P操作和一个V操作

//P操作

int my_sem_wait(int semid,int sem_num)

{

    struct sembuf  op;

    op.sem_num = sem_num;

    op.sem_op = -1;

    op.sem_flg = 0;

    if(semop(sem_id,&op,1) < 0) {

perror("fail to semop");

exit(-1);

    }

    return 0;

}

//V操作

int my_sem_wait(int semid,int sem_num)

{

    struct sembuf  op;

    op.sem_num = sem_num;

    op.sem_op = 1;

    op.sem_flg = 0;

    if(semop(sem_id,&op,1) < 0) {

perror("fail to semop");

exit(-1);

    }

    return 0;

}

int semctl ( int semid, int semnum,  int cmd…/*union semun arg*/);

控制信号灯集

参数说明:

l semid : 信号灯集ID

l semnum:要修改的信号灯编号(创建信号灯集时,信号灯的编号从0开始)

l cmd:

n IPC_STAT  获取信号灯信息,信息由arg.buf(即第四个参数)返回

n GETVAL  : 获取semnum信号灯的值

n SETVAL  : 设置semnum信号灯的值

n IPC_RMID : 从系统中删除semnum所代表的信号灯  

l 最后一个是可变参数,cmdGETVALSETVAL时,需要传递第四个参数,其参数类型为union semun。这个结构体的类型必须在应用程序中定义

定义如下,必须在应用程序中自己定义如下类型

union semun {

  int   val;

  struct  semid_ds  *buf;

  unsigned  short  *array;

  struct  seminfo  *  __buf;

};

例子1

union semun mysemun;

mysemun.val = 1;

//第一个信号灯的编号为0

if ( ( semctl (  semid  ,  0  ,  SETVAL,mysemun)) < 0) {

    perror("Fail to semctl");

    exit(EXIT_FAILURE);

}

例子2:删除一个信号灯

if(  semctl  (  semid  ,  0  ,  IPC_RMID  ,  0 ) < 0 ) {

    perror("Fail to semctl  IPC_RMID");

    exit(EXIT_FAILURE);

}

3.3.3 信号灯的典型应用:

实验:<<<<<SysV 信号灯实现前面讲共享内存区时的例子。

讲解时注重把SysV的信号灯和POSIX的信号灯做个比较\labs\5.6

>>>>>>>>>>>>>>>>>>

3.4消息队列(看时间允许就讲,可选)

重点:

掌握消息队列的使用方法

使用消息队列实现多个进程之间的复杂通信

参考:

http://www.cnblogs.com/Anker/archive/2013/01/07/2848869.html

http://blog.csdn.net/anonymalias/article/details/9829417

消息队列是由存放在内核中的消息组成的链表,由IPC id标识。

msgget创建新队列或打开已经存在的队列

msgsnd将消息添加到消息队列尾,每个消息包括正整数标识的类型,非负的长度,及数据。

msgrcv从消息队列中取消息,不必按FIFO取消息,可以通过类型字段取相应的消息。

函数

说明

int msgget(key_t key, int flag);

key是一个键值,由ftok获得;

msgflg参数是一些标志位。该调用返回与健值key相对应的消息队列描述字。

在以下两种情况下,该调用将创建一个新的消息队列:

1) 如果没有消息队列与健值key相对应,并且msgflg中包含了IPC_CREAT标志位;

2) key参数为IPC_PRIVATE;参数msgflg可以为以下:IPC_CREATIPC_EXCLIPC_NOWAIT或三者的或结果。

举例:<<<<<<samples\3.ProcessII\3.3-msgq\ msgget.c

运行ipcs看看效果  >>>>>>>>>>>>>

int msgsnd(int msqid, const void *msgp, size_t size, int flag);

msqid:消息队列的描述符;

msgp:指向存放消息的缓冲区,该缓冲区中包含消息类型消息体两部分内容。该缓冲区的结构是由用户定义的,在<sys/msg.h>中有关于该缓冲区结构定义的参版考模:与一个队列中的每个消息相关联的类型字段提供了两个特性:

1)类型字段可以用于标识消息,从而允许多个进程在单个队列上复用消息。

2)类型字段可以用做优先级,允许接收者以不同于先进先出的某个顺序读出各个消息。

size:缓冲区中消息体部分的长度;

flag:设置操作标志。可以为0IPC_NOWAIT;用于在消息队列中没有可用的空间时,调用线程采用何种操作方式。

标志为IPC_NOWAIT,表示msgsnd操作以非阻塞的方式进行,在消息队列中没有可用的空间时,msgsnd操作会立刻返回。并指定EAGAIN错误;

标志为0,表示msgsnd操作以阻塞的方式进行,这种情况下在消息队列中没有可用的空间时调用线程会被阻塞,直到下面的情况发生:

l 等到有存放消息的空间;

l 消息队列从系统中删除,这种情况下回返回一个EIDRM错误;

l 调用线程被某个捕捉到的信号中断,这种情况下返回一个EINTR错误;

int msgrcv(int msgid,  void* msgp,  size_t  size,  long msgtype,  int  flag);

msqid:消息队列的描述符;

msgp:指向待存放消息的缓冲区,该缓冲区中将会存放接收到的消息的消息类型和消息体两部分内容。该缓冲区的结构是由用户定义的,和msgsnd相对应。

size:缓冲区中能存放消息体部分的最大长度,这也是该函数能返回的最大数据量;该字段的大小应该是sizeof(msg buffer) - sizeof(long)

msgtyp:希望从消息队列中获取的消息类型。

l msgtyp0,返回消息队列中的第一个消息;

l msgtyp > 0,返回该消息类型的第一个消息;

l msgtyp < 0,返回小于或等于msgtyp绝对值的消息中类型最小的第一个消息;

flag:设置操作标志。可以为0IPC_NOWAITMSG_NOERROR;用于在消息队列中没有可用的指定消息时,调用线程采用何种操作方式。

l 标志为IPC_NOWAIT,表示msgrcv操作以非阻塞的方式进行,在消息队列中没有可用的指定消息时,msgrcv操作会立刻返回,并设定errnoENOMSG

l 标志为0,表示msgrcv操作是阻塞操作,直到下面的情况发生:

ü 消息队列中有一个所请求的消息类型可以获取;

ü 消息队列从系统中删除,这种情况下回返回一个EIDRM错误;

ü 调用线程被某个捕捉到的信号中断,这种情况下返回一个EINTR错误;

l 标志为MSG_NOERROR,表示接收到的消息的消息体的长度大于msgsz长度时,msgrcv采取的操作。如果设置了该标志msgrcv在这种情况下回截断数据部分,而不返回错误,否则返回一个E2BIG错误。

int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );

msqid:消息队列的描述符;

cmd:控制操作的命令,SUS标准提供以下三个命令:

ü IPC_RMID,删除一个消息队列。执行该命令系统会立刻把该消息队列从内核中删除,该消息队列中的所有消息将会被丢弃。这和已经讨论过的POSIX消息队列有很大差别,POSIX消息队列通过调用mq_unlink来从内核中删除一个消息队列,但消息队列的真正析构会在最后一个mq_close结束后发生。

ü IPC_SET,根据buf的所指的值来设置消息队列msqid_ds结构中的msg_perm.uidmsg_perm.gidmsg_perm.modemsg_qbytes四个成员。

ü IPC_STAT,通过buf返回当前消息队列的msqid_ds结构。

Linux下还有例如IPC_INFOMSG_INFO等命令,具体可以参考Linux手册;

buf:指向msqid_ds结构的指针;消息队列缓冲区

举例:<<<<<<课件P131-P134例子samples\3.ProcessII\3.3-msgq\ msgq.c >>>

举例: <<<<如何定义消息的格式 samples\3.ProcessII\3.3-msgq\ msgq_t.c

解决方法:根据应用的需求,扩展结构体的成员。>>>>>>>>>>>>>

实验:<<<<<<<<实验5.5使用消息队列实现本地聊天室>>>>>>>>>>>>>>>>>>>

4. 进程间通讯方式比较

除了ppt上的总结。增加自己从APUE上得到的总结:

从实际实现的简单易用性出发,pipeFIFO依然是值得推荐的常用本地进程间通讯的方法。

l SystemV提供的方法不推荐在新的程序中使用,不过还是要了解因为SystemVIPC实在太经典,其存在的时间比POSIX长,支持它的OS也更多(移植性好),在很多已有的代码中随处可见,使用更普及。实际上对应的POSIX标准(比较新)就是在SystemV的基础上改进而来的,从接口定义和效率上都比SystemV有一定的改进,对于新写的程序建议往POSIX标准上靠拢。但在我们这里就不展开讲POSIX了,大家有兴趣可以以后自己看。

l SystemV中我们重点讲了共享内存和信号量。shm:效率最高(直接访问内存),需要同步、互斥机制。sem:配合共享内存使用,用以实现同步和互斥。消息队列讲得较少,相对来说应用也没有共享内存方便。

原创粉丝点击