文章目录
前言
一、Linux进程间通信方式总览
Linux下进程间通信有如下方式
- 管道(pipe),
- 命名管道(FIFO),
- 内存映射(mapped memeory),
- 消息队列(message queue),
- 共享内存(shared memory),
- 信号量(semaphore),
- 信号(signal)
- 套接字(Socket)
二、细致学习
1.管道
概括:
管道允许一个进程和另一个与它有共同祖先的进程(或者其祖先)之间进行通信;
管道实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。
特点:
- 半双工,数据只能朝一个方向流动
- 只能用于父子进程或者兄弟进程之间( 具有亲缘关系的进程)。 比如fork或exec创建的新进程, 在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。 当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。
- 单独构成一个文件系统,管道对于其两端的进程来说,自成体系,形成了一段只存在于内存当中的文件系统
- 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
实现机制:
管道是由内核管理的一个缓冲区
,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。
管道只能在本地计算机中使用,而不可用于网络间的通信
2.命名管道
命名管道是一种特殊类型的文件,它在系统中以文件形式存在。这样克服了管道的弊端,他可以 允许没有亲缘关系的进程间通信。
命名管道的原型如下
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename,mode_t mode); //建立一个名字为filename的命名管道,参数mode为该文件的权限(mode%~umask),若成功则返回0,否则返回-1,错误原因存于errno中。
eg.mkfifo( "/tmp/cmd_pipe", S_IFIFO | 0666 );
具体的操作原型便是创建一个管道,而后便可以开始read、open、write
管道和命名管道的区别:
对于命名管道FIFO来说,IO操作和普通管道IO操作基本一样,但是两者有一个主要的区别,在命名管道中,管道可以是事先已经创建好的,比如我们在命令行下执行mkfifo myfifo
就是创建一个命名通道,我们必须用open函数来显式地建立连接到管道的通道,而在管道中,管道已经在主进程里创建好了,然后在fork时直接复制相关数据或者是用exec创建的新进程时把管道的文件描述符当参数传递进去。
一般来说FIFO和PIPE一样总是处于阻塞状态。也就是说如果命名管道FIFO打开时设置了读权限,则读进程将一直阻塞,一直到其他进程打开该FIFO并向管道写入数据。这个阻塞动作反过来也是成立的。如果不希望命名管道操作的时候发生阻塞,可以在open的时候使用O_NONBLOCK标志,以关闭默认的阻塞操作。
3.信号
信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。
常见的信号机制
- 发送信号
进程可以通过调用kill函数向包括它本身在内的其他进程发送一个信号。如果程序没有权限,kill函数会调用失败,失败的常见原因是目标进程由另一个用户所拥有,这个函数跟shell同名命令kill的功能完全一样,定义如下:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
- alarm定时信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
- 挂起程序
#include <unistd.h>
int pause(void);
作用很简单,就是把程序的执行挂起直到有一个信号出现为止,才会继续运行它下面的代码。
这个函数很有用,因为有时我们需要等待某个信号的发生,使用它便意味着程序不需要总是在执行,浪费CPU资源,对系统性能造成极大的影响。
4.消息队列(Message queues)
消息队列是内核地址空间中的内部链表(看过消息队列源码的朋友们都知道消息队列的数据结构实现并不是队列而是链表),通过linux内核在各个进程直接传递内容,消息顺序地发送到消息队列中,并以几种不同的方式从队列中获得,每个消息队列可以用 IPC标识符 唯一地进行识别。内核中的消息队列是通过IPC的标识符来区别,不同的消息队列直接是相互独立的。每个消息队列中的消息,又构成一个 独立的链表。
消息队列克服了信号承载信息量少,管道只能承载无格式字符流。
- 消息队列的本质
Linux的消息队列(queue)实质上是一个链表,它有消息队列标识符(queue ID)。 msgget创建一个新队列或打开一个存在的队列;msgsnd向队列末端添加一条新消息;msgrcv从队列中取消息, 取消息是不一定遵循先进先出的, 也可以按消息的类型字段取消息。
消息队列与命名管道的比较
消息队列跟命名管道有不少的相同之处,通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read,则在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。
与命名管道相比,消息队列的优势在于:
1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。
2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。
3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。
注:系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
5.信号量(Semaphore)
信号量是一种计数器,用于控制对多个进程共享的资源进行的访问。它们常常被用作一个锁机制,在某个进程正在对特定的资源进行操作时,信号量可以防止另一个进程去访问它。
信号量是特殊的变量,它只取正整数值并且只允许对这个值进行两种操作:等待(wait)和信号(signal)。(P、V操作,P用于等待,V用于信号)
p(sv):如果sv的值大于0,就给它减1;如果它的值大于等于0,则进程继续执行,如果它的值小于0,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有其他进程因等待sv而挂起,则给它加1
简单理解就是P相当于申请资源,V相当于释放资源
这个学过操作系统的uu肯定知道,信号量更像是一种同步机制,当然同步机制也属于进程间通信手段的一种。
6.共享内存(Share Memory)
共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,由IPC为进程创建的一个特殊地址范围,它将出现在该进程的地址空间中。其他进程可以将同一段共享内存连接到自己的地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是malloc分配的一样。如果一个进程向共享内存中写入了数据,所做的改动将立刻被其他进程看到。
共享内存是IPC最快的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换。共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅映射到各进程的地址不同而已,因此不需要进行复制,可以直接使用此段空间。
笔者的理解是,共享内存位于一块创建者进程的内存中,其他进程通过函数连接的方式去访问这块位于其他进程下的共享内存。
创建函数:
int shmget(key_t key, size_t size, int shmflg);
参数:
key:这个共享内存段名字
size:共享内存大小
shmflg:创建时用 IPC_CREAT|0644 打开直接是0
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1.
连接函数:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:共享内存标识,即创建时的返回整数
shmaddr:指定连接地址
shmflg:SHM_RND或SHM_RDONLY
返回值: 成功返回一个指针,指向共享内存第一个节;失败返回-1.
返回值: 成功返回一个指针,指向共享内存第一个节;失败返回-1.
说明:
shmaddr 为 NULL时, 核心自动选择一个地址。
shmaddr不为BNULL, 且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍,公式:shmaddr - (shmaddr % SHMLBA)
shmflg = SHM_RDONLY,表示连接操作用来只读共享内存
shmdt函数:
功能:将共享内存与当前进程脱离
int shmdt(const void *shmaddr);
参数:
shmaddr:由shmad所返回的指针
返回值: 成功返回0,失败返回-1.
注意:将共享内存与当前进程脱离不等于删除共享内存段
缺点:共享内存本身并没有同步机制,需要程序员自己控制。
7.内存映射(Memory Map)
内存映射文件,是由一个文件到一块内存的映射。内存映射文件与 虚拟内存有些类似,通过内存映射文件可以保留一个地址的区域,
同时将物理存储器提交给此区域,内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而且在对该文件进行操作之前必须首先对文件进行映射。使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作。 每一个使用该机制的进程通过把同一个共享的文件映射到自己的进程地址空间来实现多个进程间的通信(这里类似于共享内存,只要有一个进程对这块映射文件的内存进行操作,其他进程也能够马上看到)。
使用内存映射文件不仅可以实现多个进程间的通信,还可以用于 处理大文件提高效率。因为我们普通的做法是把磁盘上的文件先拷贝到内核空间的一个缓冲区再拷贝到用户空间(内存),用户修改后再将这些数据拷贝到缓冲区再拷贝到磁盘文件,一共四次拷贝。如果文件数据量很大,拷贝的开销是非常大的。那么问题来了,系统在在进行内存映射文件就不需要数据拷贝?mmap()确实没有进行数据拷贝,真正的拷贝是在缺页中断处理时进行的,由于mmap()将文件直接映射到用户空间,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间,所以只进行一次数据拷贝。效率高于read/write。
共享内存和内存映射文件的区别:
- 内存映射文件是利用虚拟内存把文件映射到进程的地址空间中去,在此之后进程操作文件,就像操作进程空间里的地址一样了,比如使用c语言的memcpy等内存操作的函数。这种方法能够很好的应用在需要频繁处理一个文件或者是一个大文件的场合,这种方式处理IO效率比普通IO效率要高
- 共享内存是内存映射文件的一种特殊情况,内存映射的是一块内存,而非磁盘上的文件。共享内存的主语是进程(Process),操作系统默认会给每一个进程分配一个内存空间,每一个进程只允许访问操作系统分配给它的哪一段内存,而不能访问其他进程的。而有时候需要在不同进程之间访问同一段内存,怎么办呢?操作系统给出了 创建访问共享内存的API,需要共享内存的进程可以通过这一组定义好的API来访问多个进程之间共有的内存,各个进程访问这一段内存就像访问一个硬盘上的文件一样。
内存映射文件与虚拟内存的区别和联系:
内存映射文件和虚拟内存都是操作系统内存管理的重要部分,两者有相似点也有不同点。
联系:虚拟内存和内存映射都是将一部分内容加载到内存,另一部放在磁盘上的一种机制。对于用户而言都是透明的。
区别:虚拟内存是硬盘的一部分,是内存和硬盘的数据交换区,许多程序运行过程中把暂时不用的程序数据放入这块虚拟内存,节约内存资源。内存映射是一个文件到一块内存的映射,这样程序通过内存指针就可以对文件进行访问。
虚拟内存的硬件基础是分页机制。另外一个基础就是局部性原理(时间局部性和空间局部性),这样就可以将程序的一部分装入内存,其余部分留在外存,当访问信息不存在,再将所需数据调入内存。
而内存映射文件并不是局部性,而是使虚拟地址空间的某个区域银蛇磁盘的全部或部分内容,通过该区域对被映射的磁盘文件进行访问,不必进行文件I/O也不需要对文件内容进行缓冲处理。
8.套接字 (Socket)
套接字机制不但可以单机的不同进程通信,而且使得跨网机器间进程可以通信。
套接字的创建和使用与管道是有区别的,套接字 明确地将客户端与服务器 区分开来,可以实现多个客户端连到同一服务器。
服务器套接字连接过程描述:
首先,服务器应用程序用socket创建一个套接字,它是系统分配服务器进程的类似文件描述符的资源。 接着,服务器调用bind给套接字命名。这个名字是一个标示符,它允许linux将进入的针对特定端口的连接转到正确的服务器进程。 然后,系统调用listen函数开始接听,等待客户端连接。listen创建一个队列并将其用于存放来自客户端的进入连接。 当客户端调用connect请求连接时,服务器调用accept接受客户端连接,accept此时会创建一个新套接字,用于与这个客户端进行通信。
客户端套接字连接过程描述:
客户端首先调用socket创建一个未命名套接字,让后将服务器的命名套接字作为地址来调用connect与服务器建立连接。
只要双方连接建立成功,我们就可以像操作底层文件一样来操作socket套接字实现通信。
基本函数定义
#include <sys/types.h>
#include <sys/socket.h>
int socket(it domain,int type,int protocal);
int bind(int socket,const struct sockaddr *address,size_t address_len);
int listen(int socket,int backlog);
int accept(int socket,struct sockaddr *address,size_t *address_len);
int connect(int socket,const struct sockaddr *addrsss,size_t address_len);
总结
以上便是Linux下进程间通信方式相关学习,计划下一篇写一篇Android进程间通信方式的比较