内存映射
内存映射文件和文件I/O过程的区别
文件系统IO:
系统调用
open(filenamep,mode)
read(fd,buf,count);
关于文件打开和文件读:https://www.cnblogs.com/lccsblog/p/11070503.html
关于设备管理:https://www.cnblogs.com/lccsblog/p/11070971.html
系统调用->页高速缓存->分配新页->磁盘->页高速缓存->用户进程空间的缓冲区
页高速缓存:
内核缓冲区在linux下的实现:
历史:
旧版本页高速缓存和缓冲区高速缓存,前者用来存放访问磁盘文件内容时生成的磁盘数据项,后者把通过文件系统访问的块的内容保留在内存中。
后来,由于效率原因,不在单独分配块缓冲区,相反把它们存放在叫做“缓冲区页”的专门页中,而缓冲区页保存在页高速缓存中。
只读 写时拷贝
引用《深入理解linux内核》:
1.页高速缓存是Linux内核所使用的主要磁盘高速缓存,在绝大多数情况下,内核在读写磁盘时都应用页高速缓存。新页被追加到页高速缓存以满足用户态进程的读请求。如果页不在高速缓存中,新页就被加到高速缓存中,然后用从磁盘读出的数据填充它。
2.页高速缓存中的每个页所包换的数据肯定属于某个文件。这个文件(索引节点)就称为页的所有者。
3.几乎所有的文件读和写操作都依赖于页高速缓存。只有在O_DIRECT标志被置位而进程打开文件的情况下才会出现例外,此时I/O数据的传送绕过了页高速缓存而使用了进程用户态地址空间的缓冲区。
4.磁盘高速缓存由内核拥有,不能被换出,并且对内核态的所有进程都是可见的。
注意区分与高速缓冲存储器的区别,高速缓冲存储器意在解决页表到内存的2次访问内存的问题,将页缓存在高速缓冲存储器中。高速缓冲存储器由快速的SRAM组成,直接制作在CPU芯片内,速度较快,几乎与cpu处于同一量级,小容量
用户进程空间缓冲区,对上层应用开发者并不透明
问题:在堆还是栈?
其实是按实现在堆上还是栈上都可以,大多数情况下都是在堆上。
先看一下系统调用:
read(fd,buf,count);
参数说明:
1.fd是文件描述符
2.buf是读出信息所应送入用户数据区首地址
3.count是要求传送的字节数。
可以看到缓冲区的地址需要应用层自己去指定。
再看下C标准库函数:
标准i/o缓冲区默认在用户空间的堆上windows下的c的fread:
fread->_fread_nolock_s->_filbuf->_getbuf()
在_getbuf()中可源代码:
FILE *stream;
/* Try to get a big buffer */
if (stream->_base = _malloc_crt(_INTERNAL_BUFSIZ))
{
/* Got a big buffer */
stream->_flag |= _IOMYBUF;
stream->_bufsiz = _INTERNAL_BUFSIZ;
}
这里_INTERNAL_BUFSIZ=4096;
由此我们可以看到,若用户没有显示设置自己的I/O缓区,那么C运行库的标准I/O程序(如fread)会自动在堆上分配一段大小为4096的缓冲区。
指定自己的缓冲区:
FILE *fp;
char buf[8192] = {0}; // 缓冲区初始化为0
char ch;
//以读/写的方式打开
if ((fp=fopen ("/Users/lichengcheng/Applications/workSpace/others/file/file/data.txt", "r+")) == NULL )
{
printf("Fail to open file\n");
return 0;
}
setvbuf(fp, buf, _IOFBF, 4096); // 设置流fp为全缓冲,缓冲区指向buf,大小为4096
// setvbuf(fp, buf,_IOLBF,1024);//设置流fp为行缓冲,缓冲区指向buf,大小为1024
// setvbuf(fp, buf,_IONBF,4096);//设置流fp为无缓冲
fread(&ch, 1, 1, fp); // 从流中读取一个字节的内容存放到变量ch中
printf("%c %c %c\n", buf[0], buf[1], buf[4095]);
这里我们在栈上开辟了一个8KB的空间作为缓冲区
总结:
I/O 缓冲区是位于哪个地方是可以我们自己决定的,
在默认的情况下,标准 I/O 缓冲区是由系统在堆中申请的一块内存,比如我们常见的的 cin / cout ,或者文件读写的 fstream 等。
但是这些I/O操作我们是可以自己给它设置缓冲区的,不过这些自定义的缓冲区想要在哪里申请,就自己决定了,可以直接在栈中申请,也可以在堆中申请。
为什么不直接将数据读到用户内存空间,内核完全有权限读写用户线程空间呀?
如下:
即必须保证I/O过程中,目的页面不能被换出
可执行文件的加载
先来看可执行目标文件的视图:
预处理->编译->汇编->链接->可执行文件
.init:定义了一个_init函数,用于可执行目标文件开始执行时的初始化工作。
.text:目标代码部分
.rodata:只读数据,如printf语句中的格式串,开关语句(switch-case)的跳转表等
.data:以初始化全局变量
.bss:未初始化的全局变量。对于auto型局部变量因为他们运行时被分配在栈中,因此既不会出现在.data节,也不会出现在.bss节
.symtab:符号表,链接前符号解析用到。不包含局部变量,链接器不需要这类变量的信息。
.strtab:字符串表
重点来看下程序头表
程序头表用来指示系统如何创建进程的存储映像:"它是一个结构数组。可执行目标文件中所有代码的位置连续,所有只读数据的位置连续,所有可读可写数据的位置连续。因而,这些连续的片段(chunk)被映射到存储空间(实际上就是虚拟地址空间)中的一个存储段,程序头表用于描述这种映射关系,一个表项说明一个连续的片段或者一个特殊的节,由elf头中的字段e_phentsize和e_phnum分别制定程序头表表项大小和表项数。"
可执行文件的加载过程:
《计算机系统基础》:"当启动一个可执行目标文件执行时",首先会通过某种方式调出常驻内存的一个称为加载器(loader)的操作系统程序来进行处理。例如:任何unix程序的加载执行都是通过execve系统调用函数来启动加载器进行的。加载器可根据可执行目标文件中的程序头表信息,将可执行目标文件中相关节的内容与虚拟地址空间中的只读代码段和可读写数据段通过页表建立映射,然后启动可执行目标文件中的第一条指令执行。"
《操作系统》从shell开始这样描述:
1.读取从键盘输入的命令行;
2.判断命令是否正确,且将命令行的参数改造为系统调用execve()内部处理所要求的形式。
3.终端进程调用fork()创建子进程,自身则用系统调用好wait()来等待子进程完成。
4.当子进程运行时,它调用execve()根据命令名制定的文件到目录中查找可执行文件,调入内存并执行这个实用程序(即执行这条命令)
5.如果命令行末尾有后台命令符号&,终端进程不执行等待系统调用,而是立即发提示符,让用户输入下一条命令,转1;如果命令末尾没有&,则终端进程要一直等待。当子进程完成处理后,向父进程报告,此时终端进程被唤醒,昨晚必要的判别工作后,再发提示符$,让用户输入新命令,重复上述处理过程。
上面过程结束之后注意:
加载器在加载可执行目标文件时,实际上只是把可执行目标文件中的只读代码段和可读写数据段通过页报映射到了虚拟地址空间中的确定位置,并没有真正把代码和数据从磁盘装入主存。
那么被建立映射的节的内容是在什么时候装入的呢:
再来看下虚拟地址到物理地址的转化,
已分配页框的情况:
由逻辑地址算页号,到到页表中找页号对应的页框号,根据物理地址=页框号*页面大小+页内偏移等到物理地址。
未分配页框的情况:
页表中的驻留标准位为0不能立即访问,产生缺页异常,存储管理根据磁盘地址将这个页面调入内存,然后重新启动响应指令。
如何找到页面对应的磁盘地址呢?页面与磁盘物理地址的对应表称为外页表,有操作系统管理。进程启动运行前系统为其建立外页表,查找外页表就能找到页面对应的磁盘存放地址。为了节省内存空间,外页表可存放在磁盘中,当发生缺页异常时才被调入。
再来看一些缺页异常引起的调页请求:
do_page_fault()首先排除编程错误而引起的对保护页面非法访问的异常,.确认时由读或者执行访问所引起的,有两种方法装入所缺的页,这取决于这个页是否被映射到一个磁盘文件。该函数通过检查vma线性区描述符的nopage字段来确定这一点,如果页被映射到一个文件,nopage字段就指向一个函数,该函数把所缺的页从磁盘装入到RAM。因此可能情况是:
1.vma->vm_ops->nopage字段不为null。在这种情况下,线性区映射了一个磁盘文件,nopage字段指向装入页的函数。
2.vma->vm_ops字段为null,或者vma->vm_ops->nopage字段为null。在这种情况下,线性区没有映射磁盘文件,也就是说,它是一个匿名映射,do_no_page()调用do_anonymous_page()函数获得一个新的页框
由此,页被分为了
有文件背景的页对应情况1,被会写的位置应当是页的所有者,即磁盘上的文件
匿名页对应情况2。匿名页对应的是程序执行时产生的堆区和栈区,这一部分页面在执行时有可能被换出到磁盘的交换区。
在上面第一种情况后续的逻辑为:设备无关层软件会去页高速缓存检查所缺页是否在页高速缓存中,如果是,则直接返回页框地址给页表更新。否则还是要启动设备控制器操作磁盘。
到此为止大家发现对于从未加载到内存的文件第一次到内存时最后的结果好像都是殊途同归:设备控制器进行磁盘操作。但是为什么这么多中间件选择用内存映射的方式去做文件I/O呢?
从上面的描述大家也许会发现,do_no_page中有一步逻辑直接返回了页高速缓存中的页框地址,即缺页中断被处理后,用户进程空间的页表拿到了属于内核空间的页高速缓存的页框地址。原本这块地址是不允许用户线程去访问的,但是在这种情况下却暴露了出来,对比之前文件I/O的操作少了一次,页高速缓存到用户空间缓存的数据拷贝。
由此可见,对比文件系统I/O过程,内存映射的方法少了一次数据的拷贝过程。另外页高速缓存是内核在管理的,相较于用户空间的缓冲区而言不会因为进程切换被换出,并且能够更好的共享,并且少的这一次拷贝其实也省去了一次内核态到用户态的过程
C语言怎么去使用内存映射读文件的
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offsize);
参数 | 说明 |
---|---|
start | 指向欲对应的内存起始地址,通常设为NULL,代表让系统自动选定地址,对应成功后该地址会返回。 |
length | 代表将文件中多大的部分对应到内存。 |
prot | 代表映射区域的保护方式,有下列组合:
|
flags | 会影响映射区域的各种特性:
在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。 |
fd | open()返回的文件描述词,代表欲映射到内存的文件。 |
offset | 文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。 |
返回值:若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/mman.h> main(){ int fd; void *start; struct stat sb; fd = open("/etc/passwd", O_RDONLY); /*打开/etc/passwd */ fstat(fd, &sb); /* 取得文件大小 */ start = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0); if(start == MAP_FAILED) /* 判断是否映射成功 */ return; printf("%s", start);
munmap(start, sb.st_size); /* 解除映射 */ }
结果如上,我们注意下flags参数的三种类型
1.共享型(MAP_SHARED):在线性区页上的任何写操作都会修改磁盘上的文件;而且如果进程对共享映射中的一个页进行写,那么这种修改对于其他映射了这同一个文件的所有进程来说都是可见的。
2.私有型(MAP_PRIVATE):当进程创建的映射只是为了读文件,而不是写文件时才会使用此种映射。出于这种目的,私有映射的效率要比共享映射的效率更高。但是对私有映射页的任何写操作都会使内核停止映射文件中的页,因此,写操作既不会改变磁盘上的文件,对访问想用文件的其他进程也不可见。但是私有内存映射中还没有被进程改变的页会因为其他进程进行的文件更新而更新。
共享内存映射的也通常都包含在页高速缓存中;私有内存映射的页只要还没有被修改,也都包含在页高速缓存中。当进程试图修改一个私有内存映射的页时,内核就把该页框进行复制,并在进程页表中用复制的页来替换原来的页框
Java用内存映射读文件
1.filechanel获取,buffer获取(只读,可写,写时复制)
FileChannel channel = FileChannel.open(path, options);
通过调用FileChannel类的map方法进行内存映射,map方法从这个通道中获得一个MappedByteBuffer对象(ByteBuffer的子类)。
你可以指定想要映射的文件区域与映射模式,支持的模式有3种:
- FileChannel.MapMode.READ_ONLY:产生只读缓冲区,对缓冲区的写入操作将导致ReadOnlyBufferException;
- FileChannel.MapMode.READ_WRITE:产生可写缓冲区,任何修改将在某个时刻写回到文件中,而这某个时刻是依赖OS的,其他映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的确切行为是依赖于系统的,但是它是线程安全的
- FileChannel.MapMode.PRIVATE:产生可写缓冲区,但任何修改是缓冲区私有的,不会回到文件中
- 这里可以看到与c里的一致
2.直接缓冲区和非直接缓冲区
非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中
直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在机 此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
//得到FileChannel
File file=new File("/etc/passwd");
FileInputStream in=new FileInputStream(file);
FileChannel channel = in.getChannel();
//文件映射
long length=channel.size();
MappedByteBuffer buffer= null;
buffer = channel.map(FileChannel.MapMode.READ_ONLY,0l,length);
System.out.println("The buffer is direct?"+buffer.isDirect());
//将文件读到一个字节数组中输出
byte res[]=new byte[(int)length];
buffer.get(res,0,(int)length);
String content=new String(res);
System.out.println(content);
可以看到通过map方法拿到的是直接缓冲区,根据直接缓冲区的特性,这里应该指的是页高速缓存中的页,不属于java用户进程管理
交换
用来为非映射页在磁盘提供给备份。从前面的讨论我们知道有三类页面必须由交换子系统处理。
1.进程匿名先行区(例如:用户态堆栈和堆)的页
2.属于进程私有内存映射的脏页(写时拷贝产生)
3.IPC共享内存区的页