您的位置:首页 > 运维架构

[IO系统]05 open流程分析

2017-01-24 15:17 344 查看
        说到IO读写流程,首先要了解操作系统是如何通过文件路径在存储设备上定位导致相应文件的。
        如前所述,用户态有两个“打开”函数——open和fopen。其中fopen是glibc对open的封装。

1.1   系统调用

        Open函数通过系统调用sys_open函数来陷入内核态,sys_open函数定义在fs/open.c文件,定义如下:
SYSCALL_DEFINE3(open, const char__user *, filename, int, flags, umode_t, mode)
{
/*检查内核是不是支持大文件,如果是大文件的话就对flags标记对应的标记位置位*/
if(force_o_largefile())
flags|= O_LARGEFILE;

returndo_sys_open(AT_FDCWD, filename, flags, mode);
}
        形参 filename是路径名;flags 表示打开模式,如只读、新建等等;mode 代表新建文件的权限,所以仅仅在创建新文件时(flags 为 O_CREAT 或O_TMPFILE)才使用。
        flags 会在64 位 Kernel 的情况下强制加上O_LARGEFILE 标志位,其余的参数都原封不动的传递给 open 的主函数 do_sys_open。
        AT_FDCWD,其定义在 include/uapi/linux/fcntl.h,是一个特殊值(-100),该值表明当 filename 为相对路径的情况下将当前进程的工作目录设置为起始路径。相对而言,可以在另一个系统调用 openat 中为这个起始路径指定一个目录,此时 AT_FDCWD 就会被该目录的描述符所替代。

1.2 函数do_sys_open

        主要功能如下:
        1. 调用build_open_flags函数标识位预处理
        2. 调用函数getname把用户空间数据复制到内核空间
        3. 调用get_unused_fd_flags分配未使用的fd
        4. 调用do_filp_open执行文件打开操作,即初始化file对象
代码片段如下:
long do_sys_open(int dfd, const char __user *filename, int flags,umode_t mode)
{
structopen_flags op;
/*
* 对标识位进行预处理和封装,
* flags/mode为用户层传递的参数, 内核会对flags/mode进行合法性检查;
* 并根据flags/mode生成新的flags值赋给fd
*/
intfd = build_open_flags(flags, mode, &op);
structfilename *tmp;

if(fd)
returnfd;

/* 把用户空间的存在内存的文件名字符串复制到内核空间*/
tmp= getname(filename);
if(IS_ERR(tmp))
returnPTR_ERR(tmp);

/* 寻找一个未使用的文件描述符,或者没有的话返回错误*/
fd= get_unused_fd_flags(flags);
if(fd >= 0) {
/* 进一步地,如果找到未使用的文件描述符,就执行文件打开操作*/
structfile *f = do_filp_open(dfd, tmp, &op);
if(IS_ERR(f)) {
/* 抱歉,打开失败,应该把文件描述符释放回家*/
put_unused_fd(fd);
fd= PTR_ERR(f);
}else {
fsnotify_open(f);
fd_install(fd,f);
}
}
putname(tmp);
returnfd;

1.2.1  标识位预处理build_open_flags

        build_open_flags 对标志位进行检查,然后包装成 struct open_flags 结构以供接下来的函数使用。
        1. mode 代表文件权限,只有在新建文件的时候才会用到。
        2. 对标志进行有效性检查和处理。  
static inline intbuild_open_flags(int flags, umode_t mode, struct open_flags *op)
{
intlookup_flags = 0;
intacc_mode = ACC_MODE(flags);
/* 如果需要创建文件才会判断mode */
if(flags & (O_CREAT | __O_TMPFILE))
op->mode= (mode & S_IALLUGO) | S_IFREG;
else
op->mode= 0;

/*Must never be set by userspace */
flags&= ~FMODE_NONOTIFY & ~O_CLOEXEC;

/*
*O_SYNC is implemented as __O_SYNC|O_DSYNC. As many places only
*check for O_DSYNC if the need any syncing at all we enforce it's
*always set instead of having to deal with possibly weird behaviour
* formalicious applications setting only __O_SYNC.
*/
if(flags & __O_SYNC)
flags|= O_DSYNC;

if(flags & __O_TMPFILE) {
if ((flags & O_TMPFILE_MASK) !=O_TMPFILE)
return-EINVAL;
if(!(acc_mode & MAY_WRITE))
return-EINVAL;
}else if (flags & O_PATH) {
/*
* If we have O_PATH in the open flag. Then we
* cannot have anything other than the belowset of flags
*/
flags&= O_DIRECTORY | O_NOFOLLOW | O_PATH;
acc_mode= 0;
}

op->open_flag= flags;

/*O_TRUNC implies we need access checks for write permissions */
if(flags & O_TRUNC) //检查写访问权限
acc_mode|= MAY_WRITE;

/*Allow the LSM permission hook to distinguish append
access from general write access. */
if(flags & O_APPEND) //检查追加写
acc_mode|= MAY_APPEND;

op->acc_mode= acc_mode;

op->intent= flags & O_PATH ? 0 : LOOKUP_OPEN;

if(flags & O_CREAT) {
op->intent|= LOOKUP_CREATE;
if(flags & O_EXCL)
op->intent|= LOOKUP_EXCL;
}

if(flags & O_DIRECTORY)
lookup_flags|= LOOKUP_DIRECTORY;
if(!(flags & O_NOFOLLOW))
lookup_flags|= LOOKUP_FOLLOW;
op->lookup_flags= lookup_flags;
return0;
}

1.2.2  缓存复制

        sys_open()->do_sys_open()->getname()->getname_flags()。
        函数所在文件fs/namei.c
        1. __getname通过kmem_cache_alloc在内核缓冲区专用队列names_cachep里申请一块内存用来放置路径名,其实这块内存就是一个 4KB 的内存页。
        2. 如果文件路径长度大于EMBEDDED_NAME_MAX,则通过kzalloc分配内存。
        3. 将文件路径字符串从用户态复制到内核态。
 

1.2.3  分配文件描述符fd

         (1)在当前进程打开的文件位图表中,找到第一个为0的位,返回这个位在位图表里面的下标,这个下标就是将用分配的未使用的文件描述符fd;【每个进程都有一个文件位图表】
        (2)把当前进程的文件表扩展一个文件(即尝试添加一个struct file到当前进程的文件列表中),进程task_struct->files_struct-> fd_array[NR_OPEN_DEFAULT]是一个structfile 数组,而NR_OPEN_DEFAULT在64位系统中等于64(因为一般进程打开的文件数大多都用这个数组就可以直接放下了),如果扩展操作导致当前进程的这个存放struct file的数组放不下了,如要装第65个struct flie结构体,那么将重新分配一片内存区专门用来存放struct file结构体,并且这个内存区的大小为128个struct file结构体,然后将当前进程的task_struct->files_struct->fdtable->fd指针指向这片内存的首地址,然后把之前数组里面的内容复制到这片内存区里面来。下次添加如果超过了128个了,则分配256个大小直到256个也装满,超过256则分配512,依次类推,总是2的幂次方,且会把之前的复制到新分配的内存里面去
注意:这里只是更新了进程的这个file table,新的进程描述符对应的struct file还没有生成进去。
        (3)设置进程的文件位图中新分配的这个文件描述符位为(1)中找到的下标,并更新下一次该分配的进程描述符起点
【由上可知,此处会与进程关联】
 

1.3   函数path_openat

        函数主要功能:
        1. 分配全新的文件描述符
        2. 通过函数path_init初始化nameidata进行“节点路径查找”;详情参看《节点路径搜索》
        3. 调用函数link_path_walk函数对路径名进行进行“节点路径查找”;详情参看《节点路径搜索》
        4. 调用do_last创建或者获取文件对应的inode对象,并且初始化file对象,至此一个表示打开文件的内存对象filp诞生。
代码片段
static struct file *path_openat(struct nameidata*nd,
conststruct open_flags *op, unsigned flags)
{
const char*s;
structfile *file;
int opened= 0;
int error;

file =get_empty_filp(); /* 分配文件描述符 */
if(IS_ERR(file))
returnfile;

file->f_flags= op->open_flag;

if(unlikely(file->f_flags & __O_TMPFILE)) {
error= do_tmpfile(nd, flags, op, file, &opened);
gotoout2;
}

s = path_init(nd,flags); /* 初始化nameidata进行“节点路径查找”*/
if(IS_ERR(s)) {
put_filp(file);
returnERR_CAST(s);
}
while(!(error = link_path_walk(s, nd)) &&/* 节点路径查找,结果记录在nd中*/
(error= do_last(nd, file, op, &opened)) > 0) {
/* 创建或者获取文件对应的inode对象,并且初始化file对象,至此一个表示打开文件的内存对象filp诞生 */
nd->flags&= ~(LOOKUP_OPEN|LOOKUP_CREATE|LOOKUP_EXCL);
s =trailing_symlink(nd);
if(IS_ERR(s)) {
error= PTR_ERR(s);
break;
}
}
terminate_walk(nd);
out2:
if(!(opened & FILE_OPENED)) {
BUG_ON(!error);
put_filp(file);
}
if(unlikely(error)) {
if(error == -EOPENSTALE) {
if(flags & LOOKUP_RCU)
error= -ECHILD;
else
error= -ESTALE;
}
file =ERR_PTR(error);
}
returnfile;
}
 

1.3.1  分配文件描述符

        分配一个全新的文件对象struct file结构体
        调用步骤do_sys_open-> do_filp_open-> path_openat-> get_empty_filp-> kmem_cache_zalloc
f= kmem_cache_zalloc(filp_cachep, GFP_KERNEL);
if(unlikely(!f))
returnERR_PTR(-ENOMEM);

【此处与内存管理slab分配器关联】
 

1.3.2  初始化file对象

        调用do_last创建或者获取文件对应的inode对象,并且初始化file对象,至此一个表示打开文件的内存对象filp诞生。
 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: