文件读写基本流程
读文件
进程调用库函数向内核发起读文件请求;
内核通过检查进程的文件描述符定位到虚拟文件系统的已打开文件列表项;
调用该文件可用的系统调用函数 read();
read() 函数通过文件表项链接到目录项模块,根据传入的文件路径,在目录项模块中检索,找到该文件的 inode;
在 inode 中,通过文件内容偏移量计算出要读取的页;
通过 inode 找到文件对应的 address_space;
在 address_space 中访问该文件的页缓存树,查找对应的页缓存结点:
如果页缓存命中,那么直接返回文件内容;
如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode 找到文件该页的磁盘地址,读取相应的页填充该缓存页;
重新进行第 6 步查找页缓存;
文件内容读取成功。
总结一下:inode 管磁盘,address_space 接内存,两者互相指针链接。
Inode 是文件系统(VFS)下的概念,通过 一个 inode 对应一个文件 使得文件管理按照类似索引的这种树形结构进行管理,通过 inode 快速的找到文件在磁盘扇区的位置;但是这种管理机制并不能满足读写的要求,因为我们修改文件的时候是先修改内存里的,所以就有了页缓存机制,作为内存与文件的缓冲区。 address_space 模块表示一个文件在页缓存中已经缓存了的物理页。它是页缓存和外部设备中文件系统的桥梁。如果将文件系统可以理解成数据源,那么 address_space 可以说关联了内存系统和文件系统。
写文件
前5步和读文件一致,在 address_space 中查询对应页的页缓存是否存在;
如果页缓存命中,直接把文件内容修改更新在页缓存的页中,写文件就结束了。这时候文件修改位于页缓存,并没有写回到磁盘文件中去。
如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过 inode 找到文件该页的磁盘地址,读取相应的页填充该缓存页。此时缓存页命中,进行第 6 步。
一个页缓存中的页如果被修改,那么会被标记成脏页,脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘:
手动调用 sync() 或者 fsync() 系统调用把脏页写回;
pdflush 进程会定时把脏页写回到磁盘。
同时注意,脏页不能被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。
传统I/O
传统读操作
read(file_fd, tmp_buf, len);
基于传统的 I/O 读取方式,read 系统调用会触发 2 次上下文切换,1 次 DMA 拷贝和 1 次 CPU 拷贝,发起数据读取的流程如下:
用户进程通过read()函数向内核 (kernel) 发起系统调用,上下文从用户态 (user space) 切换为内核态 (kernel space);
CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间 (kernel space) 的读缓冲区 (read buffer);
CPU 将读缓冲区 (read buffer) 中的数据拷贝到用户空间 (user space) 的用户缓冲区 (user buffer)。
上下文从内核态 (kernel space) 切换回用户态 (user space),read 调用执行返回。
传统写操作
当应用程序准备好数据,执行 write 系统调用发送网络数据时,先将数据从用户空间的页缓存拷贝到内核空间的网络缓冲区(socket buffer)中,然后再将写缓存中的数据拷贝到网卡设备完成数据发送。
Copywrite(socket_fd, tmp_buf, len);
基于传统的 I/O 写入方式,write() 系统调用会触发 2 次上下文切换,1 次 CPU 拷贝和 1 次 DMA 拷贝,用户程序发送网络数据的流程如下:
用户进程通过 write() 函数向内核 (kernel) 发起系统调用,上下文从用户态 (user space) 切换为内核态(kernel space)。
CPU 将用户缓冲区 (user buffer) 中的数据拷贝到内核空间 (kernel space) 的网络缓冲区 (socket buffer)。
CPU 利用 DMA 控制器将数据从网络缓冲区 (socket buffer) 拷贝到网卡进行数据传输。
上下文从内核态 (kernel space) 切换回用户态 (user space),write 系统调用执行返回。
零拷贝方式
在 Linux 中零拷贝技术主要有 3 个实现思路:用户态直接 I/O、减少数据拷贝次数以及写时复制技术。
用户态直接 I/O:应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输。这种方式依旧存在用户空间和内核空间的上下文切换,硬件上的数据直接拷贝至了用户空间,不经过内核空间。因此,直接 I/O 不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝。
减少数据拷贝次数:在数据传输过程中,避免数据在用户空间缓冲区和系统内核空间缓冲区之间的CPU拷贝,以及数据在系统内核空间内的CPU拷贝,这也是当前主流零拷贝技术的实现思路。
写时复制技术:写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么将其拷贝到自己的进程地址空间中,如果只是数据读取操作则不需要进行拷贝操作。
用户态直接 I/O
mmap + write
用户进程通过 mmap() 函数向内核 (kernel) 发起系统调用,上下文从用户态 (user space) 切换为内核态(kernel space);
将用户进程的内核空间的读缓冲区 (read buffer) 与用户空间的缓存区 (user buffer) 进行内存地址映射;
CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间 (kernel space) 的读缓冲区 (read buffer);
上下文从内核态 (kernel space) 切换回用户态 (user space),mmap 系统调用执行返回;
用户进程通过write() 函数向内核 (kernel) 发起系统调用,上下文从用户态 (user space) 切换为内核态(kernel space);
CPU 将读缓冲区 (read buffer) 中的数据拷贝到的网络缓冲区 (socket buffer) ;
CPU 利用 DMA 控制器将数据从网络缓冲区 (socket buffer) 拷贝到网卡进行数据传输;
上下文从内核态 (kernel space) 切换回用户态 (user space) ,write 系统调用执行返回;
sendfile
用户进程通过 sendfile() 函数向内核 (kernel) 发起系统调用,上下文从用户态 (user space) 切换为内核态(kernel space)。
CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间 (kernel space) 的读缓冲区 (read buffer)。
CPU 将读缓冲区 (read buffer) 中的数据拷贝到的网络缓冲区 (socket buffer)。
CPU 利用 DMA 控制器将数据从网络缓冲区 (socket buffer) 拷贝到网卡进行数据传输。
上下文从内核态 (kernel space) 切换回用户态 (user space),sendfile 系统调用执行返回。
sendfile + DMA gather copy
用户进程通过 sendfile()函数向内核 (kernel) 发起系统调用,上下文从用户态 (user space) 切换为内核态(kernel space)。
CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间 (kernel space) 的读缓冲区 (read buffer)。
CPU 把读缓冲区 (read buffer) 的文件描述符(file descriptor)和数据长度拷贝到网络缓冲区(socket buffer)。
基于已拷贝的文件描述符 (file descriptor) 和数据长度,CPU 利用 DMA 控制器的 gather/scatter 操作直接批量地将数据从内核的读缓冲区 (read buffer) 拷贝到网卡进行数据传输。
上下文从内核态 (kernel space) 切换回用户态 (user space),sendfile 系统调用执行返回。
splice
用户进程通过 splice() 函数向内核(kernel)发起系统调用,上下文从用户态 (user space) 切换为内核态(kernel space);
CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间 (kernel space) 的读缓冲区 (read buffer);
CPU 在内核空间的读缓冲区 (read buffer) 和网络缓冲区(socket buffer)之间建立管道 (pipeline);
CPU 利用 DMA 控制器将数据从网络缓冲区 (socket buffer) 拷贝到网卡进行数据传输;
上下文从内核态 (kernel space) 切换回用户态 (user space),splice 系统调用执行返回。
写时复制 需要 MMU 的支持
缓冲区共享
fbuf 的思想是每个进程都维护着一个缓冲区池,这个缓冲区池能被同时映射到用户空间 (user space) 和内核态 (kernel space),内核和用户共享这个缓冲区池,这样就避免了一系列的拷贝操作。
Linux零拷贝对比
无论是传统 I/O 拷贝方式还是引入零拷贝的方式,2 次 DMA Copy 是都少不了的,因为两次 DMA 都是依赖硬件完成的。下面从 CPU 拷贝次数、DMA 拷贝次数以及系统调用几个方面总结一下上述几种 I/O 拷贝方式的差别。
拷贝方式 CPU拷贝 DMA拷贝 系统调用 上下文切换
传统方式(read + write) 2 2 read / write 4
内存映射(mmap + write) 1 2 mmap / write 4
sendfile 1 2 sendfile 2
sendfile + DMA gather copy 0 2 sendfile 2
splice 0 2 splice 2