系统IO原理
在 Linux 中:
VFS(Virtual Filesystem Switch):虚拟文件系统,是一个目录树。树上不同的节点可以映射到物理的文件地址,可以挂载。
相当于一个解耦层,在具体的文件系统之上抽象的一层,为能够给各种文件系统提供一个通用的接口,使上层的应用程序能够使用通用的接口访问不同文件系统、不同的驱动。
inode号:文件打开的时候有一个inode号,读到pagecache页缓存中。
两个程序如果打开的是统一文件,共享的是同一个pagecache 4k。
如果后续我们对pagecache进行了修改,会产生脏数据,这时候需要使用flush刷新到磁盘中去。“脏”这个标识是内核对于上层打开的文件的一个统一的管理,并不是针对某一个文件。内核会根据自己的设定,把数据输入到磁盘中去。如果你在5秒钟内产生了3.8G数据,又恰好没有触发内核的写磁盘操作,这时候突然断电会导致你这些数数据的丢失。
如果两个线程同时修改同一块数据,会加锁。
Redis的持久化,也是受操作系统内核这种机制的约束。
虚拟目录树
boot目录在分区3中,其他目录都在分区1中。
计算机引导的时候,先挂载了3分区,又挂载了1分区。新的挂载覆盖了原有的boot目录
虚拟文件系统进行了一个抽象:一切接文件。
磁盘文件、摄像头、打印机,都被看做是文件,基于文件的这种抽象,就可以应用到IO流了。
那么如何区分这些不同的文件?我们引入文件类型:
硬链接
两个变量名指向了同一个物理位置
无论是硬链接还是软连接,如果修改任意一方,另外一个文件也会看到这个变化。
如果删掉了其中一个文件,另外一方还能找到这个文件。相当于只是删除了一个引用。
软链接
软链接是两个独立的文件。修改任意一个文件,另一个文件能看到这个变化。
如果删除原有的msb.txt
,则xxoo.txt
找不到链接了,会标红报错。
如果删除原有的msb.txt
,则xxoo.txt
找不到链接了,会标红报错。
下面我们开始搞事情,做一个小实验,证明一切皆文件
用dd命令生成一个空的mydisk.img文件
挂载到虚拟的环回设备上,挂载之后进行格式化
把新的loop0挂载到原来的/mnt/ooxx目录中去
现在/mnt/ooxx是空的
我们希望用子目录模仿根目录里面的目录结构,以及程序的摆放位置。
1、找到bash所在位置,拷贝过来
2、将bash需要动态链接的文件,也拷贝过来
将根目录切换到当前目录,并将当前目录下的bin下的bash启动
在当前bash中输出“hello mashibing”,重定向到根目录下的abc,txt文件中
可以看到abc.txt被输出到新的根目录下。
那么Docker呢?也是这个原理吗?
这个实验不同于Docker,Docker更加复杂,它不只是文件系统的命名空间的一个子域。
Docker复用的是物理机的内核,Docker里面跑的是进程,先有镜像,有了img镜像之后,才有container容器的概念。
源于整个虚拟文件系统的支撑。
文件描述符
1、lsof命令
lsof是list open files的简称,它的作用主要是列出系统中打开的文件,基本上linux系统中所有的对象都可以看作文件,lsof可以查看用户和进程操作了哪些文件,也可以查看系统中网络的使用情况,以及设备的信息。
创建一个文件描述符8,用来读取ooxx.txt
NODE列:表示Inode号
如果lsof加上-o参数的话,会显示一列OFFSET,表示当前读文件位置的指针。
使用read读文件:
新开了一个bash标签页,用一个新的文件描述符6,去读取ooxx.txt
证明两个进程读取文件时,不会相互影响:
我们可以得出这个结论,内核为每一个进程各自维护了一套数据,包括fd文件描述符。
fd维护了一些关于文件的偏移、Inode号,以及元数据信息等。
这些内容看起来和Java没什么关系,但是在使用Java进行文件IO时,关系到每种不同的写法的成本。所以还是很重要的~
Socket 文件类型示例
用文件描述符8,指向一个socket连接
关于/proc目录
与其它常见的文件系统不同的是,/proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件
/proc/$$
是当前bash的文件
$$
BASHPID
是当前bash的pid
/proc/$$/fd
是当前程序的所有的文件描述符,也可以使用lsof -op $$
看见当前进程的文件描述符的细节,包括偏移量、指针等等。
关于0
标准输入1
标准输出
<
表示输入>
表示输出
ls 命令的标准输出
ls ./ 1> ls.out
将ls的标准输出1重定向到ls.out
cat 命令的标准输入、标准输出
cat 0< test.txt 1> cat.out
将cat的标准输入重定向为 test.txt , 将其标准输出重定向为 cat.out
read命令的标准输入、标准输出
重定向操作符<
>
的对接
让两个流写到不同的文件中去:
让两个流写到相同的文件中去:
管道命令|
head
默认读前 10 行
head -3
读前 3 行
tail
默认读后 10 行
tail -3
读后 3 行
head -8 test.txt | tail -1
通过管道,让左面的输出流入右面的输入,实现仅输出第 8 行
Linux 前置知识
进程之间的父子关系
当前所在的bash的进程id号
在当前bash中新开一个bash,打印新开的bash的进程id号
使用pstree
查看进程关系
使用exit
,又回到了11053
关于变量
父进程定义的变量x,不能在子进程取到,除非使用export
这也是为什么在/etc/profile/
配环境变量的时候,要添加export
的原因
关于代码块、管道开启的子进程
在一个花括号中,所有的指令在同一个进程中执行。
bash是解释执行的,如果看到管道,会将管道左侧的命令独立开启一个子进程,管道右侧的命令独立开启一个子进程,用管道进行对接。而进程的隔离级别很高,所以在新开启的进程中对变量a的修改,不会影响父进程a的值。
$$
的优先级高于|
,$BASHPID
的优先级高于|
看一下进程号,可以发现管道两侧开启的是独立的子进程
实际上,我们进行的是如下管道操作:
下面这个图可以看到,管道左侧的进程和右侧的进程通过管道被对接起来
我们也可以使用
lsof -op 4512
lsof -op 4513
这两个命令,看到两个子进程中正在开启的管道。
PageCache
做完上面的铺垫,我们可以先对PageCache做一个粗俗的理解了。
事实上,在很多地方都有缓存:
应用程序中:在application中可以有缓冲区(比如我们在Java中常用的BufferXXX IO)内核中的页缓存硬盘上的缓冲区
如果每次应用程序想要读数据的时候,都要通过cpu去取的话,效率会很低,于是我们了加入协处理器。
下面我们搞一个事情
写一个mysh小脚本,待会儿使用的时候参数传给0
OSFileIO.java
public class OSFileIO {static byte[] data = "123456789\n".getBytes();static String path = "/root/testfileio/out.txt";public static void main(String[] args) throws Exception {switch ( args[0]) {case "0" :testBasicFileIO();break;case "1":testBufferedFileIO();break;case "2" :testRandomAccessFileWrite();case "3"://whatByteBuffer();default:}}//最基本的file写public static void testBasicFileIO() throws Exception {File file = new File(path);FileOutputStream out = new FileOutputStream(file);while(true){// Thread.sleep(10);out.write(data);}}//测试buffer文件IO// jvm 8kB syscall write(8KBbyte[])public static void testBufferedFileIO() throws Exception {File file = new File(path);BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));while(true){Thread.sleep(10);out.write(data);}}//...
用上面的mysh
启动,传参为0,也就是执行了testBasicFileIO()
这个方法
测试突然断电。断电前我们看到,out.txt文件在不断增大:
断电后重启,文件大小都是0:
上面这个测试说明了缓冲区的存在。在一段时间内,并没有进行磁盘的刷写。