一、操作系统宏观介绍

123 阅读10分钟

操作系统宏观介绍

操作系统是通过内核程序对硬件进行操作的。我们平时使用的应用程序是通过调用操作系统提供的功能接口,实现对硬件设备数据的读写操作。

应用程序要读写磁盘某一块数据时,内核程序会先把数据读取到内存中,默认是以4KB为单位进行读写的,这4KB称为pagecache。当多个应用程序要对磁盘上同一个文件进行读写时,内核程序会先判断此文件是否已经加载到内存中,如果已经加载,那么这些应用程序就操作同一个pagecache,不过可能需要加锁;如果没有就加载后再操作。

被应用程序修改过的pagecache会被内核程序标记为dirty,内核程序有一定的机制会将修改的pagecache刷新到磁盘上。内核程序提供了立即刷新的接口,如果应用程序调用此接口,修改的pagecache会立即刷新到磁盘上;如果没有调用此接口,那么什么时候刷新就由内核程序决定了。

linux操作系统内核程序对磁盘上的内容进行了抽象管理,创建了VFS(虚拟文件系统---树状结构)。 VFS将磁盘内容划分成了文件夹和文件,每个文件都有自己的inode唯一标识。

linux内核程序将inode、pagecache、dirty这些概念和操作封装成了FD(文件描述符)。应用程序是通过FD相关接口实现的磁盘数据的读写操作。

image.png

虚拟文件系统

linux操作系统中,虚拟文件系统是文件和文件夹构建成一个树状结构,并且有一个根目录,所有的文件和文件夹都在这个根目录下。

image.png

虚拟文件系统将所有的文件和文件夹放在根目录下,但是并不意味着他们来自同一个磁盘分区。

image.png

从df的命令结果可以看出,根目录下的/boot文件夹下的内容来自/dev/sda1磁盘分区,其他的路径内容来自/dev/sda3这个分区。至于tmpfs,这是个临时文件系统,是一块内存空间,数据会存在内存中。

对于linux操作系统,磁盘分区路径是在/dev目录下。例如/dev路径下的sda1和sda3就是磁盘的两个分区。

挂载/卸载路径下的磁盘分区

挂载

将某个磁盘分区挂载在某个路径下,使用的是mount命令。

命令格式:mount 磁盘分区路径(/dev/sda1) 根目录下某个路径

image.png

卸载

将某个磁盘分区挂载在某个路径下,使用的是umount命令。

命令格式:umount 根目录下某个路径

image.png

文件类型

使用ll命令可以查看路径下的所有文件以及文件类型

-:普通文件 d:文件夹 b:块设备(磁盘) c:字符设备(一些中断,例如鼠标,键盘) l:连接(硬链接和软连接) s:socket p:pipeline

文件描述符

使用lsof命令查看bash进程使用到的文件描述符

image.png

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:

image.png

ls ./ 命令是打印当前路径下的所有文件到屏幕上

ls ./ 1> ~/ls.out 1> 是将ls命令的标准输出流进行重定向,输出位置从屏幕修改为~/ls.out这个文件中。所以执行这个命令后,屏幕上不会打印任何信息,ls命令的执行结果写入了ls.out文件中。

例2:

image.png

read命令可以将一串字符输入到一个变量中,例子中是将一串字符输入到a变量中。

read a 0< cat.out 0< 是将read命令的标准输入流进行重定向,输入位置从屏幕修改为cat.out文件。所以执行这个命令后,会将cat.out文件中的第一行内容输入到a变量中。

例3:

image.png

1是标准输出流,2是错误输出流,所以执行 ls ./ /oosdfs 1> ls01.out 命令后,错误信息还是输出在了屏幕上,因为错误输出流没有重定向。而执行 ls ./ /oosdfs 1> ls01.out 2> ls02.out 命令后,错误信息就输出到ls02.out文件中了。

管道

管道的命令是:|

管道的作用是将左边命令的输出作为右边命令的输入。

linux操作系统在执行管道时,是创建出bash进程的两个子进程,然后将两个命令放到子进程中执行。管道左边子进程的输出作为右边子进程的输入。

例如:

首先查看下bash进程的进程号

image.png

执行以下命令

image.png

echo $BASHPID 命令是打印出子进程bash的进程号。

整个管道命令会创建两个子进程,右边子进程的cat命令会等待左边子进程的read命令结束。

我们另起一个终端,查看下4398这个bash进程下是否有两个子进程

image.png

可以看到有两个子进程,进程ID分别是:4512和4513

然后进入/proc/4512/fd和/proc/4513/fd这两个文件夹下,查看下进程使用的文件描述符

image.png

4512是管道左边的子进程,它的1标准输出流指向了管道;4513是管道右边的子进程,它的0标准输入流指向了管道。

pagecache

应用程序要读写磁盘某一块数据时,内核程序会先把数据读取到内存中,默认是以4KB为单位进行读写的,这4KB由称为pagecache。当多个应用程序要对磁盘上同一个文件进程读写时,内核程序会先判断此文件是否已经加载到内存中,如果已经加载,那么这些应用程序就操作同一个pagecache,不过可能需要加锁;如果没有就加载后再操作。

被应用程序修改过的pagecache会被内核程序标记为dirty,内核程序有一定的机制会将修改的数据刷新到磁盘上。内核程序提供了立即刷新的接口,如果应用程序调用此接口,修改的pagecache会立即刷新到磁盘上;如果没有调用此接口,那么什么时候刷新就由内核程序决定了。

在内核与硬件进行数据交互时,并不是通过cpu的寄存器,这样太占cpu资源,而是使用了协处理器(DMA)。协处理器专门协助cpu进行内核与硬件之间的数据读写。

image.png

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对象。