操作系统宏观介绍
操作系统是通过内核程序对硬件进行操作的。我们平时使用的应用程序是通过调用操作系统提供的功能接口,实现对硬件设备数据的读写操作。
应用程序要读写磁盘某一块数据时,内核程序会先把数据读取到内存中,默认是以4KB为单位进行读写的,这4KB称为pagecache。当多个应用程序要对磁盘上同一个文件进行读写时,内核程序会先判断此文件是否已经加载到内存中,如果已经加载,那么这些应用程序就操作同一个pagecache,不过可能需要加锁;如果没有就加载后再操作。
被应用程序修改过的pagecache会被内核程序标记为dirty,内核程序有一定的机制会将修改的pagecache刷新到磁盘上。内核程序提供了立即刷新的接口,如果应用程序调用此接口,修改的pagecache会立即刷新到磁盘上;如果没有调用此接口,那么什么时候刷新就由内核程序决定了。
linux操作系统内核程序对磁盘上的内容进行了抽象管理,创建了VFS(虚拟文件系统---树状结构)。 VFS将磁盘内容划分成了文件夹和文件,每个文件都有自己的inode唯一标识。
linux内核程序将inode、pagecache、dirty这些概念和操作封装成了FD(文件描述符)。应用程序是通过FD相关接口实现的磁盘数据的读写操作。
虚拟文件系统
linux操作系统中,虚拟文件系统是文件和文件夹构建成一个树状结构,并且有一个根目录,所有的文件和文件夹都在这个根目录下。
虚拟文件系统将所有的文件和文件夹放在根目录下,但是并不意味着他们来自同一个磁盘分区。
从df的命令结果可以看出,根目录下的/boot文件夹下的内容来自/dev/sda1磁盘分区,其他的路径内容来自/dev/sda3这个分区。至于tmpfs,这是个临时文件系统,是一块内存空间,数据会存在内存中。
对于linux操作系统,磁盘分区路径是在/dev目录下。例如/dev路径下的sda1和sda3就是磁盘的两个分区。
挂载/卸载路径下的磁盘分区
挂载
将某个磁盘分区挂载在某个路径下,使用的是mount命令。
命令格式:mount 磁盘分区路径(/dev/sda1) 根目录下某个路径
卸载
将某个磁盘分区挂载在某个路径下,使用的是umount命令。
命令格式:umount 根目录下某个路径
文件类型
使用ll命令可以查看路径下的所有文件以及文件类型
-:普通文件 d:文件夹 b:块设备(磁盘) c:字符设备(一些中断,例如鼠标,键盘) l:连接(硬链接和软连接) s:socket p:pipeline
文件描述符
使用lsof命令查看bash进程使用到的文件描述符
lsof -p $$命令中,$$表示bash进程的进程号。lsof -p 命令可以查看某个进程用到了哪些文件描述符。
PID:进程ID FD:文件描述符名称 TYPE:文件类型。DIR表示文件夹,REG表示普通文件,CHR表示字符文件 SIZE/OFF:读写偏移量 NODE:文件唯一标识 NAME:文件路径
每个程序都有0u,1u和2u这三个文件描述符。0u表示普通的输入流,1u表示普通的输出流,2u表示异常的输出流。
在/proc路径下,有很多文件夹,每个文件夹代表一个进程,文件夹的名称代表进程ID。每个进程文件夹中有一个fd文件夹,fd文件夹中是这个进程用到的文件描述符。
标准重定向操作符
每个程序都有输入和输出流,而linux系统给每个程序分配了三个输入输出流文件描述符,分别是:0表示标准输入流;1表示标准输出流;2表示错误输出流。
重定向操作符可以修改程序输入输出流的指向。例如修改输入流读取的文件和修改输出流输出的文件。
标准重定向操作符有两个:<表示输入重定向;>表示输出重定向
重定向操作符与输入输出流配合使用时,重定向操作符必须写在右侧,而且与输入输出流之间不能有其他符号。
例1:
ls ./ 命令是打印当前路径下的所有文件到屏幕上
ls ./ 1> ~/ls.out 1> 是将ls命令的标准输出流进行重定向,输出位置从屏幕修改为~/ls.out这个文件中。所以执行这个命令后,屏幕上不会打印任何信息,ls命令的执行结果写入了ls.out文件中。
例2:
read命令可以将一串字符输入到一个变量中,例子中是将一串字符输入到a变量中。
read a 0< cat.out 0< 是将read命令的标准输入流进行重定向,输入位置从屏幕修改为cat.out文件。所以执行这个命令后,会将cat.out文件中的第一行内容输入到a变量中。
例3:
1是标准输出流,2是错误输出流,所以执行 ls ./ /oosdfs 1> ls01.out 命令后,错误信息还是输出在了屏幕上,因为错误输出流没有重定向。而执行 ls ./ /oosdfs 1> ls01.out 2> ls02.out 命令后,错误信息就输出到ls02.out文件中了。
管道
管道的命令是:|
管道的作用是将左边命令的输出作为右边命令的输入。
linux操作系统在执行管道时,是创建出bash进程的两个子进程,然后将两个命令放到子进程中执行。管道左边子进程的输出作为右边子进程的输入。
例如:
首先查看下bash进程的进程号
执行以下命令
echo $BASHPID 命令是打印出子进程bash的进程号。
整个管道命令会创建两个子进程,右边子进程的cat命令会等待左边子进程的read命令结束。
我们另起一个终端,查看下4398这个bash进程下是否有两个子进程
可以看到有两个子进程,进程ID分别是:4512和4513
然后进入/proc/4512/fd和/proc/4513/fd这两个文件夹下,查看下进程使用的文件描述符
4512是管道左边的子进程,它的1标准输出流指向了管道;4513是管道右边的子进程,它的0标准输入流指向了管道。
pagecache
应用程序要读写磁盘某一块数据时,内核程序会先把数据读取到内存中,默认是以4KB为单位进行读写的,这4KB由称为pagecache。当多个应用程序要对磁盘上同一个文件进程读写时,内核程序会先判断此文件是否已经加载到内存中,如果已经加载,那么这些应用程序就操作同一个pagecache,不过可能需要加锁;如果没有就加载后再操作。
被应用程序修改过的pagecache会被内核程序标记为dirty,内核程序有一定的机制会将修改的数据刷新到磁盘上。内核程序提供了立即刷新的接口,如果应用程序调用此接口,修改的pagecache会立即刷新到磁盘上;如果没有调用此接口,那么什么时候刷新就由内核程序决定了。
在内核与硬件进行数据交互时,并不是通过cpu的寄存器,这样太占cpu资源,而是使用了协处理器(DMA)。协处理器专门协助cpu进行内核与硬件之间的数据读写。
linux操作系统数据刷盘配置:
vm.dirty_background_ratio = 0
vm.dirty_background_bytes = 1048576
vm.dirty_ratio = 0
vm.dirty_bytes = 1048576
vm.dirty_writeback_centisecs = 5000
vm.dirty_expire_centisecs = 30000
首先内存大小按照4KB来划分,可以得出整个内存可以放多少个pagecache。
vm.dirty_background_ratio:当内存中脏页个数占内存可以放的pagecache个数的比例达到这个阈值时,开始刷盘。值为0说明此功能禁用。
vm.dirty_background_bytes:当内存中脏页大小达到这个阈值时,开始刷盘。
vm.dirty_ratio:当内存中脏页个数占内存可以放的pagecache个数的比例达到这个阈值时,应用程序阻塞往pagecache中写数据,并且开始刷盘。
vm.dirty_bytes:当内存中脏页大小达到这个阈值时,应用程序阻塞往pagecache中写数据,并且脏页开始刷盘。
vm.dirty_writeback_centisecs:每个一段时间,开始刷盘。值为5000说明是50秒刷盘一次。
vm.dirty_expire_centisecs:某个脏页一直没有刷盘,并且时间达到这个时间,那么下次刷盘此脏页会优先。
可以使用pcstat命令查看某个文件或者文件夹的缓存状态,包括文件的总大小、已缓存的字节数以及未缓存的字节数。
另外,如果应用程序每调用一次flush,脏页就会触发刷盘。flush就是内核提供的脏页立即刷盘的系统调用。
引发知识点:
java提供有两种数据流,用于进行数据IO操作,分别是FileOutputStream和BufferedOutputStream。
它们的区别是BufferedOutputStream默认有一个8KB的数组,在数据写pagecache前,先写到数组中,然后一次性将数组中数据写到pagecache中。而FileOutputStream只能通过cpu的寄存器慢慢往pagecache中写。这两种方式的差异是BufferedOutputStream在单位时间内,可以减少很多次的内核态与用户态的切换,所以传输数据非常快。
文件IO
java提供了两个文件IO的包,一个是io,另一个是nio。
在io包中,java提供有两种数据流,用于进行数据IO操作,分别是FileOutputStream和BufferedOutputStream。
它们的区别是BufferedOutputStream默认有一个8KB的数组,在数据写pagecache前,先写到数组中,然后一次性将数组中数据写到pagecache中。而FileOutputStream只能通过cpu的寄存器慢慢往pagecache中写。这两种方式的差异是BufferedOutputStream在单位时间内,可以减少很多次的内核态与用户态的切换,所以传输数据非常快。
在nio包中,java提供了RandomAccessFile,通过read和write方法可以对文件进行读写。也可以通过getChannel方法获取通道。通道可以通过byteBuffer以及mapped对文件进行读写,速度会更快。
ByteBuffer
ByteBuffer有几个核心属性和方法
核心属性
pos:读写数据的起始位置
limit:读写数据的结束位置
cap:数组的容量
核心方法
put:添加数据
flip:准备读取ByteBuffer中的内容前调用的方法,会修改pos和limit的值。pos会变成0,limit会变成ByteBuffer中数据的结束位置
get:读取数据,直到limit位置结束
compact:修改pos和limit的值,pos会变成ByteBuffer中第一个空闲位置,limit会变成ByteBuffer的结尾
clear:清空数据
public void whatByteBuffer(){
// 堆内申请一个字节数组
// ByteBuffer buffer = ByteBuffer.allocate(1024);
// 堆外申请一个字节数组
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// ByteBuffer有三个属性
// pos是读写数据的起始位置
// limit是读写数据的结束位置
// cap是数组的容量
// 刚初始化的ByteBuffer,pos为0,limit为数组长度,cap为数组长度
System.out.println("postition: " + buffer.position());
System.out.println("limit: " + buffer.limit());
System.out.println("capacity: " + buffer.capacity());
System.out.println("mark: " + buffer);
// 往数组中写入123,pos会向后偏移3位
buffer.put("123".getBytes());
System.out.println("-------------put:123......");
System.out.println("mark: " + buffer);
// flip方法是准备读取ByteBuffer中的内容前调用的方法,会修改pos和limit的值
// pos会变成0,limit会变成ByteBuffer中数据的结束位置
// 如此一来,读取数据,就可以从头读到数据结束,完整的数据就读出来了
buffer.flip(); //读写交替
System.out.println("-------------flip......");
System.out.println("mark: " + buffer);
// 开始读取数据,直到limit位置结束
buffer.get();
System.out.println("-------------get......");
System.out.println("mark: " + buffer);
// compact方法也会修改pos和limit的值
// pos会变成ByteBuffer中第一个空闲位置
// limit会变成ByteBuffer的结尾
// 如此一来,就可以往ByteBuffer的空闲位置上添加数据了
buffer.compact();
System.out.println("-------------compact......");
System.out.println("mark: " + buffer);
// 清空数据
buffer.clear();
System.out.println("-------------clear......");
System.out.println("mark: " + buffer);
}
ByteBuffer只是把数据写到申请的字节数组中,还没有写到pagecache中。
ByteBuffer可以申请堆内的或者堆外的。这个堆是指JVM分配的堆空间。一般来说,针对文件的读写,堆外的效率高于堆内的。因为使用堆内的,最后还是会在堆外申请一块空间,然后将数据拷贝到堆外,再写到pagecache
RandomAccessFile
public static void testRandomAccessFileWrite() throws Exception {
//初始化RandomAccessFile对象,赋予读写权限
RandomAccessFile raf = new RandomAccessFile(path, "rw");
//往文件中写数据
raf.write("hello mashibing\n".getBytes());
raf.write("hello seanzhou\n".getBytes());
System.out.println("write------------");
//修改读写的偏移量,以此来修改文件中的数据
raf.seek(4);
raf.write("ooxx".getBytes());
System.out.println("seek---------");
//获取通道
FileChannel rafchannel = raf.getChannel();
//通过通道获取了MappedByteBuffer对象,底层原理是mmap,可以直接映射到pagecache
MappedByteBuffer map = rafchannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
//往文件中写数据,数据会直接写入pagecache。而且不会触发系统调用,这样就不会有用户态和内核态之间的转换
map.put("@@@".getBytes());
System.out.println("map--put--------");
//pagecache的数据立即落盘
//map.force();
//修改偏移量
raf.seek(0);
//申请一个堆内的ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(8192);
//申请一个堆外的ByteBuffer
//ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
//通道将文件中的内容写入ByteBuffer中
rafchannel.read(buffer);
//通道将ByteBuffer中的数据写到文件中
rafchannel.write(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)));
}
}
RandomAccessFile提供了三种读写文件的方式
第一种是通过RandomAccessFile对象,调用read和write方法。
第二种是通过管道以及mapped,底层使用mmap,直接映射到pagecache,不需要进行系统调用。
第三种是通过管道和ByteBuffer,这种方式需要进行系统调用。
针对文件IO,从效率来看,mapped > 堆外ByteBuffer > 堆内ByteBuffer > RandomAccessFile对象。