Linux 上内存分配的工作原理
在计算机中,要使进程可执行,就需要将其放置在内存中。为此,必须将字段分配给内存中的进程。内存分配是一个需要注意的重要问题,尤其是在内核和系统架构中。
让我们详细了解一下 Linux 内存分配,并了解幕后发生的事情。
内存分配是如何完成的?
大多数软件工程师不知道这个过程的细节。但如果你是一名系统程序员候选人,你应该对它了解更多。在看分配过程的时候,有必要对 Linux 和glibc库做一个小细节。
当应用程序需要内存时,它们必须从操作系统请求它。来自内核的这个请求自然需要系统调用。您不能在用户模式下自己分配内存。
malloc()系列函数负责 C 语言中的内存分配。这里要问的问题是 malloc() 作为 glibc 函数是否进行直接系统调用。
Linux 内核中没有称为 malloc 的系统调用。但是,对于应用程序的内存需求有两个系统调用,它们是brk和mmap。
由于您将通过 glibc 函数在应用程序中请求内存,您可能想知道此时 glibc 正在使用哪些系统调用。答案是两者兼而有之。
第一个系统调用:brk
每个进程都有一个连续的数据字段。通过 brk 系统调用,增加了决定数据域限制的程序中断值,并执行了分配过程。
尽管使用这种方法分配内存非常快,但并不总是可以将未使用的空间返回给系统。
例如,假设您通过 malloc() 函数为 brk 系统调用分配了五个字段,每个字段大小为 16KB。当您完成其中第二个字段时,无法返回相关资源(解除分配)以便系统可以使用它。因为如果您减少地址值以显示第二个字段的开始位置,并调用 brk,您将完成第三、四个和五个字段的释放。
为了防止这种情况下的内存丢失,glibc 中的 malloc 实现会监控进程数据字段中分配的位置,然后通过 free() 函数指定将其返回给系统,以便系统可以将空闲空间用于进一步的内存分配。
也就是说,在分配了 5 个 16KB 的区域之后,如果用 free() 函数返回第二个区域,过一段时间再请求另一个 16KB 的区域,而不是通过 brk 系统调用扩大数据区域,之前的地址是回来。
但是,如果新请求的区域大于 16KB,则数据区域将通过 brk 系统调用分配新区域来扩大,因为区域 2 无法使用。虽然二号区域没有使用,但由于大小不同,应用程序无法使用它。因为这样的场景,有一种情况叫做内部碎片,实际上你很少能充分利用内存的所有部分。
为了更好地理解,请尝试编译并运行以下示例应用程序:
#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
带有 strace 的 brk 的输出如下:
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 中内存分配的重要性
内存分配非常重要,尤其是在优化和安全问题上。如上面的示例所示,不完全理解此问题可能意味着破坏系统的安全性。
甚至许多编程语言中存在的类似于 push 和 pop 的概念也是基于内存分配操作的。能够很好地使用和掌握系统内存对于嵌入式系统编程和开发安全和优化的系统架构都是至关重要的。
如果您还想涉足 Linux 内核开发,请考虑首先掌握 C 编程语言。