当用户使用malloc申请大于128KB的堆内存时,内存分配器会通过mmap系统调用,在linux进程虚拟空间中直接映射一片内存给用户使用,这片使用mmap映射的内存区域比较神秘。当我们运行一个程序时,需要从磁盘上将该可执行文件加载到内存。将文件加载到内存有两种常用的操作方法,一种是通过常规的文件I/O操作,如read/write等系统调用接口,一种是使用mmap系统调用将文件映射到进程的虚拟空间,然后直接对这片映射区域读写即可。
文件I/O操作使用文件的API函数(open, read, write, close)对文件进行打开和读写操作。文件存储于磁盘中,我们通过指定的文件名打开一个文件,就会得到一个文件描述符,通过该文件描述符就可以找到该文件的索引节点inode,根据inode就可以找到该文件在磁盘上存储位置。然后我们就可以直接调用read write 函数到磁盘指定的位置读写数据了。文件的读写流程如图5-34左侧图所示。
buf
用户空间
内核空间
read/write
磁盘
buf
write/read
内核中的缓冲区
fsync
磁盘
磁盘属于机械设备,程序每次读写磁盘都要经过转动磁盘,磁头定位等操作,读写速度较慢,为了提高读写效率。减少IO读盘次数以保护磁盘,linux内核基于程序的局部原理提供了一种磁盘缓冲机制。如图5-34所示,在内存中以物理页为单位缓存磁盘上的普通文件或块设备文件,当应用程序读磁盘文件时,会先到缓存中看数据时否存在,若数据存在就直接读取并复制到用户空间,若不存在,则先将磁盘数据读取到页缓存中,然后从页缓存中复制数据到用户空间的buf中。当应用程序写数据到磁盘文件时,会先将用户空间buf的数据写入page cache, 当page cache中缓存的数据达到设定的阈值或者刷新时间超时,linux内核会将这些数据回写到磁盘中。
不同的进程可能会读写多个文件,不同的文件可能都要缓存到page cache物理页中,如图5-35所示,linux内核通过一个叫做radix tree 的树结构来管理这些页缓存对象,一个物理页上可以是文件页缓存。也可以是交换缓存,甚至是普通内存。以文件页缓存为例,通过一个叫作address_space 的结构体让磁盘文件和内存产生关联。我们通过文件名可以找到文件对应的inode,inode->imapping 成员指向address_space对象,物理页中的page->mapping 指向页缓存owner的address_space,这样文件名和其对应的物理页缓存就产生了关联。
文件名
inode->address_space
磁盘 页缓存
radix tree
物理页 物理页 物理页 物理页 物理页 物理页
当我们读写指定的磁盘文件时,通过文件描述符就可以找到该文件的address space, 通过传进去的文件位置偏移参数就可以到页缓存中查找对应的物理页,若查找到则读取该物理页上的数据到用户空间,若没有查找到,则linux 内核会新建一个物理页添加到页缓存,从磁盘读取数据到该物理页,最后从该物理页将数据复制到用户空间。
buf1 buf2
fwrite fread
IO缓冲区
fflush->read/write 用户空间/内核空间
页缓存
fsync 硬件
磁盘
linux内核中的页缓存机制在一定程度上提高了磁盘读写数据,但是程序通过read/write频繁的系统调用还是会带来一定的性能开销。系统调用会不停的切换CPU和操作系统的工作模式,数据也在用户空间和内核空间之间不停的复制,为了减少系统调用的次数,尝到了缓存甜头的glibc决定进一步优化。如图5-36所示,在用户空间开辟一个IO缓冲区,并将系统调用read write 进一步封装成fread/ fwrite 库函数。
在用户空间,C标准库会为每个打开的文件分配一个IO缓冲区和一个文件描述符fd,IO缓冲区信息和文件描述符fd一起封装在FILE结构体中。