图解计算机基础网站:
你们好,我是小林。
很早之前写了一篇图解虚拟显存的文章:
近来想多写一些显存管理的文章,此次我们就以malloc动态显存分配为切入点,我在文中也做了小实验:
发车!
Linux进程的显存分布长哪些样?
在Linux操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部份,不同位数的系统,地址空间的范围也不同。例如最常见的32位和64位系统,如下所示:
通过这儿可以看出:
再来谈谈,内核空间与用户空间的区别:
尽管每位进程都各自有独立的虚拟显存,而且每位虚拟显存中的内核地址,虽然关联的都是相同的化学显存。这样,进程切换到内核态后,就可以很便捷地访问内核空间显存。
接出来,进一步了解虚拟空间的界定情况,用户空间和内核空间界定的方法是不同的,内核空间的分布情况就不多说了。
我们瞧瞧用户空间分布的情况,以32位系统为例,我画了一张图来表示它们的关系:
通过这张图你可以看见,用户空间显存从低到高分别是6种不同的显存段:
在这6个显存段中,堆和文件映射段的显存是动态分配的。例如说国内linux主机,使用C标准库的malloc()或则mmap(),就可以分别在堆和文件映射段动态分配显存。
malloc是怎样分配显存的?
实际上,malloc()并不是系统调用,而是C库里的函数,用于动态分配显存。
malloc申请显存的时侯,会有两种形式向操作系统申请堆显存。
形式一实现的方法很简单,就是通过brk()函数将「堆顶」指针向高地址联通,获得新的显存空间。如右图:
形式二通过mmap()系统调用中「私有匿名映射」的形式,在文件映射区分配一块显存linux系统如何支持虚存,也就是从文件映射区“偷”了一块显存。如右图:
哪些场景下malloc()会通过brk()分配显存?又是哪些场景下通过mmap()分配显存?
malloc()源码里默认定义了一个阀值:
malloc()分配的是化学显存吗?
不是的,malloc()分配的是虚拟显存。
假如分配后的虚拟显存没有被访问的话,是不会将虚拟显存不会映射到化学显存,这样就不会占用化学显存了。
只有在访问已分配的虚拟地址空间的时侯,操作系统通过查找页表,发觉虚拟显存对应的页没有在化学显存中,都会触发缺页中断,之后操作系统会构建虚拟显存和化学显存之间的映射关系。
malloc(1)会分配多大的虚拟显存?
malloc()在分配显存的时侯,并不是老老实实按用户预期申请的字节数来分配显存空间大小,而是会预分配更大的空间作为显存池。
具体会预分配多大的空间,跟malloc使用的显存管理器有关系,我们就以malloc默认的显存管理器(Ptmalloc2)来剖析。
接下里,我们做个实验,用下边这个代码,通过malloc申请1字节的显存时,瞧瞧操作系统实际分配了多大的显存空间。
#include
#include
int main() {
printf("使用cat /proc/%d/maps查看内存分配n",getpid());
//申请1字节的内存
void *addr = malloc(1);
printf("此1字节的内存起始地址:%xn", addr);
printf("使用cat /proc/%d/maps查看内存分配n",getpid());
//将程序阻塞,当输入任意字符时才往下执行
getchar();
//释放内存
free(addr);
printf("释放了1字节的内存,但heap堆并不会释放n");
getchar();
return 0;
}
执行代码:
我们可以通过/proc//maps文件查看进程的显存分布情况。我在maps文件通过此1字节的显存起始地址过滤出了显存地址的范围。
[root@xiaolin ~]# cat /proc/3191/maps | grep d730
00d73000-00d94000 rw-p 00000000 00:00 0 [heap]
这个反例分配的显存大于128KB,所以是通过brk()系统调用向堆空间申请的显存,因而可以看见最右侧有[heap]的标示。
可以看见,堆空间的显存地址范围是00d73000-00d94000,这个范围大小是132KB,也就说明了malloc(1)实际上预分配132K字节的显存。
可能有的朋友注意到了,程序里复印的显存起始地址是d73010,而maps文件显示堆显存空间的起始地址是d73000,为何会多下来0x10(16字节)呢?这个问题,我们先放着,前面会说。
free释放显存,会归还给操作系统吗?
我们在前面的进程往下执行,瞧瞧通过free()函数释放显存后,堆显存还在吗?
从右图可以看见,通过free释放显存后,堆显存还是存在的,并没有归还给操作系统。
这是由于与其把这1字节释放给操作系统,不如先缓存着放进malloc的显存池里,当进程再度申请1字节的显存时就可以直接复用,这样速率快了好多。
其实,当进程退出后,操作系统都会回收进程的所有资源。
里面说的free显存后堆显存还存在,是针对malloc通过brk()方法申请的显存的情况。
假如malloc通过mmap形式申请的显存,free释放显存后都会归归还给操作系统。
我们做个实验验证下,通过malloc申请128KB字节的显存,来促使malloc通过mmap方法来分配显存。
#include
#include
int main() {
//申请1字节的内存
void *addr = malloc(128*1024);
printf("此128KB字节的内存起始地址:%xn", addr);
printf("使用cat /proc/%d/maps查看内存分配n",getpid());
//将程序阻塞,当输入任意字符时才往下执行
getchar();
//释放内存
free(addr);
printf("释放了128KB字节的内存,内存也归还给了操作系统n");
getchar();
return 0;
}
执行代码:
查看进程的显存的分布情况,可以发觉最右侧没有[head]标志,说明是通过mmap以匿名映射的方法从文件映射区分配的匿名显存。
之后我们释放掉这个显存瞧瞧:
再度查看该128KB显存的起始地址,可以发觉早已不存在了,说明归还给了操作系统。
对于「malloc申请的显存,free释放显存会归还给操作系统吗?」这个问题,我们可以做个总结了:
为何不全部使用mmap来分配显存?
由于向操作系统申请显存,是要通过系统调用的,执行系统调用是要步入内核态的,之后在回到用户态,运行态的切换会花费不少时间。
所以,申请显存的操作应当避开频繁的系统调用,假如都用mmap来分配显存,等于每次都要执行系统调用。
另外,由于mmap分配的显存每次释放的时侯,就会归还给操作系统,于是每次mmap分配的虚拟地址都是缺页状态的,之后在第一次访问该虚拟地址的时侯,还会触发缺页中断。
也就是说,频繁通过mmap分配的显存话,除了每次就会发生运行态的切换,就会发生缺页中断(在第一次访问虚拟地址后),这样会造成CPU消耗较大。
为了改进这两个问题,malloc通过brk()系统调用在堆空间申请显存的时侯,因为堆空间是连续的,所以直接预分配更大的显存来作为显存池,当显存释放的时侯,就缓存在显存池中。
等上次在申请显存的时侯,就直接从显存池取出对应的显存块就行了,并且可能这个显存块的虚拟地址与数学地址的映射关系还存在,这样除了降低了系统调用的次数,也降低了缺页中断的次数,这将大大增加CPU的消耗。
既然brk这么牛逼,为何不全部使用brk来分配?
后面我们谈到通过brk从堆空间分配的显存,并不会归还给操作系统linux系统如何支持虚存,这么我们那考虑这样一个场景。
假如我们连续申请了10k,20k,30k这三片显存linux计划任务,假如10k和20k这两片释放了,变为了空闲显存空间,假如上次申请的显存大于30k,这么就可以重用这个空闲显存空间。
然而若果上次申请的显存小于30k,没有可用的空闲显存空间,必须向OS申请,实际使用显存继续减小。
因而,随着系统频繁地malloc和free,尤其对于小块显存,堆内将形成越来越多不可用的碎片,致使“内存泄漏”。而这些“泄露”现象使用valgrind是难以测量下来的。
所以,malloc实现中,充分考虑了sbrk和mmap行为上的差别及异同点,默认分配大块显存(128KB)才使用mmap分配显存空间。
free()函数只传入一个显存地址,为何能晓得要释放多大的显存?
还记得,我后面提及,malloc返回给用户态的显存起始地址比进程的堆空间起始地址多了16字节吗?
这个多下来的16字节就是保存了该显存块的描述信息,例如有该显存块的大小。
这样当执行free()函数时,free会对传入进来的显存地址向左偏斜16字节,之后从这个16字节的剖析出当前的显存块的大小,自然就晓得要释放多大的显存了。