宏观概念
kernel
在计算机中有一个 kernel 的概念,也就是内核,kernel 封装了对于计算机硬件的操作,我们写的软件其实就是调用kernel提供的api来操纵硬件, IO 也属于其中之一。 VFS 在kernel中 有一个概念叫 VFS虚拟文件系统 的概念,是一个树状结构。 inode 理解inode,要从文件储存说起。
文件储存在硬盘上,硬盘的最小存储单位叫做 扇区Sector 。每个扇区储存512字节(相当于0.5KB)。
操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个块block。这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。
文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。
==每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。== PageCache 程序想要读取硬盘中的数据,会先调用 Kernel, Kernel中的 VFS找到文件中的 inode,把数据读取到 pagecache , pagecache 默认大小是 4K。 dirty & flush 程序把pagecache的数据修改了之后,该pagecache会被标记为 dirty。 可以由程序主动调用 flush把内存写入磁盘 FD文件描述符 对于应用程序,操作系统把inode封装成了一个文件描述符 FD, 想要读取这个文件的偏移量用seek表示。 文件描述符类型
- -普通文件
- d 目录
- l 连接
- c 字符设备
- s socket
- p pipeline 等等
验证理论
- FVS 虚拟文件系统其实就是linux的目录结构
- inode : 使用 stat 文件名查看文件信息
- FS 文件描述符 3.1 首先执行 exec 8< haha.txt . 表示创建输入流读取硬盘中的 haha.txt中的数据
[root@localhost ~]# exec 8< haha.txt
3.2 然后执行 cd /proc/$$/fd . 表示到当前进程的文件描述符目录中,可以看到有一个 8 指向这 haha.txt
3.3 执行 lsof -op $$ 就可以看到一个 8文件描述符 r 表示 read, nodeid 是 201674028。 跟haha.txt的文件描述符保持一致。 offset 是 0t0
3.4 执行 read a 0<& 8 表示定义一个变量 a 从标准输入流里面读取 8这个文件描述符读取到的内容。执行结束之后发现8的偏移量变成了13,因为读了13个字节
3.5 这时候打开另一个进程想要读取haha.txt,发现文件描述符的 inode相同,但是偏移量为0
socket IO
管道类型
前置知识补充
任何程序都有 0 标准输入,1标准输出,2报错输出 拿cat 举例 cat 0<read.txt 1>m.txt 表示改变cat 的标准输入读取read.txt 文件然后输出到 m.txt中
[root@localhost ~]# cat 0<read.txt 1>m.txt
管道 |
tail -f haha.txt | grep hello 这种命令大家都用过. | 就是一个管道,会把前面的输出变成后面的输入。有一点需要注意的是,管道前面和后面 bash会开启子进程来操作。
验证
创建一个管道,读取输入到x 然后输出到 y上
看到管道左边的进程id = 18234. 可以看到这个bash的标准输出1 已经被重定向到pipe管道中了
最开始的bash进程号是 13765,然后开启了两个管道 一个是 18234 一个是 18235.
可以看到 18235 的标准输入也被修改成了管道
就可以 通过 lsof 也可以看到文件描述符的管道描述
PageCache
pagecache 就是 linux内核用内存模拟的硬盘内容. java程序认为访问的硬盘读写其实都是访问linux提供的pagecache地址. 一个pagecache默认大小是 4k
假设一个应用程序大小为 10M ,计算机并不是一口气将整个程序读取到内存中。而是按page来读,一个page是4K大小。但是在程序自己看来自己是已经全部加载到内存中 程序的虚拟内存地址和物理内存地址有一个映射关系,通过cpu 的 mmu模块映射。 当程序想要读取的内存在物理内存中不存在的时候, 就会产生 ==缺页异常==,程序会等待将page从硬盘读取到内存中之后再继续被cpu执行。
不同进程读取硬盘中的文件也是从pagecache中读取,就算读取同一个pagecache也会维护不同的seek.
脏页
一个程序想要读区硬盘上的数据的时候会经过linux的 VFS , 然后在内存中创建pagecache,给程序访问,此时 该pagecache 会被标记为 dirty. 等内核或者程序调用了 flush方法,会将pagecache的数据刷回硬盘,然后pagecache会被标记为 clean. 当内存不够放pagecache的时候,会采取 lru淘汰算法,删除pagecache.只能删除clean的pagecache
NIO
bytebuffer
buf = ByteBuffer.allocate(1024)
buf.put("123".getbytes());
buf.flip()
buf.get()
buf.compact()
RandomAccessFile
RandomAccessFile accessFile = new RandomAccessFile("/", "rw");
accessFile.write("1111111".getBytes());
accessFile.write("2222222".getBytes());
accessFile.seek(4);
accessFile.write("333333".getBytes());
可以看到 accessFile.write 普通的写方法产生了系统调用
FileChannel channel = accessFile.getChannel();
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 9988);
map.put("44444".getBytes());
channel.map(FileChannel.MapMode.READ_WRITE, 0, 4096); 方法其实是在内核中生成了一个 mmap的内存映射,把java的堆外内存和内核的pagecache产生了一个直接的映射关系. MappedByteBuffer 的put 方法不会产生系统调用.
关系图如下