C++-内存分配

转自1024搜

内存分配

C++ 中内存分配使用operator new,其底层为malloc的封装,这里着重讲malloc

状态解析

内核态

当CPU执行操作系统代码时就处于内核态,在内核态下CPU可以执行任何机器指令、访问所有地址空间、不受限制的访问任何硬。

可以简单的认为内核态就是“天界”,在这里的代码(操作系统代码)无所不能。

用户态

当CPU执行我们写的“普通”代码(非操作系统、驱动程序员)时就处于用户态,粗糙的划分方法就是除了操作系统之外的代码,就像我们写的 HelloWorld 程序。

用户态就好比“人界”,在用户态我们的代码处处受限,不能直接访问硬件、不能访问特定地址空间,否则神仙(操作系统)直接将你kill掉,这就是著名的Segmentation fault、不能执行特权指令,等等。

跨界

普通程序永远也去不了内核态,只能以通信的方式从用户态往内核态传递信息。 操作系统为普通程序员留了一些特定的暗号,这些暗号就和普通函数一样,程序员通过调用这些暗号就能向操作系统请求服务了,这些像普通函数一样的暗号就被称为系统调用,System Call,通过系统调用我们可以让操作系统代替我们完成一些事情,像打开文件、网络通信等等。

标准库

虽然我们可以通过系统让操作系统替我们完成一些特定任务,但这些系统调用都是和操作系统强相关的,Linux和Windows的系统调用就完全不同。

这就引入了标准库,从分层的角度,我们的程序一般都是这个结构:

最上层是应用程序,应用程序一般只和标准库打交道(当然,也可以绕过他),标准库通过系统调用和操作系统交互,操作系统管理底层硬件。

我们分配内存时使用的malloc函数其实不是实现在操作系统里的,而是在标准库中实现的。

步骤

  1. 程序调用malloc申请内存,注意malloc实现在标准库中;
  2. malloc开始搜索空闲内存块,如果能找到一块大小合适的就分配出去,前两个步骤都是发生在用户态;
  3. 如果malloc没有找到空闲内存块那么就像操作系统发出请求来增大堆区,这是通过系统调用brk(sbrk、mmap也可以)实现的,注意,brk是操作系统的一部分,因此当brk开始执行时,此时就进入内核态了。brk增大进程的堆区后返回,malloc的空闲内存块增加,此时malloc又一次能找到合适的空闲内存块然后分配出去。

但是!

上述过程根本就没有涉及到哪怕一丁点物理内存!!!

我们确实向 malloc 申请到内存了,malloc 不够也确实从操作系统申请到内存了,但这些内存都不是真的物理内存。

实际上,进程看到的内存都是假的,是操作系统给进程的一个幻象,这个幻象就是由著名的虚拟内存系统来维护的,我们经常说的这张图就是进程的虚拟内存。

所谓虚拟内存就是假的、不是真正的物理内存,虚拟内存是给进程用的,操作系统维护了虚拟内存到物理内存的映射,当malloc返回后,程序员申请到的内存就是虚拟内存。注意,此时操作系统根本就没有真正的分配物理内存,程序员从malloc拿到的内存目前还只是一张空头支票。 那么这张空头支票什么时候才能兑现呢?也就是什么时候操作系统才会真正的分配物理内存呢?答案是当我们真正使用这段内存时,当我们真正使用这段内存时,这时会产生一个缺页错误,操作系统捕捉到该错误后开始真正的分配物理内存,操作系统处理完该错误后我们的程序才能真正的读写这块内存。

完整解析

  1. malloc开始搜索空闲内存块,如果能找到一块大小合适的就分配出去;
  2. 如果malloc找不到一块合适的空闲内存,那么调用brk等系统调用扩大堆区从而获得更多的空闲内存;
  3. malloc调用brk后开始转入内核态,此时操作系统中的虚拟内存系统开始工作,扩大进程的堆区,注意额外扩大的这一部分内存仅仅是虚拟内存,操作系统并没有为此分配真正的物理内存;
  4. brk执行结束后返回到malloc,从内核态切换到用户态,malloc找到一块合适的空闲内存后返回;
  5. 程序员拿到新申请的内存,程序继续;
  6. 当有代码读写新申请的内存时系统内部出现缺页中断,此时再次由用户态切换到内核态,操作系统此时真正的分配物理内存,之后再次由内核态切换回用户态,程序继续。

至此,结束