C语言执行动态内存分配时,操作系统是如何避免“浪费”的?

以下内容转载自 https://www.toutiao.com/i6732066093378060807/

在C语言程序开发中,提到动态内存分配时,基本上每个程序员都明白 calloc() 和 malloc() 库函数的区别——calloc() 函数不仅分配内存,还会将分配后的内存清零,而 malloc() 函数则对分配好的内存不做任何操作。

C语言执行动态内存分配时,操作系统是如何避免“浪费”的?

calloc() 函数的效率比 malloc()+memset() 函数更高?

calloc() 函数的效率比 malloc()+memset() 函数更高?

很多C语言程序员常把 calloc() 函数看作是 malloc() + memset() 函数的组合。不过,今天我在一个偶然的测试中发现 calloc() 函数和 malloc() + memset() 组合函数的效率差异还是很大的,请看:

#include<stdio.h>
#include<stdlib.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
 int i=0;
 char *buf[10];
 while(i<10)
 {
 buf[i] = (char*)calloc(1,BLOCK_SIZE);
 i++;
 }
 return 0;
}

C语言执行动态内存分配时,操作系统是如何避免“浪费”的?

calloc() 函数和 malloc() + memset() 组合函数的效率差异还是很大的

这段C语言代码调用了 calloc() 函数分配了一段内存,并且重复 10 次,编译并执行之(time 命令可以查看C语言程序运行消耗的时间),得到如下结果:

# gcc t.c
# time ./a.out 
**real 0m0.287s** 
user 0m0.095s 
sys 0m0.192s 

现在将 calloc() 函数改为 malloc() + memset() 函数,修改后的C语言代码如下,请看:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
 int i=0;
 char *buf[10];
 while(i<10)
 {
 buf[i] = (char*)malloc(BLOCK_SIZE);
 memset(buf[i],'\0',BLOCK_SIZE);
 i++;
 }.
 return 0;
}

C语言执行动态内存分配时,操作系统是如何避免“浪费”的?

修改后的C语言代码

编译并执行这段C语言代码,同样使用 time 命令查看程序运行消耗时间,得到如下结果:

# gcc t.c
# time ./a.out 
**real 0m2.693s** 
user 0m0.973s 
sys 0m1.721s 

应该清楚,这两段C语言代码的工作是一致的,都是分配一段长度为 BLOCK_SIZE 的内存并且清零,但是二者消耗的时间却相差非常大,这就有一个值得深思的问题:calloc() 函数做了相同的工作,但是效率却高得多,这是怎么回事呢?

弄清楚这一点,对于我们以后开发更高效率的C语言程序肯定有所帮助。

C语言执行动态内存分配时,操作系统是如何避免“浪费”的?

弄清楚这一点,对于我们以后开发更高效率的C语言程序肯定有所帮助

解析

在展开讨论之前,应该明白的是以后如果希望申请一段内容为 0 的内存,则应该优先考虑使用效率更高的 calloc() 函数,而不是 malloc() + memset() 函数的组合。

因为 calloc() 函数在内部实现中,会自行判断分配后的内存是否需要清零,如果某段分配好的内存原本就是零,那么清零动作就免去了。而 malloc() + memset() 函数的组合则全额做了“分配+清零”的动作,效率自然会有所差异。

C语言程序员应该明白四大点:程序,标准库,内核以及页表

像 malloc() 和 calloc() 这样的函数主要用于数百 KB 以下的内存分配,一般是直接从内存池(memory pool)中分配的。当内存池被用完,或者某段C语言代码一次性请求分配的内存超过剩余内存池容量时,malloc() 和 calloc() 将直接向内核请求内存。

C语言执行动态内存分配时,操作系统是如何避免“浪费”的?

malloc() 和 calloc() 将直接向内核请求内存

内核管理每个进程的实际内存,并确保不同进程不会干扰彼此的内存,这就是所谓的操作系统“内存保护”机制。有了这样的机制,一个进程的崩溃不会导致其他进程跟着崩溃,系统的稳定性会得到保障。

因此,在操作系统内核的管理下,当某段C语言代码需要使用一段内存时,它不能直接使用物理内存,而只能通过 mmap() 以及 sbrk() 等系统调用向内核申请,由内核修改页表为每个进程提供 RAM。

页表将内存地址映射到实际的物理 RAM,在 32 位系统上,进程地址(0x00000000到0xffffffff)不是实际的内存地址,而是虚拟内存地址,处理器将这些地址分为 4KiB 个页,通过页表,可以将每个内存页对应到不同的物理 RAM 上。

C语言执行动态内存分配时,操作系统是如何避免“浪费”的?

一些C语言程序员认为,calloc() 等内存分配函数是这样工作的

一些C语言程序员认为,calloc() 等内存分配函数是这样工作的

C语言程序调用 calloc() 申请 256KB 内存,于是标准库调用系统调用 mmap() 函数向内核申请,内核找到 256KB 未被使用的 RAM,并通过修改页表的方式将其提供给C语言程序,接着标准库调用 memset() 函数将申请到的内存清零,然后从 calloc() 函数将这段内存返回。

之后,当这段C语言程序退出后,内核会回收分配给它的内存,以便给其他进程使用。

实际上

上述过程在理论上是可行的,但是实际上并不会这样。因为内存总是有限的,内核分配给我们的C语言程序使用的内存可能是之前其他进程使用过的,如果这段内存里有密码,密钥,等其他敏感信息呢?

为了避免出现上述安全隐患,内核总是在将内存交给进程之前将其清理掉。当然了,我们也可以自己调用清零函数将使用过的内存清零,但是不管如何,mmap() 函数保证其返回的新内存是清零后的是安全的选择。

C语言执行动态内存分配时,操作系统是如何避免“浪费”的?

mmap() 函数保证其返回的新内存是清零后的是安全的选择

另外,有一些C语言程序可能很早就向内核申请了一段内存,但是却不会立刻使用它,甚至可能根本不会使用它。因此在设计操作系统内核时,为了效率的最大化,内核在收到内存分配请求时,不会立刻修改页表向程序提供实际的内存。

内核可能仅会将一些地址空间标记给C语言程序使用,但是却不做实际的分配工作。这样就避免了“分配了内存,却没被使用”带来的不必要的开销了。当然,一旦C语言程序需要读写这些地址空间,就会触发一个缺页异常,内核再将 RAN 真正的分配给这些地址,并恢复程序运行。

简而言之,内核为了避免不必要的开销,实际的内存分配只有在确保真的有C语言代码使用时(有写入动作时)才会进行。

也有些C语言程序分配内存后,可能(不做任何修改)直接就去读这些内存,这时,内核甚至会让这些C语言程序申请的内存指向同一个 4KiB 页表,因为 mmap() 返回的零填充内存都一样。

当然,如果某个C语言程序尝试对申请到的内存执行写入操作,那么将触发另一种缺页异常,内核将为该C语言程序分配一个新的内存页使用,该内存页不与其他任何进程共享。

C语言执行动态内存分配时,操作系统是如何避免“浪费”的?

避免浪费

在C语言程序开发中,一次内存分配的实际过程是这样的

C语言程序调用 calloc() 申请 256KB 内存,于是标准库调用系统调用 mmap() 函数向内核申请,内核找到 256KB 未被使用的地址空间,记下该地址空闲现在用于什么,然后返回。

现在标准库知道 mmap() 返回的结果总是用零填充,所以它不需要写入内存,因此不会出现缺页异常,内核不必直接实际分配内存。

最后C语言程序退出,内核不需要回收内存,因为内核根本就没有分配过内存。这样的效率显然很高。

如果使用 memset() 将页面清零,那么 memset() 的写入动作将触发缺页异常,内核将不得不执行分配动作,并执行写入零动作。这是一项开销巨大的工作,解释了 calloc() 比 malloc() + memset() 快的原因。

C语言执行动态内存分配时,操作系统是如何避免“浪费”的?

现在知道原理了

现在知道原理了,我们可以预言:如果最后使用了库函数分配的内存,那么 calloc() 函数可能仍然比 malloc()+memset() 快,但是二者之前的区别将不会再那么大。

最后

应该明白,并非所有的操作系统内核都具有分页虚拟内存,因此并非在所有平台上编译C语言代码都会得到相同的结果。calloc() 函数可能并不从内核申请内存,而是从共享内存池里申请,而共享内存池中可能存储了上一次被使用时残留的垃圾数据,calloc() 可以获取到这些内存,并且调用 memset() 将其清零。

不同的操作系统管理内存很可能是不一样的,有些操作系统内核会在空闲时将内存归零,已备以后需要获得归零内存时使用,而有些则不会,例如 Linux 就不会提前将内存清零。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值