Android 跨进程通信-(二)Binder机制之ServiceManager

目录

前言

一 ServiceManager

1.通过builder_open()打开Binder驱动

2.通过binder_become_context_manager()告诉Binder驱动程序该进程为ServiceManager进程

3.通过binder_loop()开启循环,等待Client进程和Server进程发来请求

4.小结

二 Binder驱动

1.初始化

2.binder_open()

3.binder_mmap()

4.binder_ioctl()

5.小结


前言

通过源码的角度来看下到底Binder机制是一个什么东西。如果不喜欢看源码,可以直接看每个章节的小结。

一 ServiceManager

在前面一篇Android 跨进程通信-(一)Binder概念引入中也总结过:Android在系统启动的时候,用户空间启动的第一个进程是init进程,ServiceManager是由init进程通过解析init.rc文件而创建的,其所对应的可执行程序/system/bin/servicemanager,所对应的源文件是framework/base/services/java/com/android/server/service_manager.c,进程名为/system/bin/servicemanager。

这样当该进程创建成功之后,后面在启动的Service都会通过ServiceManager来进程管理。ServiceManager位于frameworks/base/core/java/android/os/ServiceManager.java。

前面也提到了在Binder机制中的四大组件:Client、Server、ServiceManager以及Binder驱动,所以将这些Service的时候启动之前,其实首先要启动ServiceManager进程

该ServiceManager进程启动的时候,其实就是执行framework/native/cmds/servicemanager/servicemanager.c下对应的内容,这是Native层对ServiceManager这个功能的封装,是ServiceManager在Native层功能的体现,最终不同的方法的功能实现都包括两部分:第一部分就是Native层对Kernel层相关方法的封装;第二部分就是Kernel层具体功能实现。也就是说最终在Native层的这些方法都会调用到Kernel层的对应的方法。

int main(int argc, char** argv)
{
    struct binder_state *bs;
    union selinux_callback cb;
    char *driver;

    if (argc > 1) {
        driver = argv[1];
    } else {
        driver = "/dev/binder";
    }
    // 1.打开Binder驱动,映射内存为128k
    bs = binder_open(driver, 128*1024);
    //........
    //2.成为上下文的管理者,告诉Binder驱动该进程为ServiceManager进程
    if (binder_become_context_manager(bs)) {
        ALOGE("cannot become context manager (%s)\n", strerror(errno));
        return -1;
    }
     //........
    //3.开启循环处理消息
    binder_loop(bs, svcmgr_handler);

    return 0;
}

在整个ServiceManager进程在启动的过程中,就会完成下面的三个功能: 

1.通过builder_open()打开Binder驱动

builder_open()具体实现对应framework/native/cmds/servicemanager/builder.c中的builder_open():该方法的作用不难理解,主要有这两部分的作用:

  • (1)打开设备文件"/dev/binder"获取一个文件描述bs,通过该文件描述才可以与Binder驱动进行交互;
  • (2)将该文件映射到ServiceManager进程的虚拟地址空间

进入到builder.c的builder_open()中看下里面的几个关键流程:

struct binder_state *binder_open(const char* driver, size_t mapsize)
{   
   struct binder_state *bs; 
    //......
    //1.调用内核空间的binder驱动来打开设备文件"/dev/binder"
    bs->fd = open(driver, O_RDWR | O_CLOEXEC);
    //......
    //2.检查kernel的binder版本是否一致
    if ((ioctl(bs->fd, BINDER_VERSION, &vers) == -1) ||
        (vers.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION)) {
        fprintf(stderr,
                "binder: kernel driver version (%d) differs from user space version         (%d)\n",
                vers.protocol_version, BINDER_CURRENT_PROTOCOL_VERSION);
        goto fail_open;
    }

    //3.根据传入的内存大小,为该设备文件创建对应大小的虚拟空间
    bs->mapsize = mapsize;
    bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
    //......
    return bs;
     //......
}

(1)通过open()打开设备文件

该方法就是去打开binder的设备文件"/dev/binder",并且返回一个binder_state来记录这个设备文件的状态。其中binder_state的参数如下:

struct binder_state
{
    int fd;
    void *mapped;
    size_t mapsize;
};
  • 参数fd: 打开"/dev/binder"的返回的文件描述符,用来和Binder驱动进行交互;
  • 参数mapped:为当前的ServiceManage进程与BInder驱动进行交互分配的地址空间,用来Binder驱动为它分配内核缓冲区来保存进程间通信的数据。
  • 参数mapsize:地址空间,大小

此时open()方法会通过系统调用到Binder驱动中的binder_open()方法。具体的Binder驱动的binder_open()会在后面二 Binder驱动有介绍。

这样就从用户空间通过系统调用到了内核空间,通过Binder驱动的binder_open()打开了设备文件"/dev/binder",并且获得了一个该文件的描述符,然后通过该文件描述符与Binder驱动进行通信。

第二步:检查与Kernel版本是否一致

这里主要就是通过ioctl()方法将BINDER_VERSION指令发送给Binder驱动。具体的Binder驱动的builder_icotl()会在后面二 Binder驱动有介绍。

这样就从用户空间通过系统调用到了内核空间,通过Binder驱动的binder_ioctl()来检查与Kernel的binder版本是否一致。

第三步:mmap()

通过mmap()将设备文件"/dev/binder"映射到ServiceManager进程的128k的虚拟内存空间。同时也会调用到Binder驱动的builder_mmap()。具体的Binder驱动的builder_mmap()会在后面二 Binder驱动有介绍。

这样就从用户空间通过系统调用到了内核空间,不仅完成了设备文件"/dev/binder"映射到在ServiceMananger进程的虚拟内存空间过程,Binder驱动还在内核空间为该进程分配了内核缓冲区。

补充:mmap内存映射

mmap进程虚拟内存的映射方法,可以将一个文件、一段物理内存或者其他对象映射到进程的虚拟内存地址空间。实现内存映射之后,进程就可以采用指针的方式来读写操作这段内存,进而完成对映射对象的操作。

1.mmap用来内存映射文件:

  • 将一个设备硬件文件映射到该进程的虚拟内存空间中,实现设备硬件文件地址和该进程的虚拟内存空间中一段虚拟地址是一一对应关系。这样进程对这块虚拟内存空间进行读写操作,系统自动回写数据到对应的设备硬件文件地址。(这实现了将设备硬件文件与进程的虚拟内存空间建立映射关系)

因为在将设备文件映射到进程的虚拟地址空间的时候,还会映射在内核空间的虚拟内存空间中,所以在进程虚拟内存空间修改、内核的虚拟内存空间修改、以及设备硬件文件自身修改同时都会同步。

2.mmap在内存映射的作用:

  • 创建虚拟内存区域+与设备硬件文件建立映射关系。

3.与Linux传统的读写文件:

  • 先将设备硬件文件拷贝到内核空间,然后再将内核空间拷贝到用户空间

4.进程在用户空间调用mmap()的流程:

  • (1)在当前进程创建虚拟映射区域:即在当前进程的虚拟空间中找到一个满足要求大小的虚拟地址;
  • (2)在内核空间创建虚拟映射区域:在用户进程调用mmap(),最终调用到内核空间的mmap()。内核空间的mmap()会在内核空间创建一个虚拟空间,最终通过内核空间的虚拟文件系统定位到对应的设备硬件文件。
  • (3)分配一个物理内存,将该物理内存映射到进程的虚拟内存和内核的虚拟内存

通过上面三个流程实现了用户虚拟内容空间和设备硬件文件的映射关系。当进程在读写该设备硬件文件的时候,实际上是读写操作该虚拟空间的映射地址,这个时候才会把文件执行拷贝到对应的空间中。

5.注意的问题:

  • (1)mmap映射区域大小必须为物理页大小(默认为4k)的整数倍

遗留问题:

  • 这个进程访问设备硬件文件的时候,是不是还是间接通过内核空间来访问了对应的设备硬件文件呢?

遗留问题:那么其他像Client进程、Server进程是不是也是这么一个流程,是不是也会有申请空间呢?申请空间大小是不是也是128k?

解答:Client进程申请空间Android 跨进程通信-(三)Binder机制之Client2.APP进程初始化

2.通过binder_become_context_manager()告诉Binder驱动程序该进程为ServiceManager进程

同样该方法也是调用到builder.c中的binder_become_context_manager()方法,如下

int binder_become_context_manager(struct binder_state *bs)
{
    return ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0);
}

从代码中可以看出,通过ioctl完成BINDER_SET_CONTEXT_MGR该命令,单纯从字符串理解就是告诉Binder驱动程序该进程为ServiceManager进程,进行管理各种服务的注册以及查询。 

在Kernel中的ioctl方法对应着好多switch的匹配,根据传入的不同的命令,来实现不同的功能。

3.通过binder_loop()开启循环,等待Client进程和Server进程发来请求

同样该方法也是调用到builder.c中的binder_loop()方法,如下

void binder_loop(struct binder_state *bs, binder_handler func)
{
    // ......
    readbuf[0] = BC_ENTER_LOOPER;
    //通知Binder驱动 “本线程要进行循环状态”
    binder_write(bs, readbuf, sizeof(uint32_t));
    //for循环
    for (;;) {
        
        //bs就是对应的"/dev/builder"的设备文件,读取Binder驱动(Server注册和Client查询)中发过来的数据
        res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
        // ......
        //解析和处理从Binder驱动读来的数据
        res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func);
        // ......
    }
}

该方法首先给Binder驱动会发送BC_ENTER_LOOPER指令,通知Binder驱动"本线程要进入循环状态",然后通过for循环来时刻读取Binder驱动发送过来的数据(因为Server进程和Client进程发过来数据的时候,都需要先从Server进程和Client进程发送到Binder驱动,然后Binder驱动将信息发送给ServiceManager进程),看该ServiceManager进程是否有进程请求:如果有则交给binder_parse()处理数据;如果没有请求,则堵塞在ioctl()。

遗留问题:在这个里面涉及的binder_write(),我想通过后面对Server和Client进程创建的时候,在细细研究下,现在还没有怎么搞清楚。

4.小结

1.简答的总结下ServiceManager进程创建的过程:

  • (1)通过builder_open设备硬件文件打开“/dev/binder”,获得该文件描述符;
  • (2)通过内存映射mmap将设备硬件文件映射到ServiceManager进程的128k的虚拟内存空间,同时在内核空间也会建立一个虚拟内存空间,这样就可以直接对进程的虚拟内存空间读写,直接可以反映到设备硬件文件;
  • (3)通过对Binder驱动的ioctl()发送BINDER_SET_CONTEXT_MGR指令,将该进程注册为ServiceManager;
  • (4)通过binder_loop循环等待有请求发送到ServiceManager进程中。

上面的方法都会调用的open、mmap、ioctl方法都会调用到内核的相应方法。

2.内存映射

  • (1)所谓的内存映射就是将“一段物理内存、一个设备硬件文件或一个共享对象”,通过mmap()映射到进程的虚拟内存空间,在这个过程中,不仅在进程的虚拟内存空间会有一个映射关系,而且在内核空间也会产生一个大小一致的虚拟内存空间来映射到该“一段物理内存、一个设备硬件文件或一个共享对象”;
  • (2)我觉得可以认为这个进程的内存映射,其实是进程的虚拟内存空间间接通过内核的虚拟内存空间来访问到了“一段物理内存、一个设备硬件文件或一个共享对象”;
  • (3)由于内存映射,所以在进程、内核或者硬件的修改,都会相互同步

3.对应源码

  • (1)framework/native/cmds/servicemanager/servicemanager.c
  • (3)framework/native/cmds/servicemanager/builder.c

二 Binder驱动

所谓的驱动可以简单的理解为提供了一种软件技术来实现对硬件的相关操作,Linux将驱动分为三大类:字符设备驱动、块设备驱动、以及网络设备驱动。而这个Binder驱动属于虚拟的字符设备,没有对应的硬件,可以理解为是一种访问内存的驱动,注册在/dev/binder

Binder驱动提供了Binder协议(就是通过ioctl发送那些指令),负责建立进程间的通信。

1.初始化

Binder驱动在初始化的时候就是在设备上创建了一些目录:

static int __init binder_init(void)
{
	int ret;
    //创建一个工作队列
	binder_deferred_workqueue = create_singlethread_workqueue("binder");
	if (!binder_deferred_workqueue)
		return -ENOMEM;
    //1.创建 /proc/binder 文件夹
	binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL);
	if (binder_debugfs_dir_entry_root)
    //2.创建 /proc/binder/proc文件夹
		binder_debugfs_dir_entry_proc = debugfs_create_dir("proc",
						 binder_debugfs_dir_entry_root);
	ret = misc_register(&binder_miscdev);
    //后面又陆续创建了state/stats/transaction_log/failed_transaction_log
	//.....
	return ret;
}

device_initcall(binder_init);
  • /proc/binder/proc目录

每个使用了Binder进程通信的进程都会在该目录有一个文件,文件以进程的ID来命名。可以读取到每个进程的Binder线程池、Binder实体对象、Binder引用对象以及内核缓冲区的信息

  • /dev/binder文件

设备文件里有操作方法:binder_open、binder_mmap、binder_ioctl。这些方法的具体实现路径为:kernel/goldfish/drivers/staging/android/binder.c 下相应的代码。

  • 其他目录

读取Binder驱动的运行状况

2.binder_open()

该方法的作用就是创建一个包含该进程信息的结构体builder_proc,并将该结构体builder_proc加入到全局的hash队列builder_procs中,具体实现的逻辑大体如下:

static int binder_open(struct inode *nodp, struct file *filp)
{
    struct binder_proc *proc;
//....
  //1.为创建的binder_proc赋值
    get_task_struct(current);
    proc->tsk = current;
    INIT_LIST_HEAD(&proc->todo);
    init_waitqueue_head(&proc->wait);
  //1.为创建的binder_proc赋值
    proc->default_priority = task_nice(current);
    mutex_lock(&binder_lock);
    binder_stats_created(BINDER_STAT_PROC);
//将创建的binder_proc加入到binder_procs
    hlist_add_head(&proc->proc_node, &binder_procs);
  //1.为创建的binder_proc赋值
    proc->pid = current->group_leader->pid;
    INIT_LIST_HEAD(&proc->delivered_death);
    filp->private_data = proc;
    mutex_unlock(&binder_lock);

    if (binder_debugfs_dir_entry_proc) {
        char strbuf[11];
        snprintf(strbuf, sizeof(strbuf), "%u", proc->pid);
  //1.为创建的binder_proc赋值
        proc->debugfs_entry = debugfs_create_file(strbuf, S_IRUGO,
            binder_debugfs_dir_entry_proc, proc, &binder_proc_fops);
    }

    return 0;
}

主要实现的功能总结如下:

  • (1)创建代表该进程(如ServiceManager进程或Client进程或Server进程)的信息的binder_proc结构体;
  • (2)将该binder_proc结构体加入到hash队列builder_procs中(Tip:所有打开设备文件"/dev/binder"的进程都会加入到全局队列builder_procs中,通过遍历该队列就知道当前系统一共有多少进程在使用Binder机制通信);
  • (3)返回该设备文件“/dev/binder”的文件描述,传入到builder_mmap()、builder_ioctl()中,这样在内核空间中就可以找到binder_proc结构体,也就是对应的进程信息(如ServiceManager进程或Client进程或Server进程),对应的在framework/native/cmds/servicemanager/builder.c中的builder_open()方法调用过程如下:

3.binder_mmap()

前面也提到Native层最终调用的就是Binder驱动的binder_mmap()为该进程在内核空间分配了内核缓冲区。具体的代码实现逻辑如下:

//vm_area_struct是传入的用户空间地址
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
    int ret;
    //内核缓冲区对应的内核的空间地址
    struct vm_struct *area;
    //取出文件描述符里的关于进程信息的结构体   
    struct binder_proc *proc = filp->private_data;
    const char *failure_string;
    //内核缓冲区的列表
    struct binder_buffer *buffer;
    //1.会判断传入的数据是否超过4M,如果超过4M,会默认的为4M空间
    if ((vma->vm_end - vma->vm_start) > SZ_4M)
        vma->vm_end = vma->vm_start + SZ_4M;
    //......
    //2.获取到内核空间虚拟地址
    area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
    //将该内核空间地址的信息保存到进程结构体的buffer中
    proc->buffer = area->addr;
    //用户空间和内核空间地址的差值
    proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
   //......
    proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
   //......
    proc->buffer_size = vma->vm_end - vma->vm_start;
    //对用户空间地址的信息更新
    vma->vm_ops = &binder_vm_ops;
    vma->vm_private_data = proc;
    //......
    //3.分配一个物理空间,将该物理空间分别映射到内核虚拟地址和用户虚拟地址
    if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {
        ret = -ENOMEM;
        failure_string = "alloc small buf";
        goto err_alloc_small_buf_failed;
    }
    //内核起始虚拟地址作为第一个builder_buffer的地址
    buffer = proc->buffer;
    INIT_LIST_HEAD(&proc->buffers);
    list_add(&buffer->entry, &proc->buffers);
    buffer->free = 1;
    binder_insert_free_buffer(proc, buffer);
    proc->free_async_space = proc->buffer_size / 2;
    barrier();
    //更新进程结构体里面的信息
    proc->files = get_files_struct(current);
    //更新进程结构体里面的用户虚拟地址内存的信息
    proc->vma = vma;

    return 0;
  //......
}

vm_area_struct对应存放设备文件“/dev/binder”的映射到用户空间的虚拟内存的结构体 ,vm_struct是设备文件“/dev/binder”的映射到内核空间对应的虚拟内存的结构体

主要实现的功能如下:

  •  (1)检查用户空间申请的设备文件“/dev/binder”内存映射空间是否超过4M,如果超过了4M,会强制设置为4M;
  • (2)通过get_vm_area()在内核空间中创建与用户空间的内存映射空间相同的内核空间虚拟地址vm_struct;
  • (3)通过binder_update_page_range()分配物理空间,将该物理空间分别映射到内核虚拟地址和用户虚拟地址,这样用户空间和内核空间就使用了同一块物理内存;
  • (3)更新进程信息的binder_proc结构体以及用户空间的虚拟内存的vm_area_struct结构体

通过builder_mmap()就完成了为该进程在内核空间的虚拟内存中,也分配了与用户空间的虚拟空间相同的虚拟地址空间,然后为进程的虚拟空间和内核的虚拟空间其分配物理内存(即/proc/binder/proc/进程ID),该物理内存同时映射内核虚拟空间地址和用户虚拟空间地址。这样用户空间通过用户空间的虚拟内存地址来访问这块内存缓冲区的信息,而内核空间通过内核空间的虚拟地址来访问这块缓冲区的信息。

此时这个内核空间的虚拟内存的信息和包含进程信息的binder_proc结构体关联到一起。

4.binder_ioctl()

通过维护着很多switch的case匹配来处理对应着进程发过来的请求。里面的内容后面在总结Server的时候在补充,我发现现在对里面的有些内容还不是很清楚。

5.小结

因为对Kernel的内容不是很了解,在看代码的时候也是从字符串上去看大体的功能,里面有描述不准确的地方,还希望大家指正。里面的方法大多为Native层通过系统调用。

  • (1)Binder驱动可以理解为提供给用户空间来访问内存的一种驱动程序;
  • (2)Binder驱动的mmap会根据用户空间的虚拟内存的大小,同样在内核空间分配同样大小的虚拟内存空间;
  • (3)在mmap中还会分配一块物理内存,分别映射到进程的虚拟内存和内核空间的虚拟内存;
  • (4)通过(2)(3),Binder驱动通过mmap为该进程在内核空间分配了内核缓冲区,里面即有进程的虚拟空间地址,也有内核空间的虚拟内存地址。进程通过用户空间的虚拟空间地址来访问该内核缓冲区,内核空间通过内核的虚拟空间地址来访问该内核缓冲区。

遗留问题:

  • (1)当然这个分配内核缓冲区的概念还没有非常理解,后面在看下Client进程和Server进程的创建的时候,在反过来看看这个地方的一个理解程度
  • (2)binder_ioctl()中的一次拷贝在Client进程和Server进程都了解之后,在看下这个过程,目前看了一段时间,发现还有点想不通。

继续加油,后面来看下Server进程(见Android 跨进程通信-(四)Binder机制之Server)和Client进程的创建(见Android 跨进程通信-(三)Binder机制之Client)。加油!!!!!

Android 跨进程通信-(十一)Binder机制之ServiceManager对系统Service的管理会从源码的角度来看Client(system_server进程)到Server(ServiceManager进程)的一次IPC

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值