美文网首页
Android之Linux跨进程通信的方式

Android之Linux跨进程通信的方式

作者: 小天使999999 | 来源:发表于2020-07-08 01:51 被阅读0次

    As we all know,Android是基于Linux内核开发的,而市面上几乎所有的App都离开跨进程通信。可能你会说Android是通过Binder完成进程之间的通信的。但是Binder是怎么来的?为什么安卓的开发人员放着好好的Linux系统跨进程通讯方式不使用,反而创建一种新的跨进程通信方式呢?
    Linux系统一共提供了六种跨进程通讯方式,我们分别讲解。

    一、管道pipe

    管道又分为,匿名管道和命名管道。

    1、匿名管道

    匿名管道是基于fork机制建立。当一个进程需要进行跨进程通讯时,如果两个进程之间存在亲缘关系,通常是指父子关系。那么,那么在fork进程的过程中,会同时复制父进程的读写管道的能力。这样两个进程都具有了读写管道的能力,Linux内核提供了一个特殊缓存区,两个进程可以同时对该缓冲区进行读写。但是,同一个进程只能读或写。如果此时将process1的写入机制和process1的读取机制同时打开,便可以完成跨进程通信机制。

    匿名管道通信
    值得注意的是,fork完成后的进程同时具备读写功能,如下图所示:
    fork初始结果
    由于匿名管道是半双工通信,所以读写功能不能同时使用,通信效率是50%。
    其实,Linux内核系统并不存在所谓的管道,只是而是借助了其文件系统的file结构和VFS的索引节点inode,通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。
    读写功能
    从图中可以看出,管道只是一种借助于文件结构的抽象,系统没有这种结构。
    关于读写机制,如果process2读的时候,process1正在将数据放入管道,process2就必须等待,这就牵涉到了 同步机制的概念。系统通过对VFS 索引节点进行锁定,让读取数据的进程进入休眠等待队列。待process1写入操作完成,内存区被解锁同时process2被唤醒读取数据。同样,process2读取时,process1也必须进入等待唤醒写入。
    注意:读写进程是同步操作,进程向管道中写消息时,管道另一端的读进程必须打开,否则写进程就会阻塞(默认情况下),命名管道也要遵守这个原则。

    2、命名管道

    但是匿名管道仅限于亲缘进程间的通信,这显然是有弊端的,系统便提供了命名管道作为解决。Linux系统提供了一种叫FIFO的特殊文件。如果进程A已读的方式打开FIFO文件,进程B以写的方式打开该 文件,系统就会在这两个文件之间建立管道。说到底,仍然是由Linux内核完成内存管理的。FIFO的好处在于文件在系统中都有自己的路径,我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接。
    命名管道的安全问题:
    a. 命名管道的读写规则:读操作时,会对FIFO文件设置写阻塞标记;写操作时会顿FIFO文件设置读阻塞标记。
    b. 如果有多个进程同时向同一个FIFO文件写数据,而只有一个读进程读取数据时,会发生怎么样的情况呢?会造成数据块的相互交错。
    为了解决这个问题,系统规定,在一个以O_WRONLY(即阻塞方式)打开的FIFO中,如果写入的数据长度小于等于PIPE_BUF,那么或者写入全部字节,或者一个字节都不写入。如果所有的写请求都是发往一个阻塞的FIFO的,并且每个写请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据绝不会交错在一起。其实这就是线程安全中的原子性操作。
    但是,通信效率依旧是50%。

    二、消息队列

    管道是一种逻辑上的结构,而且它通信效率低、不能传递格式化数据、需要进行同步读取,这给开发人员带来了很大的不便。
    消息队列则不然,它在Linux内核层拥有属于自己的格式,整个队列是链式结构,拥有写权限的进程可以向队列中写消息,拥有读权限的进程可以从队列中读消息,进程之间不必保持同步。消息队列是随内核持续的,只有在内核重起或者显示删除一个消息队列时,该消息队列才会真正被删除。
    系统提供结构体msg_queue用来描述消息队列,其内部实现参考下图:


    消息队列内部结构

    从图中可以看到,消息队列中封装了最后一次发送消息的时间、最后一次接收消息的时间、最后一次变化时间、当前队列中的字节数、当前队列中的消息数、当前队列最大字节数,甚至还包括上一次发送消息的进程号和上一次接收消息的进程号。消息列表、接收器列表和发送器列表。很明显,通过这些信息可以完成应用层和内核层之间的通信。
    通信流程参考下图:
    其中,struct ipc_ids msg_ids是内核中记录消息队列的全局数据结构;struct msg_queue是每个消息队列的队列


    内核访问消息队列
    从上图可以看出,全局数据结构 struct ipc_ids msg_ids 可以访问到每个消息队列头的第一个成员:struct kern_ipc_perm;而每个struct kern_ipc_perm能够与指定的消息队列对应起来。因为在该结构中,有一个key_t类型成员key,而key则唯一确定一个消息队列。kern_ipc_perm结构如下:
    struct kern_ipc_perm {   //内核中记录消息队列的全局数据结构msg_ids能够访问到该结构;
                key_t   key;    //该键值则唯一对应一个消息队列
                uid_t   uid;
                gid_t   gid;
      uid_t   cuid;
      gid_t   cgid;
      mode_t  mode;
      unsigned long seq;
    }
    

    消息队列是用来封装信息的,消息的结构类型通常如下:

    struct msgbuf{
    long mtype;
    char mtext[size];
    };
    

    mtype是消息的唯一id,mtext是消息内容,系统根据这些消息可以确定具体消息的大小,同样也根据id读取消息。
    消息队列作为跨进程通信方式,允许多进程竞争读写消息,拥有独立的结构体,跟随内核的持续性,而且它规范化的消息结构,为竞争消息的进程优先级提供了方便。
    但是,消息队列也有自身的局限:

    1. 每个消息队列的容量(所能容纳的字节数)都有限制,该值因系统不同而不同。
    2. 每个消息队列所能容纳的最大消息数:在redhad 8.0中,该限制是受消息队列容量制约的:消息个数要小于消息队列的容量(字节数)
      注:上述两个限制是针对每个消息队列而言的,系统对消息队列的限制还有系统范围内的最大消息队列个数,以及整个系统范围内的最大消息数。

    三、共享内存

    顾名思义,共享内存就是系统中多个进程共用同一块内存区域。其通信原理如下图:


    共享内存通信原理

    说白了就是,逻辑地址指向同一块物理地址。既然是共享必然要满足内存共享的基本规则:

    1. 可见性
      某个进程对共享内存的修改,对其他进程来说是可见的。
    2. 原子性
      当前内存被某个进程占用的时候,其他进程不得访问共享内存进行修改操作。
      基于以上两点,使用共享内存需要同步机制,但共享内存并未提供同步机制,所以需要借助于其他机制来完成,比如信号量、同步锁Mutex
      通过同步机制,在数据被写入之前不允许进程从共享内存中读取信息、不允许两个进程同时向同一个共享内存地址写入数据。
    3. 使用规则
      要使用一块共享内存进程必须首先分配它,随后需要访问这个共享内存块的每一个进程,都必须将这个共享内存绑定到自己的地址空间中。当完成通信之后,所有进程都将脱离共享内存,并且由其中一个进程释放该共享内存块。
    4. Android现状


      SharedMemory

      可见,在API27,也就是Android8.1版本,系统专门提供了SharedMemory类供开发者使用。SharedMemory用于对匿名共享内存区的创建,映射和保护控制。
      (1)优点:使用共享内存进行进程之间的通信是非常方便的,函数的接口也比较简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,加快了程序的效率。
      (2)缺点:共享内存没有提供同步机制,这使得我们在使用共享内存进行进程之间的通信时,往往需要借助其他手段来保证进程之间的同步工作。

    四、信号量

    共享内存资源在进行同步时,通常使用信号量机制。信号量就是一个计算器,记录当前内存的使用状态。计数值为正,表示资源未被锁定,任何进程都可以访问资源;计数值为0,表示资源被锁定,想要访问它的进程只能挂起等待。

    信号量的原理

    信号量只能进行等待发送两种操作,即P(sv)和V(sv):

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

    五、信号

    信号有很多种,如下图:


    信号分类

    每一种信号都有自己对应的处理程序。信号的发送端在内核层,处理端在用户区域的应用层。比如,杀死进程信号会造成应用崩溃;停止信号会使应用退出。
    这种一一对应的关系,要求信号只能处理一些单一操作,或者边缘操作。

    六、套接字(socket)

    套接字也是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不同计算机通过网络连接进行跨进程通信。也因为这样,套接字明确地将客户端和服务器区分开来。
    也就是说,它在基于网络的通信上更擅长,其稳定性受限于网络。

    Linux系统跨进程通信方式,虽各有千秋,但在面对单用户、多进程的Android系统需求时,仍捉襟见肘。因此,Binder诞生了。

    相关文章

      网友评论

          本文标题:Android之Linux跨进程通信的方式

          本文链接:https://www.haomeiwen.com/subject/hoigqktx.html