前景知识
1 程序 & 进程
当我们使用shell程序执行某个指令时,其本质是shell帮我们找到这个指令对应的binary file文件并执行。这个可执行binaryfile就是所谓的程序
执行的一个程序,需要依赖操作系统为其分配内存,用来加载binary file和存储程序运行的过程中要操作的数据和产生的计算结果。加载到内存中运行起来的程序在操作系统中被称为进程。
我们可以将操作系统看作是一家外包公司,我们执行每一个程序就是对应着外包公司接受的一个项目,项目启动前必须起草好项目执行计划书,项目执行需要依赖于执行计划书中一行一行指令(程序binaryfile);每个项目启动需要独立的办公空间,通常是一个会议室(物理内存),会议室中不仅用来存放项目执行计划书,同时用来存储项目执行中的中间数据。
2 操作系统
操作系统是一个包含基本程序的集合,它用来有效管理控制硬件资源,并提供计算机运行所需要的功能(如网络功能)
![](https://img.haomeiwen.com/i9793627/88ddae9d450d343d.gif)
内核 (kernel)
内核 (kernel)是一段计算机程序,这个程序直接管理管理硬件,包括CPU、内存空间、硬盘接口、网络接口等;所有的计算机和硬件操作都要通过内核传递给硬件。
系统调用(system call)
Linux将内核的功能接口制作成系统调用(system call),这样用户不需要了解内核的复杂结构,就可以使用内核。
系统调用是操作系统的最小功能单位
3 物理内存
程序的运行离不开物理内存,物理内存在操作系统中被分隔成一块一块的页,每一个页的大小为4KB小空间,每一个页对应一个物理地址。
物理内存和会议室一样,它被分隔一个一个小的空间,每一个空间都对应着一个编号,比如3F-10,用来表示三楼十号会议室房间物理地址。
4 虚拟地址空间
操作系统在管理内存时,为保证每个进程分配的内存空间都是独立的,会为每一个进程都分配一个独立的虚拟地址空间,每一个虚拟地址空间都通过其内部的页表映射到真实物理内存地址。这样不同的进程在分配给自己独立内存中运行。
如果内存作为会议室,那么操作系统内存管理相当于会议室管理部门,每个进程都是一个项目组,他们都会向会议室管理部门获取一个虚拟地址如项目A被分配了一个"项目A-会议室1"这个虚拟地址,其映射到的是3F-3这个物理地址真实内存。
![](https://img.haomeiwen.com/i9793627/5cebdc911a6f7218.png)
5 虚拟地址空间分布
Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样,进程就可以很方便地访问内存,更确切地说是访问虚拟内存。
虚拟地址空间的内部又被分为内核空间和用户空间两部分。
内核空间
操作内核 (kernel)是一段计算机程序,这个程序直接管理管理硬件。那么内核 (kernel)运行同样需要分配的虚拟地址空间。分配给操作内核 (kernel)的虚拟地址空间,被称为内核空间
用户空间
每一个程序运行分配的虚拟地址空间,被称为用户空间
![](https://img.haomeiwen.com/i9793627/3bd446d026ad8868.jpg)
6 上下文切换
程序的执行本质就是将程序的二进制文件转换了CPU能识别的指令,被CPU执行,因此我们需要将程序的可执行文件加载到内存中,并在被操作系统内核调度时将内存中指令和指令相关的数据加载到CPU寄存器中。
![](https://img.haomeiwen.com/i9793627/0eda8b48c6b407c2.jpg)
当程序指令需要调用操作系统函数访问硬件资源时,这时会将进程在CPU执行当前的寄存器数据(上下文)保存下来,并让CPU重新加载内核的指令的和数据执行。
当执行完毕,重新调度到A进程时,会将A进程的上下文数据还原到CPU寄存器中继续执行。
![](https://img.haomeiwen.com/i9793627/ebec20ced358b743.jpg)
7 传统数据读写
RandomAccessFile readFile = new RandomAccessFile("write3.txt", "rw");
FileChannel fileChannel = readFile.getChannel();
ByteBuffer allocate = ByteBuffer.allocate(writeLen.intValue());
fileChannel2.read(allocate);
读取文件
![](https://img.haomeiwen.com/i9793627/ed9257f2dd6e15e9.jpg)
-
1、应用程序发起读数据操作,JVM会发起read()系统调用。
-
2、这时操作系统OS会进行一次上下文切换(把用户空间切换到内核空间)
-
3、通过磁盘控制器把数据copy到内核缓冲区中,这里的就发生了一次DMA Copy
-
4、然后内核将数据copy到用户空间的应用缓冲区中,发生了一次CPU Copy
-
5、read调用返回后,会再进行一次上下文切换(把内核空间切换到用户空间)
写入文件
RandomAccessFile writeFile = new RandomAccessFile("write3.txt", "rw");
FileChannel fileChannel2 = writeFile.getChannel();
ByteBuffer buffer=ByteBuffer.wrap("abcd".getBytes());
Long writeLen = fileChannel.write(buffers);
![](https://img.haomeiwen.com/i9793627/1ec124b85582c4df.jpg)
-
1、应用发起写操作,OS进行一次上下文切换(从用户空间切换为内核空间)
-
2、并且把数据copy到内核缓冲区Socket Buffer,做了一次CPU Copy
-
3、内核空间再把数据copy到磁盘或其他存储(网卡,进行网络传输),进行了DMA Copy
-
4、写入结束后返回,又从内核空间切换到用户空间
总结
传统数据读写为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。
6 MMAP
mmap是一种内存映射文件的方法,即将设备或者硬盘存储的一块空间映射到物理内存,然后通过页表将用户态进程空间的一段mmap映射区虚拟地址映射到文件对应物理内存。实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。用户态进程可以通过用户态的虚拟地址,经过页表映射后对映射文件内存进行读写操作。同时操作系统会内核态内存中创建一个临时映射到文件物理内存中,用来处理对这块物理资源的管理(操作系统负责统一管理硬件资源)。例如当物理内存数据发送改变,操作系统内核会通过内核态的映射将物理内存中脏页面(被修改)回写到对应的文件磁盘上。
![](https://img.haomeiwen.com/i9793627/a74d116eb83eaf7f.jpg)
6.1 mmap内存映射原理
-
进程启开始映射,并在虚拟地址空间中为映射创建vm_area_struct(虚拟映射区域结构)
-
调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
-
进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
6.2 mmap和传统数据读写的区别
RandomAccessFile readFile = new RandomAccessFile("map_write.txt", "rw");
FileChannel fileChannel = readFile.getChannel();
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
byte[] arr = new byte[(int) file.length()];
raf.read(arr);
socket.getOutputStream().write(arr);
整体流程的核心区别就是,把数据读取到内核缓冲区后,应用程序进行写入操作时,直接是把内核的Read Buffer的数据 复制到 Socket Buffer 以便进行写入,这次内核之间的复制也是需要CPU参与的。这个流程就少了一个CPU Copy,提升了IO的速度。不过发现上下文的切换还是4次,
6.3 mmap优点总结
-
1 对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。
-
2、实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
-
3、提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。
-
4 可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。
6.4 mmap的使用
将此通道的文件区域直接映射到内存中可以通过下列3 种模式将文件区域映射到内存中。
- 只读:试图修改得到的缓冲区将导致抛出ReadOnly BufferException 异常。( MapMode. READ ONLY)
- 读取/写人:对得到的缓冲区的更改最终将传播到文件;该更改对映射到同一文件的其他程序不一定是可见的。( MapMode.READ_ WRITE)
- 专用:对得到的缓冲区的更改不会传播到文件,并且该更改对映射到同一文件的其他程序也不是可见的;相反,会创建缓冲区已修改部分的专用副本。( MapMode.PRIVATE)
/**
* @param mode
* 根据只读、读取/写入或专用(写入时复制)来映射文件,分别为FileChannel.
* MapMode 类中所定义的READ ONLY 、READ_WRITE 和PRIVATE;
*
* @param position
* position : 文件中的位置,映射区域从此位置开始;必须为非负数。
*
* @param size
* 要映射的区域大小;必须为非负数且不大于Integer.MAX VALUE 。
*
*/
public abstract MappedByteBuffer map(MapMode mode,long position, long size)
只读模式案例
@Test
public void test_map_readOnly() throws Exception {
RandomAccessFile readFile = new RandomAccessFile("map_read.txt", "rw");
FileChannel fileChannel = readFile.getChannel();
try{
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
System.out.println((char) map.get());
System.out.println((char) map.get());
System.out.println((char) map.get());
System.out.println((char) map.get());
System.out.println((char) map.get());
System.out.println((char) map.get());
System.out.println((char) map.get());
System.out.println((char) map.get());
System.out.println((char) map.get());
System.out.println(map);
byte[] destArray = new byte[3];
map.get(destArray,0,3);
System.out.println(new String(destArray, Charset.forName("UTF-8")));
map = fileChannel.map(FileChannel.MapMode.READ_ONLY, 2, 2);
System.out.println((char) map.get());
System.out.println((char) map.get());
//map.putChar('a');
}finally {
readFile.close();
fileChannel.close();
}
}
读写模式案例
@Test
public void test_map_readAndWrite() throws Exception {
RandomAccessFile readFile = new RandomAccessFile("map_write.txt", "rw");
FileChannel fileChannel = readFile.getChannel();
try{
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
System.out.println( (char) map.get());
System.out.println( (char)map.get());
System.out.println( (char)map.get());
System.out.println( (char)map.get());
System.out.println( (char)map.get());
map.position(0);
map.put((byte) 'o');
map.put((byte) 'p');
map.put((byte) 'd');
map.put((byte) 'r');
map.put((byte) 's');
ByteBuffer allocate = ByteBuffer.allocate(5);
fileChannel.read(allocate);
String s = new String(allocate.array());
System.out.println(s);
}finally {
readFile.close();
fileChannel.close();
}
}
专用模式案例
@Test
public void test_map_private() throws Exception {
RandomAccessFile readFile = new RandomAccessFile("map_write.txt", "rw");
FileChannel fileChannel = readFile.getChannel();
try{
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.PRIVATE, 0, 5);
map.position(0);
map.put((byte) 'k');
map.put((byte) 'k');
map.put((byte) 'k');
map.put((byte) 'k');
map.put((byte) 'k');
map.flip();
System.out.println( (char) map.get());
System.out.println( (char)map.get());
System.out.println( (char)map.get());
System.out.println( (char)map.get());
System.out.println( (char)map.get());
ByteBuffer allocate = ByteBuffer.allocate(5);
fileChannel.read(allocate);
String s = new String(allocate.array());
System.out.println(s);
}finally {
readFile.close();
fileChannel.close();
}
}
force
将此缓冲区所做的内容更改强制写入包含映射文件的存储设备中
@Test
public void test_map_force() throws Exception {
RandomAccessFile writeFile = new RandomAccessFile("map_write2.txt", "rw");
FileChannel fileChannel = writeFile.getChannel();
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5000);
try{
/** 写入文件字节缓冲区数据 **/
ByteBuffer buffer=ByteBuffer.wrap("11111".getBytes());
long begin = System.currentTimeMillis();
for (int i=0;i<=100;i++){
map.put(buffer);
buffer.clear();
}
long end = System.currentTimeMillis();
System.out.println(end-begin);
ByteBuffer buffer1=ByteBuffer.wrap("22222".getBytes());
begin = System.currentTimeMillis();
for (int i=0;i<=100;i++){
map.put(buffer1);
map.force();
buffer1.clear();
}
end = System.currentTimeMillis();
System.out.println(end-begin);
}finally {
writeFile.close();
fileChannel.close();
}
}
load & isLoaded
isLoaded() 判断此缓冲区的内容是否位于物理内存中
load() 将此缓冲区内容加载到物理内存中
@Test
public void test_map_Load() throws Exception {
RandomAccessFile writeFile = new RandomAccessFile("map_write2.txt", "rw");
FileChannel fileChannel = writeFile.getChannel();
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5000);
System.out.println(map.isLoaded());
map.load();
System.out.println(map.isLoaded());
}
参考
网友评论