本文解答如下问题:
- 什么是系统调用?
- 用户缓冲区?
- 什么是内核缓冲区?
- 调用
read()
时linux内核整个IO过程 - 怎么绕过缓存直接IO
系统调用
系统资源,在用户进程中是无法被直接访问的,只能通过操作系统来访问,所以也把操作系统提供的这些功能成为:“系统调用”。
系统资源包括:
用户缓冲区
调用read()
申请的buffer
就是用户缓冲区
每次调用read()
的过程如下:
- 用户进程通过
read()
方法向操作系统发起调用,此时上下文从用户态转向内核态 - DMA控制器把数据从硬盘中拷贝到读缓冲区
- CPU把读缓冲区数据拷贝到应用缓冲区,上下文从内核态转为用户态,
read()
返回
内核缓冲区
看上图,linux的IO栈是分为几层的:
File System
+Page Cache
:内核缓冲区指的就是这块。系统调用的read(2)/write(2)
和真实的磁盘读写之间也存在一层 buffer,这里用术语Kernel buffer cache来指代这一层缓存。在Linux下,文件的缓存习惯性的称之为Page Cache
Block IO Layer
:管理块设备的 IO 队列,对 IO 请求进行合并、排序Device
: 设备层,通过 DMA 与内存直接交互,完成数据和具体设备之间的交互
当调用read()
时,到达文件系统这一层,发现Page Cache
中不存在该位置的磁盘映射,然后创建相应的Page Cache
并和相关的扇区关联。然后请求继续到达块设备层,在 IO 队列里排队,接受一系列的调度后到达设备驱动层,此时一般使用DMA方式读取相应的磁盘扇区到 Cache 中,然后read(2)
拷贝数据到用户提供的用户态buffer 中去(read(2)
的参数指出的)
上述过程拷贝次数:2次
- 从磁盘到
Page Cache
算第一次的话 - 从
Page Cache
到用户态buffer就是第二次
mmap
还是看上面的图,mmap(2)
直接把Page Cache
映射到了用户态的地址空间里了,所以mmap(2)
的方式读文件是没有上面第二次拷贝过程的。
linux下的write的同步方式
linux下cache同步方式有两种:Write Through
(写穿)和Write back
(写回)
Write Through
就是指write(2)
操作将数据拷贝到Page Cache
后立即和下层进行同步的写操作,完成下层的更新后才返回Write back
正好相反,指的是写完Page Cache
就可以返回了。Page Cache
到下层的更新操作是异步进行的。 Linux下Buffered IO默认使用的是Write back
机制,即文件操作的写只写到Page Cache
就返回,之后Page Cache
到磁盘的更新操作是异步进行的。Page Cache
中被修改的内存页称之为脏页(Dirty Page),脏页在特定的时候被一个叫做pdflush (Page Dirty Flush)
的内核线程写入磁盘,写入的时机和条件如下:
- 当空闲内存低于一个特定的阈值时,内核必须将脏页写回磁盘,以便释放内存。
- 当脏页在内存中驻留时间超过一个特定的阈值时,内核必须将超时的脏页写回磁盘吧
- 用户进程调用
sync(2)
、fsync(2)
、fdatasync(2)
系统调用时,内核会执行相应的写回操作。
sync()、fsync()、和msync
sync
:sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。通常称为update的系统守护进程会周期性地(一般每隔30秒)调用sync函数。这就保证了定期冲洗内核的块缓冲区。命令sync(1)也调用sync函数。
#include <unistd.h>
int sync(void);\
返回值:返回 0.
fsync()
: fsync函数只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回.该调用会阻塞等待直到设备报告IO完成。
#include <unistd.h>
int fsync(int fd);
msync():
在使用mmap
,将文件的page cache直接映射到进程的地址空间,通过写内存的方式修改文件使用,可以精确指定刷新区域
#incude <sys/mman.h>
int msync(void *addr, size_t length, int flags)
相关内核参数
`# flush每隔5秒执行一次`
`root@082caa3dfb1d / $ sysctl vm.dirty_writeback_centisecs`
`vm.dirty_writeback_centisecs = 500`
`# 内存中驻留30秒以上的脏数据将由flush在下一次执行时写入磁盘`
`root@082caa3dfb1d / $ sysctl vm.dirty_expire_centisecs`
`vm.dirty_expire_centisecs = 3000`
`# 若脏页占总物理内存10%以上,则触发flush把脏数据写回磁盘`
`root@082caa3dfb1d / $ sysctl vm.dirty_background_ratio`
`vm.dirty_background_ratio = 10`
如何写穿?
默认是写回方式,如果想指定某个文件是写穿方式呢?即写操作的可靠性压倒效率的时候,能否做到呢?当然能,除了之前提到的fsync(2)
之类的系统调用外,在open(2)
打开文件时,传入O_SYNC
这个 flag 即可实现。