Linux内核学习之 -- 系统调用open()和write()的实现笔记
目录
- 环境
- 1.open()系统调用。
- 1.1 getname()
- 1.2 get_unused_fd_flags()
- 1.3 do_filp_open()
- 1.4 fd_install()
- 2. write()系统调用
环境
- linux 4.19
很多注释都写在了代码里,就不单独总结出来放到文章中了,看一下代码+注释回忆一下即可
1.open()系统调用。
用户空间使用open(),最终调用到内核的函数如下,至于系统调用的过程,请看我的另外一篇博客:Linux内核学习之 – ARMv8架构的系统调用
fs/open.c:
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode) // g,三个参数为filename, flags, mode
{
if (force_o_largefile()) // g, (BITS_PER_LONG != 32),其中BITS_PER_LONG应该是定义long类型的大小,32位架构long就是32,64位架构long是64
flags |= O_LARGEFILE; // g, 如果64位架构,补上O_LARGEFILE标志
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
只调用了一个函数,就是do_sys_open(),进一步分析do_sys_open()函数:
fs/open.c:
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_flags op; // g,strcut open_flags这个结构体会包含一些控制flag,根据用户传入的参数进行初始化,之后open的操作就不再依赖于用户参数,而是依赖于这个结构体
int fd = build_open_flags(flags, mode, &op); // g, 根据传入的flagse会mode,来初始化op结构体,open的模式,flag等都在open_flags op结构体中
struct filename *tmp; // g, struct filename会在open过程中记录处理过后的文件名信息。之后open对h文件名的获取依赖于这个结构体
if (fd)
return fd;
// g, 根据一个字符串指针,返回一个struct filename*,该结构体的->name域所指内存存储着文件名
tmp = getname(filename);
if (IS_ERR(tmp))
return PTR_ERR(tmp);
fd = get_unused_fd_flags(flags); // g, 分配一个没用过的,符合要求的fd号,同时更新current->files->fdt里面的三个重要bitmap,有必要的话会分配新的占用更多内存的fdt替换原来的fdt
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op); // g, 该函数解析传入的文件路径名(主要是获取路径的最后一个分量对应的dentry以及inode),然后创建一个新的struct file结构体,使用找到的dentry和inode初始化这个file
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fsnotify_open(f); // g, 通知其他相关项如事件通知、audit机制、文件系统监控系统等等,该文件已经打开
fd_install(fd, f); // g, 把file和fd关联起来,建立了current(当前进程的tast_struct)->files->fdt->fd[fd]与f的联系(fdt->fd是个struct file**指针,其实就是current fd array)
}
}
putname(tmp);
return fd;
}
在这里做一个总结,do_sys_open()实现的功能可以概括为:
- 解析用户传入的各种参数,包括模式,权限,文件名
- 分配一个fd号
- 根据解析的参数,创建并初始化一个struct file结构体。
- 建立fd号与创建的struct file的联系,后续的write(),read()等可以直接通过fd找到对应的struct file
接下来对其中的几个重要函数进行分析,在此之前先整理一个调用层次:
do_sys_open()
-->build_open_flags() // 解析用户参数
-->getname() // 解析用户传入的文件名
-->getname_flags()
-->get_unused_fd_flags() // 获取一个未使用过的fd
-->__alloc_fd()
-->do_filp_open() // 打开文件,创建并初始化一个新的struct file结构体
-->set_nameidata()
-->path_openat()
-->path_init()
-->link_path_walk()
-->do_last()
-->fsnotify_open() // 通知其他相关项如事件通知、audit机制、文件系统监控系统等等,该文件已经打开
-->fd_install() // 建立分配的fd号与创建的struct file的关系
-->__fd_install()
所有函数调用并没有全部展示,只是列出了open()系统调用中几个重要的函数
1.1 getname()
该函数用来转换用户传入的路径名,解析用户传入的路径名来填充为一个内核中的结构体struct filename:
fs/namei.c:
getname(const char __user * filename)
{
return getname_flags(filename, 0, NULL);
}
其中只是单纯的调用了函数getname_flags():
fs/namei.c:
struct filename *
getname_flags(const char __user *filename, int flags, int *empty)
{
struct filename *result;
char *kname;
int len;
BUILD_BUG_ON(offsetof(struct filename, iname) % sizeof(long) != 0);
// g, 与linux的审计机制有关,会记录一些自己设置的规则信息,如果开启了此功能就可以从此模块获取filename结构体。
// g, 需要开启一个宏CONFIG_AUDITSYSCALL才有效果,看了下.config中CONFIG_AUDITSYSCALL is not set,所以暂时不去研究这个审计模块
result = audit_reusename(filename);
if (result)
return result;
result = __getname(); // g, 是个宏,= kmem_cache_alloc(names_cachep, GFP_KERNEL),该函数会从names_cachep的slab分配器里分配一个struct filename出来
if (unlikely(!result))
return ERR_PTR(-ENOMEM);
/*
* First, try to embed the struct filename inside the names_cache
* allocation
*/
kname = (char *)result->iname;
result->name = kname; // g, result->name和result->iname[]都将指向同一个char
len = strncpy_from_user(kname, filename, EMBEDDED_NAME_MAX); // g, 应该跟copy_from_user大同小异,是一个用户空间和内存空间的字符串拷贝函数,最大复制的字符数不能超过EMBEDDED_NAME_MAX
if (unlikely(len < 0)) {
__putname(result); // g, 如果有问题,直接释放这个slab
return ERR_PTR(len);
}
/* -------------------- 分割线,上面是文件名不太大的情况,下面是当文件名超过了EMBEDDED_NAME_MAX = 4096 - offsetof(struct filename, iname) 之后的操作
* Uh-oh. We have a name that's approaching PATH_MAX. Allocate a
* separate struct filename so we can dedicate the entire
* names_cache allocation for the pathname, and re-do the copy from
* userland.
*/
// g, note: likely和unlikely也有点说法,可以优化if和else的执行顺序
if (unlikely(len == EMBEDDED_NAME_MAX)) { // g, 如果文件名字符串很长,已经 >= EMBEDDED_NAME_MAX了
// g, offsetof:返回iname[]在filenamed中的偏移值
const size_t size = offsetof(struct filename, iname[1]);
kname = (char *)result; // g, 使kname当temp指针记录一下result,暂时称为result_old
/*
* size is chosen that way we to guarantee that
* result->iname[0] is within the same object and that
* kname can't be equal to result->iname, no matter what.
*/
result = kzalloc(size, GFP_KERNEL); // g,分配一段&(struct filename.iname[1]) - &(struct filemae)这么大小的内存,此时result指向新的内存空间了,暂时称为result_new
if (unlikely(!result)) {
__putname(kname);
return ERR_PTR(-ENOMEM);
}
result->name = kname; // g,相当于result_new->name = result_old
len = strncpy_from_user(kname, filename, PATH_MAX); // g, 然后覆盖result_old,相当于把原来从slab中分配的内存全部当做文件名的缓存区了,result_old不再是一个struct filename结构体,而是变成了一段内存
if (unlikely(len < 0)) {
__putname(kname);
kfree(result);
return ERR_PTR(len);
}
if (unlikely(len == PATH_MAX)) { // g, 文件名
__putname(kname);
kfree(result);
return ERR_PTR(-ENAMETOOLONG);
}
}
result->refcnt = 1; // g,可能是个标记位置
/* The empty path is special. */
if (unlikely(!len)) {
if (empty)
*empty = 1;
if (!(flags & LOOKUP_EMPTY)) {
putname(result);
return ERR_PTR(-ENOENT);
}
}
result->uptr = filename; // g,保存上层传过来的指针
result->aname = NULL;
audit_getname(result);
return result;
}
该函数会把用户空间的文件名内存copy到内核内存中。这片内核内存可能存在于在struct filename结构体内部,也可能是一段单独分配的内核内存,也就是会内嵌或者外挂一段内存来存放文件路径名。 对于这一段代码,解释如下:
因为strcut filename中最后一个域->iname是一个空数组iname[],所以可以存放一定长度的文件名,如果文件名不长,则文件名所占用的内存完全可以由一个struct filename消化掉。
但是struct filenamed最后总size不能超过4096,如果sizeof(struct filename) + 文件名长度不超过4096,那就用iname[]来存储文件名,然后使filename->name指向filename->iname[]
如果文件名比较长,文件名大小+sizeof(struct filename)超过了4096,就不用iname[]来存储,需要另外申请一段内存来存储文件名,然后使filename->name指向另外申请的内存段
有4096大小的限制是因为struct filename是从slab中分配的,分配通过一个宏:
include/linux/fs.h
#define __getname() kmem_cache_alloc(names_cachep, GFP_KERNEL)
在vfs初始化的时候,调用vfs_caches_init()创建这个slab时是以4096字节创建的node:
fs/dcache.c:
void __init vfs_caches_init(void)
{
...
names_cachep = kmem_cache_create_usercopy("names_cache", PATH_MAX, 0, // g, PATH_MAX = 4096
SLAB_HWCACHE_ALIGN|SLAB_PANIC, 0, PATH_MAX, NULL);
...
}
1.2 get_unused_fd_flags()
该函数用于根据一些规则,获取一个未使用过的fd号:
int get_unused_fd_flags(unsigned flags)
{
// g, RLIMIT_NOFILE作为一个数组的索引号:rlimit(RLIMIT_NOFILE) = current->signal->rlim[RLIMIT_NOFILE].rlim_cur,
// g, rlim[7]存放着最大文件数限制,默认为1024
return __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags); // g,传入task_struct的files域,即struct files_struct,保存与该进程有关的文件信息,比如保存着打开的文件/文件描述符等等
}
其中单纯的调用了__alloc_fd()函数,并且设置了进程可以打开的最大文件数的限制,默认为1024。__alloc_fd()的实现如下:
int __alloc_fd(struct files_struct *files, // g,传入task_struct的fils域,即struct files_struct,保存与该进程有关的文件信息,比如保存着打开的文件/文件描述符等等
unsigned start, unsigned end, unsigned flags)
{
unsigned int fd;
int error;
struct fdtable *fdt;
spin_lock(&files->file_lock); // g, 本进程files->file_lock上锁
repeat:
fdt = files_fdtable(files); // g, 找到task_struct->files域中的fdtable,sh起就是直接获取(files)->fdt,但是这个宏会检查files->file_lock是否锁住了
fd = start;
if (fd < files->next_fd) // g, files->next_fd缓存着下一个可用的fd,至少从->next_fd起才可用,传入的start不一定有效
fd = files->next_fd;
if (fd < fdt->max_fds) // g, 如果fd没有超过最大打开文件数量限制,current->files->fdt在初始化的时候会设置max_fdset = 1024(最大打开的文件数),max_fds初始化 = 32。max_fds会动态变化
fd = find_next_fd(fdt, fd); // g, 共两级bitmap查找能用的fd,第一级map一个bit表示64个文件,第二级一个bit表示一个文件,先找到第一级的空bit,再去找对应的第二级bitmap
/*
* N.B. For clone tasks sharing a files structure, this test
* will limit the total number of files that can be opened.
*/
error = -EMFILE;
if (fd >= end) // g,这个end完全看调用方传入,不一定是最大打开文件数量限制(默认1024),你想传入多少传入多少,不过open系统调用传入的是1024
goto out;
// g, 关于这个max_fds有必要说两点,这个max_fds,表示了当前fdt可以容纳的数量,会动态分配,否则浪费内存。比如说初始化为32,当fd>=32时会再加一点,然后再加一点
// g, 主要通过三个函数:
// g, 1.new_fdt = alloc_fdtable(fd)--申请足以表示fd个文件描述符的内存空间;2.copy_fdtable(new_fdt, cur_fdt)--文件描述符信息拷贝;3.rcu_assign_pointer(files->fdt, new_fdt)--文件描述符表指针更新
error = expand_files(files, fd);
if (error < 0)
goto out;
/*
* If we needed to expand the fs array we
* might have blocked - try again.
*/
if (error)
goto repeat; // g, 扩展fdt_table成功,重新到repeat处,重新分配fd(因为刚才没扩展之前,fd没有 < fdt->max_fds,跳过了fd的分配)
if (start <= files->next_fd)
files->next_fd = fd + 1; // g, 更新缓存域,而且就是单纯的递增,表示下一个可以用的fd至少是刚分配的fd + 1
__set_open_fd(fd, fdt); // g, 更新fdt表,是bitmap实现的,会使用__set_bit来更新fdt->open_fds域和fdt->full_fds_bits域,即一级bitmap和二级bitmap,二级bitmap没满64个才会更新一个一级bitmap
if (flags & O_CLOEXEC) // g, 这个标志使的新进程创建时在继承父进程的文件描述符表时,会关闭父进程打开过的文件
__set_close_on_exec(fd, fdt); // g, 通过current->files->fdt->close_on_exec,也是一个位图,记录在exec时要关闭的fd
else
__clear_close_on_exec(fd, fdt);
error = fd;
#if 1
/* Sanity check */
if (rcu_access_pointer(fdt->fd[fd]) != NULL) {
printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
rcu_assign_pointer(fdt->fd[fd], NULL);
}
#endif
out:
spin_unlock(&files->file_lock);
return error;
}
该函数主要就是对分配的fd是否满足要求进行检查(是否大于最大可分配fd,是否大于当前进程允许分配的fd等等),检查通过后进行分配。
分配主要依赖于一个二级bitmap结构,第一级为粗粒度bitmap数组,一个bit可以表示64个fd,第二级bitmap数组为细粒度,一个bit表示一个fd。
同时会缓存下一个可用的fd,从实现中可以看到,fd的分配顺序是递增的。上一次分配了n,则下一次就会分配n+1。
1.3 do_filp_open()
该函数是open()系统调用中作用最大的函数,大部分工作都在这个函数中完成。该函数负责解析传入的文件路径名(主要是获取路径的最后一个分量对应的dentry以及inode),然后创建一个新的struct file结构体,使用找到的dentry和inode初始化这个新创建的struct file:
struct file *do_filp_open(int dfd, struct filename *pathname,
const struct open_flags *op)
{
struct nameidata nd; // nameidata类型的nd在整个路径查找过程中充当中间变量,它既可以为当前查找输入数据,又可以保存本次查找的结果。但是看上去该函数执行完后nd会被释放
int flags = op->lookup_flags; // g, op->lookup_flags可能有三种:LOOKUP_DIRECTORY(对应用户传参O_DIRECTORY),LOOKUP_FOLLOW(对应用户传参O_NOFOLLOW),0
struct file *filp;
set_nameidata(&nd, dfd, pathname); // g, 初始化新的nameidata,并且使current->nameidata(新)->save = current->nameidata(旧), current->nameidata(新) = nd, 。dfd默认-100
// g, path_openat有可能会被调用三次。通常内核为了提高效率,会首先在RCU模式下进行文件打开操作
// g, 该函数会分配一个struct file结构体,并且初始化这个新的struct file结构体,过程比较复杂
filp = path_openat(&nd, op, flags | LOOKUP_RCU);
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);
restore_nameidata();
return filp;
}
其中struct nameidata作为一个解析路径的辅助结构体,协助path_openat()函数进行路径解析。path_openat()有可能会被调用三次。通常内核为了提高效率,会首先在RCU模式下进行文件打开操作。而path_openat()函数则会创建并返回一个struct file,并且已经为其完成了填充工作:
static struct file *path_openat(struct nameidata *nd,
const struct open_flags *op, unsigned flags)
{
struct file *file; // g, 虽然是要返回一个file结构体,但是整个过程其实是围绕nameidata *nd进行的,可以认为是open上下文中比较重要的一个结构体
int error;
file = alloc_empty_file(op->open_flag, current_cred()); // g, 分配file结构体,并且会检查一些文件最大数之类的,检查通过才会分配。分配方式是slab,并初始化了部分成员变量
if (IS_ERR(file))
return file;
// g, 接下来根据file->f_flags执行不同的操作,file->f_flags在alloc_empty_file中被初始化为op->open_flag,op->open_flag根据用户传入的flag和mode初始化的
if (unlikely(file->f_flags & __O_TMPFILE)) { // g, __O_TMPFILE表示匿名文件,文件描述符关闭文件自动删除
error = do_tmpfile(nd, flags, op, file); // g, 匿名文件指的是:能够像普通的文件一样进行读写,但是不会再磁盘上创建对应的文件(磁盘上找不到对应的文件名),就是匿名inode。
} else if (unlikely(file->f_flags & O_PATH)) { // g, O_PATH不会真正打开一个文件,只是准备好该文件的文件描述符,也就是说O_PATH表示仅仅获取一个fd,没有真正打开文件
error = do_o_path(nd, flags, file);
} else {
// g, 大部分open应该都会走到这里,上面那俩flag不常用。path_init函数做了两个工作:
// 1.获取要打开的文件路径(早已储存在nd->name中)
// 2.初始化nd的一些重要成员,若s为相对路径:nd->path = current->fs->pwd,nd->inode = current->fs->pwd.dentry->d_inode;因为fs->pwd保存着进程当前所处的工作目录
// 若s为绝对路径:nd->root = fs->root
// 然后接下来的路径搜索,就会以此为起点(nd->path)
const char *s = path_init(nd, flags);
// g, 关于这个do_last函数,是设置struct file* file的主要函数,最终会将file->path.dentry指向link_path_walk()解析出的最后一个分量的dentry,而且会找到该dentry指向的inode,使file->inode = 这个inode
// 这样就实现了struct file与struct dentry + struct inode的关联。解析过程较为复杂。
// g, 其中有一步关键函数:vfs_open()->do_entry_open():
// 1.如果找到的inode->file_operations不为空,则会设置这个file->file_operations等于inode->file_operations。
// 2.调用一次inode->file_operations->open(),所以说在用户空间open一个设备驱动,最终能调用到驱动绑定的file_ops->open。
while (!(error = link_path_walk(s, nd)) && // g, 是解析路径的主要函数,用于将pathname转换成最终该文件对应的的dentry,也就是路径最后一个分量对应的dentry。并存储于nd->path.dentry中,交由do_last()进行处理
(error = do_last(nd, file, op)) > 0) // g, open的最后一步。创建(对于一个新的文件来说)或者获取文件对应的inode对象(通过上一步得到的dentry来获取对应的inode),并且初始化file对象
{
nd->flags &= ~(LOOKUP_OPEN|LOOKUP_CREATE|LOOKUP_EXCL);
s = trailing_symlink(nd); // g, 这里面会设置nd->flags |= LOOKUP_PARENT,检查当前打开的文件是否是符号链接,以及是否有权限来得到符号链接的真实链接
}
terminate_walk(nd);
}
if (likely(!error)) { // g, 如果到这里没啥问题
if (likely(file->f_mode & FMODE_OPENED)) // g, 并且已经打开成功
return file; // g, 可以顺利返回了
WARN_ON(1);
error = -EINVAL;
}
fput(file);
if (error == -EOPENSTALE) {
if (flags & LOOKUP_RCU)
error = -ECHILD;
else
error = -ESTALE;
}
return ERR_PTR(error);
}
该函数中首先调用了path_init()函数,该函数决定了当前的路径解析会以哪个路径作为起点:
static const char *path_init(struct nameidata *nd, unsigned flags)
{
const char *s = nd->name->name; // g, nameidata->name已经在初始化的时候设置为了filename->name,实际上也就是用户态传来的路径名
if (!*s)
flags &= ~LOOKUP_RCU;
if (flags & LOOKUP_RCU)
rcu_read_lock();
nd->last_type = LAST_ROOT; /* if there are only slashes... */
nd->flags = flags | LOOKUP_JUMPED | LOOKUP_PARENT;
nd->depth = 0;
if (flags & LOOKUP_ROOT) { // g, note: 没见过有这个flags哈,所以这个if应该是不执行的,具体这个flag代表什么意思后续可以研究下
struct dentry *root = nd->root.dentry;
struct inode *inode = root->d_inode;
if (*s && unlikely(!d_can_lookup(root)))
return ERR_PTR(-ENOTDIR);
nd->path = nd->root;
nd->inode = inode;
if (flags & LOOKUP_RCU) {
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
nd->root_seq = nd->seq;
nd->m_seq = read_seqbegin(&mount_lock);
} else {
path_get(&nd->path);
}
return s;
}
nd->root.mnt = NULL;
nd->path.mnt = NULL;
nd->path.dentry = NULL; // g, 如果没有LOOK_UP flags,暂时把目录结构体设置为null
nd->m_seq = read_seqbegin(&mount_lock);
if (*s == '/') { // g, '/'起手,说明传入的是绝对路径
set_root(nd);
if (likely(!nd_jump_root(nd)))
return s;
return ERR_PTR(-ECHILD);
} else if (nd->dfd == AT_FDCWD) { // g, 这个if是要执行的,因为dfd在系统调用的第一步就被初始化为了AT_FDCWD,该flag表示这个相对路径是以当前路径pwd作为起始的
if (flags & LOOKUP_RCU) { // g, 如果使用RCU锁
struct fs_struct *fs = current->fs; // g, 获取当前task_strcut的fs域,该域包含了文件系统相关的内容,比如当前正在执行的文件,当前工作目录(pwd),根目录等
unsigned seq; // g, note: seq是和顺序读取/顺序锁有关的域,早期版本的linux内核中fs_struct中是没有这个域的,什么作用后续再看
do {
seq = read_seqcount_begin(&fs->seq);
nd->path = fs->pwd; // g, pwd域即为当前工作目录,是一个struct path结构体,里面记录了该路径对应的目录项dentry结构体
nd->inode = nd->path.dentry->d_inode; // g, 算是拐弯抹角的获取到了对应的inode了,其实也就是current->fs->pwd.dentry->d_inode
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
} while (read_seqcount_retry(&fs->seq, seq)); // g, note: 关于顺序锁,不是很熟悉,后续再研究一下
} else {
get_fs_pwd(current->fs, &nd->path);
nd->inode = nd->path.dentry->d_inode;
}
return s; // g, 到这里应该就返回了
} else {
/* Caller must check execute permissions on the starting path component */
struct fd f = fdget_raw(nd->dfd);
struct dentry *dentry;
if (!f.file)
return ERR_PTR(-EBADF);
dentry = f.file->f_path.dentry;
if (*s && unlikely(!d_can_lookup(dentry))) {
fdput(f);
return ERR_PTR(-ENOTDIR);
}
nd->path = f.file->f_path;
if (flags & LOOKUP_RCU) {
nd->inode = nd->path.dentry->d_inode;
nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
} else {
path_get(&nd->path);
nd->inode = nd->path.dentry->d_inode;
}
fdput(f);
return s;
}
}
之后便是解析路径的主要函数link_path_walk(),该函数比较复杂, 是解析路径的主要函数,用于将pathname转换成最终该文件对应的的dentry,也就是路径最后一个分量对应的dentry。并存储于nd->path.dentry中,交由do_last()进行处理。对其实现方式进行了简单的分析:
fs/namei.c:
static int link_path_walk(const char *name, struct nameidata *nd) // g, name:用户传入的路径
{
int err;
if (IS_ERR(name))
return PTR_ERR(name);
while (*name=='/') // g, 如果是根目录的话跳过根目录
name++;
if (!*name)
return 0;
/* At this point we know we have a real path component. */
// g, 通过循环,将用户所指定的路径name从头至尾进行了搜索,至此nd保存了最后一个目录项的信息(i比如./abc/a/b,最后会保存b这个目录项的信息),但是内核并没有确定最后一个目录项是否真的存在,这些工作将在do_last()中进行
for(;;) {
u64 hash_len;
int type;
err = may_lookup(nd); // g, 检查存放的索引节点的访问模式和运行进程的权限,就是检查当前进程对nd中保存的inode的各种权限。
if (err)
return err;
hash_len = hash_name(nd->path.dentry, name); // g, 在用nd->path.dentry和name来计算一个hash值。这是文件系统维护的通过散列表搜索dentry的方式
type = LAST_NORM;
if (name[0] == '.') switch (hashlen_len(hash_len)) { // g, 如果传入的路径第一个是'.'(意味着是相对路径)
case 2:
if (name[1] == '.') {
type = LAST_DOTDOT; // g, 相对路径情况1:上一层路径
nd->flags |= LOOKUP_JUMPED; // g, 设置一层LOOKUP_JUMPED flag
}
break; // g, 如果是..,则需要跳出循环,尝试返回父目录,但是好像没有return啊?这搞集贸啊
case 1:
type = LAST_DOT; // g, 相对路径情况2:本层路径
}
if (likely(type == LAST_NORM)) { // g, 如果是正常分量,不是'.'或者'..'
struct dentry *parent = nd->path.dentry;
nd->flags &= ~LOOKUP_JUMPED; // g, 清空LOOKUP_JUMPED标志
if (unlikely(parent->d_flags & DCACHE_OP_HASH)) { // g,warn: 如果d_flags里面没有DCACHE_OP_HASH标志,个人猜测就是没加入到散列表缓存中,所以需要加进去
struct qstr this = { { .hash_len = hash_len }, .name = name }; // g, 其实struct dentry的d_name域就是一个struct qstr结构体,里面记录着目录项的名字
err = parent->d_op->d_hash(parent, &this); // g, d_hash()一般是由文件系统来实现,作用是把目标dentry加入到散列表中,这样后面用d_lookup()查找散列表时可以快速地返回对应的dentry结构体,省时省力
if (err < 0)
return err;
hash_len = this.hash_len;
name = this.name;
}
}
// g, 保存一下当前的分量的各个属性,因为要遍历到下一个了
nd->last.hash_len = hash_len; // g, 保存一下当前分量计算得到的hash值
nd->last.name = name; // g, 保存一下当前name
nd->last_type = type; // g, 保存一下当前分量的hash值
name += hashlen_len(hash_len); // g, 跳过当前分量,现在name指向了下一个分量,比如/a/b/c,循环过程就是/a/b/c -> /b/c - >/c
if (!*name)
goto OK;
/*
* If it wasn't NUL, we know it was '/'. Skip that
* slash, and continue until no more slashes.
*/
do {
name++;
} while (unlikely(*name == '/')); // g, 依然是跳过'/',找下一级分量
if (unlikely(!*name)) {
OK:
/* pathname body, done */
if (!nd->depth)
return 0;
name = nd->stack[nd->depth - 1].name; // g, nd还维护了一个name栈,就是在解析路径时的每一个分量吧
/* trailing symlink, done */
if (!name)
return 0;
/* last component of nested symlink */
// g, note:没太看懂这个函数,涉及的太多了,看别人介绍的:walk_component()处理当前目录项,更新nd和next;如果当前目录项为符号链接文件,则只更新next
err = walk_component(nd, WALK_FOLLOW); // g, 主要就是不断查找路径是否正确。还是以/a/b/c为例,每次循环该函数都会查找:/下是否有a,/a下是否有b, /a/b下是否有c
} else {
/* not the last component */
err = walk_component(nd, WALK_FOLLOW | WALK_MORE);
}
if (err < 0)
return err;
if (err) {
const char *s = get_link(nd);
if (IS_ERR(s))
return PTR_ERR(s);
err = 0;
if (unlikely(!s)) {
/* jumped */
put_link(nd);
} else {
nd->stack[nd->depth - 1].name = name;
name = s;
continue;
}
}
if (unlikely(!d_can_lookup(nd->path.dentry))) {
if (nd->flags & LOOKUP_RCU) {
if (unlazy_walk(nd))
return -ECHILD;
}
return -ENOTDIR;
}
}
}
其中最重要的函数是walk_component():
fs/namei.c:
static int walk_component(struct nameidata *nd, int flags)
{
struct path path;
struct inode *inode;
unsigned seq;
int err;
/*
* "." and ".." are special - ".." especially so because it has
* to be able to know about the current root directory and
* parent relationships.
*/
if (unlikely(nd->last_type != LAST_NORM)) { // g, 如果刚才的分量不是NORMAL,那就说明是"."或者"..",那就要单独处理了
err = handle_dots(nd, nd->last_type);
if (!(flags & WALK_MORE) && nd->depth)
put_link(nd);
return err;
}
err = lookup_fast(nd, &path, &inode, &seq); // g, lookup_fast查询dentry中的缓存,看一下是否命中,如果没有命中,则会用lookup_slow下降到文件系统层进行路径查找。
if (unlikely(err <= 0)) {
if (err < 0)
return err;
path.dentry = lookup_slow(&nd->last, nd->path.dentry,
nd->flags);
if (IS_ERR(path.dentry))
return PTR_ERR(path.dentry);
path.mnt = nd->path.mnt;
err = follow_managed(&path, nd); // g, follow_managed函数会检查当前dentry是否是个挂载点,如果是就跟下去
if (unlikely(err < 0))
return err;
if (unlikely(d_is_negative(path.dentry))) {
path_to_nameidata(&path, nd); // g, 至此,如果当前目录项查找成功,则通过path_to_nameidata()更新nd,更新nd->path.dentry = path->dentry
return -ENOENT;
}
seq = 0; /* we are already out of RCU mode */
inode = d_backing_inode(path.dentry);
}
return step_into(nd, &path, flags, inode, seq); // g, step_into进行下一级目录的解析。
}
过于复杂,没有深入分析。该函数就是来解析目录是否存在的。这些文章对该函数进行了比较深入的分析,后续有时间可以再研究一下:
- linux虚拟文件系统-文件的打开
- Linux open系统调用(四)
- LINUX VFS分析之五open接口分析与总结
解析完整个文件路径后,会保存最后一个分量的dentry到nd->path.dentry,接下来就交由do_last()函数做最后的处理。
关于这个do_last函数,是设置struct file* file的主要函数,最终会将file->path.dentry指向link_path_walk()解析出的最后一个分量的dentry,如果最后一个分量的类型是NORMAL,也就是说是正常文件而不是".“或者”…",就会找到该dentry指向的inode。如果inode不存在,并且传入了O_CREATE标志,则会创建一个inode。使file->inode指向这个inode。这样就实现了struct file与struct dentry + struct inode的关联。
这个过程较为复杂,能力不够,没法深入去了解。后面有时间再研究一下。
但是其中有一步关键函数:vfs_open()->do_entry_open()。就是如果找到的inode->file_operations不为空,则会设置这个file->file_operations等于inode->file_operations。
然后会调用一次inode->file_operations->open(),所以说在用户空间open一个设备驱动,最终能调用到驱动绑定的file_ops->open()。
当做完这一切之后,如果成功,返回成功结果,也就是返回新创建并初始化完的struct file
1.4 fd_install()
该函数单一的调用了__fd_install()函数
fs/file.c:
void fd_install(unsigned int fd, struct file *file)
{
__fd_install(current->files, fd, file);
}
__fd_install()函数的实现就比较简单,就是把file和fd关联起来,建立了current(当前进程的tast_struct)->files->fdt->fd[fd]与f的联系(fdt->fd是个struct file**指针,其实就是current fd array):
void __fd_install(struct files_struct *files, unsigned int fd,
struct file *file)
{
struct fdtable *fdt;
rcu_read_lock_sched();
if (unlikely(files->resize_in_progress)) {
rcu_read_unlock_sched();
spin_lock(&files->file_lock);
fdt = files_fdtable(files);
BUG_ON(fdt->fd[fd] != NULL);
rcu_assign_pointer(fdt->fd[fd], file); // g, 建立fdt->fd数组中index为fd的位置与file结构体的对应关系
spin_unlock(&files->file_lock);
return;
}
/* 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);
rcu_read_unlock_sched();
}
这样用户空间的write()/read()等函数,就可以根据fd号,找到对应的struct file了。
2. write()系统调用
与open()相比,write()就非常简单了:
fs/read_write.c:
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
loff_t *pos)
{
if (file->f_op->write)
return file->f_op->write(file, p, count, pos);
else if (file->f_op->write_iter)
return new_sync_write(file, p, count, pos);
else
return -EINVAL;
}
...
...
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
// g, 这里做权限检查,查一下是否有读写权限。file->f_mod应该是在open的时候创建struct fd的时候就初始化了
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); // g, 在确认文件可读写的区域
if (!ret) {
if (count > MAX_RW_COUNT) // g, 最大写入量有限制
count = MAX_RW_COUNT;
file_start_write(file);
ret = __vfs_write(file, buf, count, pos);
if (ret > 0) {
fsnotify_modify(file);
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file);
}
return ret;
}
...
...
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
// g, 最终也是通过fd找到current->files->fdt->fd[fd],然后把struct file强行转为struct fd
struct fd f = fdget_pos(fd); // g, 根据文件描述符fd获取结构体类型struct fd,里面包含两个域:struct file域(其中的f_path域包含struct dentry,也就是该fd对应的目录项)和uint flags域
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file); // g, 读取当前f_pos
ret = vfs_write(f.file, buf, count, &pos); // g, 调用虚拟文件系统提供的write
if (ret >= 0)
file_pos_write(f.file, pos); // g, 重新记录f_pos,光标偏移
fdput_pos(f);
}
return ret;
}
...
...
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
return ksys_write(fd, buf, count); // g,这就是write最终的系统调用了
}
不过多解释,对于字符设备来说最终也是调用到设备的inode实现的write,也就是字符设备的write。对于正常文件,则是与相应的文件系统有关,这个后面需要单独写一篇文章整理一下。