参考:
https://sylvanassun.github.io/2017/10/29/2017-10-29-virtual_memory/
地址空间:非负整数地址的有序集合,如 { 0 , 1 , 2 , . . . } \{0,1,2,...\} {0,1,2,...}
线性地址空间:如果地址空间中的整数是连续的,则称为线性地址空间
虚拟地址空间:在一个带虚拟内存的系统中,如果CPU用n位2进制数表示虚拟地址,则该连续的虚拟地址形成的范围 ( 0 , 1 , . . 2 n − 1 ) (0,1,..2^n-1) (0,1,..2n−1)称为“虚拟地址空间” { 0 , 1 , 2 , . . . , 2 n − 1 } \{0,1,2,...,2^n-1\} {0,1,2,...,2n−1}
例如linux系统中用32位来表示虚拟地址,则虚拟地址空间为 { 0 , 1 , 2 , . . . , 2 32 − 1 } \{0,1,2,...,2^{32}-1\} {0,1,2,...,232−1},大小为4GB.
虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)
------------------------------------------------------参考《深入理解计算机系统》
linux内核控制并且管理硬件资源,包括进程的调度和管理、内存管理、文件系统管理、设备驱动管理、网络管理等等。并且提供应用程序统一的系统调用接口
Linux中每个进程都有自己独立的4G虚拟内存空间,各个进程的内存空间具有类似的结构。这4GB的虚拟地址空间划分成两个部分:内核空间和用户空间
Linux中每个用户进程都有自身的虚拟地址范围(用户空间),从0到TASK_SIZE。用户空间之上的区域(从TASK_SIZE到 2 32 2^{32} 232)保留给内核专用(内核空间),用户进程不能访问。TASK_SIZE是一个特定于计算机体系结构的常数,把地址空间按给定比例划分为两部分。linux的用户空间为3GB(用户进程自己使用),内核空间为1GB(被所有进程共享)
一个新进程建立的时候,内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体,task_struct中有一个struct mm_struct指针,mm_struct结构体抽象了进程自己的虚拟地址空间。
参考:https://www.cnblogs.com/Rofael/archive/2013/04/13/3019153.html
关于进程控制块的具体内容参考:https://www.cnblogs.com/Rofael/archive/2013/04/13/3019153.html
在每个mm_struct又都有一个pgd_t 指针pgd指向页表,然后通过页表实现从虚拟地址到物理地址的转换。
每个mm_struct中还有一个vm_area_struct指针mmap,vm_area_struct结构体描述的是一段连续的、具有相同访问属性的虚存空间。vm_area_struct结构所描述的虚存空间以vm_start、vm_end成员表示,它们分别保存了该虚存空间的首地址和末地址后第一个字节的地址,以字节为单位,所以虚存空间范围可以用[vm_start, vm_end)表示。
参考:https://blog.csdn.net/ywf861029/article/details/6114794
通常,进程所使用到的虚存空间不连续,且各部分虚存空间的访问属性也可能不同。所以一个进程的虚存空间需要多个vm_area_struct结构来描述。
在vm_area_struct结构的数目较少的时候,各个vm_area_struct按照升序排序,以单链表的形式组织数据(通过vm_next指针指向下一个vm_area_struct结构)。
但是当vm_area_struct结构的数据较多的时候,仍然采用链表组织的化,势必会影响到它的搜索速度。针对这个问题,vm_area_struct还添加了vm_avl_hight(树高)、vm_avl_left(左子节点)、vm_avl_right(右子节点)三个成员来实现红黑树,以提高vm_area_struct的搜索速度。
进程建立vm_area_struct结构后,只是说明进程可以访问这个虚存空间,但有可能还没有分配相应的物理页面并建立好页面映射。在这种情况下,若是进程执行中有指令需要访问该虚存空间中的内存,便会产生一次缺页异常。这时候,就需要通过vm_area_struct结构里面的vm_ops->nopage所指向的函数来将产生缺页异常的地址对应的文件数据读取出来。
总结: linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域(这个区域只是整个虚拟内存空间中的一小块),由于linux整个虚拟内存空间(3GB)中的虚拟内存区域功能(text段,Data段,BBS段,Heap段,MMAP段,Stack段)都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问,如下图所示:
关于linux虚拟内存空间分段的具体内容请看https://blog.csdn.net/love_gaohz/article/details/41310597
段名 | 存储内容 | 分配方式 | 生长方向 | 读写特点 | 运行态 |
---|---|---|---|---|---|
代码段text | 程序指令、字符串常量、虚函数表 | 静态分配 | 由低到高 | 只读 | 用户态 |
数据段data | 初始化的全局变量和静态变量 | 静态分配 | 由低到高 | 可读可写 | 用户态 |
BSS段bbs | 未初始化的全局变量和静态变量 | 静态分配 | 由低到高 | 可读可写 | 用户态 |
堆heap | 动态申请的数据 | 动态分配 | 由低到高 | 可读可写 | 用户态 |
映射段 | 动态链接库、共享文件、匿名映射对象 | 动态分配 | 由低到高 | 可读可写 | 用户态 |
栈stack | 局部变量、函数参数与返回值、函数返回地址、调用者环境信息 | 静态+动态分配 | 由高到低 | 可读可写 | 用户态 |
下面以 C++ 为例,看一下常见变量所属的内存段
来自https://blog.csdn.net/K346K346/article/details/45592329
#include
int a = 0; // a在数据段,0为文字常量,在代码段
char *p1; // BSS段,系统默认初始化为NULL
void main()
{
int b; //栈
char *p2 = "123456"; //字符串"123456"在代码段,p2在栈上
static int c =0; //c在数据段
const int d=0; //栈
static const int d; //数据段
p1 = (char*)malloc(10); //分配的10字节在堆
strcpy(p1,"123456"); //"123456"放在代码段,编译器可能会将它与p2所指向的"123456"优化成一个地方
}
下面补充一点关于内存管理的基础知识,并不针对linux内核,linux内核的内存管理请参考如下两篇文章:
1.Linux内存管理(上)
2.Linux内存管理(下)
这里有三个页的概念
虚拟页(VP, Virtual Page),虚拟空间中的页;
物理页(PP, Physical Page),物理内存中的页;
磁盘页(DP, Disk Page),磁盘中的页。
各个进程页面可以离散地分配到物理内存的页框中,为了记录每个页面与每个页框的对应关系,引入了页表
对于每个进程的虚拟内存页对应到物理内存页地址时,存在如下三种情况-
未分配:未分配的虚拟内存块上不会有任何数据,所以不占用任何物理内存空间(内存和磁盘)
缓存的:当前已缓存在内存中的已分配的页
未缓存的:未缓存在物理内存中的已分配页,它在保存磁盘中
多级页表:
简单地将就是进程虚拟地址先转化成进程的【页号:页内偏移】;然后查询页表去找到对应在物理内存中的【页框号】,页内偏移地址是一样的,
关于虚拟地址到物理地址的转换
虚拟内存系统利用某种方法来判定一个虚拟页是否缓存在DRAM(内存)中,如果是,还必须确定这个虚拟页存放在哪个物理页中。这些功能是由软硬件联合提供的,包括操作系统软件、MMU中的地址翻译硬件和一个存放在物理内存中叫做页表的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换未物理地址时,都会读取页表,操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。
下图展示了MMU如何利用页表来实现这种映射。CPU中一个控制寄存器,页表基址寄存器(Page Table Base Register,PTBR)指向当前页表。N位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(Virtual Page Offset,VPO)和一个(n-p)位的虚拟页号(Virtual Page Number,VPN)。MMU利用VPN来选择适当的PTE.
------------------------------------------------------参考《深入理解计算机系统》
按照程序自身的逻辑关系将进程的虚拟地址空间划分成若干个段,每个段都有一个段名,每段从0开始编址,地址格式【段名:偏移】
物理内存的分配以进程的段为单位分配,每个段在物理内存中占据连续空间,但各段之间可以不相邻
同理,进程被分为多段,各段被离散地装入内存,为了保证程序能正常远行,就必须记录进程的各段与物理内存的各段之间的对应关系,即段表
分页式管理:主要目的是实现离散分配,提高内存利用率,分页仅仅是系统管理上的需要,完全是系统行为,对用户不可见。页的大小固定且由系统决定。
分段式管理:主要目的是更好地满足用户需求,一个段通常包含一组属于一个逻辑模块的的信息,分段对用户是可见的,用户编程时需要显示地给出段名。段的长度不固定,决定于用户编写的程序。
分段有利于段的共享:在分段系统中,段的共享是通过段表中相应表项指向被共享段的同一个物理副本来实现的。当一个作业正在从共享段中读取数据时,必须防止另一个作业修改此共享段中的数据。不能修改的代码称为纯代码或可重入代码,这样的代码和不能修改的数据是可以共享的,可修改的代码或数据不能共享。(这也说明了为啥共享库的的代码只会在内存中保存一次,然后所有的程序都可以共享这一段代码)
段页式存储管理方式:将分段和分页结合,即先将用户进程分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。
段表中存储的是段号、状态、页表大小、页表起始。每个段表项的长度相等,一般段号是隐藏的。
每个页表由页号、页面存放的页框号组成。每个页表项的长度相等,一般页号是隐藏的。
在linux中CPU进程有两种状态:核心态和用户态,也即两种特权级别,核心态的进程可以访问地址高于TASK_SIZE的内存区域,用户态禁止访问内核空间,这样可以防止进程无意间修改内核的数据。
当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。
在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。
所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。
Linux 系统进程被划分为用户态和内核态两种特权级别
在内核态下,进程运行在内核地址空间中, 此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。
在用户态下,进程运行在用户地址空间中,被执行的代码只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址。
所以,区分内核空间和用户空间本质上是要提高操作系统的稳定性及可用性。
其实所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务。
比如应用程序要读取磁盘上的一个文件,它可以向内核发起一个 “系统调用” 告诉内核:“我要读取磁盘上的某某文件”。其实就是通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并且拿到了想要的数据。
再比如,库接口malloc申请动态内存,malloc的实现内部最终还是会调用brk()或者mmap()系统调用来分配内存。也是先切换到内核态,分配成功后在切换会用户态。
系统调用,其实系统调用本身就是中断,其是软件中断,跟硬中断不同。
异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常。
外设中断:当外设完成用户的请求时,会向CPU发送中断信号。
参考:https://www.cnblogs.com/huxiao-tee/p/4660352.html
1,打开或创建文件,得到文件描述符,
2,将内存中的数据以一定的格式和顺序写入文件,或者将文件中的数据以一定的格式和顺序读入到内存;
3,关闭文件描述符
将文件从磁盘读取到内核空间,在从内核空间读取到用户空间,一共发生了两次数据拷贝
1,首先打开文件,使用的函数原型如下:
int open( //返回值:大于等于0代表操作成功,返回打开的文件描述符号,=-1
const char *pathname, //要打开的文件名
int flags, //打开的方式,打开方式包括:O_RDONLY 只读方式 O_WRONLY 只写,O_RDWR读写,O_CREAT创建,O_EXCL文件如果存在,使用此标记,会返回错误
mode_t mode); //指定创建文件的权限,只对创建文件有效,对于打开无效;
2,获取文件大小
int fstat(int fd,//文件描述符号
struct stat*buf);//返回文件属性结构体
返回值:成功返回0;失败返回-1
3,把文件映射成虚拟内存
void *mmap(void *addr, //从进程的那个地址开始映射,如果为NULL,由系统指定;
size_t length, //映射的地址空间的大小
int prot, //内存的保护模式
int flags,//映射模式 有匿名,私有,保护等标记 具体查询man手册;
int fd, //如果为文件映射,则此处为文件的描述符号
off_t offset);//如果为文件映射,则此处代表定位到文件的那个位置,然后开始向后映射。
返回值:映射成功,返回首地址;
4,通过对内存的读写来实现对文件的读写
通常使用:memset 和memcpy来实现操作;
5,卸载映射
int munmap(void *addr, //要卸载的内存的地址
size_t length);//内存的大小
6,关闭文件
int close(int fd); //要关闭的文件描述符号 ,成功返回0,错误返回-1,错误参照errorno;
文件磁盘地址映射到虚拟内存区域,这一步没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,此时通过已经建立好的映射关系,使用一次数据拷贝,就将文件从磁盘中传入内存的用户空间中,供进程使用
mmap内存映射的实现过程,总的来说可以分为三个阶段:
(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
1、进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
2、在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
3、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
4、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
5、为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
6、通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。
7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
8、通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。
(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。
9、进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
10、缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
11、调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
12、之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。
#include
#include
#include
#include
#include
#include
typedef struct{
char name[20];
short age;
float score;
char sex;
}student;
int main()
{
student *p,*pend;
//打开文件描述符号
int fd;
/*打开文件*/
fd=open("user.dat",O_RDWR);
if(fd==-1){//文件不存在
fd=open("user.dat",O_RDWR|O_CREAT,0666);
if(fd==-1){
printf("打开或创建文件失败:%m\n");
exit(-1);
}
}
//打开文件ok,可以进行下一步操作
printf("open ok!\n");
//获取文件的大小,映射一块和文件大小一样的内存空间,如果文件比较大,可以分多次,一边处理一边映射;
struct stat st; //定义文件信息结构体
/*取得文件大小*/
int r=fstat(fd,&st);
if(r==-1){
printf("获取文件大小失败:%m\n");
close(fd);
exit(-1);
}
int len=st.st_size;
/*把文件映射成虚拟内存地址*/
p=static_cast<student*>(mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0));
if(p==NULL || p==(void*)-1){
printf("映射失败:%m\n");
close(fd);
exit(-1);
}
/*定位到文件开始*/
pend=p;
/*通过内存读取记录*/
int i=0;
while(i<(len/sizeof(student)))
{
printf("第%d个条\n",i);
printf("name=%s\n",p[i].name);
printf("age=%d\n",p[i].age);
printf("score=%f\n",p[i].score);
printf("sex=%c\n",p[i].sex);
i++;
}
/*卸载映射*/
munmap(p,len);
/*关闭文件*/
close(fd);
}