Linux 上的内存分配如何工作
系统上的每个活动应用程序都需要内存才能运行。那么Linux机器上的内存是如何分配和共享的呢?
在计算机中,为了使进程可执行,需要将其放置在内存中。为此,必须将一个字段分配给内存中的进程。内存分配是一个需要注意的重要问题,尤其是在内核和系统架构中。
让我们详细了解一下 Linux 内存分配并了解幕后发生的事情。
内存分配是如何完成的?
大多数软件工程师不知道这个过程的细节。但如果你是一名系统程序员候选人,你应该对此了解更多。在查看分配过程时,有必要详细介绍一下 Linux 和 glibc 库。
当应用程序需要内存时,它们必须向操作系统请求内存。这个来自内核的请求自然需要系统调用。您无法在用户模式下自行分配内存。
malloc() 函数系列负责 C 语言中的内存分配。这里要问的问题是 malloc() 作为 glibc 函数是否进行直接系统调用。
Linux 内核中没有名为 malloc 的系统调用。然而,有两个系统调用满足应用程序的内存需求,分别是brk和mmap。
由于您将通过 glibc 函数在应用程序中请求内存,因此您可能想知道 glibc 此时正在使用哪些系统调用。答案是两者皆有。
第一个系统调用:brk
每个进程都有一个连续的数据字段。通过brk系统调用,增加决定数据字段限制的程序中断值并执行分配过程。
尽管使用此方法分配内存非常快,但并不总是可以将未使用的空间返回给系统。
例如,假设您通过 malloc() 函数使用 brk 系统调用分配了 5 个字段,每个字段大小为 16KB。当您完成其中第二个字段时,无法返回相关资源(解除分配)以便系统可以使用它。因为如果您通过调用 brk 减少地址值以显示字段 2 开始的位置,则您将完成字段 3、4 和 5 的释放。
为了防止这种情况下的内存丢失,glibc中的malloc实现监视进程数据字段中分配的位置,然后指定使用free()函数将其返回给系统,以便系统可以使用空闲空间来获取更多内存分配。
也就是说,分配了 5 个 16KB 区域后,如果用 free() 函数返回第二个区域,一段时间后再次请求另一个 16KB 区域,则不是通过 brk 系统调用扩大数据区域,而是之前的地址回。
但是,如果新请求的区域大于 16KB,则由于区域 2 无法使用,因此将通过使用 brk 系统调用分配新区域来扩大数据区域。尽管第二个区域未被使用,但由于大小差异,应用程序无法使用它。因为这样的场景,就会出现一种叫做内部碎片的情况,而实际上,你很少能够充分利用内存的所有部分。
为了更好地理解,请尝试编译并运行以下示例应用程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
char *ptr[7];
int n;
printf("Pid of %s: %d", argv[0], getpid());
printf("Initial program break : %p", sbrk(0));
for(n=0; n<5; n++) ptr[n] = malloc(16 * 1024);
printf("After 5 x 16kB malloc : %p", sbrk(0));
free(ptr[1]);
printf("After free of second 16kB : %p", sbrk(0));
ptr[5] = malloc(16 * 1024);
printf("After allocating 6th of 16kB : %p", sbrk(0));
free(ptr[5]);
printf("After freeing last block : %p", sbrk(0));
ptr[6] = malloc(18 * 1024);
printf("After allocating a new 18kB : %p", sbrk(0));
getchar();
return 0;
}
当您运行该应用程序时,您将得到类似于以下输出的结果:
Pid of ./a.out: 31990
Initial program break : 0x55ebcadf4000
After 5 x 16kB malloc : 0x55ebcadf4000
After free of second 16kB : 0x55ebcadf4000
After allocating 6th of 16kB : 0x55ebcadf4000
After freeing last block : 0x55ebcadf4000
After allocating a new 18kB : 0x55ebcadf4000
brk 和 strace 的输出如下:
brk(NULL) = 0x5608595b6000
brk(0x5608595d7000) = 0x5608595d7000
正如您所看到的,0x21000已被添加到数据字段的结束地址中。您可以从值0x5608595d7000来理解这一点。因此大约分配了 0x21000 或 132KB 的内存。
这里有两点需要考虑。第一个是分配的数量超过了示例代码中指定的数量。另一个是哪一行代码导致了提供分配的 brk 调用。
地址空间布局随机化:ASLR
当您依次运行上述示例应用程序时,您每次都会看到不同的地址值。以这种方式使地址空间随机变化会大大增加安全攻击的复杂性并提高软件安全性。
然而,在 32 位架构中,通常使用 8 位来随机化地址空间。增加位数并不合适,因为剩余位的可寻址区域将非常低。此外,仅使用 8 位组合并不会让攻击者感到困难。
另一方面,在 64 位架构中,由于可以分配用于 ASLR 操作的位数太多,因此提供了更大的随机性,并且安全程度也随之提高。
Linux 内核还支持基于 Android 的设备,并且 ASLR 功能在 Android 4.0.3 及更高版本上完全激活。即使仅出于这个原因,可以说 64 位智能手机比 32 位版本提供了显着的安全优势。
通过使用以下命令暂时禁用 ASLR 功能,之前的测试应用程序每次运行时都会返回相同的地址值:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
要将其恢复到之前的状态,只需在同一个文件中写入 2 而不是 0。
第二个系统调用:mmap
mmap 是 Linux 上用于内存分配的第二个系统调用。通过 mmap 调用,内存任何区域中的空闲空间都将映射到调用进程的地址空间。
在以这种方式完成的内存分配中,当您想要使用前面的 brk 示例中的 free() 函数返回第二个 16KB 分区时,没有任何机制可以阻止此操作。相关内存段从进程的地址空间中删除。它被标记为不再使用并返回到系统。
因为与brk相比,使用mmap分配内存非常慢,所以需要brk分配。
使用 mmap,内存的任何空闲区域都会映射到进程的地址空间,因此在该进程完成之前分配的空间的内容会被重置。如果不以这种方式进行重置,则属于先前使用相关内存区域的进程的数据也可以被下一个不相关的进程访问。这使得谈论系统安全变得不可能。
Linux 中内存分配的重要性
内存分配非常重要,尤其是在优化和安全问题上。如上面的示例所示,不完全理解此问题可能意味着破坏系统的安全性。
甚至许多编程语言中存在的类似于入栈和出栈的概念也是基于内存分配操作的。能够很好地使用和掌握系统内存对于嵌入式系统编程和开发安全且优化的系统架构都至关重要。
如果您也想涉足 Linux 内核开发,请考虑首先掌握 C 编程语言。