概述
介绍
IPC全称为Inter-Process Communication,含义为进程间通信,是指两个进程之间进行数据交换的过程。IPC不是Android中所独有的,任何一个操作系统都需要有相应的IPC机制,比如Windows上可以通过剪贴板等进行进程间通信;Linux上可以通过管道(Pipe)、信号(Sinal)、信号量(Semophore)、消息队列(Message)、共享内存(Share Memory)和套接字(Socket)等来进行进程间的通信。可以看到不同的操作系统有着不同IPC机制,对于Android来说,它是一种基于Linux内核的移动操作系统,它的进程间通信方式就是Binder了,通过Binder可以轻松实现进程间的通信。本文主要介绍Linux中的IPC机制,以及相关的使用场景。
1.管道
管道是Linux有UNIX继承过来的进程间的通信机制,它是UNIX早期的一个重要通信机制,管道的主要思想是在内存中创建一个共享文件,从而使通信双方利用这个共享文件来传递信息。这个共享文件比较特殊,他不属于文件系统并且只存在于内存中。另外管道采用半双工通信方式,数据只能在一个方向流动。管道的简单模型如下图:
通过简单的模型图,相信应该比较容易理解通过管道通信,传递数据,需要两次拷贝,进程A将数据拷贝至管道,进程B再从管道拷贝。
1.1管道应用场景之父子进程
管道应用场景一般在父子进程之间使用,当然如果是有名管道,只要通信的两个进程知道管道名称就可以通信了。父进程孵化克隆子进程时,就是使用的(无名)管道,通过管道将父进程的数据资源拷贝到子进程,通过调用pipe(fd)生成一对描述符,一个用来写,另一个用来读。fd[0]是用来读的,fd[1]是用来写的,通过fork()创建子进程,子进程会继承父进程的一对管道描述符。现在的需求是只是希望父进程往子进程里面写东西,那就可以把父进程的读描述符关了,子进程的写描述符关了。现在父进程往写描述符里面写入数据字符串,子进程就可以从读描述符把字符串读出来,读到buf里面,也对应了前面提到的半双工通信方式,数据只能在一个方向流动/传输,要么读,要么写,即管道一端写入数据,另一端读取数据。
int main(void) {
int n,fd[2];
char buf[SIZE];
pipe(fd);
pid_t pid = fork();
if(pid == 0) {
//关闭子进程写描述符
close(fd[1]);
//子进程向读描述符fd[0],读取数据到buf里面
read(fd[0],buf,SIZE);
} else if(pid > 0) {
//关闭父进程读描述符
close(fd[0]);
//父进程向写描述符fd[1],写入字符串"Hello"
write(fd[1],"Hello",5);
}
}
对应的模型图如下:
1.2管道应用场景之Looper
管道在FrameWork中还有哪些应用场景呢?java层的loop在native层会有对应的Looper类,它的构造里面就会创建一个管道,Android4.4 用的是管道,在更高的版本如6.0,管道被换成了eventFd。现在主要是了解一下管道的用法。通过pipe创建管道,有读端,有写端。创建epoll,注册监听事件,监听读事件,即读描述符的读事件,如果有另一个进程拿到这个管道的写描述符,往里面写入数据的话,那么读端就能收到通知了。
Looper::looper(bool allowNonCallbacks){
int wakeFds[2];
//创建管道,生成对应的描述符
int result = pipe(wakeFds);
//读描述符
mWakeREadPipeFd = wakeFds[0];
//写描述符
mWakeWritePipeFd = wakeFds[1];
...
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
//监听(读描述符)的读事件
struct epoll_EVENT eventItem;
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeReadPipeFd;
epoll_ctl(mEpollFd, EPOLL_CTL_ADD,mWakeReadPipeFd, &eventItem);
}
接下来看epoll是如何监听读端事件的
int Looper::pollInner(int timeoutMillis) {
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
int eventCount = epoll_wait(mEpollFd,eventItems,...);
for(int i=0;i<eventCount;i++){
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
if (fd == mWakeReadPipeFd) {
if (epollEvents & EPOLLIN) {
awoken();
}
}
}
...
return result;
}
epoll_wait阻塞在这里,等待事件。等到它返回时,返回值eventCount表示被触发事件的数量。然后在循环里面依次处理,if fd== mWakeReadPipeFd, 如果事件是可读的,就调用awoken(); 将管道里面的数据读出来,不然管道满了,写端就不能往管道写入数据了。并不关注往管道写入的是什么,只要往往管道写入数据,就会唤醒读端阻塞的线程,就可以去处理消息了。当别的线程给我们线程发消息时,就会通过wake函数往管道里面写入数据。
void Looper::wake() {
nWrite = write(mWakeWritePipeFd,"W",1);
}
管道使用还是挺方便的,主要是它可以和epoll相结合,监听读写事件,它在进程里面可以用,跨进程也可以用,在数据量不大的跨进程通信中还是不错的。
2.Socket
套接字是更基础的进程间的通信机制,与其他通信机制不同的是,套接字可用于不同机器之间的进程间通信。它是全双工的,即在任何时候,通信两端都可以发送或接收数据,即读写操作。在Android中有没有socket的应用场景呢?答案是有的,在Zygote的Java框架层中会创建一个Server端的Socket,这个Socket用来等待AMS请求Zygote来创建新的应用程序进程。
public static void main(String argv[]) {
......
registerZygoteSocket(socketName);
......
runSelectLoop(abiList);
}
在Zygote的main函数中,通过registerZygoteSocket创建一个本地socket,做好一些准备工作之后,就进入runSelectLoop循环,去检测socket有没有新过来的连接或数据。
void runSelectLoop(String abiList) {
......
while(true) {
Os.poll(pollFds, -1);
for (int i = pollFds.length - 1; i >= 0; --i) {
if (i == 0) {
//处理新过来的连接
} else {
//处理发过来的数据
peers.get(i).runOnce();
}
}
}
}
runOnce处理数据是通过Socket读取发送过来的参数,根据参数去执行相关指令,这里的应用场景执行的就是创建新的应用进程,创建之后,将进程的pid通过socket写给对方。
Tips:问题来了,我们知道Binder是Android较为高效的跨进程通信机制,此处为何会采用Socket通信呢?
Zygote进程创建/孵化子进程,fork做了如下事情:
- 父进程的内存数据会原封不动的拷贝到子进程中
- 子进程在单线程状态下被生成
不采用binder通信,是为了避免binder线程有锁,然后子进程的主线程一直等待子线程(从父进程拷贝过来的子线程)的资源,但其实父进程的子线程没有拷贝过来,容易造成死锁。所以fork不允许在多线程环境,即不采用binder通信。
3.共享内存
共享内存的多个进程可以直接读写一块内存空间,是针对其他通信机制运行效率低而设计的,为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。这样,进程就可以直接读写这一块内存而不需要进行数据的复制了,从无需数据拷贝的角度来看,提高了效率,但是要避免一方修改了数据,另一方读取的数据还是之前的数据,就需要同步机制,开销大。同步机制容易造成数据不同步和死锁等问题。所以说它内存管理机制比较复杂。
Ashmem是一种共享内存的机制,它利用了Linux的mmap系统调用,将不同进程中的同一段物理内存映射到进程各自的虚拟地址空间,从而实现高效的进程间共享。而MemoryFile就是对Ashmem(匿名共享内存)做了封装。
native_open调用ashmem_create_region创建一块匿名共享内存,返回描述符mFD , native_mmap调用native层的mmap将描述符mFD映射到当前进程的内存空间,mAdress就是内存空间的地址。接下来是MemoryFile的读和写方法:
jint android_os_MemoryFile_read(JNIEnv* env,jobject clazz,...) {
......
env-> SetByteArrayRegion(buffer,destOffset,count,...);
......
return count;
}
jint android_os_MemoryFile_write(JNIEnv* env,jobject clazz,...) {
......
env-> SetByteArrayRegion(buffer,srcOffset,count,...);
......
return count;
}
read是将共享内存数据读到应用层的buffer里面,通过setByteArrayRegion方法将native buffer的数据拷贝到java的数组里面,write就是反过来了。
4.信号
信号是原件层次上对中断机制的一种模拟,信号是一种异步通信方式,进程不必通过任何操作来等待信号的到达。信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些事件。具有以下特点:
- 单向的,发出去之后怎么处理是别人的事
- 只能带个信号,不能带别的参数
- 知道进程pid就能发信号了,也可以一次给一群进程发信号(root权限或者和其他进程uid相同,才能发信号)
从以上特点可知,信号不适用于信息交换/数据传递,比较适用于单纯的通知预警作用,比如进程中断控制。
有时候要杀掉应用进程调用killProcess函数,就是给进程发送SIGNAL_KILL信号,pid就是进程pid,不是想杀谁就能杀谁的,受权限控制的。两个进程的userID相同才能给别人发送信号,应用进程都是zygote进程fork出来的,uid默认都是和zygote相同的。但是进程启动之后都会马上重新设置自己的uid,所以不能随便给别人发送信号的。zygote进程会关注SIGCHLD信号,zygote启动子进程之后需要关注子进程退出了没有,如果退出了,需要及时把它的资源回收掉。
public class Process {
public static final void killProcess(int pid) {
sendSignal(pid,SIGNAL_KILL);
}
}
最后
以上就是紧张短靴为你收集整理的Linux中传统的IPC机制的全部内容,希望文章能够帮你解决Linux中传统的IPC机制所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复