Linux文件系统可以分为两层,虚拟文件系统(VFS)和驱动。VFS主要和驱动对接,以实现对不同文件系统的适配和管理。本文阅读的read/write函数是VFS层面的,源码如下:https://codebrowser.dev/linux/linux/fs/read_write.c.html
大致结构如下,当然以下所指的设备不一定是真实的物理设备。
VFS-驱动
read
read的入口如下,输入参数是文件fd、buffer、长度。回顾之前的文章中学习过的,fd是指文件在task_struct中files table的下标,通过fd可以找到struct file,从而拿到inode,获取到文件内容。
1
2
3
4
SYSCALL_DEFINE3 ( read , unsigned int , fd , char __user * , buf , size_t , count )
{
return ksys_read ( fd , buf , count );
}
ksys_read展开如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ssize_t ksys_read ( unsigned int fd , char __user * buf , size_t count )
{
struct fd f = fdget_pos ( fd );
ssize_t ret = - EBADF ;
if ( f . file ) {
loff_t pos , * ppos = file_ppos ( f . file );
if ( ppos ) {
pos = * ppos ;
ppos = & pos ;
}
ret = vfs_read ( f . file , buf , count , ppos );
if ( ret >= 0 && ppos )
f . file -> f_pos = pos ;
fdput_pos ( f );
}
return ret ;
}
其中,fdget_pos会调用到以下这个函数:
1
2
3
4
5
6
7
8
9
10
11
12
unsigned long __fdget_pos ( unsigned int fd )
{
unsigned long v = __fdget ( fd );
struct file * file = ( struct file * )( v & ~ 3 );
if ( file && ( file -> f_mode & FMODE_ATOMIC_POS )) {
if ( file_count ( file ) > 1 ) {
v |= FDPUT_POS_UNLOCK ;
mutex_lock ( & file -> f_pos_lock );
}
}
return v ;
}
fdput_pos展开如下:
1
2
3
4
5
6
static inline void fdput_pos ( struct fd f )
{
if ( f . flags & FDPUT_POS_UNLOCK )
__f_unlock_pos ( f . file );
fdput ( f );
}
如果文件被标记了FMODE_ATOMIC_POS,并且有多线程使用,那么f_pos会被加锁保护(认为是原子操作了)。在最后fdput_pos的时候则会对应解锁。注意到struct file *file = (struct file *)(v & ~3);这在tagged-pointer-让指针包含更多信息 一文是介绍过的。
再回ksys_read到先是通过unsigned int fd拿到struct fd,其定义如下:
1
2
3
4
struct fd {
struct file * file ;
unsigned int flags ;
};
这时候可以拿到struct file了,回顾一下其中重要的成员有:
1
2
3
4
5
6
struct file {
struct path f_path ;
struct inode * f_inode ; /* cached value */
const struct file_operations * f_op ;
//...
}
path包含dentry、mnt信息,inode可以指向物理设备中的信息,file_operations保护一组操作表。
ksys_read接下来会尝试获取文件的pos(这里是读位置)信息:
1
loff_t pos , * ppos = file_ppos ( f . file );
file_ppos的返回值被赋给了long long*,为什么是一个指针?展开file_ppos看看:
1
2
3
4
5
/* file_ppos returns &file->f_pos or NULL if file is stream */
static inline loff_t * file_ppos ( struct file * file )
{
return file -> f_mode & FMODE_STREAM ? NULL : & file -> f_pos ;
}
如果不是file stream的情况下,会返回struct file成员f_pos的地址。为什么要返回一个地址,而不是值呢?关注这段逻辑:
1
2
3
4
if ( ppos ) {
pos = * ppos ;
ppos = & pos ;
}
ppos是指针,ppos本来是指向struct file的f_pos成员,上述逻辑走完后,ppos指向了一个临时的pos,这时候f_pos相当于被保护起来了。上述说法说得通,但是为什么不直接赋值给pos,这样也不会影响f_pos呀?回到file_ppos,因为对file stream的情况需要返回NULL,如果直接赋值给pos再处理这种NULL情况的话,事情也不会变得更简单,因此引入一个ppos是合理的。
再往下看:
1
2
3
4
ret = vfs_read ( f . file , buf , count , ppos );
if ( ret >= 0 && ppos )
f . file -> f_pos = pos ;
fdput_pos ( f );
这段逻辑也可以说ppos起到了保护作用,在vfs_read之后,如果成功了,则会更新f_pos的值,如果失败也就不会更新了。这样做是可以保证失败的时候不会动f_pos,但是如果是多线程情况,且没有FMODE_ATOMIC_POS标记,f_pos的值不是乱套了吗?先进去vfs_read看看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t vfs_read ( struct file * file , char __user * buf , size_t count , loff_t * pos )
{
//... 校验 ...
if ( count > MAX_RW_COUNT )
count = MAX_RW_COUNT ;
if ( file -> f_op -> read )
ret = file -> f_op -> read ( file , buf , count , pos );
else if ( file -> f_op -> read_iter )
ret = new_sync_read ( file , buf , count , pos );
else
ret = - EINVAL ;
if ( ret > 0 ) {
fsnotify_access ( file );
add_rchar ( current , ret );
}
inc_syscr ( current );
return ret ;
}
如果驱动实现了read接口,那么就调用read接口,这里依赖驱动实现,就先跳过了;如果驱动没有实现read,而是实现read_iter,那么就是调用new_sync_read,进去看看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static ssize_t new_sync_read ( struct file * filp , char __user * buf , size_t len , loff_t * ppos )
{
struct kiocb kiocb ;
struct iov_iter iter ;
ssize_t ret ;
init_sync_kiocb ( & kiocb , filp );
kiocb . ki_pos = ( ppos ? * ppos : 0 );
iov_iter_ubuf ( & iter , READ , buf , len );
ret = call_read_iter ( filp , & kiocb , & iter );
BUG_ON ( ret == - EIOCBQUEUED );
if ( ppos )
* ppos = kiocb . ki_pos ;
return ret ;
}
这里遇到了之前没有接触过的结构体(概念)struct kiocb和struct iov_iter,定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct kiocb {
struct file * ki_filp ;
loff_t ki_pos ;
void ( * ki_complete )( struct kiocb * iocb , long ret );
void * private ;
int ki_flags ;
u16 ki_ioprio ; /* See linux/ioprio.h */
struct wait_page_queue * ki_waitq ; /* for async buffered IO */
};
struct iov_iter {
u8 iter_type ;
bool nofault ;
bool data_source ;
bool user_backed ;
union {
size_t iov_offset ;
int last_offset ;
};
size_t count ;
union {
const struct iovec * iov ;
const struct kvec * kvec ;
const struct bio_vec * bvec ;
struct xarray * xarray ;
struct pipe_inode_info * pipe ;
void __user * ubuf ;
};
union {
unsigned long nr_segs ;
struct {
unsigned int head ;
unsigned int start_head ;
};
loff_t xarray_start ;
};
};
这两个结构体可以获取什么信息呢?一个是struct kiocb扩展了struct file的一些信息,一个是struct iov_iter包含用户需要填充的buffer信息。(关于这两个概念先不深挖了,这里是我的知识盲区,似乎涉及到VFS更下面的东西。)经过init_sync_kiocb和iov_iter_ubuf的填充,就会调用到call_read_iter。不过还要注意到ppos是会被更新的。
1
2
3
4
5
static inline ssize_t call_read_iter ( struct file * file , struct kiocb * kio ,
struct iov_iter * iter )
{
return file -> f_op -> read_iter ( kio , iter );
}
回到vfs_read的最后,read完之后,就是通过fsnotify_access发送一个access fsnotify。
小结
以上就是VFS这一层read的大致逻辑。大致流程就是先通过fd获取到struct file(这就是为什么read之前要先open),然后再获取当前的read指针位置,然后调用驱动的read/read_iter方法,如果读取成功则发送access fsnotify,最后把read指针更新后的位置回写给struct file。
上文还遗留一个问题,如果多线程读怎么办?可以假设驱动层的read/read_iter是原子的(那是驱动的事情),但是glibc的read默认是没有保护的,所以可能存在一些不安全的情况,比如f_pos的更新没有被保护,就可能导致多线程读错位。
关于多线程读的问题,可以再参考fread:
1
2
3
4
5
6
7
8
9
10
11
12
13
size_t
_IO_fread ( void * buf , size_t size , size_t count , FILE * fp )
{
size_t bytes_requested = size * count ;
size_t bytes_read ;
CHECK_FILE ( fp , 0 );
if ( bytes_requested == 0 )
return 0 ;
_IO_acquire_lock ( fp );
bytes_read = _IO_sgetn ( fp , ( char * ) buf , bytes_requested );
_IO_release_lock ( fp );
return bytes_requested == bytes_read ? count : bytes_read / size ;
}
可以看到,因为_IO_acquire_lock和_IO_release_lock,这是可以保证fread默认情况下就是线程安全的。
write
write流程和read类似的,区别在vfs_write的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t vfs_write ( struct file * file , const char __user * buf , size_t count , loff_t * pos )
{
// ... 校验 ...
file_start_write ( file );
if ( file -> f_op -> write )
ret = file -> f_op -> write ( file , buf , count , pos );
else if ( file -> f_op -> write_iter )
ret = new_sync_write ( file , buf , count , pos );
else
ret = - EINVAL ;
if ( ret > 0 ) {
fsnotify_modify ( file );
add_wchar ( current , ret );
}
inc_syscw ( current );
file_end_write ( file );
return ret ;
}
两者区别除了write发送的是fsnotify_modify通知外,vfs_write默认会保护struct file的读,怎么做的呢?file_start_write最终会调到__sb_start_write:
1
2
3
4
static inline void __sb_start_write ( struct super_block * sb , int level )
{
percpu_down_read ( sb -> s_writers . rw_sem + level - 1 );
}
这时候给读加上了锁,可以参考percpu-rw-semaphore 。
在写完之后file_end_write调用__sb_end_write给读解锁:
1
2
3
4
static inline void __sb_end_write ( struct super_block * sb , int level )
{
percpu_up_read ( sb -> s_writers . rw_sem + level - 1 );
}