系统调用、用户缓冲区、内核缓冲区、底层IO知识笔记

2,492 阅读4分钟

image.png 本文解答如下问题:

  • 什么是系统调用?
  • 用户缓冲区?
  • 什么是内核缓冲区?
  • 调用read()时linux内核整个IO过程
  • 怎么绕过缓存直接IO

系统调用

系统资源,在用户进程中是无法被直接访问的,只能通过操作系统来访问,所以也把操作系统提供的这些功能成为:“系统调用”。

系统资源包括:

用户缓冲区

调用read()申请的buffer就是用户缓冲区

每次调用read()的过程如下:

  1. 用户进程通过read()方法向操作系统发起调用,此时上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. CPU把读缓冲区数据拷贝到应用缓冲区,上下文从内核态转为用户态,read()返回

内核缓冲区

image.png

看上图,linux的IO栈是分为几层的:

  1. File System+Page Cache:内核缓冲区指的就是这块。系统调用的read(2)/write(2)和真实的磁盘读写之间也存在一层 buffer,这里用术语Kernel buffer cache来指代这一层缓存。在Linux下,文件的缓存习惯性的称之为Page Cache
  2. Block IO Layer:管理块设备的 IO 队列,对 IO 请求进行合并、排序
  3. 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)的内核线程写入磁盘,写入的时机和条件如下:
  1. 当空闲内存低于一个特定的阈值时,内核必须将脏页写回磁盘,以便释放内存。
  2. 当脏页在内存中驻留时间超过一个特定的阈值时,内核必须将超时的脏页写回磁盘吧
  3. 用户进程调用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 即可实现。