聊一聊binder传递文件fd原理及新版本性能优化

发布于:2025-03-14 ⋅ 阅读:(22) ⋅ 点赞:(0)

背景:

近期有vip学员朋友,问道了一个问题,那就是关于binder跨进程传递fd具体是怎么做到的呢?这块其实binder跨进程传递文件fd,本质的原理的实现在binder驱动中,所以给这个同学找一下相关的binder驱动中的代码,但是发现这块代码还和我以前看过的跨进程传递fd代码有较大的差别了,这个差别还不是简单改变一下方法,而是设计思路上就有变化,所以这里整理一下给大家分享出来。

老版本binder驱动代码传递fd

安卓kernel内核版本:

VERSION = 4
PATCHLEVEL = 4
SUBLEVEL = 302
EXTRAVERSION =

一般在client端调用调用了Parcel.writeFileDescriptor(fd)进行fd传递,接下来跨进程通信会一路调用到binder内核的binder_transaction代码:

static void binder_transaction(struct binder_proc *proc,
			       struct binder_thread *thread,
			       struct binder_transaction_data *tr, int reply,
			       binder_size_t extra_buffers_size)
{
	//省略

		hdr = (struct binder_object_header *)(t->buffer->data + *offp);
		off_min = *offp + object_size;
		switch (hdr->type) {
//省略

//针对binder传递的是fd类型
		case BINDER_TYPE_FD: {
			struct binder_fd_object *fp = to_binder_fd_object(hdr);
			int target_fd = binder_translate_fd(fp->fd, t, thread,
							    in_reply_to);

			fp->pad_binder = 0;
			fp->fd = target_fd;
		} break;

老版本内核处理fd转换:

static int binder_translate_fd(int fd,
			       struct binder_transaction *t,
			       struct binder_thread *thread,
			       struct binder_transaction *in_reply_to)
{
	struct binder_proc *proc = thread->proc;
	struct binder_proc *target_proc = t->to_proc;
	int target_fd;
	struct file *file;
	int ret;

//这个fd的是源进程的,通过fd获取到对应的file结构
	file = fget(fd);
//这里只是安全坚持,file是否可以传递过去
	ret = security_binder_transfer_file(proc->cred, target_proc->cred, file);

//这里会从目标进程中获取没有使用的fd,赋值给target_fd
	target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC);
//直接把从目标进程获取的空闲target_fd与file进行绑定
	task_fd_install(target_proc, target_fd, file);
	return target_fd;

}

也看看fget和task_fd_install这两个核心方法:
fget其实是调用到__fget

static struct file *__fget(unsigned int fd, fmode_t mask, unsigned int refs)
{
	struct files_struct *files = current->files;
	struct file *file;

	rcu_read_lock();
loop:
	file = fcheck_files(files, fd);
//省略
	rcu_read_unlock();

	return file;
}

再看看task_fd_install方法:

void __fd_install(struct files_struct *files, unsigned int fd,
		struct file *file)
{
	struct fdtable *fdt;

	might_sleep();
	rcu_read_lock_sched();

	while (unlikely(files->resize_in_progress)) {
		rcu_read_unlock_sched();
		wait_event(files->resize_wait, !files->resize_in_progress);
		rcu_read_lock_sched();
	}
	/* coupled with smp_wmb() in expand_fdtable() */
	smp_rmb();
	fdt = rcu_dereference_sched(files->fdt);
	BUG_ON(fdt->fd[fd] != NULL);
	rcu_assign_pointer(fdt->fd[fd], file);//这里就是源进程file与新进程fd进行了绑定
	rcu_read_unlock_sched();
}

其实老版本的binder传递fd还是比较好理解的,主要就以下几个步骤:
1、根据源进程fd获取内核中对应的file
2、在目标进程中获取空闲没有使用的fd值
3、把第1步获取的file对象与第2步获取的fd值进行绑定既可以

老版本binder驱动的fd传递基本工作都直接在binder_translate_fd这个方法中完成,但是新版本binder驱动这块就有了较大的差异。
总结一个简单图给大家更容易理解:

在这里插入图片描述

新版本binder驱动代码传递fd

安卓kernel内核版本:

VERSION = 6
PATCHLEVEL = 1
SUBLEVEL = 90
EXTRAVERSION =
NAME = Curry Ramen

下面来看看这个6.1对应的源码是怎么传递fd的


static int binder_translate_fd(u32 fd, binder_size_t fd_offset,
			       struct binder_transaction *t,
			       struct binder_thread *thread,
			       struct binder_transaction *in_reply_to)
{
	struct binder_proc *proc = thread->proc;
	struct binder_proc *target_proc = t->to_proc;
	struct binder_txn_fd_fixup *fixup;
	struct file *file;

 //这里也是通过源进程fd获取对应的file结构
	file = fget(fd);
	//检验传递安全性
	ret = security_binder_transfer_file(proc->cred, target_proc->cred, file);
	

	/*
	 * Add fixup record for this transaction. The allocation
	 * of the fd in the target needs to be done from a
	 * target thread.
	 */
	 
	 //以下是差异部分
	//构造出一个binder_txn_fd_fixup结构
	fixup = kzalloc(sizeof(*fixup), GFP_KERNEL);
	//检验传递安全性
	fixup->file = file;//获取file
	fixup->offset = fd_offset;
	fixup->target_fd = -1;//注意这里开始构造时候没有值
	
	//该代码将 fixup 结构体(类型为 binder_txn_fd_fixup)的成员 fixup_entry 
	//添加到事务 t的 fd_fixups 链表尾部,用于‌延迟处理文件描述符(fd)的跨进程映射‌。
	list_add_tail(&fixup->fixup_entry, &t->fd_fixups);

	return ret;
}

上面可以看出与老版本巨大差别在于,新版本根本没有直接在binder_translate_fd中获取target_fd和install target_fd到file,只是构造了binder_txn_fd_fixup对象,赋值file后,然后加入到事物t的fd_fixups列表中。

下面来看看‌binder_txn_fd_fixup 结构体‌
表示一个待处理的 fd 映射修正项,包含以下关键字段:

struct binder_txn_fd_fixup {
    struct file *file;       // 源进程的 file 对象
    binder_size_t offset;    // fd 在事务数据缓冲区中的偏移
    int target_fd;           // 目标进程的 fd(初始为 -1)
    struct list_head fixup_entry; // 链表节点(通过 list_add_tail 链接)
};

上面看完后最大疑问肯定是:
1、没有看到有target_fd在目标进行进行获取啊?
2、也没有看到file与target_fd进行映射?

那么新版本到底是如何通过binder_txn_fd_fixup就可以实现的fd的跨进程传递呢?又为什么要这样做呢?

按照上面源码分析可以看到最后的binder_txn_fd_fixup被放到了传递事物binder_transaction中去了,那么什么时候执行处理这个呢?

目标进程处理事务‌,这里处理事物肯定在目标进程的binder_thread_read时候:

在 binder_thread_read 或事务处理函数中,遍历 t->fd_fixups 链表:

static int binder_thread_read(struct binder_proc *proc,
			      struct binder_thread *thread,
			      binder_uintptr_t binder_buffer, size_t size,
			      binder_size_t *consumed, int non_block)
{
		//省略
		//使用binder_apply_fd_fixups方法来专门处理fd相关
		ret = binder_apply_fd_fixups(proc, t);
		//省略
	return 0;
}

下面看看真正干活的binder_apply_fd_fixups

   /**
 * binder_apply_fd_fixups() - finish fd translation
 * @proc:         binder_proc associated @t->buffer
 * @t:	binder transaction with list of fd fixups
 *
 * Now that we are in the context of the transaction target
 * process, we can allocate and install fds. Process the
 * list of fds to translate and fixup the buffer with the
 * new fds first and only then install the files.
 *
 * If we fail to allocate an fd, skip the install and release
 * any fds that have already been allocated.
 */
static int binder_apply_fd_fixups(struct binder_proc *proc,
				  struct binder_transaction *t)
{
	struct binder_txn_fd_fixup *fixup, *tmp;
	int ret = 0;

	list_for_each_entry(fixup, &t->fd_fixups, fixup_entry) {
	//获取目标进程的空闲fd
		int fd = get_unused_fd_flags(O_CLOEXEC);
		fixup->target_fd = fd;//赋值给target_fd
		if (binder_alloc_copy_to_buffer(&proc->alloc, t->buffer,
						fixup->offset, &fd,
						sizeof(u32))) {
		}
	}
	list_for_each_entry_safe(fixup, tmp, &t->fd_fixups, fixup_entry) {
		//把源file与上面赋值的fixup->target_fd进行关联
		fd_install(fixup->target_fd, fixup->file);
		list_del(&fixup->fixup_entry);
		kfree(fixup);
	}

	return ret;
}

上面源码就很清楚看出来,fd的获取和关联file结构都是在目标进程进行read的时候才操作的,而不是和老版本那样在源进程进行binder_transaction时候做的,那么这样做是基于什么考虑呢?
个人认为有以下几个部分:

‌延迟处理设计‌

原因‌:目标进程的 fd 分配必须在其自身上下文中执行(涉及进程的 fd 表),避免竞态或权限问题。
‌流程‌:
‌收集阶段‌:在源进程的 Binder 线程中,通过 binder_translate_fd 收集所有待映射的 fd,形成 fd_fixups 链表。
‌处理阶段‌:当目标进程的线程处理该事务时,遍历 fd_fixups 链表,为每个 fixup 分配 target_fd,并修改事务数据缓冲区中的 fd 值。

有以下几个优势:
安全性‌
‌权限隔离‌:目标进程自行分配 fd,避免源进程直接操作目标资源。
‌策略检查‌:在 binder_translate_fd 阶段已通过 SELinux 检查(security_binder_transfer_file)。

原子性‌
所有 fd 修正项集中处理,确保事务数据的一致性。

性能优化‌
延迟分配减少上下文切换,结合 Binder 的 mmap 机制实现零拷贝。

文章参考来源:https://mp.weixin.qq.com/s/PabuIBgrX1EPcY0FLWSgGg

更多framework实战开发,关注下面“千里马学框架”


网站公告

今日签到

点亮在社区的每一天
去签到