可参考上一篇文章:struct file结构体
read系统调用是glibc库里面的一个函数,是对系统调用函数sys_read()的封装与实现。glic库会将read函数在用户态下进行解析,通过寄存器将参数保存起来,并借助于系统调用名称获得系统调用号,该系统调用号又可以作为系统调用函数在sys_call_table中的索引获取函数入口地址,该表位于linux-4.13.16\arch\x86\entry\syscalls\syscall_64.tbl中,接下来就是即将要分析的sys_read调用了
1.内核源码特殊用法
该属性的主要目的是便于通过 sparse 工具检查 Linux内核代码。[include/linux/compile.h]
中定义了该属性:
# define __user __attribute__((noderef, address_space(1)))
其中__attribute__是gcc的编译属性,主要用于改变所声明或定义的函数或数据的特性,它有很多子项,用于改变作用对象的特性。比如对函数,noline将禁止进行内联扩展、noreturn表示没有返回值、pure表明函数除返回值外,不会通过其它(如全局变量、指针)对函数外部产生任何影响。此处的__user告诉编译器它是一个用户数据,这样在进行内核编译时就可以进行语法检查。这类特性均是内核开发人员需要关注的事情,目前无需关注。如果使用了 __user 宏的指针不在用户地址空间初始化, 或者指向内核地址空间, 设备地址空间等等, Sparse会给出警告.
asmlinkage在内核源码中出现的频率非常高,它是告诉编译器在本地堆栈中传递参数,与之对应的是fastcall;fastcall是告诉编译器在通用寄存器中传递参数。运行时,直接从通用寄存器中取函数参数,要比在本地堆栈(内存)中取,速度快很多。
2.read code
#define EBADF 9 /* Bad file number */(定义在errno.h中的错误代码)
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{//定义在linux-4.13.16\fs\read_write.c文件中
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_read(f.file, buf, count, &pos);
if (ret >= 0)
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
可以看到函数的实现开始于fdget_pos()函数(定义于linux-4.13.16\include\linux\file.h),该函数得到一个struct fd结构:
struct fd {
struct file *file;
unsigned int flags;
};
static inline struct fd fdget_pos(int fd)
{
return __to_fd(__fdget_pos(fd));
}
static inline struct fd __to_fd(unsigned long v)
{
return (struct fd){(struct file *)(v & ~3),v & 3};
}
可知fdget_pos(int fd)首先调用linux-4.13.16\fs\file.c中的__fdget_pos(int fd)函数,然后调用__to_fd()函数,最后总之其主要目的就是将给定的只有数字的文件描述符转化为其对应的 fd 结构。接下来先检查是否成功找到文件,然后调用file_pos_read()函数:
static inline loff_t file_pos_read(struct file *file)
{//定义在linux-4.13.16\fs\read_write.c文件中
return file->f_pos; //该文件在当前进程中的文件偏移量
}
该函数返回fd文件在当前进程中的文件偏移量,接下来调用vfs_read(f.file, buf, count, &pos)函数,此函数功能为从指定文件的指定位置读入数据到指定缓冲中。此处不深入vfs_read函数的细节,在下面再回过头来解析。vfs_read结束相关工作后, 检查结果若成功执行,使用file_pos_write 函数改变在文件中的位置:
static inline void file_pos_write(struct file *file, loff_t pos)
{
file->f_pos = pos;
}
即通过调节struct file 中的f_pos的值改变下一次读/写文件的入口地址。最后调用fdput_pos(f)函数:
static inline void fdput_pos(struct fd f)
{
if (f.flags & FDPUT_POS_UNLOCK)
__f_unlock_pos(f.file);
fdput(f);
}
该函数是在解锁共享文件描述符的线程并发读文件时保护文件位置的互斥量f_pos_lock。最后返回此次系统调用成功读取的字节数,即vfs_read()函数的返回值ret。
注:f_pos_lock是struct file 的成员变量struct mutex f_pos_lock
3.vfs_read函数解析
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{//定义在linux-4.13.16\fs\read_write.c文件中
ssize_t ret;
......
if (!ret) {
......
ret = __vfs_read(file, buf, count, pos);
if (ret > 0) {
fsnotify_access(file);
add_rchar(current, ret);
}
inc_syscr(current);
}
return ret;
}
故忽略一些技术性的检查措施之后其代码如上所示,接下来主要实现由__vfs_read()函数实现:
#define EINVAL 22 /* Invalid argument */
typedef long long __kernel_loff_t;//在linux-4.13.16\include\uapi\asm-generic\posix_types.h中
typedef __kernel_loff_t loff_t; //linux-4.13.16\include\linux\types.h中
ssize_t __vfs_read(struct file *file, char __user *buf, size_t count,
loff_t *pos)
{
if (file->f_op->read)
return file->f_op->read(file, buf, count, pos);
else if (file->f_op->read_iter)
return new_sync_read(file, buf, count, pos);
else
return -EINVAL;
}
其实该文件启用的f_op->read操作是该文件对应的文件系统的read的操作,此处所指的文件系统即我们常说的虚拟文件系统是一个统一接口,而具体的文件系统有ext3/4文件系统。下面以ext4为例剖析ext4文件系统读写操作:
const struct file_operations ext4_file_operations = {
.llseek = ext4_llseek,
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.mmap = ext4_file_mmap,
.open = ext4_file_open,
.release = ext4_release_file,
.fsync = ext4_sync_file,
.get_unmapped_area = thp_get_unmapped_area,
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
.fallocate = ext4_fallocate,
};//linux-4.13.16\fs\ext4\file.c
显然ext4没有定义read与write操作,故此时会调用f_op->read_iter也即 ext4_file_operations->ext4_file_read_iter:
static ssize_t ext4_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
{//linux-4.13.16\fs\ext4\file.c
......
return generic_file_read_iter(iocb, to);
}
ext4_file_read_iter最终会调用generic_file_read_iter函数:
ssize_t
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
......//direct IO
if (iocb->ki_flags & IOCB_DIRECT) {
struct address_space *mapping = file->f_mapping;
struct inode *inode = mapping->host;
......
retval = mapping->a_ops->direct_IO(iocb, iter);
......
}
retval = do_generic_file_read(file, &iocb->ki_pos, iter, retval);//buffer IO
out:
return retval;
}
该函数的主要目的是区分是否使用缓存读取数据。显然当发现定义有IOCB_DIRECT时会直接从磁盘上读取数据,否则则调用do_generic_file_read()函数从缓存读取,大多数文件系统默认使用缓存。