Linux内核读写文件有两个常用的函数,分别是vfs_write/vfs_read、kernel_write/kernel_read,两个也都内核导出函数(EXPORT_SYMBOL)。两者有什么区别呢?分别在什么场景下使用?
两者都在 kernel/fs/read_write.c 中实现,我们直接打开源码(基于kernel5.4)分析下。
1、先来看看 vfs_write。
第二个参数 buf 前面有__user修饰符,这就要求 buf 指针应该指向用空空间的内存,如果传递内核空间的指针,函数会返回失败-EFAULT。具体地内存地址检测是在 access_ok() 实现。
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (unlikely(!access_ok(buf, count)))
return -EFAULT;
ret = rw_verify_area(WRITE, file, pos, count);
if (!ret) {
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;
}
vfs_write内部会调用__vfs_write,后者会调用具体的文件系统的对应接口。
static 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;
}
2、先看看 kernel_write
可以看到,kernel_write内部也是调用了vfs_write,但在前后分别调用了set_fs()。该函数的作用是改变kernel对内存地址检查的处理方式,参数有两个取值:USER_DS(32位为0x3FFFFFFF),KERNEL_DS(0x00000000),分别代表用户空间和内核空间。默认情况下,access_ok()会检验将要操作的地址范围是否在当前进程的用户地址空间中,要使内核空间的内存指针也能校验通过,就需要使用set_fs(KERNEL_DS)进行设置。
ssize_t kernel_write(struct file *file, const void *buf, size_t count,
loff_t *pos)
{
mm_segment_t old_fs;
ssize_t res;
old_fs = get_fs();
set_fs(KERNEL_DS);
/* The cast to a user pointer is valid due to the set_fs() */
res = vfs_write(file, (__force const char __user *)buf, count, pos);
set_fs(old_fs);
return res;
}
3、分析下access_ok() 的原理
如上面描述,access_ok()默认情况下会检验将要操作的地址范围是否在当前进程的用户地址空间中,两个参数分别表示要操作的内存地址和大小,返回0表示校验通过。
其真正的实现体为 __range_ok(),后者的核心代码为汇编程序。
#define access_ok(addr, size) (__range_ok(addr, size) == 0)
#define __range_ok(addr, size) ({ \
unsigned long flag, roksum; \
__chk_user_ptr(addr); \
__asm__(".syntax unified\n" \
"adds %1, %2, %3; sbcscc %1, %1, %0; movcc %0, #0" \
: "=&r" (flag), "=&r" (roksum) \
: "r" (addr), "Ir" (size), "0" (current_thread_info()->addr_limit) \
: "cc"); \
flag; })
汇编程序主要有3条语句,解析如下:
第一条:adds %1, %2, %3; 表示 rosum = addr + size。这个操作影响进位标志C,当进位了(C=1),则后面两条指令都不执行,返回值 flag 就为初始值 current_thread_info()->addr_limit;当没进位(C=0),则继续执行后面两条指令。
adds %1, %2, %3; //%1、%2、%3分别代表rosum、addr、size
第二条: sbcscc %1, %1, %0; 表示 rosum = rosum - flag,也影响进位标志C。也就是说如果 addr + size) >= (current_thread_info()->addr_limit) ,则C=1,则后面的指令都不执行,返回值 flag 同为初始值;否则 C=0,继续执行后面的指令。
sbcscc %1, %1, %0;
第三条: movcc %0, #0 表示给flag赋值0
movcc %0, #0
综上所述:__range_ok宏其实等价于:
如果(addr + size) >= (current_thread_info()->addr_limit) ,返回非0,校验未通过。
如果(addr + size) < (current_thread_info()->addr_limit),返回0,校验通过。
而set_fs()就是设置current_thread_info()->addr_limit的值为USER_DS(32位为0x3FFFFFF),KERNEL_DS(0x00000000),所以当set_fs(KERNEL_DS),返回值永远是0。