glibc-read/write源码阅读

Linux文件系统可以分为两层,虚拟文件系统(VFS)和驱动。VFS主要和驱动对接,以实现对不同文件系统的适配和管理。本文阅读的read/write函数是VFS层面的,源码如下:https://codebrowser.dev/linux/linux/fs/read_write.c.html

大致结构如下,当然以下所指的设备不一定是真实的物理设备。

https://bu.dusays.com/2023/01/08/63ba3fb60141e.png
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 filef_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 kiocbstruct 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_kiocbiov_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);
}