Android进程间的通信方案

什么是进程间通信

Android是基于Linux kernel搭建起来的系统。Linux操作系统内存分为用户空间和内核空间。我们的应用程序运行在用户空间。 在用户空间的进程都运行在自己独立的内存空间中,并防止进程的非法访问(如进程直接访问另一个进程的内存空间,这是不允许)。就像我们日常生活中, 不能直接去别人家里一样。但是进程之间总会有通信的需要。操作系统提供了一些方案来实现。因此进程间的通信,就是两个进程在操作系统提供的方案上进行信息交换。

也就是说,不同的进程之间以某种方式进行数据交换就是进程通信。比如说运行在同一台机器上的两个或两个以上的进程进行的数据交换。也可以在运行在两个或两个以上的不同机器上的进程进行的数据交换,但是这种方式需要通过网络连接在一起。

我们列举出一些备选的方案。

共享内存

需要跨进程通信的进程直接访问同一块内存空间。在速度上是非常理想的,不需要进行复制,直接访问就行。这种方案需要解决进程间的同步协调问题。抛开这个问题不谈,它的大概过程:

  1. 首先在内存创建共享区域,这需要向操作系统提出申请,因为内存是由操作系统统一管理的,你当然得向它申请。在Linux中, 用​​int shmget(key_t,size_t,int)​​来发出申请,key_t的值为IP_PRIVATE就是申请创建一块新的内存区域,或key_t的值不为IP_PRIVATE,但最后一个参数为IPC_CREATE也会创建一个新内存区域,创建成功后,操作系统会返回新内存区域的id值(该区域的唯一标识)。这个申请工作只需要通信双方的任意一方提出申请即可,如进程1;
  2. 将内存共享区域映射到进程空间,这样进程就可以通过访问自己的内存空间来达到与共享区域的数据交换。感觉就是数据就放在自己的进程空间里一样。比如进程1把共享区域映射到它的进程空间的例子,它可以用​​char *shmat(int shmid,void *shmaddr,int shmflag)​​这个系统调用来完成映射,shmid就是调用shmget申请创建共享区域成功后得到的内存id值,shmaddr就将共享区域映射到进程空间的指定位置,如果它的值是0,那么操作系统将自动为你分配地址,映射成功后,会返回内存共享区域的起始地址;
  3. 进程2关联到进程1创建的共享区域,进程2依然使用​​int shmget(key_t,size_t,int)​​​函数,传入的key_t的值与进程1的一模一样,获得进程1创建的共享区域的唯一标识id,然后再调用​​char *shmat(int shmid,void *shmaddr,int shmflag)​​完成共享区域到进程2空间的映射,你可以将本地的图片直接拖拽到编辑区域直接展示;
  4. 当共享内存的各个进程都完成了内存映射后,就可以利用该共享区域进行数据交换,通信同步协调的问题也是出现在这个环节;
  5. 撤销内存共享区域到进程空间的映射,调用​​int shmdt(char *shmaddr)​​来撤销映射,shmaddr是映射到进程空间的地址;
  6. 删除内存共享区域,调用​​int shmctl(int shmid,int cmd,struct shmid_ds *buf)​​删除内存区域。shmid就是shmget申请时返回的id值,cmd值有几种,如IPC_STAT查询状态,将结果存入buf,如果是IPC_SET,则在权限允许的情况下,将共享内存状态更新为buf中的数据,如果是IPC_RMID则删除共享内存段。

管道

管道也是一种进程间通信的方式。管道就是水管一样,只能是单向的。如果即要有读,也要有写就得两条管道。
读和写管道是相对,比如是管道1在进程1是用于读的,那么在另一个进程的一端就是写的,反之亦然。

#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char * argv[])
{
int pipe_fd[2]; // 声明两条管道
char pipe_buf; // 用于缓存接收的数据
memset(pipe_fd,0,sizeof(int)*2);

if(pipe(pipe_fd) == -1){// 打开管道
return -1;
}

pid_t child_pid = fork(); // 创建一个子线程
if(child_pid == 0){// 子进程负责读取父进程的数据
close(pipe_fd[1]);// 关闭写管道,相对另一边就是读管道。
while(read(pipe_fd[0], &pipe_buf, 1) > 0){// 读取管道中的数据,并存入缓存区
write(STDOUT_FILENO, &pipe_buf, 1);// 写出数据
}
close(pipe_fd[0]);// 管道使用完成后关闭它
return 0;
} else {// 父进程的处理逻辑
close(pipe_fd[0]); // 关闭读管道
write(pipe_fd[1],"D",1);// 通过写管道,写出一个‘D’
close(pipe_fd[1]);// 管道使用完成后,关闭管道
wait(NULL);// 等待子进程读取完成并退出
return 0;
}
}

上面通过fork()函数创建一个子进程,因为两个进程都持有pipe_fd,这一点让它们的通信成为可能。这种管道叫做匿名管道。这种管道不能给其他进程使用,必须是在这一种父子进程间使用。所以为了让不相关进程也能使用管道进行彼此通信,所以出现了命名管道,即FIFO。这种管道会一直存在, 不会像上面那样,进程执行完就消失了。所以在不使用这种管道时要记得删除。

Unix Domain Socket

这个与Network Socket是不一样的,对于同一机器内的进程间通信,Network Socket的效率不是很理想,它是以TCP/IP协议栈为基础的。Unix Domain Socket则是针对同一机器内进程间通信提出的,专门设计用于本地的进程间通信的,因此它也被称为本地socket。它的所有工作都在内核中进行。network socket监听的是IP地址加接口。Unix Domain Socket的监听对象则是它命名空间里的地址,这个地址关联到文件系统上的一个文件。

除此之外,Android 进程间通信机制还有 Binder, Messenger 和 Intents。

估计Unix Domain Socket在未来会更加普及。