移步系列Android跨进程通信IPC系列

1 Linux下进程通信

Linux下进程通信有以下七种:

  • 1、匿名管道(pipe)
  • 2、命名管道(FIFO)
  • 3、信号(signal)
  • 4、信号量(semaphore)
  • 5、消息队列(message queue)
  • 6、共享内存(share memory)
  • 7、套接字(Socket)

2 匿名管道(pipe)

2.1 什么是匿名管道?

匿名管道(pipe)是Linux支持的最初Unix IPC形式之一,具有以下特点:

  • 匿名管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立两个管道;
  • 只能作用于父子进程或者兄弟进程之间(具有亲缘关系的进程)
  • 单独构成的一种独立的文件系统:匿名管道对于管道两端的进程而言,就是一个文件,但它不是普通文件,它不属于某种文件系统,而是自理门户,单独构成一种文件系统,去并且只存在于内存中。
  • 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓存区的头部读出数据。

2.2 匿名管道的实现机制

  • 匿名管道是右内核管理的一个缓冲区,相当于我们放入内存的中一个纸条。匿名管道的一端连接一个进程的输出。这个进程会向管道中放入信息。
  • 匿名管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。
  • 一个缓存区不需要很大,它被设计成为唤醒的数据结构,以便管道可以被循环利用。
  • 当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。
  • 当管道被放满信息的时候,尝试放入信息的进程就会等待,直到另一端的进程取出信息。
  • 两个进程都终结的时候,管道也会自动消失。


    5713484-1d562f8269b81345.png
  • 从原理上,匿名管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。
    1. 最开始的时候,上面的两个箭头都连接到同一个进程Process 1上(连接在Process 1上的两个箭头)。
    1. 当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。
    1. 随后,每个进程关闭在自己不需要的一个连接(两个黑色的箭头被关闭;Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了上图的PIPE。
5713484-cfdbad83904e2279.png

2.3 匿名管道实现细节

  • 在Linux中,匿名管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构,和VFS的索引节点inode。
  • 通过将两个file结构指向同一个临时的VFS节点,而这个VFS索引节点又指向了一个物理页面而实现的。
5713484-7d91e2fab54d6c95.png

2.4 关于匿名管道的读写

  • 匿名管道的实现的源代码在fs/pipe.c中,在pipe.c中有很多函数,其中有两个函数比较重要,即匿名管道pipe_read()读函数和匿名管道写函数pipe_write()。
  • 匿名管道写函数通过将字节复制到VFS索引节点指向物理内存而写入数据,而匿名管道读函数则通过复制物理内存而读出数据。
    当然,内核必须利用一定的同步机制对管道的访问,为此内核使用了等待队列、和信号
  • 当写入进程向匿名管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的file结构。
  • file结构中制定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。
  • 写入函数在向内存中写入数据之前,必须首先检查VFS索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作
  • 内存中有足够的空间可以容纳所有要写入的数据。
  • 内存没有被读程序锁定。
  • 如果同时满足上述条件,写入函数首先会锁定内存,然后从写进程的地址空间中复制数据到内存。
  • 否则,写进程就休眠在VFS索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。
  • 写进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接受到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。
  • 进程可以在没有数据或者内存被锁定时立即返回错误信息,而不是阻塞该进程,这一来于文件或管道的打开模式。
  • 进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页被释放。
  • VFS
    VFS(virtual File System/虚拟文件系统):是Linux文件系统对外的接口。任何要使用文件系统的程序都必须经由这层接口来使用它。它是采用标准的Unix系统调用读写位于不同物理介质上的不同文件系统。VFS是一个可以让open()、read()、write()等系统调用不用关系底层的存储介质和文件系统类型就可以工作的粘合层。在Linux中,VFS采用的是面向对象的编程方法。

3 命名管道(FIFO/named PIPE)

  • 匿名管道(pipe):这个方式的一个缺陷,就是这些就进程都是由一个共同的祖先进程启动,这给我们在不相关的进程之间交换数据带来了不方便。

3.1 什么是命名管道

  • 命名管道也被称为FIFO或者named pipe,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的匿名管道类似。
  • FIFO(First in, First out) 为一种特殊的文件类型,它在文件系统中有对应的路径。
  • 当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。
  • 叫FIFO,因为管道本质上是一个** 先进先出的队列数据结构 ,最早放入的数据被最先读出来,从而保证信息交流的顺序
  • 写模式的进程向FIFO中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。FIFO的好处在于我们可以通过 文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接 **

3.2 命名管道的读写规则

  • 1、从FIFO中读取数据的约定:如果一个进程为了从FIFO中读取数据而阻塞打开了FIFO,那么该进程内的读操作 为设置了阻塞标志的读操作。
  • 2、从FIFO中写入数据的约定:如果一个进程为了想FIFO中写入数据而阻塞打开了FIFO,那么该进程内的写操作 为设置了阻塞标志的写操作。

3.3 命名管道的安全问题

  • 让写操作原子化,怎么才能使写操作原子化呢?
  • 系统规定,在一个以O_WRONLY(即阻塞方式)打开的FIFO中,如果写入的数据长度小于等于PIPE_BUF,那么或者写入全部字节,或者一个字节都不写入。如果所有的写请求都是发往一个阻塞的FIFO的,并且每个写请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据绝不会交错在一起。

4 信号(Signal)

4.1 什么是信号?

  • 信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;
  • Linux除了支持Unix早期信号语义函数sigal外,还支持语义服务Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,有能够统一对外接口,用sigaction函数重新实现了signal函数)

4.2 信号的种类

5713484-b703c7840590eaeb.png
  • 每种信号类型都有对应的信号处理程序(也叫信号的操作),就好像每个中断都有一个中断服务例程一样。
  • 大多数信号的默认操作是结束接受信号的进程;然而一个进程通常可以请求系统采取某些代替的操作
  • 各种代替操作是
  • 忽略信号。随着这一选项的设置,进程将忽略信号的出现。有两个信号不可以被忽略:SIGKILL,它将结束进程:SIGSTOP,它是作业控制机制的一部分,将挂起作业的执行。
  • 恢复信号的默认操作
  • 执行一个预先安排的信号处理函数。进程可以登记特殊的信号处理函数。当进程收到信号时,信号处理函数将像中断服务例程一样被调用,当从信号处理函数返回时,控制被返回给主程序,并且继续正常执行。
  • 但是,信号和中断有所不同。中断的响应和处理都发生在内核空间,而信号的响应发生在内核空间,信号处理程序的执行却发生在用户空间

那么什么时候检测和响应信号?通常发生在两种情况下:

  • 当前进程由于系统调用、中断或异常而进入内核空间以后,从内核空间返回到用户空间前戏
  • 当前进程在内核进入睡眠以后刚被唤醒的时候,由于检测到信号的存在而提前返回到用户空间

4.3 信号的本质

  • 信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。
  • 信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
  • 信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。
  • 信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息

4.4 信号来源

  • 信号事件的发生有两个来源:硬件来源(比如我们按下键盘或者其他硬件故障);润健来源,最常用发送信号的系统函数是kill,raise,alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

4.5 关于信号处理机制的原理(内核角度)

  • 内核给一个进程发送中断信号,是在进程所在的进程表项的信号域设置对应于该信号的位。
  • 如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒正在睡眠的进程;否则仅设置进程表中信号域相应的位,而不是唤醒进程。
  • 进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者,在一个进程进入或离开一个适当的低调度优先级睡眠状态时。
  • 内核处理一个进程吸收的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。
  • 内核处理一个进程收到的软中断信号是在该进程的上下文中。因此,进程必须处于运行状态。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。
  • 而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数出,从函数返回再弹出栈顶时,才返回原先进入内核的地方,接着原来的地方继续运行。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行。

4.6 信号的生命周期

5713484-025dbc4847a838ef.png

5 信号量(semaphore)

4.1 什么是信号量

  • 信号量又称为信号灯,它用来协调不用进程间的数据对象,而最主要的应用是共享内存方式的进程间通信。
  • 本质上,信号量时一个计数器,他用来记录某个资源(如共享内存)的存取状况。信号量的使用,主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。
  • 信号量的值为正的时候,说明它空闲。所有的线程可以锁定而使用它。若为0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。
  • 信号量的值为正的时候,说明它空闲。所有的线程可以锁定而使用它。若为0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。

4.2 信号量的注意事项

  • 为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要这一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能由一个执行线程访问代码的临界区域
  • 临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就说信号量临界区是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就说信号量是用来协调进程对共享资源的访问。
  • 信号量时一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P-信号变量)和发送(即V信号变量)信息操作。
  • 最简单的信号量只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量

4.3 信号量的原理

由于信号量只能进行两种操作即"等待"和"发送",即P(sv)和V(sv),他们的行为是这样:

  • P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
  • V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。
  • 举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,他将得到信号量,并可以进如临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会挂起以等待第一个进程离开临界区并执行(sv)释放信号。

4.4 信号量的分类

Linux提供两种信号量

  • 内核信号量:由内核控制路径使用
  • 用户态进程使用的信号量:这种信号量又分为POSIX信号量和SYSTEM V信号量
  • POSIX辛信号量又分为有名信号量和无名信号量
  • 有名信号量:其值保存在文件中,所以它可以用于线程也可以用于进程间同步
  • 无名信号量:其值保存在内存。

POSIX信号量和SYSTEM V信号量的比较

  • 对POSIX来说,信号量是个非负数。常用于线程间同步
    而SYSTEM V信号量则是一个或者多个信号量集合,他对应的是一个信号量的结构体,这个结构体为SYSTEM V IPC服务的,信号量只不过是它的一部分。常用语进程间同步。

  • POSIX信号量的引用头文件是,而SYSTEM V信号量的引用头文件是

  • 从使用的角度,System V信号量是简单的。比如,POSIX信号量的创建和初始化PV操作就很方便。

5 消息队列(message)

5.1 消息队列也称为报文队列

  • 消息队列也成为报文队列,消息队列是随内核持续的,只有在内核重启或者显示删除一个消息队列时,该消息队列才会真正删除。
  • 系统中记录消息队列的数据结构体 struct ipc_ids_msg_ids位于内核中,系统中所有消息队列都可以在结构msg_ids中找到访问入口。

5.2 消息队列的原理及注意事项

  • 消息队列其实就是一个消息的链表
  • 每个消息都有一个队列头,称为struct_msg_queue,这个队列头描述了消息队列的key值,用户ID,组ID等信息,但它存于内核中
  • 结构体struct msqid_ds能够返回或设置消息队列的信息,这个结构体位于用户空间中,与msg_queue结构相似的消息队列允许一个或多个进程向它写入或读取消息,消息队列是消息的链表。
  • 消息是按消息类型访问,进程必须指定消息类型来读取消息,同样,当向消息队列中写入消息事业必须给出消息的类型,如果读队列使用消息的类型为0,则读取队列中的第一条消息。
  • 内核空间的结构体msg_queue描述了对应key值消息队列的情况,而对应用户空间的msqid_ds这个结构体,因此,可以操作msgid_ds这个结构体来操作消息队列。

6 共享内存(share memory)

共享内存是进程间通信中最简单的方式之一。

6.1 什么是共享内存?

  • 共享内存是系统处于多个进程之间通讯的考虑,而预留的一块内存区。
  • 共享内存允许两个或更多的进程访问同一块内存,就如同malloc()函数向不同进程返回了指向同一个物理内存区域的指针。
  • 当一个进程改变了这块地址中的内容的时候,其他进程都会觉察到这个更改。

6.2 关于共享内存

  • 当一个程序加载进内存后,它就被分成叫做页的块。
  • 通信将存在内存的两个页之间或者两个独立的进程之间。
  • 当一个程序想和另外一个程序通信的时候,那内存将会为这两个程序生成一块公共的内存区域。这块被两个进程分享的内存区域叫做共享内存
  • 由于所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率
  • 访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其他需要切入内核的过程来完成。同时它也也避免了对数据的跟中不必要的复制。
  • 如果没有共享内存的概念,那一个进程不能存取另外一个进程的内存部分,因而导致共享数据或者通信失效。因为系统内核没有对访问共享内存进行同步,开发者必须提供自己的同步措施
  • 解决了这些问题的常用方法是是通过信号量进行同步。不过通常我们程序只有一个进程访问了共享内存,因此在集中展示了共享内存机制的同时,我们避免了让代码被同步逻辑搞的混乱不堪。
  • 为了简化共享数据的完整性和避免同时存取数据,内核提供了一种专门存取共享内存资源的机制。这称为互斥体或者Mutex对象

6.3 Mutex对象

  • 例如,在数据被写入前不允许进程从共享内存中读取信息、不允许两个进程同时向一个共享内存地址写入数据等。
  • 当一个基础想和两一个进程通信的时候,它将按以下顺序运行:
  • 1、获取Mutex对象,锁定共享区域
  • 2、将要通信的数据写入共享区域
  • 3、释放Mutex对象
  • 当一个进程从这个区域读取数据的时候,它将重复同样的步骤,只是将第二步变成读取。

6.4 内存模型

要使用一块共享内存

  • 进程必须首先分配它
  • 随后需要访问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中。
  • 当完成通信之后,所有进程都脱离共享内存,并且由一个进程释放该共享内存块。
  • 在/proc/sys/kernel/目录下,记录着共享内存的一些限制,如一个共享内存区的最大字节数shmmax,系统范围内最大的共享内存区标志符数shmmni等。

6.5 Linux系统内存模型

  • 在Linux系统中,每个进程的虚拟内存是被分为许多页面的。这些内存页面中包含了实际的数据。每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。尽管每个进程都有自己的内存地址,不同的进程可以同时将同一个页面页面映射到自己的地址空间,从而达到共享内存的目的。
  • 分配一个新的共享内存块会创建新的内存页面。因为所有进程都希望共享对同一块内存的访问,只应由一个进程创建一块新的共享内存。再次分配一块已经存在的内存块不会创建新的页面,而只是会返回一个标示该内存块的标识符。
  • 一个进程如需使用这个共享内存块,则首先需要将它绑定到自己的地址空间中。
  • 这样会创建一个从进程本身虚拟地址到共享页面的映射关系。当对共享内存的使用结束之后,这个映射关系将被删除
  • 当再也没有进程需要使用这个共享内存块的时候,必须有一个(有且只有一个)进程负责释放这个被共享的内存页面。
  • 所有共享内存块的大小必须是系统页面大小的整数倍。系统页面大小指的是系统中单个内存页面包含的字节数。在Linux系统中,内存页面大小是4KB,不过您仍然应高通过调用getPageSize获取这个值。

6.6 Linux共享内存的实现步骤

共享内存的实现分为两个步骤:

  • 创建共享内存,使用shmget函数
  • 映射共享内存,将这段创建的共享内存映射到具体的进程空间中,使用shmat函数

7 套接字(socket)

  • 套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
  • 更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是右Unix系统的BSD分之开发出来的,但是现在一般可以移植其他类Unix系统上。比如Linux和System V的变种都支持套接字。

8 Linux的几种跨进程通信的方式的比较

8.1 效率比较

类型 无连接 可靠 流控制 优先级
匿名PIPE N Y Y N
命名PIPE(FIFO) N Y Y N
信号量 N Y Y Y
消息队列 N Y Y Y
共享内存 N Y Y Y
UNIX流SOCKET N Y Y N
UNIX数据包SOCKET Y Y N N

PS:无连接是指无需调用某种行动是OPEN,就有发送消息的能力流控制,如果系统资源短缺或者不能接受更多的消息,则发送进程能进进行流量控制

8.2 优缺点比较

  • 匿名管道(pipe):速度慢,容量有限,只有父子进程能通讯
  • 有名管道(FIFO): 任务进程都能通讯,但速度慢
  • 消息队列(message queue):容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据问题。
  • 信号量:不能传递复杂消息,只能用来同步
  • 共享内存区:能够容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题。相当于线程中的线程安全,当然,共享内存区同样可以做线程间通讯,不过没有这个必要,线程间本来就已经共享了同一进程内的一块内存

8.3 使用场景

  • 如果用户传递的信息较少或是需要通过信号来出发某些行为,上面提到的软中断信号机制不失为一种简洁有效的一种进程间通信方式。但若是进程间要求传递的信息量比较大或者进程间存在交换数据的要求,那就需要考虑别的通信方式。
  • 匿名管道简单方便,但局限于单向通信的工作方式,并且只能创建它的进程及其子孙进程之间实现管道的共享。
  • 有名管道虽然可以提供给任意关系的进程使用,但是由于其长期存在于系统之中,使用不当容易出错。所以不建议初级开发者使用。
  • 消息缓存可以不再局限于父子进程,而允许任意进程间通过共享消息队列来实现进程间通信,并由系统调用函数来实现消息发送和接受方之间的同步,从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题,使用方便,但是信息的复制需要额外的消耗CPU的时间,不适宜信息量大或操作频繁的场合。
  • 共享内存针对消息缓存的缺点而改进,利用了内存缓存区直接交换信息,无需复制,快捷、信息量大的是其优点。但是共享内存的通信方式是通过将共享内存缓存直接附加到进程的虚拟地址空间中来实现的。因此这些进程之间的读写操作的同步问题操作系统无法实现。必须由各进程利用其它同步工具解决。另外, 由于内存实体存在于计算机系统中,所以只能由处于同一个计算机系统中的其它进程共享,不方便网络通信。
  • 共享内存块提供了在任意数量的进程之间进行高效双向通信的机制。每个使用者都可以读取写入数据,但是所有程序之间必须达成并遵守一定的协议,以防止诸如在读取信息之前覆写内存空间等竞争状态的出现。不行的是,Linux无法严格保证提供对共享内存块的独占访问,同时,多个使用共享内存块的进程之间必须协调使用同一个键值。

参考

Android跨进程通信IPC之1——Linux基础

更多相关文章

  1. 解决Android数据库锁的问题
  2. Android进程间通信(二):通过AIDL介绍Binder的工作机制
  3. [Android]高性能MMKV数据交互分析-MMKV初始化
  4. Android应用程序四大组件之使用AIDL如何实现跨进程调用Service
  5. Android(安卓)Application 理解
  6. Android(安卓)内存管理机制
  7. Android(安卓)App 性能优化之稳定性
  8. Android内核解读-Android系统的开机启动过程
  9. Android(安卓)3.2 应用程序联机(devices)测试失败提示INSTALL_FAIL

随机推荐

  1. android core dump测试
  2. Linux安装mitmproxy并监控android数据包
  3. android一些属性的总结
  4. Android学习系列(2)--App自动更新之通知
  5. Android(安卓)进度条算法 更新进度条算法
  6. Ubuntu通过MTP访问Android设备
  7. 终于搞定Eclipse下看Android的源码
  8. Android特殊字体引入,以及描边和投影
  9. Android(安卓)Phone和Pad UA区别
  10. Android(安卓)XML属性在文档中的位置