堆在操作系统中应用

操作系统中栈与堆的理:https://blog.csdn.net/xiaokugua_250/article/details/42777147


在32位系统,内存的寻址可以达到4G。 理论上,用户可以使用一个32位的指针访问任意内存地址。
然而,事实上并非如此,在编程中我们却常常会遇到segment fault错误,指示我们非法内存访问。证明,有些位置我们是无权访问的,或者说暂时无权访问,只有申请了该内存才能访问。
  其实,大多数操作系统会把4G的内存空间进行划分,比如linux通常会默认把高地址的1G空间分配给内核(内核空间给内核线程执行特权操作,比如维护打开文件表等),剩余的作为内存空间。内存空间又大致分为堆空间,栈空间,代码区等。

栈: 用户维护函数调用上下文。由高地址向低地址生长,通常以M为单位,由操作系统维护。[不能申请占用过大的内存的局部变量,会导致栈爆掉而core.如果变量太大,可以考虑放到全局变量区或者使用堆]
堆: 动态申请内存,即使用new or malloc等分配到的内存,可以比栈大很多,需用户自己释放[new/delete,malloc/free成对出现,否则会导致内存泄露,可以使用查找内存泄露的工具监控或者自己写代码监控]
代码区:存放代码的内存映像
保留区:禁止访问的一些区域,比如NULL[使用*访问置为NULL的指针会出错,也需要小心野指针]

内核态和用户态的区别?什么叫做系统调用时会陷入内核

内核线程和用户线程是存在一定的对应关系,这个由线程库来实现。内核并不感知用户级别的线程。用户需要做一些操作,比如访问文件,开辟共享内存等特权操作但是用户又没有权限时,只能通过系统调用,让操作系统的内核线程来做。称为陷入内核,此时,内核会通过调用相应的中断处理程序来执行特权操作。但是用户往往只能拿到资源的代号而无法拿到资源的实际指针,比如文件描述符。其实是内核系统给进程维护的打开文件表数组下标,而实际的文件指针是存储在下标中的元素。所以,用户拿到了描述符还是只能通过系统调用来进行操作,而无法直接拿到指针进行访问,这样,就绕过了操作系统。这是不被允许的。

  1. 进程,线程概念
    实际上,linux将所有运行的实体(进程,线程)称为任务(task),每一个task概念上类似于一个单线程的进程,具有内存空间,执行实体,文件资源等。不过。linux下不同的任务之间可以选择共享内存空间,因而实际上可以定义,共享了一个内存空间的不同任务构成了一个进程,而这些任务则称为进程里的线程。

 接口:fork(写时复制),exec(替换程序),clone

从数据角度来看,线程数据和进程数据可以分为:

线程私有现场共享(进程所有)
局部变量(栈,寄存器)全局变量、动态申请的数据(堆)
函数参数函数里的静态变量
TLS数据代码区、打开文件列表

堆是一块巨大的内存空间,常常占据整个虚拟空间的绝大部分。在这片空间里,程序可以请求一块连续内存,并自由的使用,这块内存在程序主动放弃之前都会一直保持有效。

malloc实现的两种方法:1)把进程的内存管理交给操作系统内核去做,由于内核管理着进程的地址空间,当其提供一个系统调用,使程序可以通过这个系统调用申请内存。该方法理论可行,但是性能比较差(系统调用时性能开销很大,频繁调用的话会严重影响程序性能)。2)程序向操作系统申请一块适当大小的堆空间,然后程序自己管理这块空间,具体来说,管理着堆空间分配的往往是程序的运行库。

在进程的地址空间中,除了可执行文件、共享库和栈之外。剩余的未分配的空间都可以用来作为堆空间。

在Linux系统下,其提供两种堆空间分配方式,即两个系统调用:一个是brk()系统调用,另一个是mmap()。brk()的C语言形式声明如下:

int brk(void * end_data_segment)

brk()的作用实际上是设置进程数据段的结束地址,即它可以扩大或者缩小数据段(Linux下数据段和BSS合并在一起统称为数据段)。如果我们将数据段的结束地址向高地址移动,那么扩大的那部分空间就可以被我们使用,最常见的做法就是将这个空间视为堆空间。

mmap()的作业和window系统下的VituralAlloc很相似,它的作用就是向操作系统申请一段虚拟地址空间,当然这块虚拟地址空间可以映射到某个文件(这也是这个系统调用的最初作用),当它不将地址空间映射到某个文件时,我们又将这块空间称为匿名空间(Anonymous),匿名空间就可以拿来作为堆空间。其声明如下

void * mmap(

void *start,

size_t length,

int prot,

int flags,

int fd.

off_t offset);

mmap的前两个参数分别用于指定需要申请的空间的起始地址和长度,如果起始地址设置为0,那么Linux系统会自动挑选合适的其实地址。prot/flags这两个参数分别用于设置申请空间的权限(可读、可写、可执行)以及映射类型(文件映射、匿名空间等),最后两个参数是用于文件映射时指定文件描述符和文件偏移的。

对于malloc函数,处理用户空间请求时:对于128kb的请求来说,其会在现有的堆空间里面,按照堆分配算法为其分配一块空间并返回:对于大于128kb的请求来说,它会使用mmap()函数为它分配一块匿名空间,然后在这个匿名空间中为用户分配空间。当然也可以直接使用mmap()函数来实现malloc函数。

void *malloc(size_t nbytes){
void * res=mmap(0,nbytes,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONMOUS,0,0);
if(ret==MAP_FAILED)
 {return 0;}
return ret;
}

在malloc中申请的内存,在进程结束会便不会存在。因为当进程结束后,所有与进程相关的资源,包括进程的地址空间、物理内存、打开的文件、网络链接等都被操作系统关闭或者收回、所以无论malloc申请了多少内存,进程结束后都不会存在

malloc申请的空间:如果是指虚拟空间的话,其为连续的,即每次malloc分配后返回的空间都可以看作是一块连续的地址;如果空间是指“物理空间”则并不一定联系,对于一块连续的虚拟空间来说,其可能是由若干个不连续的物理页拼凑而成。

堆分配算法:

1)**空闲链表**--其方法是把堆中各个空闲的块按照链表的方式连接起来,当用户请求一块空间时,可以遍历整个列表,直到找到合适大小的块并且将它拆分。当用户释放空间时将它合并到空闲链表中。

2)**位图(Bitmap)**--核心思想是将整个堆划分为大量的块(block),每个块大小相同。当用户请求内存的时候,总是分配整数个块的空间给用户,第一个块我们称为已分配区域的头(Head),其余的称为已分配区域的主体(Body).而我们可以使用一个整数数组来记录块的使用情况,由于每个块只有头/主体/空闲三种状态,因而仅仅需要两位即可表示一个块,因此称为位图。位图的实现具有以下优点:(1)速度快--由于整个堆的空闲信息存储在一个数组内,因此访问该数组时cache容易命中(2)稳定性好--为了避免用户越界读写破坏数据,我们只需简单的备份一下位图即可。而且即使部分数据被破坏,也不会导致整个堆无法工作(3)块不需要额外信息,易于管理;其缺点有:(1)分配时容易产生碎片(2)如果堆很大,或者设定的块很小(可以减少碎片),那么位图会很大,可能失去cache命中率高的优势,并且浪费一定的空间。此种情形出现时可以使用多级的位图。

(3)**对象池**--其思路为:如果每一次分配的空间大小都一样,那么可以按照这个每次请求分配的大小作为一个单位,把整个堆空间划分为大量的小块,每次请求的时候只需要找到一个小块就可以了。对象池的管理方法可以采用空闲链表,也可以采用位图,与他们的区别仅仅在于它假定每次请求的都是一个固定的大小,因此实现起来很容易,由于每次总是只请求一个单位的内存,因此请求得到满足的速度非常快,无需查找足够大的空间。

  在现实应用中,堆分配算法往往是采取多种算法复合而成的。比如对与glibc来说,它对小于64字节的空间申请是采用类似于对象池的方法;而对于大于512字节的空间申请采用的是最佳适配算法,对于大于64字节而小于512字节的,会根据情况采用上述方法中的折衷策略;对于大于128kb的申请,它会使用mmap机制直接向操作系统申请空间。

转载于:https://blog.51cto.com/12656963/2142593

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值