图解 Binder:系统调用 open

1,649 阅读30分钟

这是一系列的 Binder 文章,会从内核层到 Framework 层,再到 Java 层,深入浅出,介绍整个 Binder 的设计。详见《图解 Binder:概述》。

本文基于 Android platform 分支 android-13.0.0_r1 和内核分支 common-android13-5.15 解析。

一些关键代码的链接,可能会因为源码的变动,发生位置偏移、丢失等现象。可以搜索函数名,重新进行定位。

本文以Android Binder的open()调用为例,解析系统调用open()从用户空间到内核空间的流程。

Binder的open()的调用代码如下:

open("/dev/binder", O_RDWR | O_CLOEXEC);

open函数可分为用户空间调用、内核空间调用两部分。内核空间的相关调用,又涉及了Linux的虚拟文件系统VFS这个概念。大致的流程如下:

所以本文主要分为三个部分:

  • 用户空间的相关调用
  • 虚拟文件系统
  • 内核空间的相关调用

用户空间的相关调用

用户空间的相关调用如下:

__openat()openat(),它实际是一个汇编函数,是由gensyscalls.py根据SYSCALLS.TXT里记录的信息,生成相关代码。

SYSCALLS.TXT

int __openat:openat(int, const char*, int, mode_t) all

gensyscalls.py根据上述信息,将会根据不同的cpu架构,生成不同的汇编代码,比如:

arm32

ENTRY(__openat)
    mov     ip, r7
    .cfi_register r7, ip
    ldr     r7, =__NR_openat
    swi     #0
    mov     r7, ip
    .cfi_restore r7
    cmn     r0, #(MAX_ERRNO + 1)
    bxls    lr
    neg     r0, r0
    b       __set_errno_internal
END(__openat)

arm64

ENTRY(__openat)
    mov x8, __NR_openat
    svc #0

    cmn x0, #(MAX_ERRNO + 1)
    cneg x0, x0, hi
    b.hi __set_errno_internal

    ret
END(__openat)

__NR_openat是一个系统调用号。swisvc会触发软中断,进入内核态,并根据系统调用号,从系统调用表中,定位到对应的函数地址,完成系统调用。

何为软中断?来自@贝贝先生的一段解释:

中断在本质上是软件或者硬件发生了某种情形而通知处理器的行为,处理器进而停止正在运行的指令流,去转去执行预定义的中断处理程序。软件中断也就是通知内核的机制的泛化名称,目的是促使系统切换到内核态去执行异常处理程序。这里的异常处理程序其实就是一种系统调用,软件中断可以当做一种异常。总之,软件中断是当前进程切换到系统调用的过程。

虚拟文件系统

内核空间的相关调用,比较复杂,涉及了Linux的虚拟文件系统VFS。所以,在分析内核空间的相关调用源码之前,我们需要先了解一些VFS相关的知识点。

虚拟文件系统VFS(Virtual Filesystem Switch)不是一种实际的文件系统,它只存在于内存中。它是一个内核软件层,是在具体的文件系统之上抽象的一层,用来处理与Posix文件系统相关的所有调用。它屏蔽了底层各种文件系统复杂的调用实现,为各种文件系统提供一个通用的接口。

在open()的系统调用的内核代码中,是通过VFS,最终访问到Binder文件系统,找到Binder的binder_open(),并执行。如下图:

VFS的四大对象和一张表

VFS有四大对象:super_block、dentry、inode和file,贯穿了VFS的设计。

  • super_block:超级块。存放挂载的文件系统的相关信息,挂载Binder文件系统到全局文件系统树时,会创建属于它的super_block。
  • dentry:目录项。文件系统将目录也当作文件,目录的数据由目录项组成,而每个目录项存储一个目录或文件的名称、对应的inode等内容。open一个文件"/home/xxx/yyy.txt",那么/、home、xxx、yyy.txt都是一个文件路径组件(组件这词主要来自源码中的component),都有一个对应的dentry。
  • inode:索引节点。描述磁盘上的一个文件元数据(文件属性、位置等)。
  • file:文件对象。open()时创建,close()时销毁。open一个文件/home/xxx/yyy.txt,会用file来记录yyy.txt对应的inode、dentry以及file_operations。

这几大对象之间的关联如下:

  • 一个文件系统中,只会有一个super_block。
  • dentry中有指向super_block的指针,即d_sb。dentry中有指向它对应的inode的指针,即d_inode。dentry中有指向它的父dentry的指针,即d_parent
  • 多个dentry能会指向同一个inode。不同的dentry可能是同一个inode的别名,inode里有个i_dentry字段,它是一个链表,链表的节点是dentry里的d_alias。当持有一个inode对象时,利用i_dentryd_adlias,我们可以通过container_of获取到对应的dentry。详见d_find_alias()
  • inode中有指向super_block的指针,即i_sb
  • file中有指向对应的inode的指针,即f_inode。file里的f_path是结构体path的对象,它记录了指向对应的dentry的指针。
  • file中的f_op,通常等价于对应的inodei_fopf_op是结构体file_operations的对象,记录着一些函数指针,代表系统调用open()的目标文件所支持的操作,比如open()、read()、write()等。

VFS有一张表:dentry_hashtable。它是一个hash表,用来保存对应一个dentry的hlist_bl_node,用拉链法来解决哈希冲突。它的好处是,能根据路径组件名称的hash值,快速定位到对应的hlist_bl_node,最后通过hlist_bl_node获得对应的dentry。

dentry_hashtable是一个指向hlist_bl_head的指针的数组。通过name的hash值,调用d_hash()即可获取dentry_hashtable中对应的hlist_bl_head

struct hlist_bl_head {
	struct hlist_bl_node *first;
};

而结构体hlist_bl_node类似一个双向链表,采用的是头插法(hlist_bl_add_head())。它的定义如下:

struct hlist_bl_node {
	struct hlist_bl_node *next, **pprev;
};

pprev这个二级指针,指向的不是前面节点,而是指向前面节点的next指针的存储地址(如果是头节点,pprev会指向hlist_bl_headfirst的存储地址)。这样的好处是:从链表中移除自身的时候,只需要将next->pprev = pprev即可。详见__hlist_bl_del()

  • 判断节点是否在链表中,只需要判断pprev是否为空。 详见hlist_bl_unhashed()

  • dentry_hashtable的初始化:可能的初始化地方有两个,dcache_init_early()dcache_init()。都是通过alloc_large_system_hash()分配一个容量是2的整数次幂的内存块。该内存块即一个数组,用来保存denstry。

  • hlist_bl_node中并没有dentry相关信息,查询dentry_hashtable,如何通过hlist_bl_node指针获取对应的dentry?dentry里有一个字段struct hlist_bl_node d_hash,我们可以用hlist_bl_node指针,通过container_of()获得对应的dentry。

  • 如何区分是否是目标dentry? dentry_hashtable是采用拉链法解决冲突的(链表遍历详见hlist_bl_for_each_entry_rcu())。由于是根据路径组件名称来做hash的,所以:

    • 当hash相同,路径组件名称不同,比较一下路径组件名称的字符串就知道了
    • 当hash相同,路径组件名称相同,通过hlist_bl_node获取对应的dentry,判断一下dentry->d_parent是否是它的parent就知道了。因为同一目录下不可能有2个同名的文件,所以名称相同的,parent肯定不同。

VFS的挂载

挂载是指将一个文件系统,挂载到全局文件系统树上。除根文件系统外,挂载点要求是一个已存在的文件系统的目录。

根文件系统rootfs,将会在系统启动的时候创建,挂载到"/",也是全局文件系统树的根节点。

一个目录被文件系统挂载时,原来目录中包含的其他子目录或文件会被隐藏。以后进入该目录时,我们将会通过VFS切换到新挂载的文件系统所在的根目录,看到的是该文件系统的根目录下的内容。

VFS挂载有三大对象

  • mount:对应一次文件系统的挂载。记录了挂载点的dentry、vfsmount以及文件系统在文件系统树上的父节点mount。也记录了代表它自身的hlist_node,与dentry类似,也是可以用hlist_node的指针,通过container_of()获取到对应的mountpoint()
  • vfsmount:记录了一个文件系统的super_block和根目录的dentry。
  • mountpoint:记录一个挂载点的dentry和代表它自身的hlist_node,可以用hlist_node的指针,通过container_of()获取到对应的mountpoint()。
  • mount持有指向mountpoint的指针。它的mnt_mountpoint和mountpoint的m_dentry是指向同一个dentry。该dentry对应的是挂载目录。
  • mount持有的是vfsmount的对象,不是指针。当持有指向该vfsmount的指针时,就可以通过container_of()获得该mount了,详见real_mount()

VFS挂载有两张表:两张表的结构设计和dentry_hashtable是一样的。

VFS的路径行走

访问"/dev/binderfs/binder"时,可能经历了什么?

假设"/"是根文件系统rootfs的根目录。"dev"是根文件系统的根目录下的一个普通的目录,非挂载点。"binderfs"既根文件系统下的位于/dev里的目录,又是是binder文件系统的挂载点。"binder"是binder文件系统的挂载点下的一个代表binder设备的文件。

下图是访问"/dev/binderfs/binder",VFS的路径行走流程:

  • path是一个vfsmount、dentry二元组。path中的vfsmount记录的是当前所在文件系统的根目录信息,而dentry是当前路径行走所在的组件。
  • VFS路径行走的起点是在根文件系统rootfs的根目录。所以一开始,path①记录的是根文件系统的vfsmount①和代表根文件系统根目录"/"的dentry①。
  • 当我们进入到"dev"时,path②记录的dentry将是代表组件"dev"的dentry,由于还是在根文件系统,所以记录的vfsmount仍是vfsmount①。
  • 当我们进入到"binderfs"时,path③记录的dentry将是代表组件"binderfs"的dentry,由于还是在根文件系统,所以记录的vfsmount仍是vfsmount①。但是,当我们检查dentry时,发现它有个DCACHE_MOUNTED的flag,表示它是一个挂载点。这时,我们就要利用path里的信息,即父mount①的vfsmount①和挂载点的dentry,从mount_hashtable获得Binder文件系统的mount②,实现函数是__lookup_mnt()。最终获得vfsmount②里的dentry④,成功切换到Binder文件系统的根目录(Binder文件系统的根目录在我们访问的路径上是看不出来的,VFS屏蔽了用户的感知)。
  • 在整个VFS的路径行走中,我们需要先进入根文件系统,最后再进入Binder文件系统。
  • 注意,dentry③和dentry④是有区别的。dentry③是由根文件系统创建。而dentry④是由Binder文件系统,在挂载的时候创建的。

VFS的路径行走,可以说是open()的内核调用的核心设计。

VFS的重复挂载

VFS可能会存在在一个挂载点上,先后挂载多个的文件系统的情况(如果挂载的文件系统类型相同,文件系统所在磁盘分区不同,也是可以的)。比如,我们在"binderfs"上,先挂载ext2文件系统,再挂载ext4系统,最后再挂载Binder文件系统。这时候,只有最后挂载的Binder文件系统是生效的。它们的挂载关联如下:

因为重复挂载的缘故,为了找到最后挂载的Binder文件系统的mount,我们需要轮询调用__lookup_mnt()。边轮询调用__lookup_mnt(),边更新path,直到__lookup_mnt()返回的mount*为NULL:

//循环调用的示例代码
void sample_lookup(struct path *path) {
    struct vfsmount *mnt = path->mnt;
    struct dentry *dentry = path->dentry;
    for (;;) {
        if (flags & DCACHE_MOUNTED) {
                //通过父mount的vfsmount和挂载点的dentry,获取对应的mount
                struct mount *mounted = __lookup_mnt(path->mnt, dentry);
                //mounted不为NULL,表示不是最后一个挂载在该挂载点的文件系统
		if (mounted) {
                        //更新path
			path->mnt = &mounted->mnt;
                        //更新path里的dentry
			dentry = path->dentry = mounted->mnt.mnt_root;
			flags = dentry->d_flags;
			continue;
		}
                //return的时候,path里记录的就是最后一个挂载在该挂载点的文件系统的vfsmount
                //和文件系统根目录"/"的dentry。这也意味着我们切换到了该文件系统。
                return;
        }
        return;
    }
}


//__lookup_mnt实现
struct mount *__lookup_mnt(struct vfsmount *mnt, struct dentry *dentry)
{
        //通过父mount的vfsmount和在父文件系统的挂载点的dentry,获得hlist_head
	struct hlist_head *head = m_hash(mnt, dentry);
	struct mount *p;

        //遍历hlist_node链表,找到对应的mount
	hlist_for_each_entry_rcu(p, head, mnt_hash)
		if (&p->mnt_parent->mnt == mnt && p->mnt_mountpoint == dentry)
			return p;
	return NULL;
}

当我们取消挂载Binder文件系统时,之前挂载的ext4系统文件又会重新生效。同样,取消挂载ext4时,ext2又会重新生效。

Binder文件系统的注册与挂载

在分析系统调用open("/dev/binder")的内核代码之前,我们还得先了解一下Binder文件系统的注册和挂载。

open("/dev/binder")最终其实是调用Binder文件系统的open()实现。而想要调用该open()实现,我们得先提前注册和挂载Binder文件系统。

注册

Binder驱动初始化的时候,会将Binder文件系统注册到全局文件系统中。

Binder驱动初始化相关可以参考《图解 Binder:初始化

代码流程如下:

static struct file_system_type binder_fs_type = {
	.name			= "binder",
	.init_fs_context	= binderfs_init_fs_context,
	.parameters		= binderfs_fs_parameters,
	.kill_sb		= kill_litter_super,
	.fs_flags		= FS_USERNS_MOUNT,
};

int __init init_binderfs(void)
{
    ret = register_filesystem(&binder_fs_type);
}

binder_fs_type记录了Binder文件系统的一些信息。init_fs_context是一个函数指针,用来初始化文件系统上下文,这里被赋值了binderfs_init_fs_context

static const struct fs_context_operations binderfs_fs_context_ops = {
	.free		= binderfs_fs_context_free,
	.get_tree	= binderfs_fs_context_get_tree,
	.parse_param	= binderfs_fs_context_parse_param,
	.reconfigure	= binderfs_fs_context_reconfigure,
};

static int binderfs_init_fs_context(struct fs_context *fc)
{
    fc->ops = &binderfs_fs_context_ops;
}

binderfs_fs_context_ops里的get_tree指向了binderfs_fs_context_get_tree。当将Binder文件系统挂载到全局文件系统树时,它就会执行get_tree,即binderfs_fs_context_get_tree

挂载

Binder文件系统挂载到VFS的时机在init进程启动的时候。相关挂载指令在system/core/rootdir/init.rc中:

mkdir /dev/binderfs
mount binder binder /dev/binderfs stats=global
chmod 0755 /dev/binderfs

symlink /dev/binderfs/binder /dev/binder
symlink /dev/binderfs/hwbinder /dev/hwbinder
symlink /dev/binderfs/vndbinder /dev/vndbinder

mount指令的解析函数是do_mount()

symlink指令的解析函数是do_symlink()

do_mount()会调用mount()将Binder文件系统挂载到路径"/dev/binderfs"。

do_symlink()会调用symlink(),为相应的文件路径创建链接。比如,"/dev/binder"其实就是"/dev/binderfs/binder"的链接。当我们执行open("/dev/binder", O_RDWR | O_CLOEXEC)时,实际就是打开位于"/dev/binderfs/binder"的Binder驱动。

mount()最终会调用内核的do_mount()(不同于上面提到的init.rc里的mount指令的解析函数do_mount())。

由于do_mount()不是本文分析的主要内容,所以这里简单给出do_mount()挂载已注册的Binder文件系统到"/dev/binderfs"的大致调用流程。感兴趣的,可以自己点击相关源码链接,进行分析。

  • do_mount() //把Binder文件系统挂载到"/dev/binderfs"
    • user_path_at() //分析路径"/dev/binderfs",最终获得组件"binderfs"对应的path
    • path_mount() //完成Binder文件系统的挂载
      • do_new_mount()
        • get_fs_type() //获取我们之前注册的file_system_type类型的binder_fs_type
        • fs_context_for_mount() //为即将挂载的Binder文件系统创建一个上下文fs_context
        • vfs_get_tree() //(vfs_get_tree()的调用,后面会进一步详细分析)完成Binder文件系统的super_block、根目录的dentry、inode的创建和初始化。
        • do_new_mount_fc() //根据super_block等相关信息,完成挂载

vfs_get_tree()

vfs_get_tree()是在挂载的时候,利用之前注册时提供的信息,完成Binder文件系统的super_block、根目录的dentry、inode的创建和初始化。

static int binderfs_fill_super(struct super_block *sb, struct fs_context *fc)
{
    struct binderfs_device device_info = {};
    const char *name;
    
    //创建Binder文件系统根目录的inode
    inode = new_inode(sb);
    //创建binder-control设备,并为其创建对应的inode和dentry
    ret = binderfs_binder_ctl_create(sb);
    //创建Binder文件系统根目录的dentry
    sb->s_root = d_make_root(inode);
    //binder_devices_param为"binder,hwbinder,vndbinder"
    name = binder_devices_param;
    for (len = strcspn(name, ","); len > 0; len = strcspn(name, ",")) {
            strscpy(device_info.name, name, len + 1);
            //会执行3次,分别创建代表binder、hwbinder、vndbinder这几个Binder设备的inode和dentry
            ret = binderfs_binder_device_create(inode, NULL, &device_info);
            name += len;
            if (*name == ',')
		name++;
    }
}

static int binderfs_binder_device_create(struct inode *ref_inode,
					 struct binderfs_device __user *userp,
					 struct binderfs_device *req)
{
    struct dentry *dentry, *root;
    struct super_block *sb = ref_inode->i_sb;
    inode = new_inode(sb);
    inode->i_fop = &binder_fops;
    
    root = sb->s_root;
    //根据name,在dentry_hashtable中查找是否存在对应的dentry,没有则创建一个,加入到dentry_hashtable
    //该dentry的d_parent会指向sb->s_root
    dentry = lookup_one_len(name, root, name_len);
    
    //inode的i_private会记录对应的binder设备信息
    inode->i_private = device;
    //dentry的d_inode会指向inode,并将dentry的d_alias加入到inode的i_dentry链表
    d_instantiate(dentry, inode);
}

注意在binderfs_binder_device_create()中,inode->i_fop = &binder_fopsbinder_fops是结构体file_operations的对象,它记录着一些函数指针,代表Binder文件系统支持的系统调用的最终实现。

const struct file_operations binder_fops = {
	.owner = THIS_MODULE,
	.poll = binder_poll,
	.unlocked_ioctl = binder_ioctl,
	.compat_ioctl = compat_ptr_ioctl,
	.mmap = binder_mmap,
	.open = binder_open,
	.flush = binder_flush,
	.release = binder_release,
};

binder_open就是Binder文件系统的open()实现open("/dev/binder", O_RDWR | O_CLOEXEC)最终就是通过VFS,获取到代表该binder设备的inode,根据i_fop记录的信息,获取到指向binder_open()的函数指针,完成调用。

内核空间的相关调用

了解了VFS后,我们可以进一步分析内核空间的相关调用。

从上文中,我们知道调用汇编函数__openat,触发软中断,进入内核态后,就来到了内核空间。内核最终会根据系统调用号,从系统调用表中,找到对应的系统调用函数,完成内核空间的相关调用。

系统调用表

系统调用表是一个数组,我们以对应的系统调用号为下标,就可以获得指向对应的系统调用函数的指针。

系统调用表sys_call_table的定义在common/arch/arm64/kernel/sys.c

const syscall_fn_t sys_call_table[__NR_syscalls] = {
	[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/unistd.h>
};

系统调用号__NR_openat与对应的系统调用函数

前面我们知道了open()对应的系统调用号是__NR_openat。它在用户空间和内核空间都有对应的定义:

用户空间的__NR_openat定义在bionic/libc/kernel/uapi/asm-generic/unistd.h

#define __NR_openat 56

内核空间的__NR_openat定义在 common/include/uapi/asm-generic/unistd.h

#define __NR_openat 56
__SYSCALL(__NR_openat, sys_openat)

__SYSCALL的定义在common/arch/arm64/kernel/sys.c

#undef __SYSCALL
#define __SYSCALL(nr, sym)	asmlinkage long __arm64_##sym(const struct pt_regs *);

展开就是:

asmlinkage long __arm64_sys_openat(const struct pt_regs *);

__NR_openat其实就是对应__arm64_sys_openat函数。该函数的定义在common/fs/open.c

SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename, int, flags,
		umode_t, mode)
{
	if (force_o_largefile())
		flags |= O_LARGEFILE;
	return do_sys_open(dfd, filename, flags, mode);
}

SYSCALL_DEFINE4这个宏定义的相关代码在common/include/linux/syscalls.hcommon/arch/arm64/include/asm/syscall_wrapper.h中:

//syscalls.h
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)

#define __MAP0(m,...)
#define __MAP1(m,t,a,...) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)

#define __SC_DECL(t, a)	t a
#define __TYPE_AS(t, v)	__same_type((__force t)0, v)
#define __TYPE_IS_L(t)	(__TYPE_AS(t, 0L))
#define __TYPE_IS_UL(t)	(__TYPE_AS(t, 0UL))
#define __TYPE_IS_LL(t) (__TYPE_AS(t, 0LL) || __TYPE_AS(t, 0ULL))
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
#define __SC_CAST(t, a)	(__force t) a
#define __SC_ARGS(t, a)	a
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))

//syscall_wrapper.h
#define SC_ARM64_REGS_TO_ARGS(x, ...)				\
	__MAP(x,__SC_ARGS					\
	      ,,regs->regs[0],,regs->regs[1],,regs->regs[2]	\
	      ,,regs->regs[3],,regs->regs[4],,regs->regs[5])

#define __SYSCALL_DEFINEx(x, name, ...)						\
	asmlinkage long __arm64_sys##name(const struct pt_regs *regs);		\
	static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));		\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));	\
	asmlinkage long __arm64_sys##name(const struct pt_regs *regs)		\
	{									\
		return __se_sys##name(SC_ARM64_REGS_TO_ARGS(x,__VA_ARGS__));	\
	}									\
	static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))		\
	{									\
		long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));	\
		__MAP(x,__SC_TEST,__VA_ARGS__);					\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));		\
		return ret;							\
	}									\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

SYSCALL_DEFINE4(openat, int, dfd,...)这部分代码进行宏展开后就是:

asmlinkage long __arm64_sys_openat(const struct pt_regs *regs);
static long __se_sys_openat(long dfd, long filename, long flags, long mode);
static inline long __do_sys_openat(int dfd, const char __user * filename, int flags, umode_t mod);

asmlinkage long __arm64_sys_openat(const struct pt_regs *regs)
{
    return __se_sys_openat(regs->regs[0],regs->regs[1],regs->regs[2],regs->regs[3]);
}

static long __se_sys_openat(long dfd, long filename, long flags, long mode)
{
    long ret = __do_sys_openat((int) dfd, (const char __user *) filename, (int) flags, (umode_t) mode);
    ...
    return ret;
}

static inline long __do_sys_openat(int dfd, const char __user * filename, int flags, umode_t mod)
{
    if (force_o_largefile())
        flags |= O_LARGEFILE;
    return do_sys_open(dfd, filename, flags, mode);
}

SYSCALL_DEFINE4这个宏其实定义了下面几个函数,并完成了调用:

  • __arm64_sys_openat()
    • __se_sys_openat()
      • __do_sys_openat()

内核空间的调用链路

整个调用链路的核心设计,就是VFS的路径行走:

do_filp_open()

先来看看do_filp_open():

struct file *do_filp_open(int dfd, struct filename *pathname,
		const struct open_flags *op)
{
    //初始化结构体nameidata的实例
    struct nameidata nd;
    struct file *filp;
    //将open()相关参数保存在nd中
    set_nameidata(&nd, dfd, pathname, NULL);
    //第一次调用path_openat,flags中加入LOOKUP_RCU
    filp = path_openat(&nd, op, flags | LOOKUP_RCU);
    //path_openat失败,后续可能再调两次,这里就不深入分析了
    if (unlikely(filp == ERR_PTR(-ECHILD)))
	filp = path_openat(&nd, op, flags);
    if (unlikely(filp == ERR_PTR(-ESTALE)))
	filp = path_openat(&nd, op, flags | LOOKUP_REVAL);
}

static void __set_nameidata(struct nameidata *p, int dfd, struct filename *name)
{
    p->depth = 0;
    //dfd是在用户空间调用__openat()时传的第一个参数,固定为AT_FDCWD
    p->dfd = dfd;
    //把"/dev/binder"保存在nameidata的name字段中
    p->name = name;
}
  • 结构体nameidata:主要记录open()相关参数。后续在遍历分析"/dev/binder"的各个路径名name时,会用nameidata来记录当前name对应的path和inode。
struct nameidata {
    struct path	path; //当前路径组件对应的dentry、vfsmount
    struct qstr	last;
    struct path	root; //根文件系统根目录的Path
    struct inode	*inode
    struct filename	*name; //filename里有个const char *name,即"/dev/binder"
    ...
}

path_openat()

现在进入到path_openat()看看:

static struct file *path_openat(struct nameidata *nd,
			const struct open_flags *op, unsigned flags)
{
	struct file *file;
        //分配一个VFS的空的file
        file = alloc_empty_file(op->open_flag, current_cred());
        if (...) {
	} else {
                //s即"/dev/binder"
                const char *s = path_init(nd, flags);
                //
                while (!(error = link_path_walk(s, nd)) &&
		       (s = open_last_lookups(nd, file, op)) != NULL)
			;
		if (!error)
			error = do_open(nd, file, op);
        }
}

static const char *path_init(struct nameidata *nd, unsigned flags)
{
        //取出之前保存在nameidata中的文件路径
        const char *s = nd->name->name;
        //RCU加锁
        if (flags & LOOKUP_RCU)
		rcu_read_lock();
        if (*s == '/' && !(flags & LOOKUP_IN_ROOT)) {
                //获取文件系统的根目录,并保存在nameidata中
		error = nd_jump_root(nd);
		return s;
	}
        ...
}

static int nd_jump_root(struct nameidata *nd)
{
	if (!nd->root.mnt) {
                //获取根目录的Path信息,保存到nd->root中
		int error = set_root(nd);
	}
	if (nd->flags & LOOKUP_RCU) {
                //将根文件系统的Path、inode记录到nameidata中
		struct dentry *d;
		nd->path = nd->root;
		d = nd->path.dentry;
		nd->inode = d->d_inode;
		nd->seq = nd->root_seq;
	} else {
		path_put(&nd->path);
		nd->path = nd->root;
		path_get(&nd->path);
		nd->inode = nd->path.dentry->d_inode;
	}
	nd->state |= ND_JUMPED;
	return 0;
}
        
static int set_root(struct nameidata *nd)
{
        //获取当前进程的fs_struct
	struct fs_struct *fs = current->fs;
        
	if (nd->flags & LOOKUP_RCU) {
		unsigned seq;
		do {
			seq = read_seqcount_begin(&fs->seq);
                        //从fs中获取根目录"/"的Path,里面有它对应的dentry,保存在nd->root中
			nd->root = fs->root;
			nd->root_seq = __read_seqcount_begin(&nd->root.dentry->d_seq);
		} while (read_seqcount_retry(&fs->seq, seq));
	}
	return 0;
}
  • RCU是Read-copy_update,是一种数据同步机制,允许读写同时进行。读操作不存在睡眠、阻塞、轮询,不会形成死锁,相比读写锁效率更高。写操作时先拷贝一个副本,在副本上进行修改、发布,并在合适时间释放原来的旧数据。
  • current的定义在common/arch/arm64/include/asm/current.h里。current是一个结构体task_struct,表示当前正在运行进程的进程描述符,记录着进程相关信息,包括进程状态、内存信息等等。
  • 结构体fs_struct记录当前进程的文件系统相关信息。比如根文件系统的根目录Path(Path中记录了dentry和vfsmount)。

link_path_walk()

现在我们来看看VFS是如何解析文件路径组件,获取对应的dentry、inode。

  • link_path_walk() //路径行走:逐个解析文件路径组件(除了最后一个组件),获取对应的dentry、inode
static int link_path_walk(const char *name, struct nameidata *nd)
{
        int depth = 0; // depth <= nd->depth
        //指针移动,"/dev/binder"变成了"dev/binder"
        while (*name=='/')
		name++;
        //轮询处理各个文件路径名
        for(;;) {
                const char *link;
		u64 hash_len;
		int type;
                //计算name中除根路径组件外的第一个路径组件的hash值和名字长度。
                //第一次循环的第一个路径组件是"dev"
                //返回值hash_len是64位的无符号整数,高32位记录name的长度len,低32位记录name的hash值
                hash_len = hash_name(nd->path.dentry, name);
                
                type = LAST_NORM;
                //处理路径组件中含有'.'的情况,不作分析
                if (name[0] == '.') switch (hashlen_len(hash_len)) {
                    ...
                }
                if (likely(type == LAST_NORM)) {
                        //第一次循环parent代表根目录"/",在前面的path_init()中获取到的
                        //第二次循环parent代表目录"dev"...
			struct dentry *parent = nd->path.dentry;
			nd->state &= ~ND_JUMPED;
		}
                //保存信息到nameidata中
                nd->last.hash_len = hash_len;
		nd->last.name = name;
		nd->last_type = type;
                
                //指针移动,第一次循环时,"dev/binder"变成了"/binder"
                name += hashlen_len(hash_len);
                //注意通常循环处理到最后一个组件时,if (!*name)是为true的
                if (!*name)
			goto OK;
                //指针移动,第一次循环时,"/binder"变成了"binder"
                do {
			name++;
		} while (unlikely(*name == '/'));
                
                if (unlikely(!*name)) {
OK:
			/* pathname or trailing symlink, done */
			if (!depth) {
                                //路径最后一个组件,不管是否是symlink,
                                //都会走到这里,不会执行walk_component()
				nd->dir_uid = i_uid_into_mnt(mnt_userns, nd->inode);
				nd->dir_mode = nd->inode->i_mode;
				nd->flags &= ~LOOKUP_PARENT;
				return 0;
			}
			/* last component of nested symlink */
                        //symlink即链接。只有嵌套在文件路径中的symlink才可能走这里。
                        //如果是位于文件路径末尾的symlink,都会先视为最后一个组件,不会走这里
			name = nd->stack[--depth].name;
			link = walk_component(nd, 0);
		} else {
			/* not the last component */
                        //不是最后一个组件,查找对应的dentry、inode,检查是否是挂载点
			link = walk_component(nd, WALK_MORE);
		}
                //link指symlink对应的真实路径
		if (unlikely(link)) {
			/* a symlink to follow */
                        //只有嵌套在文件路径中的symlink,才会走到这里。
                        //如"/a/b/c",检查组件a时,发现它其实是个嵌套symlink,代表文件路径"e/f"。
                        //在执行walk_component()后,返回的link就是"e/f"
                        //这时候,name就是"b/c",将它保存在nd->stack中,并自增depth。
                        //walk_component完symlink对应的真实路径后,才会取出来"b/c",对它执行walk_component()
			nd->stack[depth++].name = name;
                        //将"e/f"赋值给name,继续循环,执行walk_component()
			name = link;
			continue;
		}
        }
}

static const char *walk_component(struct nameidata *nd, int flags)
{
	struct dentry *dentry;
	struct inode *inode;
        unsigned seq;
        
        //处理路径组件中含有'.'的情况,不作分析
        if (unlikely(nd->last_type != LAST_NORM)) {
		...
	}
        //先执行快查找
	dentry = lookup_fast(nd, &inode, &seq);
	if (unlikely(!dentry)) {
                //快速查找失败,才会执行慢查找
		dentry = lookup_slow(&nd->last, nd->path.dentry, nd->flags);
	}
	if (!(flags & WALK_MORE) && nd->depth)
		put_link(nd);
        //检查是否是链接、挂载点:
        //是链接,则获取到对应的真实路径。
        //是挂载点,则获取最后一个挂载在挂载点的文件系统信息。
	return step_into(nd, flags, dentry, inode, seq);
}

前面在讲Binder文件系统挂载的时候,有提到:/dev/binder就是/binderfs/binder的symlink。不过binder是路径"/dev/binder"的最后一个组件,所以它在第一次执行link_path_walk(),不会对其用walk_component()进行查找。整体的处理逻辑,得看调用link_path_walk()的地方path_openat():

static struct file *path_openat(struct nameidata *nd,
			const struct open_flags *op, unsigned flags)
{
        
        //s即"/dev/binder"
        const char *s = path_init(nd, flags);
        //在第一次循环时,s是"/dev/binder",执行link_path_walk(),"binder"是最后一个组件,
        //跳出link_path_walk()的循环处理,调用open_last_lookups()处理,但发现"binder"是一个symlink。
        //open_last_lookups()会返回"binder"对应的"binderfs/binder"(又或者是"dev/binderfs/binder"?)。
        
        //第二次循环时,s已经是"binderfs/binder", 再次执行link_path_walk(),"binder"是最后一个组件,
        //不过这个"binder"不同于上次循环那个,不是symlink,而是一个真实存在的路径组件
        //最后调用open_last_lookups()即可获得最后一个组件"binder"对应的dentry、inode等信息
        while (!(error = link_path_walk(s, nd)) &&
		(s = open_last_lookups(nd, file, op)) != NULL)
		;
}

lookup_fast()

lookup_fast()是根据路径组件的名称,快速找到对应的dentry、inode的实现,主要分为rcu-walkref-walk,具体设计详见path-lookup.txt

static struct dentry *lookup_fast(struct nameidata *nd,
				  struct inode **inode,
			          unsigned *seqp)
{
	struct dentry *dentry, *parent = nd->path.dentry;
        //flags里有LOOKUP_RCU标记,则执行rcu-walk,否则执行ref-walk
        if (nd->flags & LOOKUP_RCU) {
                //rcu-walk
		dentry = __d_lookup_rcu(parent, &nd->last, &seq);
		if (unlikely(!dentry)) {
                        //移除flags里的LOOKUP_RCU标记,尝试切换到ref-walk
                        //成功则在下一个组件的lookup中,会采用ref-walk。
                        //当前的组件,看流程,不会换到ref-walk,而是用lookup_slow进行查找
			if (!try_to_unlazy(nd))
				return ERR_PTR(-ECHILD);
			return NULL;
                }
                ...
        } else  {
                //ref-walk
                dentry = __d_lookup(parent, &nd->last);
                if (unlikely(!dentry))
			return NULL;
        }
        return dentry;
}

rcu-walk和ref-walk

rcu-walk的实现是__d_lookup_rcu()

ref-walk的实现是__d_lookup()

两者的实现代码类似,都是从dentry_hashtable中查询,主要差异有:

  1. __d_lookup_rcu()遍历查找目标dentry的时候,使用了顺序锁seqlock,读操作不会被写操作阻塞,写操作也不会被读操作阻塞。但读操作可能会反复读取相同的数据:当它发现sequence发生了变化,即它执行期间,有写操作更改了数据。另外,seqlock要求进入临界区的写操作只有一个,多个写操作之间仍然是互斥的。关键代码如下:
struct dentry *__d_lookup_rcu(const struct dentry *parent,
				const struct qstr *name,
				unsigned *seqp)
{
        //hashlen_hash()获取hash值。
        //d_hash()根据hash值,从dentry_hashtable中获取对应的hlist_bl_head
        struct hlist_bl_head *b = d_hash(hashlen_hash(hashlen));
	struct hlist_bl_node *node;
	struct dentry *dentry;
        
        //遍历链表b里的dentry,查找目标dentry
        hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) {
		unsigned seq;
seqretry:
        //获取当前dentry->d_seq,即sequence
        seq = raw_seqcount_begin(&dentry->d_seq);
        //检查是否有写操作,导致sequence发生了变化,有则跳转到seqretry处,重新执行读操作
        if (read_seqcount_retry(&dentry->d_seq, seq)) {
            cpu_relax();
            goto seqretry;
        }
    ...
}
  • dentry->d_seq的类型是seqcount_spinlock_t。seqcount_spinlock_t经过一些复杂的宏定义包含了seqcount_t,可以简单认为seqcount_spinlock_t就是一个int序列号。
  • seqlock的设计可以参考文档
  1. __d_lookup()遍历查找目标dentry的时候,使用了自旋锁spin_lock,多个读操作会发生锁竞争。它还会更新查找到的dentry的引用计数,这也是它为什么叫"ref-walk"的原因。关键代码如下:
struct dentry *__d_lookup(const struct dentry *parent, const struct qstr *name)
{
    //遍历链表b里的dentry,查找目标dentry
    hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) {
        //spin_lock加锁
        spin_lock(&dentry->d_lock);
        ...
        //查找到目标dentry,增加引用计数
        dentry->d_lockref.count++;
	found = dentry;
        //解锁
	spin_unlock(&dentry->d_lock);
	break;
next:
        //没有查找到目标dentry会跳转到next这里,解锁,继续查找
	spin_unlock(&dentry->d_lock);  
    }
    ...
}
  • RCU仍然用于ref-walk中的dentry哈希查找,但不是在整个ref-walk过程中都使用。
  • 频繁地加减reference count可能造成cacheline的刷新,这也是ref-walk开销更大的原因之一。

lookup_slow()

lookup_slow()与lookup_fast()不一样。lookup_fast的两种模式,都是查询的dentry_hashtable。而lookup_slow是兜底方案,lookup_fast失败后,才会调用。

lookup_slow()是通过当前所在的文件系统,获取对应的信息,创建对应的dentry和inode,将新的dentry添加到dentry_hashtable中。

  • lookup_slow() //在lookup_fast()中没有找到dentry,会获取父dentry对应的inode,通过inode->i_op->lookup去查找、创建
    • __lookup_slow()
      • d_alloc_parallel() //创建一个新的dentry,并用in_lookup_hashtable检测、处理并发的创建操作
      • inode->i_op->lookup 通过父dentry的inode去查找对应的dentry:其实就是通过它所在的文件系统,获取对应的信息,创建对应的dentry

要注意的是,在前面的Binder文件系统挂载的这一小节,我们已经提到过binderfs_binder_device_create()会为binder设备创建对应的dentry、inode,并将dentry加入到dentry_hashtable中。所以lookup_slow()通常是没有机会走的。

step_into()

step_into()主要是处理dentry是一个链接或者挂载点的情况。

static const char *step_into(struct nameidata *nd, int flags,
		     struct dentry *dentry, struct inode *inode, unsigned seq)
{
	struct path path;
        //处理组件是挂载点的情况
	int err = handle_mounts(nd, dentry, &path, &inode, &seq);
        //检查是否是链接
	if (likely(!d_is_symlink(path.dentry)) ||
	   ((flags & WALK_TRAILING) && !(nd->flags & LOOKUP_FOLLOW)) ||
	   (flags & WALK_NOFOLLOW)) {
		/* not a symlink or should not follow */
		if (!(nd->flags & LOOKUP_RCU)) {
			dput(nd->path.dentry);
			if (nd->path.mnt != path.mnt)
				mntput(nd->path.mnt);
		}
                //更新path
		nd->path = path;
		nd->inode = inode;
		nd->seq = seq;
		return NULL;
	}
        //处理组件是链接的情况
	return pick_link(nd, &path, inode, seq, flags);
}

do_open()

do_open()是我们的最后一站。到这里时,我们已经获得了最后路径组件的inode,也就是找到了我们的binder设备,可以尝试打开它了。

  • do_open() //根据最后路径组件的inode,调用binder设备的binder_open()
    • vfs_open()
      • do_dentry_open() //将binder设备支持的file_operations,保存在file中。最后调用file中的file_operations里的open函数指针,最终会调用binder_open()
static int do_open(struct nameidata *nd,
		   struct file *file, const struct open_flags *op)
{
        if (!error && !(file->f_mode & FMODE_OPENED))
		error = vfs_open(&nd->path, file);
}

int vfs_open(const struct path *path, struct file *file)
{
	file->f_path = *path;
	return do_dentry_open(file, d_backing_inode(path->dentry), NULL);
}

static int do_dentry_open(struct file *f,
			  struct inode *inode,
			  int (*open)(struct inode *, struct file *))
{
        f->f_inode = inode;
        //将binder设备支持的file_operations,保存在file中
        f->f_op = fops_get(inode->i_fop);
        if (!open)
                //f->f_op->open即binder_open()
		open = f->f_op->open;
        if (open) {
        //调用binder_open()
	error = open(inode, f);
}

//common/drivers/android/binder.c
static int binder_open(struct inode *nodp, struct file *filp)
{
    //打开binder设备,执行相关初始化
    ...
}

参考资料

Linux 虚拟文件系统四大对象:超级块、inode、dentry、file之间关系

深入理解Linux文件系统之文件系统挂载(上)

深入理解Linux文件系统之文件系统挂载(下)

RCU到底是什么?为什么快?为什么可以读写并行?

Linux下多挂载点mount实验