在linux中,进程打开一个文件,返回一个整数的文件描述符,然后就可以在这个文件描述符上对该文件进行操作。那么文件描述符和文件到底是什么关系?进程使用的是虚拟地址,不同进程间是地址隔离的,如何在两个进程中传递文件描述符,然后指向同一文件(binder传递文件描述符)?
下图是linux内核中打开文件的结构体之间的关系图(只是大概,细节可以参考各种内核书籍):
内核中每个进程都使用task_struct结构体表示,其成员files是一个files_struct结构体,表示该进程打开的所有文件相关信息;
files_struct结构体中的fd_array数组(上层应用程序返回的文件描述符其实就是这个数组的下标),是个struct file,内核对打开的文件都用file结构体描述,而成员fdt是个struct fdtable,其成员fd代表数组fd_array的起始地址;
struct file中的f_dentry表示文件的dentry结构,而dentry中包含了文件唯一的inode,即d_inode,d_inode唯一表示了该文件(每个具体文件只有一个inode结构,而多个dentry可以指向同一个inode,例如软链接)。
下面简要分析下代码。
上层:应用程序调用c库中(实现依赖使用的c库)的open()函数打开一个文件,open()返回一个文件描述符;
内核:上层open调用系统调用open函数,
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)
{
ret = do_sys_open(AT_FDCWD, filename, flags, mode);
return ret;
}
实际调用的是
long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{
struct open_flags op;
int lookup = build_open_flags(flags, mode, &op);
char *tmp = getname(filename);
int fd = PTR_ERR(tmp);
if (!IS_ERR(tmp)) {
//①
//获取进程中未使用的fd,就是从0开始的整数,使用一个往上+1
fd = get_unused_fd_flags(flags);
if (fd >= 0) {
//②
//构建file结构体
struct file *f = do_filp_open(dfd, tmp, &op, lookup);
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fsnotify_open(f);
//③
//将fd和file结构体建立关系
fd_install(fd, f);
}
}
putname(tmp);
}
return fd;
}
void fd_install(unsigned int fd, struct file *file)
{
//当前进程的files_struct结构
struct files_struct *files = current->files;
struct fdtable *fdt;
spin_lock(&files->file_lock);
//获取files_struct中的fdt结构体
fdt = files_fdtable(files);
BUG_ON(fdt->fd[fd] != NULL);
//将fdt->fd[fd],也就是fd_array[fd],赋值为file,
rcu_assign_pointer(fdt->fd[fd], file);
spin_unlock(&files->file_lock);
}
do_sys_open主要就完成了上面注释的3步:
获取进程中未使用的fd,就是从0开始的整数,使用一个往上+1;
根据文件的路径,构建file结构体,即构建其中的dentry等;
将fd、file结构体同本进程task_struct结构体建立关系。
经过上面的分析,我们可以得到一个结论:
内核用struct file描述文件,在单个进程中,打开的文件都保存在进程结构体task_struct中files_struct结构体中的fd_array数组中,而上层返回的文件描述符就是这个数组的下标,用来和struct file保持对应关系。因此在不同的进程中打开同一文件,内核都会创建一个新的file结构用来和实际文件关联(但是同一文件在不同进程中的file结构体主要内容是相同的,都描述了所指向的实际文件),然后给上层返回不同的文件描述符。
那么如何将不同进程中的文件描述符关联为同一文件?下面我们看看android binder通过传递文件描述符如何实现同一文件描述符的共享。
下面是内核binder_transaction函数中的一部分,
static void binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr, int reply)
{
case BINDER_TYPE_FD: {
int target_fd;
struct file *file
//通过本进程的fd,获取对应的file结构体
file = fget(fp->handle);
//获取目标进程未使用的文件描述符
target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC);
//将file结构和目标进程的fd等联系起来
task_fd_install(target_proc, target_fd, file);
fp->handle = target_fd;
} break;
}
上述主要工作和前面open函数的实现类似,只不过是去获取目标进程的未使用文件描述符,然后将本进程的file结构体去和目标进程挂钩(file结构体主要内容描述的都是具体文件相关的,例如dentry,所以不同进程间可以共享,因为都是指向同一实际文件),这样binder驱动就悄无声息的帮我们在内核中在目标进程中新建了文件描述符,并将原进程的file结构与之挂钩,就像在目标进程中打开了原进程中的该文件一样,只不过返回给目标进程上层的描述符是新的target_fd。