Android Binder为什么只需要一次拷贝?

网友投稿 1169 2022-05-30

Binder是Android特有的一种IPC(进程间通信)方式。

IPC机制是从Unix系统发展而来的,它只能提供原始进程间通信手段,通信双方需要处理线程同步、内存管理等问题。

传统的IPC方式有:Socket、匿名管道(Pipe),命名管道(FIFO),信号量(Semaphore),消息队列。Android系统只保留了Socket、匿名管道(Pipe)两种。

Binder的IPC机制融合了RPC(远程过程调用)概念,它是一种面向对象的远程调用。

Android系统使用了Linux内核。Linux中的进程是被隔离开,不能直接通信的。Android的应用程序也是Linux的进程,因此它也是被隔离开,不能直接通信的。

Linux中的进程是如何通信的,了解这一点可以为我们了解Binder的IPC通信作个铺垫。

Android Binder为什么只需要一次拷贝?

在Linux的用户空间中的进程都是被隔离开的,使得进程之间不能共享数据,不能互相访问,彼此独立。这也是出于进程安全的考虑才这样做。也因此有了IPC(进程间通信)的机制,提供一种安全的手段使用进程间能够互相通信。这与内核空间不一样,内核空间的进程是可以直接互相访问,直接通信的。

用户空间的进程要访问内核空间需要通过系统调用。内核空间的代码都是linux内核的代码和数据。这些代码都是有权限直接操作周围的硬件。用户空间要使用这些硬件都只能通过linux内核来完成,一来简化了用户空间程序的编写,二来保证了系统的安全。

内核在创建进程的时候会创建进程控制块以及进程的堆栈。每个进程都有两个栈:用户栈、内核栈。用户栈在用户空间中,内核栈在内核空间中。

用户栈用于保存用户进程的子程序间相互调用的参数、返回值以及局部变量等信息。

在谈到内核栈前,要先说说系统调用。系统调用的实质是通过指令产生中断(软中断)。由于系统调用或其他中断,进程会从用户态转换为内核态时,意味着将要执行内核中的代码,那么进程所使用的栈就不能再是用户栈了,而要使用内核中的栈。实际上,进程在进入内核态之后,会把用户态的堆栈地址保存在内核态堆栈中,然后设置堆栈寄存器地址为内核栈地址,这样就从用户栈转换成内核栈。

当内核中的代码执行完后,进程就要回到它的用户空间,所以进程从内核态转换回用户态时,会将堆栈寄存器的地址再重新设置成用户态的堆栈地址,这就是所谓的现场恢复。

在linux中,内核在创建进程时,会给进程分配task_struct结构体,同时分配两块连续的物理空间,一块供堆栈使用,另一块则保存进程描述符task_struct,这个整体就叫做进程的内核栈。

每当进程从用户态切换到内核态时,内核栈总是空的。当进程在用户态执行时,使用的是用户栈,切换到内核态运行时,内核栈保存进程在内核中运行的相关信息。重回到用户态时,内核栈中保存的信息全部恢复,因此,进程从内核态切换到用户态时,内核栈总是空的。用户态切换到内核栈时,正因为内核栈是空的,所以只需将栈寄存器值设置成内核栈栈顶指针即可。

通过上面的介绍,我们对Linux的内核有了更多的了解,现在来聊聊Linux进程间的通信。在Linux进程之间要交换数据必须通过内核,首在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走。

从用户空间拷数据到内核空间可以用copy_from_user()或get_user()。

从内核空间向用户空间拷数据可以用copy_to_user()或put_user()。

这就是所谓的两次拷贝了。

Binder一次拷贝将用到Linux内存映射的技术。我们先来了解内存映射,再来看Binder的一次拷贝。

mmap()这个就是内存映射的系统调用函数。它可以将一个文件、一段物理内存,甚至内核空间映射到进程的虚拟内存地址(这个虚拟内存地址可以这么理解,我们写的程序的逻辑地址都从0开始,可以一直往后延伸。平时我们写代码不用考虑这个,但是我们的电脑在运行就不得不考虑这个了,这个虚拟地址可以通过某种方式,如地址转换器,计算出真实的物理地址。但是对程序员而言只管逻辑地址即可。)。有了这种映射关系后,就可以直接完成对这个文件或这段内存直接读写了。

我们经常听说Binder驱动。那些什么是驱动,Binder驱动有什么特别之外?首先,驱动是一种用于实现对硬件进行相关操作的程序,驱动可以为上层应用屏蔽掉硬件特性。一般,驱动都是操作相关的硬件的。但是Binder驱动并没有相关硬件与之对应,所以我们说Binder驱动虚拟的字符设备(Linux驱动有三大类:字符设备驱动、块设备驱动、网络设备驱动),它注册在/dev/binder中。Binder驱动定义了一套Binder通信协议,用于负责建立进程间的Binder通信,提供了数据包在进程之间传递的支持。

由于Binder驱动不是Linux原有的,而是Android特有的,因此想Linux内核支持Binder驱动,就要把Binder驱动动态加载到Linux内核中来运行。因此Binder驱动就只能做成一个模块,然后利用Linux可加载内核模块(Loadable Kernal Module)机制动态加载Binder驱动,使其在内核中与内核一起运行。既然Binder驱动运行在内核了,那么用户空间的程序对其调用也必须通过系统调用来完成。

在我们了解Binder具体是如何完成数据包在进程间的传递之前,让我们来熟悉一下Linux的IPC是如何工作?因为Binder IPC的出现也不是一下就完成的:

通常Linux 的IPC通信,首先会将消息发送方将要发送的数据放在内存的缓存区中;

然后调用系统调用进入内核态,Linux为发送方进程在内核空间开避一块内核缓存区(分配内核空间);

紧接着调用copy_from_user()将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。

对于接收方而言:

接收方首先在接收数据前,在自己的用户空间开辟一块内存缓存区,用于接收数据;

然后Linux为接收方进程在内核空间中调用copy_to_user()将数据从内核缓存区拷贝到接收方进程的用户空间的内存缓存区。这样就完成了一次进程间的通信。

所以传统的Linux IPC有两次拷贝。这些拷由都需要用到系统调用,开销会比较大。Binder是如何实现一次拷贝,将这种开销降低的?

Binder IPC

有了前面的知识做铺垫,在这个时候来介绍Binder IPC是合适的。

Binder服务端进程在启动后,通过系统调用调用Binder驱动(其运行在内核空间)在内核空间创建一个数据接收缓存区(通过调用binder_open来完成),紧接着再通过系统调用调用binder_mmap()建立映射。具体过程是首先申请一块物理内存,然后建立Binder服务端的用户空间与内核空间一块区域的映射关系, 这个映射关系就记录在申请的这一块物理内存中。依靠这个建立起来的映射,用户空间与内核空间创建的数据接收缓存区之间就建立了映射关系。

当Client向Binder服务端发送通信请求时,这个请求数据打包好后通过系统调用先到Binder驱动,再由Binder驱动调用copy_from_user()将数据从client的用户空间拷贝到内核空间的数据接收缓存区中。因为内核空间的数据接收缓存区与接收进程用户空间的内存存在映射,因此也就相当于把数据发送到了接收方进程的用户空间,从而减少了一次拷贝。

整个过程只有一次拷贝。这种一次拷贝带来的效率是明显的,为什么呢?因为首先它减少了数据的拷贝次数,用存读写取代了I/O读写,提高了文件读取效率。

你明白了吗?

Android Linux 任务调度

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:论文解读系列二十九:无监督视觉表征学习的动量对比
下一篇:初识Python之文件操作篇(下)
相关文章