概念

在linux中,内存分为物理内存和虚拟内存,即ps命令中的RSS和VSZ。虚拟内存存在的原因是为了解决在物理内存上直接划分内存存在的一些问题,比如:

  • 频繁申请和回收内存导致的空间碎片化
  • 可以随意访问用于其他用途的内存区域,不安全
  • 难以执行多任务

进程能看见的是虚拟地址空间,且地址段时连续的,而系统上搭载的内存的实际的地址是物理地址。通过命令 readelfcat /proc/<pid>/maps输出的就是进程的虚拟地址。

虚拟内存以页为单位进行划分的,在x86_64的架构中页大小默认是4KB,getconf PAGESIZE可以查看页大小。通过内核管理的页表可以完成从虚拟地址到物理地址的转换,每个进程拥有独立的虚拟地址空间,进程的虚拟内存是连续的,但是在物理内存中不一定是连续,且进程只能访问自己的虚拟内存段,没法访问到其他进程的虚拟内存空间和物理内存。

利用虚拟内存机制的重要功能:

  • 文件映射
  • 请求分页
  • 写时复制
  • swap
  • 多级页表
  • 标准大页

请求分页

如果内核直接从物理内存中获取需要的区域,然后设置页表并关联虚拟地址空间与物理地址空间,这样会导致内存的浪费,有一部分内存获取后可能进程到运行结束都不会使用,如:

  • 用于大规模程序中的、程序运行时未使用的功能代码段和数据段
  • 由glibc保留的内存池中未被用户利用的部分

所以利用请求分页来解决这个问题。对于虚拟地址空间内的各个页面,只有在进程初次访问页面时才会为这个页面分配物理内存。

过程:

① 进程访问入口点

② CPU 参照页表,筛选出入口点所属的页面中哪些虚拟地址未关联物理地址

③ 在CPU中引发缺页中断

④ 内核中的缺页中断机构为页面分配物理内存并更新页表

⑤ 回到用户模式继续运行进程

测试

现用以下测试代码观察虚拟内存和物理内存的分配关系

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <unistd.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>
#define BUFFER_SIZE (100*1024*1024)
#define NCYCLE 10
#define PAGE_SIZE 4096

int main(void)
{
    char *p;
    time_t t;
    char *s;

    t = time(NULL);
    s = ctime(&t);
    printf("%.*s: before allocation, please press Enter key\n", (int)(strlen(s) - 1), s);
    getchar();

    p = malloc(BUFFER_SIZE);  //申请100MB的内存
    if (p == NULL)
        err(EXIT_FAILURE, "malloc() failed");
    t = time(NULL);
    s = ctime(&t);
    printf("%.*s: allocated %dMB, please press Enter key\n", (int)(strlen(s) - 1), s, BUFFER_SIZE / (1024 * 1024));
    getchar();

    int i; // 以页为单位访问内存,每10M且隔1秒就输出一次信息
    for (i = 0; i < BUFFER_SIZE; i += PAGE_SIZE) {
        p[i] = 0;
        if (i != 0 && i % (BUFFER_SIZE/NCYCLE) == 0) {
            t = time(NULL);
            s = ctime(&t);
            printf("%.*s: touched %dMB\n", (int) (strlen(s) - 1), s, i / (1024*1024));
            sleep(1);
        }
    }
    t = time(NULL);
    s = ctime(&t);
    printf("%.*s: touched %dMB, please press Enter key\n", (int) (strlen(s) - 1), s, BUFFER_SIZE / (1024 * 1024));
    getchar();
    exit(EXIT_SUCCESS);
}

示例说明:

  1. 输出一条开始信息,等待按下enter继续
  2. 获取100MB的内存
  3. 输出信息提示获取内存成功,等待按下enter继续
  4. 按页访问已获取的内存,每访问够10MB就输出一条信息提示
  5. 获取完100MB后等用户按下enter

执行此代码,同时执行sar -r 1 每秒输出内存使用统计信息, 另外,可以通过 sar -B 1查看缺页中断(fault/s)的情况。 用 ps -eo pid,comm,vsz,rss,maj_flt,min_flt查看虚拟内存量和已分配的物理内存量,以及在创建进程后发生的硬性缺页中断和软性缺页中断的总次数。 执行测试代码同时执行下面小脚本查看各项指标的变化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# cat memtest.sh 
#!/bin/bash

while true
do
    DATE=$(date | tr -d '\n')
    INFO=$(ps -eo pid,comm,vsz,rss,maj_flt,min_flt | grep memtest | grep -v grep)
    if [ -z "$INFO" ]; then
        echo "$DATE: target process seems to be finished"
        break 
    fi
    echo "$DATE: $INFO"
    sleep 1
done

执行结果:

page-request

在44分42秒时vsz增加了100MB,而rss还是512未变,44分45秒开始按页访问内存的时候,vsz不变,rss每秒增加大约10M的大小,且软中断次数递增。 显然已获取的内存在访问之前是不会分配物理内存的,在访问时才会触发缺页中断,内核分配物理内存并做好页表的映射关系。

另外,虚拟内存也存在内存不足的情况,它和物理内存没关系,在x86架构上虚拟内存最大也就4G,x86_64有128T。

其他

物理内存使用的组成部分

linux-free

1
2
3
4
# free
              total        used        free      shared  buff/cache   available
Mem:        1882000      317444      115548         692     1449008     1376388
Swap:             0           0           0
  • buff/cache字段:缓冲区缓存与页面缓存占用的内存。当系统可用内存量(free字段的值)减少时,可通过内核将他们释放出来

  • available字段:实际的可用内存量,值为free字段的值加上当内存不足时内核中可释放的内存量。“可释放的内存”指缓冲区缓存与页面缓存中的大部分内存,以及内核中除此之外的用于其他地方的部分内存

内存的分配时机

  • 创建进程时

    通过系统调用fork或execve函数创建进程时开辟内存空间,

  • 创建后,动态分配

    mmap系统调用

一般在上层应用会用C标准库中的函数malloc用于获取内存,在linux中这个函数的底层调用了mmap函数,即 c程序 –> malloc() –> glibc –> mmap() –> 内存管理系统。

mmap函数以页为单位获取内存,而malloc以字节为单位。为了以字节为单位获取内存,glibc 事先通过系统调用 mmap() 向内核请求一大块内存区域作为内存池,当程序调用 malloc() 函数时,从内存池中根据申请的内存量划分出相应大小(以字节为单位)的内存并返回给程序。 s 使用python获取内存的图例:

python-malloc

参考:《linux是怎样工作的》