最近在学习linux内存管理的知识,之前只是有个模糊的了解,现在重新查阅了些资料,系个人理解,总结如下。
1.几个资料中经常提到,又容易混淆的地址概念。
逻辑地址:由段基址(segment)和偏移量(offset)组成。偏移量指明了从段基址到实际地址间的距离。逻辑地址只有x86支持,一般32系统中逻辑地址共48位,段选择符16位,偏移量32位。linux中简化了对段的支持,即段基址为0。
线性地址:我们所说的地址空间,就是指的线性地址空间,32位系统中是4G的范围,x86中0~3G(0xC000 0000)为用户空间,3G-4G为内核空间。linux系统中程序使用的或者%p打印出来的地址其实就是线性地址。
虚拟地址:linux中,上述的线性地址就是一般意义上的虚拟地址,但是这样说也不太准确,一般线性地址是与物理地址一一映射的,而内核空间中的高端内存是没有跟物理内存一一映射的关系的,如vmalloc分配的地址,也称为虚拟地址,但不是线性地址。通常我们说的虚拟地址其实就是指的线性地址。
物理地址:处理器访问内存或外设所使用的地址。
2.几种地址的转换关系。
3.程序中几个重要的区域。
一般程序会分为代码段、数据段、bss段、堆、栈五个区域。
代码段:存放程序的静态代码,只读。
数据段:存放静态变量、已初始化的全局变量。
bss段:存放未初始化的全局变量,且全部置0。
堆(heap):程序运行时动态内存分配的区域。
栈(stack):存放局部变量、函数的部分参数等。
在内存中的关系如下图:
其中数据段、bss、堆在内存是连续的,栈从用户空间的顶部往下生长。
下面这个例子是程序各段运行是打印的地址:
(原形取自《 User-Level Memory Management》)
#include<stdio.h>
#include<malloc.h>
#include<unistd.h>
int bss_var;
int data_var0=1;
int main(int argc,char **argv)
{
printf("below are addresses of types of process's mem/n");
printf("Text location:/n");
printf("/tAddress of main(Code Segment):%p/n",main);
printf("____________________________/n");
int stack_var0=2;
printf("Stack Location:/n");
printf("/tInitial end of stack:%p/n",&stack_var0);
int stack_var1=3;
printf("/tnew end of stack:%p/n",&stack_var1);
printf("____________________________/n");
printf("Data Location:/n");
printf("/tAddress of data_var(Data Segment):%p/n",&data_var0);
static int data_var1=4;
printf("/tNew end of data_var(Data Segment):%p/n",&data_var1);
printf("____________________________/n");
printf("BSS Location:/n");
printf("/tAddress of bss_var:%p/n",&bss_var);
printf("____________________________/n");
char *b = sbrk((ptrdiff_t)0);
printf("Heap Location:/n");
printf("/tInitial end of heap:%p/n",b);
brk(b+4);
b=sbrk((ptrdiff_t)0);
printf("/tNew end of heap:%p/n",b);
return 0;
}
它的结果如下
below are addresses of types of process's mem
Text location:
Address of main(Code Segment):0x8048388
____________________________
Stack Location:
Initial end of stack:0xbffffab4
new end of stack:0xbffffab0
____________________________
Data Location:
Address of data_var(Data Segment):0x8049758
New end of data_var(Data Segment):0x804975c
____________________________
BSS Location:
Address of bss_var:0x8049864
____________________________
Heap Location:
Initial end of heap:0x8049868
New end of heap:0x804986c
4.编程的角度分析内存管理的应用。
1)首先明确几个概念 :
linux虚拟内存区域 :linux将虚拟存储器组织成一些区域的集合,也叫段,一个区域就是已经存在的(已经分配的)虚拟存储器的连续片,如代码段、数据段、bss段等,不属于某个区域的虚拟页是不存在的,即程序中引用的虚拟地址必然包含在某一个段中。内核为每个进程分配一个任务结构tast_struct,其中的mm域指向mm_struct,描述了虚拟存储器当前状态,mm_struct中的pgd域指向全局页表、mmap域指向vm_area_struct结构的链表,内核中每个区域结构由vm_area_struct描述,每个进程的所有vm_area_struct分别用链表和红黑树两种数据结构组织便于查找。
内部碎片 :为了一小段内存的需要不得不分配一大块连续区域,未用的区域为内部碎片。用slab方式解决。
外部碎片 :虽然系统有足够多的空闲内存总量,但都不连续无法满足大块连续内存的需要。伙伴系统解决,但只能尽量减小外部碎片的产生,不能彻底消除。
2)用户态 :用户对内存的管理主要涉及文件的重映射,接口是mmap(),用来创建新的虚拟内存区域,并将对象映射到这些区域中,而这里的对象如磁盘上的文件等。原型如下:
#include<unistd.h>
#include<sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *start, size_t length);
start参数仅仅是一个暗示,通常定义为NULL,munmap()中的start为mmap返回的起始地址;
prot参数包含新映射区域的访问权限位:
PROT_EXEC
PROT_READ
PROT_WRITE
PROT_NONE:这个区域的页面不能被访问
flags参数由描述被映射对象类型的位组成:
MAP_ANON:被映射的是匿名对象,且相应的虚拟页面是请求二进制0的
MAP_PRIVATE:被映射的对象是私有的,写时拷贝的
MAP_SHARED:共享对象
注:如果要通过映射并修改一个文件的内容,需要在映射时设置为共享对象,否则修改的内容不会被保存到原文件,而且mmap必须以PAGE_SIZE为单位进行映射,不能映射串口及其他面向流的设备。
例:
bufp = mmap(-1, size, PROT_READ, MAP_PRIVATE|MAP_ANON, 0, 0);
使用mmap映射到用户空间的好处有,如在系统中负责和显存间读写大量数据,比用lseek、write极大得提高了吞吐量。再如控制PCI设备的程序,大多数PCI外围设备将控制寄存器映射到内存地址中,高性能的应用程序更愿意直接访问寄存器,而不是不停得调用ioctl去获得需要的信息。
fork()函数的工作原理 :
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配PID,为了给新进程创建虚拟存储器,它创建了当前进程的mm_struct、区域结构和页表的原样拷贝,它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都设置为写时拷贝。以后任何一个进程进行写操作时,写时拷贝机制就会创建新页面。
execve()函数的工作原理 :
如执行execve("a.out", NULL, NULL),加载并执行a.out需要经过以下步骤:
删除已存在的用户区域:删除当前进程虚拟地址空间的用户部分中的已存在的区域结构。
映射私有区域:为新程序的文本、数据、bss、栈创建新的内存区域结构,并分别映射a.out的相关部分,所有这些区域都是私有的、写时拷贝的。
映射共享区域:如果程序与共享库链接,如libc.so,则映射到用户虚拟地址空间中的共享区域内。
设置程序计数器PC:execve做的最后一件事是设置pc指针,是之指向新文本区域的入口。
用户态动态内存分配 :
接口是malloc(),malloc可以以字节为单位申请,但是内核中仍是以页为单位进行分配,malloc中调用了brk()系统调用。
3)内核态 :
进程中的每个虚拟内存区域由一个mm_area_struct描述,以链表和红黑树两种形式组织。
内核分配空闲页框接口是get_free_page(s)(),整页分配基于buddy实现,内核struct page存在mem_map[]中。
kmalloc是slab提供的接口,用于小内存的分配(<128KB)。
vmalloc()函数的说明:
分配的每个块都对应一个vm_struct结构,与进程虚拟内存区域(VMA)结构vm_area_struct不同,vm_area_struct在内核中管理并与进程的每个内存区域对应。
分配时物理页可以不连续,返回的虚拟空间地址连续,因此需要更新页表,而kmalloc()与get_free_page()不需要,且缺页时才分配物理页。
分配的地址不能在处理器之外使用,因为需要处理器中的mmu来翻译,如dma中用的地址就不能用vmalloc分配,一般用于装载模块时分配,且不能用在原子上下文中。
ioremap()函数的说明:
用于映射PCI缓冲区到内核空间(专用于I/O内存区域分配虚拟地址),但并不实际分配内存,也会建立新页表,注意不能把其返回的地址直接当指针直接访问,如直接赋值,要用read8()等接口访问。
4)内存操作常见错误 :
段错误:引用的地址不包含在任一个vm_area_struct的start和end区间内,如scanf("%d", val) //&val、用mmap映射的虚拟内存区已经被munmap释放,再去引用其中地址。
malloc申请的内存未初始化就使用。
声明的指针未指向一块内存就使用。
指针运算加减是以指向的数据类型长度为单位,而不是以字节为单位。
数据过界:数组栈溢出、混淆指针长度与指向的数据类型长度、循环时边界条件控制不准。
引用指针而不是指向的对象。如*val++;//(*val)++, ++的优先级高。
引用返回的栈指针,函数返回后栈已无效。
引用野指针或引用已释放数据块中的数据。
内存泄漏。
5)内核kmalloc、__get_free_page、zone_sizes_init调用关系 。
运维网声明
1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网 享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com