分类: LINUX
2014-07-28 10:17:52
原文地址:Linux VFS中read系统调用实现原理 作者:up哥小号
目录
Read系统调用在内核里面的入口函数为sys_read. 1
根据用户空间传入的文件描述符fd取出对应的struct file结构体... 2
获取struct file 结构体的当前偏移量指针... 2
WORD里面的目录复制过来似乎不能直接用。。还是放在这里当主线看吧..
grep read /usr/include/asm/unistd_64.h
#define __NR_read 0
__SYSCALL(__NR_read, sys_read)
#define __NR_pread64 17
__SYSCALL(__NR_pread64, sys_pread64)
#define __NR_readv 19
__SYSCALL(__NR_readv, sys_readv)
#define __NR_readlink 89
__SYSCALL(__NR_readlink, sys_readlink)
#define __NR_readahead 187
__SYSCALL(__NR_readahead, sys_readahead)
#define __NR_set_thread_area 205
__SYSCALL(__NR_set_thread_area, sys_ni_syscall) /* use arch_prctl */
#define __NR_get_thread_area 211
__SYSCALL(__NR_get_thread_area, sys_ni_syscall) /* use arch_prctl */
#define __NR_readlinkat 267
__SYSCALL(__NR_readlinkat, sys_readlinkat)
#define __NR_preadv 295
__SYSCALL(__NR_preadv, sys_preadv)
#define __NR_process_vm_readv 310
__SYSCALL(__NR_process_vm_readv, sys_process_vm_readv)
这里根据经验判断,通常read调用应该是sys_read,这里我们讨论sys_read函数的内核实现
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{//这里SYSCALL_DEFINE3 read到sys_read的转换请参看前面的文章Linux 编程中的API函数和系统调用的关系
//这里unsigned int fd表示用户空间的文件描述符
//char __user *buf是存放从文件读取内容的一个用户空间内存区
//size_t count表示用户空间希望读取多大的内容长度
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
file = fget_light(fd, &fput_needed);
if (file) {
loff_t pos = file_pos_read(file);
ret = vfs_read(file, buf, count, &pos);
file_pos_write(file, pos);
fput_light(file, fput_needed);
}
return ret;
}
由于这个函数比之前那个open系统调用简单许多了。。这里可以具体讲讲代码实现。。
Struct file file=fget_light(fd, &fput_needed);
此函数主要返回当前进程的所有文件表中下标为fd(即文件描述符)的struct file结构体(current为当前进程task_struct),即返回current->files_struct->fdtable->file[fd]这个struct file结构体;(这里我用结构体名字表示成员变量是为了方便理解)
这里之所以可以这么直接取出来,是因为在调用sys_read系统调用之前,用户一般都已经通过调用open函数已经调用了sys_open系统调用(前面文章Linux 中open系统调用),把进程的这个fd对应的文件从硬盘上读取或创建好了,所以这里可以之前从数组里面取。
所以用户编程一般会先调用open函数在调用read函数,当然,如果有些库函数同时封装了这两个函数在一个函数里面的情况,这里不考虑了,最终结果一样。
对文件的内容读写都通过这个当前偏移量指针来操作,如对文件的随机位置访问就通过这个指针的随机偏移值来完成的。
loff_t pos = file_pos_read(file);
此函数返回file->f_pos,即返回当前struct file结构体的当前操作指针位置
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
即把内核读取的文件内容存入char __user* buf指向的内存地址
如果文件系统没有实现file_operation或者既没有实现file_operation->read,也没有实现file_operation->aio_read,则报错。(即文件系统即没有实现同步读,也没有实现异步读,那就报错返回错误了)
如果文件系统实现了file->file_operation->read(还记得我吗在open系统调用中讲到的吗,在open系统调用中file->file_operation设置为了inode->file_operation)函数,则调用它来完成。
否则(说明文件系统没有实现read,但是实现了file_operation->aio_read)调用内核的默认函数do_sync_read(file, buf, count, pos);来做同步读取操作;而内核的do_sync_read函数内部实现是
ssize_t do_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)函数
。。。省略次要
struct iovec iov = { .iov_base = buf, .iov_len = len };//用户空间的内存地址作为iov_base,到时候好存放从磁盘读取上来的数据
for (;;) {
ret = filp->f_op->aio_read(&kiocb, &iov, 1, kiocb.ki_pos);
if (ret != -EIOCBRETRY)
break;
wait_on_retry_sync_kiocb(&kiocb);
}
,最终调用的是file_operation->aio_read,可是iov数组的长度为1.最终由文件系统的file_operation->aio_read函数去把数据从磁盘上读上来。
可是由于aio_read读取可能由于磁盘忙没有完成读取数据的任务,那就需要不停的去读取直到不需要retry为止,其中wait_on_retry_sync_kiocb(&kiocb)函数如下
static void wait_on_retry_sync_kiocb(struct kiocb *iocb)
{
set_current_state(TASK_UNINTERRUPTIBLE);
if (!kiocbIsKicked(iocb))
schedule();
else
kiocbClearKicked(iocb);
__set_current_state(TASK_RUNNING);
}
总之,do_sync_read最后调用的是file_operation->aio_read方法,但是iov数组长度为1,并且读取过程中如果读取操作没有完成则显示调用进程调度函数,本进程可能被挂起来且进程状态为TASK_UNINTERRUPTIBLE。直到最终读取成功,读取成功后进程状态会变为TASK_RUNNING,且数据直接存放在了用户空间传进来的那个buf内存去中。
fsnotify_access(file);这里暂不做进一步分析,不是我们的重点
读取操作完了之后,文件的当前指针的位置很可能变了,故此设置file->f_pos = pos;来根系文件当前指针
点击(此处)折叠或打开
点击(此处)折叠或打开
这些代码表述下面的意思
这里如果当前进程在读取的时候,struct file结构体的引用次数在自己读取之前加1正好等于1,说明本次读取可能是唯一的struct file的引用者,那么read函数结束的时候再次检查struct file的引用是否还是1(继续再检查一次是可能多进程竞争引起的,所以必须再次检查),如果还是,那么就把这个struct file结构体从超级块的文件链表中也删除掉(但是struct file结构体此时还没有从内存中释放)。
如果上述第一次检查就不等于1,那本进程不用释放这个struct file即相关释放工作
如果上述第一次检查等于1而第二次不等于1,本进程也不会释放struct file即相关释放工作
(我说的等于1是我自己方便描述,实际内核调用的是atomic_long_dec_and_test之类的函数来先减1,再和0比较之类的)
释放操作实际只是注册了一个回调函数,通过下面两行
init_task_work(&file->f_u.fu_rcuhead, ____fput); //____fput是一个实际释放操作的回调函数
task_work_add(task, &file->f_u.fu_rcuhead, true);
其中,____fput函数会释放struct file结构体,以及尝试释放起对应的dentry,mnt(之所以叫尝试是因为调用dput(dentry),dput(mnt),而dput(denty),dput(mnt)会继续检查dentry,mnt是否还在被使用,如果没有任何引用则真正释放所占内存,否则仅减少其引用计数)。
init_task_work中,file->f_u.fu_rcuhead是一个rcu_head节点,内核中
/* struct callback_head - callback structure for use with RCU and task_work
* @next: next update requests in a list
* @func: actual update function to call after the grace period.
*/
struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);
};
#define rcu_head callback_head
总之,init_task_work把____fput函数进行file->f_u.fu_rcuhead->func=___fput设置
而task_work_add(task, &file->f_u.fu_rcuhead, true);会吧这个rcu_head节点加入task->task_works, 并且会调用set_notify_resume(task)把进程的thread_info的标识里设置上TIF_NOTIFY_RESUME
这样,在进程从内核态返回用户态的时候会调用tracehook_notify_resume把task->task_works链表中的所有注册好的函数都会执行一遍(此时___fput函数就会被调用到了),并且清除TIF_NOTIFY_RESUME标识位
所以,struct file结构体要释放也是在内核返回用户态的时候才执行的,在内核态的时候一直还保留着。
注意,这里__fput中执行的释放操作并没有把进程所拥有的这个文件描述符及其在位图中的占位清空,如果执行了__fput只是这个文件描述符对应的的struct file=NULL了而已,文件描述符还站着呢。这需要后面用户空间再发个sys_close调用才能完成后续清除文件描述符等任务。详见下一篇
注意,这些释放都是内存操作,磁盘上面的文件,inode等并没有释放。
参考:kernel 3.6.7源码