我是靠谱客的博主 紧张短靴,最近开发中收集的这篇文章主要介绍Linux中传统的IPC机制,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

介绍

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机制所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(53)

评论列表共有 0 条评论

立即
投稿
返回
顶部