
Inter-Process
Communication
进程间通信。
UNIX进程间通信(IPC)方式包括:管道、FIFO、信号。
System
V进程间通信(IPC)包括:System
V消息队列、System
V信号灯、System
Posix进程间通信(IPC)包括:Posix消息队列、Posix信号灯、Posix共享内存。
现在Linux中的进程通信主要有:管道(Pipe)、信号(Signal)、消息队列、共享内存、信号量、套接字(Socket)
正如上一章所说, 跨进程通信是需要内核空间做支持的. 传统的 IPC 机制如 管道, Socket, 都是内核的一部分, 因此通过内核支持来实现进程间通信自然是没问题的.
但是 Binder 并不是 Linux 系统内核的一部分, 那怎么办呢, 这得益于 Linux 的动态内核可加载模块 (Loadable Kernel Module, LKM)的机制
这样 Android 系统就可以通过动态添加一个内核模块运行在内核空间, 用户进程进程之间通过这个内核模块作为桥梁来实现通信.
那么在 Android 系统中用户进程之间是如何通过这个内核模块 (Binder Driver)来实现通信的呢? 显然不是和上一章的传统 IPC 通信一样,进行两次 copy 了, 不然Binder 也不有在性能方面的优势了.
Binder IPC 机制中设计到的内存映射通过 mmap() 来实现, mmap() 是 *** 作系统中一种内存映射的方法.
内存映射能减少数据 copy 的次数, 实现用户空间和内核空间的高效互动. 两个空间各自的修改也能直接反应在映射的内存区域, 从而被对方空间及时感知. 也正因为如此, 内存映射能够提供对进程间通信的支持.
Binder IPC 正是基于内存映射( mmap() ) 来实现的, 但是 mmap() 通常是用在有物理介质的文件系统上的.
比如进程中的用户区域是不能直接和物理设备打交道的, 如果想要把磁盘上的数据读取到进程的用户区域, 需要两次 copy (磁盘 ->内核空间 ->用户空间). 通常在这种场景下 mmap() 就能发挥作用, 通过在物理介质和用户空间之间建立映射, 减少数据的 copy 次数, 用内存读写代替 I/O 读写, 提高文件读取效率.
而 Binder 并不存在物理介质, 因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间映射, 而是用来在内核空间创建数据接收的缓存空间.
一次完整的 Binder IPC 通信过程通常是这样:
这样就完成了一次进程间通信
如下图:
介绍完 Binder IPC 的底层通信原理, 接下来我们看看实现层面是如何设计的
一次完成的进程间通信必然至少包含两个进程, 通常我们称通信的双方分别为客户端进程(Client) 和服务端进程(Server), 由于进程隔离机制的存在, 通信双方必然需要借助 Binder 来实现.
BInder 是基于 C/S 架构. 是由一些列组件组成. 包括 Client, Server, ServiceManager, Binder 驱动.
Binder 驱动就如如同路由器一样, 是整个通信的核心. 驱动负责进程之间 Binder 通信的建立 / 传递, Binder 引用计数管理, 数据包在进程之间的传递和交互等一系列底层支持.
ServiceManager 作用是将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用, 使得 Client 能够通过 Binder 的名字获得对 Binder 实体的引用.
注册了名字的 Binder 叫实名 Binder, 就像网站一样除了 IP 地址以外还有自己的网址.
Server 创建了 Binder, 并为它起一个字符形式, 可读易记的名字, 将这个 BInder 实体连同名字一起以数据包的形式通过 Binder 驱动 发送给 ServiceManager, 通知 ServiceManager 注册一个名字为 "张三"的 Binder, 它位于某个 Server 中, 驱动为这个穿越进程边界的 BInder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用, 将名字以及新建的引用打包传给 ServiceManager, ServiceManager 收到数据后从中取出名字和引用填入查找表.
ServiceManager 是一个进程, Server 又是一个另外的进程, Server 向 ServiceManager 中注册 BInder 必然涉及到进程间通信. 当实现进程间通信又要用到进程间通信, 这就好像蛋可以孵出鸡的前提确实要先找只鸡下蛋! Binder 的实现比较巧妙, 就是预先创造一只鸡来下蛋. ServiceManager 和其他进程同样采用 Binder 通信, ServiceManager 是 Server 端, 有自己的 Binder 实体, 其他进程都是 Client, 需要通过这个 Binder 的引用来实现 Binder 的注册, 查询和获取. ServiceManager 提供的 Binder 比较特殊, 它没有名字也不需要注册. 当一个进程使用 BINDERSETCONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的那只鸡). 其实这个 Binder 实体的引用在所有 Client 中都固定为 0 , 而无需通过其他手段获得. 也就是说, 一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个 0 号引用和 ServiceManager 的 Binder 通信. 这里说的 Client 是相对于 ServiceManager 而言的, 一个进程或者应用程序可能是提供服务的 Server, 但是对于 ServiceManager 来说它仍然是个 Client.
Server 向 ServiceManager 中注册了 Binder 以后, Client 就能通过名字获得 Binder 的引用. Client 也利用保留的 0 号引用向 ServiceManager 请求访问某个 Binder. 比如,Client 申请访问名字叫"张三"的 Binder 引用. ServiceManager 收到这个请求后从请求数据包中取出 Binder 名称, 在查找表里找到对应的条目, 取出对应的 Binder 引用, 作为回复发送给发起请求的 Client. 从面相对象的角度看, Server 中的 Binder 实体现在有两个引用: 一个位于 ServiceManager 中, 一个位于发起请求的 Client 中. 如果后面会有更多的 Client 请求该 Binder, 系统中就会有更多的引用指向这个 Binder, 就像 Java 中一个对象有多个引用一样.
我们已经解释清楚 Client, Server 借助 Binder 驱动完成跨进程通信的实现机制了, 但是还有个问题需要弄清楚, 比如 A 进程想要 B 进程中的某个对象(object) 是如何实现的呢, 毕竟它们属于不同的进程, A 进程没办法直接使用 B 进程中的 object.
前面我们说过跨进程通信的过程都有 Binder 驱动的参与, 因此在数据流经 Binder 驱动的时候 Binder 驱动会对数据做一层转换.
我们在 Client端,向 ServiceManager 获取具体的 Server 端的 Binder 引用的时候,会首先进过 Binder 驱动,Binder 驱动它并不会把真正的 Server 的 Binder 引用返回给 Client 端,而是返回一个代理的 java 对象,该对象具有跟 Server 端的 Binder 引用相同的方法签名,这个对象为 ProxyObject,他具有跟 Server 的 Binder 实例一样的方法,只是这些方法并没有 Server 端的能力,这些方法只需要把请求参数交给 Binder 驱动即可. 对于 Client 端来说和直接调用 Server 中的方法是一样的.
了解了上面之后, 我们大致可以推算出 Binder 的通信过程
1. 注册 ServiceManager
2. 注册 Server
3. Client 获取 Server 的 Binder 引用
4. Client 与 Server 通信
共享内存指在多处理器的计算机系统中,可以被不同中央处理器(CPU)访问的大容量内存。由于多个CPU需要快速访问存储器,这样就要对存储器进行缓存(Cache)。任何一个缓存的数据被更新后,由于其他处理器也可能要存取,共享内存就需要立即更新,否则不同的处理器可能用到不同的数据。共享内存 (shared memory)是 Unix下的多进程之间的通信方法 ,这种方法通常用于一个程序的多进程间通信,实际上多个程序间也可以通过共享内存来传递信息。共享内存的创建
共享内存是存在于内核级别的一种资源,在shell中可以使用ipcs命令来查看当前系统IPC中的状态,在文件系统/proc目录下有对其描述的相应文件。函数shmget可以创建或打开一块共享内存区。函数原型如下: #include <sys/shm.h> int shmget( key_t key, size_t size, int flag ) 函数中参数key用来变换成一个标识符,而且每一个IPC对象与一个key相对应。当新建一个共享内存段时,size参数为要请求的内存长度(以字节为单位)。 注意:内核是以页为单位分配内存,当size参数的值不是系统内存页长的整数倍时,系统会分配给进程最小的可以满足size长的页数,但是最后一页的剩余部分内存是不可用的。 当打开一个内存段时,参数size的值为0。参数flag中的相应权限位初始化ipc_perm结构体中的mode域。同时参数flag是函数行为参数,它指定一些当函数遇到阻塞或其他情况时应做出的反应。shmid_ds结构初始化如表14-4所示。
编辑本段初始化
shmid_ds结构数据 初 值 shmid_ds结构数据 初 值
shm_lpid 0 shm_dtime 0
shm_nattach 0 shm_ctime 系统当前值
shm_atime 0 shm_segsz 参数 size
下面实例演示了使用shmget函数创建一块共享内存。程序中在调用shmget函数时指定key参数值为IPC_PRIVATE,这个参数的意义是创建一个新的共享内存区,当创建成功后使用shell命令ipcs来显示目前系统下共享内存的状态。命令参数-m为只显示共享内存的状态。 (1)在vi编辑器中编辑该程序如下: 程序清单14-8 create_shm.c 使用shmget函数创建共享内存 #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <stdlib.h> #include <stdio.h> #define BUFSZ 4096 int main ( void ) printf ( "successfully created segment : %d \n", shm_id ) system( "ipcs -m")/*调用ipcs命令查看IPC*/ exit( 0 ) } (2)在shell中编译该程序如下: $gcc create_shm.c–o create_shm (3)在shell中运行该程序如下: $./ create_shm successfully created segment : 2752516 ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x00000000 65536 root 600 393216 2 dest 0x00000000 2654209 root 666 4096 0 0x0056a4d5 2686978 root 600 488 1 0x0056a4d6 2719747 root 600 131072 1 0x00000000 2752516 root 666 4096 0 上述程序中使用shmget函数来创建一段共享内存,并在结束前调用了系统shell命令ipcs –m来查看当前系统IPC状态。
编辑本段共享内存的 *** 作
由于共享内存这一特殊的资源类型,使它不同于普通的文件,因此,系统需要为其提供专有的 *** 作函数,而这无疑增加了程序员开发的难度(需要记忆额外的专有函数)。使用函数shmctl可以对共享内存段进行多种 *** 作,其函数原型如下: #include <sys/shm.h> int shmctl( int shm_id, int cmd, struct shmid_ds *buf ) 函数中参数shm_id为所要 *** 作的共享内存段的标识符,struct shmid_ds型指针参数buf的作用与参数cmd的值相关,参数cmd指明了所要进行的 *** 作,其解释如表14-5所示。
编辑本段cmd参数详解
cmd的值 意 义
IPC_STAT 取shm_id所指向内存共享段的shmid_ds结构,对参数buf指向的结构赋值
IPC_SET 使用buf指向的结构对sh_mid段的相关结构赋值,只对以下几个域有作用,shm_perm. uid shm_perm.gid以及shm_perm.mode 注意此命令只有具备以下条件的进程才可以请求: 1.进程的用户ID等于shm_perm.cuid或者等于shm_perm.uid 2.超级用户特权进程
IPC_RMID 删除shm_id所指向的共享内存段,只有当shmid_ds结构的shm_nattch域为零时,才会真正执行删除命令,否则不会删除该段 注意此命令的请求规则与IPC_SET命令相同
SHM_LOCK 锁定共享内存段在内存,此命令只能由超级用户请求
SHM_UNLOCK 对共享内存段解锁,此命令只能由超级用户请求
使用函数shmat将一个存在的共享内存段连接到本进程空间,其函数原型如下: #include <sys/shm.h> void *shmat( int shm_id, const void *addr, int flag ) 函数中参数shm_id指定要引入的共享内存,参数addr与flag组合说明要引入的地址值,通常只有2种用法,addr为0,表明让内核来决定第1个可以引入的位置。addr非零,并且flag中指定SHM_RND,则此段引入到addr所指向的位置(此 *** 作不推荐使用,因为不会只对一种硬件上运行应用程序,为了程序的通用性推荐使用第1种方法),在flag参数中可以指定要引入的方式(读写方式指定)。 %说明:函数成功执行返回值为实际引入的地址,失败返回–1。shmat函数成功执行会将shm_id段的shmid_ds结构的shm_nattch计数器的值加1。 当对共享内存段 *** 作结束时,应调用shmdt函数,作用是将指定的共享内存段从当前进程空间中脱离出去。函数原型如下: #include <sys/shm.h> int shmdt( void *addr) 参数addr是调用shmat函数的返回值,函数执行成功返回0,并将该共享内存的shmid_ds结构的shm_nattch计数器减1,失败返回–1。 下面实例演示了 *** 作共享内存段的流程。程序的开始部分先检测用户是否有输入,如出错则打印该命令的使用帮助。接下来从命令行读取将要引入的共享内存ID,使用shmat函数引入该共享内存,并在分离该内存之前睡眠3秒以方便查看系统IPC状态。 (1)在vi编辑器中编辑该程序如下: 程序清单14-9 opr_shm.c *** 作共享内存段 #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <stdlib.h> #include <stdio.h> int main ( int argc, char *argv[] ) shm_id = atoi(argv[1])/*得到要引入的共享内存段*/ /*引入共享内存段,由内核选择要引入的位置*/ if ( (shm_buf = shmat( shm_id, 0, 0)) <(char *) 0 ) printf ( " segment attached at %p\n", shm_buf )/*输出导入的位置*/ system("ipcs -m") sleep(3)/* 休眠 */ if ( (shmdt(shm_buf)) <0 )printf ( "segment detached \n" ) system ( "ipcs -m " )/*再次查看系统IPC状态*/ exit ( 0 ) } (2)在shell中编译该程序如下: $gcc opr_shm.c–o opr_shm (3)在shell中运行该程序如下: $./ opr_shm 2752516 segment attached at 0xb7f29000 ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x00000000 65536 root 600 393216 2 dest 0x00000000 2654209 root 666 4096 0 0x0056a4d5 2686978 root 600 488 1 0x0056a4d6 2719747 root 600 131072 1 0x00000000 2752516 root 666 4096 1 segment detached ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x00000000 65536 root 600 393216 2 dest 0x00000000 2654209 root 666 4096 0 0x0056a4d5 2686978 root 600 488 1 0x0056a4d6 2719747 root 600 131072 1 0x00000000 2752516 root 666 4096 0 上述程序中从命令行中读取所要引入的共享内存ID,并使用shmat函数引入该内存到当前的进程空间中。注意在使用shmat函数时,将参数addr的值设为0,所表达的意义是由内核来决定该共享内存在当前进程中的位置。由于在编程的过程中,很少会针对某一个特定的硬件或系统编程,所以由内核决定引入位置也就是shmat推荐的使用方式。在导入后使用shell命令ipcs –m来显示当前的系统IPC的状态,可以看出输出信息中nattch字段为该共享内存时的引用值,最后使用shmdt函数分离该共享内存并打印系统IPC的状态。
编辑本段共享内存使用注意事项
共享内存相比其他几种方式有着更方便的数据控制能力,数据在读写过程中会更透明。当成功导入一块共享内存后,它只是相当于一个字符串指针来指向一块内存,在当前进程下用户可以随意的访问。缺点是,数据写入进程或数据读出进程中,需要附加的数据结构控制,共享内存通信数据结构示意如图14-9所示。
编辑本段结构示意
%说明:图中两个进程同时遵循一定的规则来读写该内存。同时,在多进程同步或互斥上也需要附加的代码来辅助共享内存机制。 在共享内存段中都是以字符串的默认结束符为一条信息的结尾。每个进程在读写时都遵守这个规则,就不会破坏数据的完整性。
另外,站长团上有产品团购,便宜有保证
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)