app 应用程序和硬件之间隔着一个内核,内核通过 pagecache 来维护数据,若 pagecache 数据被标识为 dirty
,就会有一个 flush 刷新的过程,刷写到磁盘中去,什么时候刷新决定着 IO 的模型
多个 app 应用程序可能共享一份 pagecache,只是它们对应的指针和偏移量 seek 不同,也就是说它们有各自不同的 fd 文件描述符
;若应用程序同时对一个 pagecache 同一个位置进行了修改,内核也会像 Java 一样有一个加锁的过程,也就是锁总线、屏障这些工作来保障安全性.
Linux 以页作为高速缓存的单位,当进程修改了高速缓存中的数据时,该页就会被内核标记为脏页,内核会在合适的时间把脏页的数据刷写到磁盘中去,以保持高速缓存中的数据与磁盘中的数据是一致的.
在 Linux 中将一切抽象为文件,维护着一个虚拟的文件系统
引用【Linux 内核设计与实现】 书籍:虚拟文件系统(有时也称作为虚拟文件交换,更常见的是简称 VFS)作为内核子系统,为用户空间程序提供了文件和文件系统的接口。系统中所有文件系统不但依赖 VFS 共存,而且也依赖 VFS 系统协同工作。通过虚拟文件系统,程序可以利用标准的 Unix 系统调用对不同的文件系统,甚至对不同介质上的文件系统进行读写操作,如下图:
目录树结构趋向于稳定,有一个 映射
过程,底下有不同的文件类型,通过 ll
命令可查看列表开头部分
-
:普通文件(可执行、图片、文本、REG)d
:目录l
:连接 -> 软链接、硬链接stat text.txt:查看文件的元数据信息
建立软链接:ln -sf /opt/vnjohn/softlink.txt /usr/local/text.txt
,text.txt 文本内容来自于 softlink.txt
硬链接 inode 号是一样的,指向的是同一块物理位置,同时会进行计数,当某一个文件删除就会减 1
软链接 inode 号是不一样的,但是和硬链接一样修改后的内容会同步更新
b
:快设备c
:字符设备 CHRs
:socketp
:pipeline 管道fd:文件描述符代表打开的文件,有 inode 号、seek 偏移指针的概念
任何程序都有:0-标准输入、1-标准输出、2-报错输出,有输入 <
、输出 >
两种 IO 存在
lsof -op $$
:查看当前进程下所有的文件描述符信息,o 代表 offset、p 代表 pid
整理一下文件符下常用的命令:
ll /proc/$$/fd
: 查看当前进程下所有的文件描述符信息,$$ 可以替换为某个进程号exec 8< vnjohn.txt
:创建一个文件描述符 8 标准输入来自于 vnjohn.txt 文件创建一个 vnjohn.txt 文本文本,并输入一些内容进去
vim vnjohn.txt
Hello World!
Study every day,not stopping!
read x 0<& 8
:让 x 变量的标准输入
来自于文件描述符 8,读取后再查看 lsof -op $$
,会发现文件描述符 8 的 offset 不再是 0t0,read 命令对换行符特别敏感,遇到换行符会马上停止,会用一个偏移量来补换行符执行:echo $x 命令,输出的内容是 vnjohn.txt 文本文件第一行的内容:Hello World!
如下图,ot13 = 第一行的内容长度 12 + 换行符 = 13
echo $$
:输出当前进程号/bin/bash
:进入到子进程父子进程之间数据是相互隔离的,即在父进程创建的变量到子进程中读取不到;除非变量是具备导出功能的,如:export 变量名
head -8 | tail -1
:显示的是第八行的内容ls /vnjohn 1> ls.txt 2>&1
:/vnjohn 是一个不存在的目录,会报错(有报错输出)标准输出到 ls.txt 文件中,报错输出到 1 号文件描述符中ls.txt 文件内容:
ls: cannot access /vnjohn: No such file or directory
管道
| 可以衔接输入和输出,左边是一个进程,右边又是另外一个进程;$$ 优先级高于管道,使用 $BASHPID 就可以
如:{ echo $BASHPID; read x; } | { cat;echo $BASHPID; read y; }
日常开发中,查看 Redis 进程,ps -ef | grep redis 也是应用到了管道的概念
Page Cache 由内核进行维护,算是中间层,以 4 KB 为单位进行存储,向应用程序分配数据时并不是全量去分配,只是分配它们所需要用到的那些 Page Cache,多个 app 应用程序可能共享一份 pagecache,只是它们对应的指针和偏移量 seek 不同
进程调度的是那些活跃的线程,当应用程序 app 经过 CPU 一、二、三级缓存到内核读取数据时(保护现场:用户态切换到内核态)发现 pagecache 不存在,触发缺页,此时需要 DMA(Direct Memory Access) 协处理器
到磁盘中拿数据,app 此时会是一个挂起状态,当从硬盘中拿到数据以后,DMA 发出中断标识给到 CPU,这时 app 就会变为一个可以运行的状态,等待被调度执行
App 应用程序都有自己对应的缓冲区,当要写入或读取数据时,比如:read(fd8)
,会触发系统调用:int 0x80Hex
中断描述符,对应软中断标识符表的条目,负责 用户态->内核态切换
,到达内核中,不管你读取多大的数据:1 byte 或多少 byte,内核都是以 4KB 为单位去读取的
CPU->硬盘执行速度是很慢的
),所以此时需要 DMA 协调处理器来处理借助于 pcstat 来观察 pagecache 元数据信息,此时先安装好 pcstat
https://github.com/tobert/pcstat/releases/download/v0.0.1/pcstat-0.0.1-Linux-arm64.tar.gz
查看内核所有的配置:sysctl -a
筛选与脏页有关的内核配置:sysctl -a | grep dirty
编辑修改内核配置:vi /etc/sysctl.conf
# 后台运行:内存向磁盘写数据触发的阈值
# 脏页刷写到磁盘,不是脏页的进行淘汰,单独起一个线程去处理这些数据
vm.dirty_background_ratio = 0
vm.dirty_background_bytes = 1048576
# 前台运行:当分配 PageCache 时大小达到比例以后,阻塞住,进行 LRU 淘汰
# 脏页刷写到磁盘,不是脏页的进行淘汰
vm.dirty_ratio = 0
vm.dirty_bytes = 1048576
# 将脏页写回磁盘花费时间 5s
vm.dirty_writeback_centisecs = 5000
# 脏页的生命周期可以存入多久 30s
vm.dirty_expire_centisecs = 30000
sysctl -p:保存并立即执行所修改的内核配置
OSFile.java 源码,待编译后运行的
public class OSFileIO {
static byte[] data = "123456789\n".getBytes();
static String path = "/opt/test-file-io/out.txt";
public static void main(String[] args) throws Exception {
switch (args[0]) {
case "0":
testBasicFileIO();
break;
case "1":
testBufferedFileIO();
break;
case "2":
testRandomAccessFileWrite();
default:
}
}
/**
* 最基本的 file 写
* @throws Exception
*/
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 file IO
* @throws Exception
*/
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);
}
}
/**
* 测试 nio file IO
* @throws Exception
*/
public static void testRandomAccessFileWrite() throws Exception {
RandomAccessFile raf = new RandomAccessFile(path, "rw");
raf.write("hello world\n".getBytes());
raf.write("study every day\n".getBytes());
System.out.println("write------------");
System.in.read();
raf.seek(5);
raf.write("vnjohn".getBytes());
System.out.println("seek---------");
System.in.read();
FileChannel rafchannel = raf.getChannel();
// mmap 堆外和文件映射的 byte not object
MappedByteBuffer map = rafchannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
// 不是系统调用,但是数据会到达内核的 pagecache
map.put("@@".getBytes());
// 曾经我们是需要 out.write() 这样的系统调用,才能让程序的 data 进入内核的 pagecache
// 曾经必须有用户态内核态切换
// mmap 内存映射,依然是内核的 pagecache 体系所约束的!!!换言之,还是会丢数据
System.out.println("map--put--------");
System.in.read();
// map.force(); // flush
raf.seek(0);
ByteBuffer buffer = ByteBuffer.allocate(8192);
// ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// buffer.put()
int read = rafchannel.read(buffer);
System.out.println(buffer);
buffer.flip();
System.out.println(buffer);
for (int i = 0; i < buffer.limit(); i++) {
Thread.sleep(200);
System.out.print(((char) buffer.get(i)));
}
}
}
创建一个脚本文件【/opt/test-file-io/my.sh、chmod +x my.sh】,内容如下:
rm -rf *out*
/usr/local/jdk8/bin/javac OSFileIO.java
strace -ff -o /opt/test-file-io/out /usr/local/jdk8/bin/java OSFileIO $1
javac 将 OSFileIO 文件编译为 class 文件
strace -ff:追踪进程所执行的结果输出对应的前缀文件【/opt/test-file-io/out】中
跳转目录:cd /opt/test-file-io
执行最基本 IO 写:./my.sh 0
执行 pcstat 观察页缓存信息:ll -h && pcstat out.txt
,显示每个文件的大小并把 Page Cache 的使用情况显示到 out.txt 文件下
为什么 Buffer 写比普通 File 要快?
用户态、内核态切换的次数不同,因为 buffer 在 JVM 中是以 8KB byte[] 字节数组存起来再调用内核写入内存的,但是普通 File 是每次写入都会调用内核写入到内存
Buffer、普通 File 在内核调用的方式区别如下:
write(4, "123456789\n", 10) = 10 byte
write(4, "123456789\n123456789\n123456789\n12"..., 8190) = 8190 ≈ 8K
接下来继续测试 Buffer IO 情况
1、执行 Buffer IO 写:./my.sh 1
2、执行 pcstat 观察页缓存信息:ll -h && pcstat out.txt
,显示每个文件的大小并把 Page Cache 的使用情况显示到 out.txt 文件下
3、使用 free -h
命令查看磁盘占用量大小
[root@node3 ~]# free -h
总大小. 使用大小. 空闲大小. 缓冲区大小. 可用大小.
total used free shared buff/cache available
Mem: 1.4G 139M 1.1G 8.6M 201M 1.2G
Swap: 2.0G 0B 2.0G
会出现以下两种情况:
ByteBuffer 基础认识
@Test
public void whatByteBuffer() {
// ByteBuffer buffer = ByteBuffer.allocate(1024); // 堆内分配
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);// 堆外分配
System.out.println("postition: " + buffer.position());
System.out.println("limit: " + buffer.limit());
System.out.println("capacity: " + buffer.capacity());
System.out.println("mark: " + buffer);
buffer.put("123".getBytes());
System.out.println("-------------put:123......");
System.out.println("mark: " + buffer);
buffer.flip(); // 读写交替:读取之前先要 flip
System.out.println("-------------flip......");
System.out.println("mark: " + buffer);
buffer.get();// 取一个字节:position 位置向右移
System.out.println("-------------get......");
System.out.println("mark: " + buffer);
buffer.compact();// // 写操作:从未读取的位置开始重新写入
System.out.println("-------------compact......");
System.out.println("mark: " + buffer);
buffer.clear();
System.out.println("-------------clear......");
System.out.println("mark: " + buffer);
}
flip:当往里面取出字节时,pos 向左移,limit 就要占用之前 pos 在右边的位置
compact:当往里面存放字节时,pos 向右移(将读取过的拿出去,重新回归到未存数据的地方)
接下来继续测试 NIO 情况
执行 NIO写:./my.sh 2
执行结果如下:
write------------ # 调用了普通 IO 写
seek--------- # 随机写
map--put-------- # 堆外映射写
java.nio.HeapByteBuffer[pos=4096 lim=8192 cap=8192]
java.nio.HeapByteBuffer[pos=0 lim=4096 cap=8192]
@@llovnjohn
study every day # ByteBuffer 写
Java 进程
里面的如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!
大家的「关注❤️ + 点赞 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!