对于一个进程而言,他的虚拟内存空间会分为内核内存空间,还有用户内存空间

  • 用户空间:Text Segment(代码)、Data Segment(静态常量) 和 BSS Segment(未初始化静态变量)、堆(由程序员分配的内存)、函数栈
  • 内核空间:内核栈、同样有 Text Segment、Data Segment 和 BSS Segment

一个内存管理系统至少应该做三件事情:

  • 第一,虚拟内存空间的管理,每个进程看到的是独立的、互不干扰的虚拟地址空间;
  • 第二,物理内存的管理,物理内存地址只有内存管理模块能够使用;
  • 第三,内存映射,需要将虚拟内存和物理内存映射、关联起来。

独享内存空间

为什么需要?很显然,如果不隔离,就会不安全、就会泄密,因而,我们说每个进程应该有自己的内存空间。内存空间都是独立的、相互隔离的。对于每个进程来讲,看起来应该都是独占的。

当程序要访问虚拟地址的时候,
由内核的数据结构进行转换,转换成不同的物理地址,这样不同的进程运行的时候,写入的是不同的物理地址,这样就不会冲突了。

规划虚拟地址空间

这样一个程序。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int max_length = 128;
  4. char * generate(int length) {
  5. int i;
  6. char * buffer = (char*) malloc (length+1);
  7. if (buffer == NULL)
  8. return NULL;
  9. for (i=0; i<length; i++) {
  10. buffer[i]=rand()%26+'a';
  11. }
  12. buffer[length]='\0';
  13. return buffer;
  14. }
  15. int main(int argc, char *argv[]) {
  16. int num;
  17. char * buffer;
  18. printf ("Input the string length : ");
  19. scanf ("%d", &num);
  20. if(num > max_length) {
  21. num = max_length;
  22. }
  23. buffer = generate(num);
  24. printf ("Random string is: %s\n",buffer);
  25. free (buffer);
  26. return 0;
  27. }

这个程序比较简单,就是根据用户输入的整数来生成字符串,最长是 128。由于字符串的长度不是固定的,因而不能提前知道,需要动态地分配内存,使用 malloc 函数。当然用完了需要释放内存,这就要使用 free 函数。
我们来总结一下,这个简单的程序使用哪些内存的几种方式:

  • 代码需要放在内存里面;
  • 全局变量,例如 max_length;
  • 常量字符串”Input the string length : “;
  • 函数栈,例如局部变量 num 是作为参数传给 generate 函数的,这里面涉及了函数调用,局部变量,函数参数等都是保存在函数栈上面的;
  • 堆,malloc 分配的内存在堆里面;
  • 这里面涉及对 glibc 的调用,所以 glibc 的代码是以 so 文件的形式存在的,也需要放在内存里面。

这就完了吗?还没有呢,别忘了 malloc 会调用系统调用,进入内核,所以这个程序一旦运行起来,内核部分还需要分配内存:

  • 内核的代码要在内存里面;
  • 内核中也有全局变量;
  • 每个进程都要有一个 task_struct;
  • 每个进程还有一个内核栈;
  • 在内核里面也有动态分配的内存;
  • 虚拟地址到物理地址的映射表放在哪里?

首先,这么大的虚拟空间一切二,一部分用来放内核的东西,称为内核空间,一部分用来放进程的东西,称为用户空间

用户空间在下,在低地址, 从最低位开始排起,先是Text Segment、Data Segment 和 BSS Segment。Text Segment 是存放二进制可执行代码的位置,Data Segment 存放静态常量,BSS Segment 存放未初始化的静态变量。是不是觉得这几个名字很熟悉?没错,咱们前面讲 ELF 格式的时候提到过,在二进制执行文件里面,就有这三个部分。这里就是把二进制执行文件的三个部分加载到内存里面。

接下来是(Heap)。堆是往高地址增长的,是用来动态分配内存的区域,malloc 就是在这里面分配的。
接下来的区域是Memory Mapping Segment。这块地址可以用来把文件映射进内存用的,如果二进制的执行文件依赖于某个动态链接库,就是在这个区域里面将 so 文件映射到了内存中。

再下面就是(Stack)地址段。主线程的函数调用的函数栈就是用这里的。

如果普通进程还想进一步访问内核空间,是没办法的,只能眼巴巴地看着。如果需要进行更高权限的工作,就需要调用系统调用,进入内核。

但是到了内核里面,无论是从哪个进程进来的,看到的都是同一个内核空间,看到的都是同一个进程列表。虽然内核栈是各用个的,但是如果想知道的话,还是能够知道每个进程的内核栈在哪里的。所以,如果要访问一些公共的数据结构,需要进行锁保护。

内核的代码访问内核的数据结构,大部分的情况下都是使用虚拟地址的,虽然内核代码权限很大,但是能够使用的虚拟地址范围也只能在内核空间,也即内核代码访问内核数据结构

内核里面也会有内核的代码,同样有 Text Segment、Data Segment 和 BSS Segment,别忘了咱们讲内核启动的时候,内核代码也是 ELF 格式的。