Linux 版本:Linux version 3.18.24
概要
在内核态进行文件读写,我们不能直接使用用户态的系统调用,而是需要使用一些特定的函数。
以下是一些常用的函数:filp_open、filp_close、vfs_read、vfs_write、set_fs、get_fs等函数。
函数声明
extern struct file *filp_open(const char *, int, umode_t);
参数说明:
第一个参数表明要打开或创建文件的名称(包括路径部分)。
第二个参数文件的打开方式,可以取O_CREAT,O_RDWR,O_RDONLY等。
第三个参数创建文件时使用,设置创建文件的读写权限,其它情况可以设为0
该函数返回strcut file*结构指针,供后继函数操作使用,该返回值用IS_ERR()来检验其有效性。
extern int filp_close(struct file *, fl_owner_t id);
参数说明:
第一个参数是filp_open返回的file结构体指针
第二个是参数POSIX线程,ID基本上都是NULL
extern ssize_t vfs_read(struct file *, char __user *, size_t, loff_t *);
extern ssize_t vfs_write(struct file *, const char __user *, size_t, loff_t *);
参数说明:
第一个参数是filp_open返回的file结构体指针
第二个参数是buf,注意,这个参数有用__user修饰,表明buf指向用户空间的地址,如果传入内核空间的地址,就会报错,并返回-EFAULT。
第三个参数表明文件要读写的起始位置。
但在kernel中,要使这两个读写函数使用kernel空间的buf指针才能正确工作,需要使用set_fs()
static inline void set_fs(mm_segment_t fs)
该函数的作用是改变kernel对内存地址检查的处理方式,
其实该函数的参数fs只有两个取值:USER_DS,KERNEL_DS,分别代表用户空间和内核空间,
默认情况下,kernel取值为USER_DS,即对用户空间地址检查并做变换。
那么要在这种对内存地址做检查变换的函数中使用内核空间地址,就需要使用set_fs(KERNEL_DS)进行设置,
它的作用是取得当前的设置,这两个函数的一般用法为:
filp_open()
mm_segment_t old_fs;
old_fs = get_fs();
set_fs(KERNEL_DS);
...... //与内存有关的操作
set_fs(old_fs);
filp_close
函数具体解析
filp_open —— 打开文件并返回文件指针
filp_open
-> file_open_name
-> build_open_flags // 使用传入的文件flag,初始化struct open_flags实例op
-> if (flags & (O_CREAT | __O_TMPFILE))
-> if (flags & __O_SYNC)
-> if (flags & O_CREAT)
......
-> do_filp_open
-> struct nameidata nd;
-> filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU); // 内核为了提高效率,会首先在RCU模式(rcu-walk)下进行文件打开操作
if (unlikely(filp == ERR_PTR(-ECHILD)))
filp = path_openat(dfd, pathname, &nd, op, flags); // 如果在此方式下打开失败,则进入普通模式(ref-walk)
if (unlikely(filp == ERR_PTR(-ESTALE)))
filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
真正的文件open工作,都在path_openat里面完成。在do_filp_open函数中,除了声明了要返回的file以外还声明了一个结构体nameidata,其作用是保存本次查找的结果。
enum { MAX_NESTED_LINKS = 8 };
struct nameidata {
struct path path; /* 当前搜索的目录 path里保存着dentry指针和挂载信息vfsmount */
struct qstr last; /* 下一个待处理的component。只有last_type是LAST_NORM时这个字段才有用*/
struct path root; /* 保存根目录的信息 */
struct inode *inode; /* path.dentry.d_inode */
unsigned int flags; /* 查找相关的标志位 */
unsigned seq; /* 目录项的顺序锁序号 */
int last_type; /* This is one of LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT, or LAST_BIND. */
unsigned depth; /* 解析符号链接过程中的递归深度 */
char *saved_names[MAX_NESTED_LINKS + 1]; /* 相应递归深度的符号链接的路径 */
};
do_filp_open中调用3次path_openat,下面分别分析下三次的path_openat。
第一次调用尝试以 rcu模式打开,当 flags = LOOKUP_FOLLOW | LOOKUP_RCU 会成功执行这个模式
path_openat
-> file = get_empty_filp(); // 初始化新的file结构,分配前会对当前进程的权限和当前系统的文件最大数进行检测
-> error = path_init(dfd, pathname->name, flags | LOOKUP_PARENT, nd, &base); //对路径遍历做准备工作,主要是判断路径遍历的起始位置
-> error = link_path_walk(pathname->name, nd); // 对所打开文件路径进行逐一解析,每个目录项的解析结果都存在nd参数中
-> error = do_last(nd, &path, file, op, &opened, pathname); // 根据最后一个目录项的结果,do_last()将填充filp所指向的file结构
-> while (unlikely(error > 0)) .... //filp为空,说明当前文件为符号链接文件
// 如果设置了LOOKUP_FOLLOW标志,则通过follow_link()进入符号链接文件所指文件,填充file
// 否则,直接返回当前符号链接文件的filp;
if (!(nd->flags & LOOKUP_FOLLOW)) {
path_put_conditional(&path, nd);
path_put(&nd->path);
filp = ERR_PTR(-ELOOP);
break;
}
error = follow_link(&link, nd, &cookie);
......
path_openat主要动作:
1、通过get_empty_filp初始化file
2、调用path_init固定路径查找点
3、调用link_path_walk遍历路径下的文件
4、使用do_last得到相应的file
path_init固定路径查找点
path_init
-> nd->last_type = LAST_ROOT; /* if there are only slashes... */
-> nd->flags = flags | LOOKUP_JUMPED;
-> nd->depth = 0;
-> nd->root.mnt = NULL;
-> nd->m_seq = read_seqbegin(&mount_lock);
-> if (*name=='/') { // 假如输入的路径为/sys/log
if (flags & LOOKUP_RCU) {
rcu_read_lock();
nd->seq = set_root_rcu(nd);
-> nd->root = fs->root; // 根目录
} else {
set_root(nd);
-> nd->root = fs->root; // 根目录
path_get(&nd->root);
}
nd->path = nd->root;
} else if (dfd == AT_FDCWD) { // 相对路径是以当前路径pwd作为起始的,因此通过pwd设置nd
......
} else { // 这个相对路径是用户设置的,需要通过dfd获取具体相对路径信息,进而设置nd
......
}
-> nd->inode = nd->path.dentry->d_inode;
path_init 主要是用来初始化struct nameidata实例中的path、root、inode等字段。
重点 link_path_walk遍历路径下的文件
static int link_path_walk(const char *name, struct nameidata *nd)
{
struct path next;
int err;
while (*name=='/') // 跳过开始的‘/’字符
name++;
if (!*name)
return 0;
/* At this point we know we have a real path component. */
for(;;) {
u64 hash_len;
int type;
err = may_lookup(nd);
if (err)
break;
hash_len = hash_name(name); // 获取下一个path component的hash和len,并复制给hash_len。
type = LAST_NORM;
if (name[0] == '.') switch (hashlen_len(hash_len)) {
case 2:
if (name[1] == '.') {
type = LAST_DOTDOT;
nd->flags |= LOOKUP_JUMPED;
}
break;
case 1:
type = LAST_DOT;
}
if (likely(type == LAST_NORM)) {
struct dentry *parent = nd->path.dentry;
nd->flags &= ~LOOKUP_JUMPED;
if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
struct qstr this = { { .hash_len = hash_len }, .name = name };
err = parent->d_op->d_hash(parent, &this);
if (err < 0)
break;
hash_len = this.hash_len;
name = this.name;
}
}
nd->last.hash_len = hash_len; // 将该path component的信息赋值给nd->last字段。
nd->last.name = name;
nd->last_type = type;
name += hashlen_len(hash_len); // 修改name的值,使其指向path的下一个component。
if (!*name)
return 0;
/*
* If it wasn't NUL, we know it was '/'. Skip that
* slash, and continue until no more slashes.
*/
do {
name++;
} while (unlikely(*name == '/'));
if (!*name) // 如果下一个component为空,则goto到OK这个label,执行一些操作之后,最后return 0给上层。
return 0;
//如果下一个component不为空,则执行walk_component方法,找到nd->last字段指向的component对应的dentry、inode等信息,并更新nd->path、nd->inode等字段,使其指向新的路径。
err = walk_component(nd, &next, LOOKUP_FOLLOW);
if (err < 0)
return err;
if (err) {
err = nested_symlink(&next, nd);
if (err)
return err;
}
if (!d_can_lookup(nd->path.dentry)) {
err = -ENOTDIR;
break;
}
}
terminate_walk(nd);
return err;
}
以open /sys/log为例,该方法最终的结果是,更新struct nameidata实例指针nd中的path、inode字段,使其指向路径/sys/,更新nd中的last值,使其为log。
do_last得到相应的file
do_last
-> error = lookup_fast(nd, path, &inode); // 找路径中的最后一个component
if (likely(!error))
goto finish_lookup;
-> finish_lookup:
error = step_into(nd, &path, 0, inode, seq);
...
error = vfs_open(&nd->path, file);
如果成功,就会跳到finish_lookup对应的label,然后执行step_into方法,更新nd中的path、inode等信息,使其指向目标路径。
之后,调用vfs_open方法,继续执行open操作。
最后,返回error给上层,如果成功,error为0。
filp_close —— 内核空间的文件关闭操作
内核中的文件如果不在使用,需要将文件进行关闭,释放其中的资源。Linux内核中关闭文件的函数为filp_close()
int filp_close(struct file *filp, fl_owner_t id)
{
int retval = 0;
if (!file_count(filp)) {
printk(KERN_ERR "VFS: Close: file count is 0\n");
return 0;
}
if (filp->f_op->flush)
retval = filp->f_op->flush(filp, id);
if (likely(!(filp->f_mode & FMODE_PATH))) {
dnotify_flush(filp, id);
locks_remove_posix(filp, id);
}
fput(filp);
return retval;
}
vfs_read/write —— 读写对应文件内容
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_READ)) //判断文件是否可读
return -EBADF;
if (!(file->f_mode & FMODE_CAN_READ)) //是否定义文件读方法
return -EINVAL;
if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
return -EFAULT;
ret = rw_verify_area(READ, file, pos, count); //读校验
if (ret >= 0) {
count = ret;
if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos); //调用文件读操作方法
else if (file->f_op->aio_read)
ret = do_sync_read(file, buf, count, pos); //通用文件模型读方法
else
ret = new_sync_read(file, buf, count, pos);
if (ret > 0) {
fsnotify_access(file);
add_rchar(current, ret);
}
inc_syscr(current);
}
return ret;
}
如果该文件索引节点inode定义了文件的读实现方法的话,就调用此方法. Linux下特殊文件读往往是用此方法, 一些伪文件系统如:proc,sysfs等,读写文件也是用此方法 . 而如果没有定义此方法就会调用通用文件模型的读写方法.它最终就是读内存,或者需要从存储介质中去读数据。
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_WRITE)) //判断文件是否可写
return -EBADF;
if (!(file->f_mode & FMODE_CAN_WRITE)) //是否定义文件写方法
return -EINVAL;
if (unlikely(!access_ok(VERIFY_READ, buf, count)))
return -EFAULT;
ret = rw_verify_area(WRITE, file, pos, count); //写校验
if (ret >= 0) {
count = ret;
file_start_write(file);
if (file->f_op->write)
ret = file->f_op->write(file, buf, count, pos); //调用文件写操作方法
else if (file->f_op->aio_write)
ret = do_sync_write(file, buf, count, pos); //通用文件模型写方法
else
ret = new_sync_write(file, buf, count, pos);
if (ret > 0) {
fsnotify_modify(file);
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file);
}
return ret;
}
这个函数和vfs_read()都是差不多的,只是调用的文件操作方法不同而已(file->f_op->write) ,如果没有定义file->f_op->write ,同样也需要do_sync_write()调用同样文件写操作, 首先把数据写到内存中,然后在适当的时候把数据同步到具体的存储介质中去。