内核内存布局和堆管理


Linux 内核内存布局

64位Linux 一般使用48位来表示虚拟地址空间,45(有的是44 或者 46)位表示物理地址,通过命令:cat /proc/cpuinfo 输出地址大小,具体的其他选项参数: vendor_id : GenuineIntel //厂商 cpu family : 6 //CPU产品代号 mode : 79 //cpu 属于其系列的具体代号 mode name: Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz stepping : 1//cpu 属于制作更新版本 cpu MHz : 2199.997 //cpu 主频 cache size: 56320 KB //二级缓存 physical id : 1 //单个cpu 编号 siblings: 16 //单个cpu逻辑物理核心数 core id : 7 //单前物理核心,在其所处cpu中的编号 cpu cores: 8 //逻辑核所在cpu的物理核数 apicid: 31 //用来区分不同逻辑核的编号 bogomips: 4462.15 //系统内核启动时测算的cpu速度 clflush size: 64 // 每次刷新缓存的大小单位 cache_alignment : 64 //缓存地址空间对齐单位 address sizes: 44 bits physical, 48 bits virtual //可以访问的地址空间位数。

x86_64 内核分布情况

通过cat /proc/meminfo 输出系统架构内存分布情况,具体如下:

  1. MemTotal: 65901144 kB //所有可用内存空间
  2. MemFree: 4112612 kB //系统还没有使用的内存
  3. MemAvailable: 28757984 kB //真正系统可以使用的内存,可以回收
  4. Buffers: 990772 kB //专用用来给块设备做缓存的内存
  5. Cached: 23363496 kB //分配给文件缓冲区的内存
  6. SwapCached: 66376 kB //被调整缓冲缓存使用的交换空间大小
  7. Active: 38459180 kB //使用高速缓冲存储器页面文件大小
  8. Inactive: 20560836 kB // 没有经常使用的告诉缓存存储器页面大小
  9. Active(anon): 32334804 kB // 活跃的匿名内存
  10. Inactive(anon): 2616968 kB //不活跃的匿名内存
  11. Active(file): 6124376 kB //活跃的文件使用内存
  12. Inactive(file): 17943868 kB //不活跃的文件使用内存
  13. Unevictable: 0 kB // 不能被释放的内存页
  14. Mlocked: 0 kB //系统调用mlock 允许程序在物理内存上锁住部分或全部地址空间
  15. SwapTotal: 2097148 kB //交换空间总内存大小
  16. SwapFree: 843452 kB // 交换空间空闲的内存大小
  17. Zswap: 0 kB
  18. Zswapped: 0 kB
  19. Dirty: 10440 kB //等待被写回到磁盘
  20. Writeback: 0 kB//正在被写加的大小
  21. AnonPages: 34600316 kB //未映射页的内存/映射到用户空间的非文件页表大小
  22. Mapped: 1553816 kB //映射文件内存
  23. Shmem: 307256 kB // 已经被分配的共享内存
  24. KReclaimable: 1254992 kB // 可回收的slab 内存
  25. Slab: 1798984 kB //内存数据结构缓存大小
  26. CommitLimit: 35047720 kB //系统实际可以分配的内存
  27. Committed_AS: 72138556 kB //系统当前已经分配的内存
  28. VmallocTotal: 34359738367 kB // 预留虚拟内存的总量
  29. VmallocUsed: 288216 kB //已经被使用的虚拟内存
  30. VmallocChunk: 0 kB //可以分配的最大逻辑地址连续的虚拟内存。

linux 内核动态内存分配通过系统接口分配

 alloc_pages/__get_free_page :以页为单位分配
 vmalloc :以字节为单位分配虚拟地址连续的内存块
 kmalloc : 以字节为单位分配物理地址连续的内存块,以slab为中心。

我们也可以通过vmalloc 分配的内存将它统计输出,具体如下: grep vmalloc /proc/vmallocinfo

ARM64 架构采用48位物理寻址方式,最大可寻找256TB的物理地址空间,对于目前应用完全足够,不需要扩展到64位。虚拟地址也同样最大支持48位寻址,Linux内核在大多数体系结构上将地址空间划分为:用户空间和内核空间。

  1. 用户空间(User space):0x0000_0000_0000 至 0x0000_FFFF_FFFF_FFFF.
  2. 内核空间(Kernel space):0xFFFF_0000_0000至0xFFFF_FFFF_FFFF_FFFF.

ARM 架构处理器linux 内核分布

内核空间
    线性映射区域
    .data
    .init
    .text
    modules 
    PCI I/O
    vmemmap
    vmalloc
    ......
不规范地址(不允许使用)
用户空间
    用户
  1. KASAN(影子区):它是一个动态检测内存错误的工具,原理利用额外的内存标记可用内存的状态(将1/8内存作为影子区)。
  2. modules:内核模块使用的虚拟地址空间;
  3. vmalloc :vmalloc 函数使用的虚拟地址空间;
  4. .text :代码段
  5. .init: 模块初始化数据
  6. .data:数据段
  7. .bss: 静态内存分配段
  8. fixed:固定映射区域
  9. PCI I/O PCI设备的I/O地址空间
  10. vmemmap: 内存的物理地址如果不连续的话,就会存在内存空洞(稀疏内存)vmemmap就用来存放稀疏内存的page结构体的数据和虚拟地址空间。
  11. memory :线性映射区域

我们可以通过内存布局打印出来:linux 内核初始化完成后,整体布局稳定,通过vexpress 平台输出即可, 首先经过start_kernel 即(asmlinkage __visible void __init start_kernel(void) mem_init()即 mm_init(void) mem_init(); 最后打印 printk();

堆管理

堆是进程中主要用于动态分配变量和数据的内存区域,堆的管理对应程序员不是直接可见的,malloc 和内核之间的经典接口 是brk系统调用,负责扩展/收缩堆。

堆是一个连续的内存区域,在扩展时自下至上增长,其中mm_struct 结构,包含堆在虚拟地址空间的起始和当前结束地址(start_brk和brk)。

内存描述mm_struct 结构体当中,有堆起始地址和结束的成员,具体源码如下:
```
    struct mm_struct {
        struct {
            struct vm_area_struct *mmap;
            struct rb_root mm_rb;
            u64 vmacache_seqnum;
    #ifdef CONFIG_MMU
        unsigned long (*get_unmapped_area) (struct file *filp, unsigned long addr, unsigned long len ,unsigned long pgoff, unsigned long flags);
    #endif
    ....
        }
    }
```
```
unsigned long total_vm;
unsigned long locked_vm;
atomic64_t pinned_vm;
unsigned long data_vm;
unsigned long exec_vm;
unsigned long stack_vm;
unsigned long def_flags;
spinlock_t arg_lock;
unsigned long start_code, end_code ,start_data,end_data;
unsigned long start_brk,brk,start_stack;
unsigned long arg_start ,arg_end,env_start,env_end;
```
  1. brk 系统调用指定堆在虚拟地址空间中新的结束地址(如果堆将要收缩,当然可以小于当前值) brk系统调用动态分配 ``` SYSCALL_DEFINE1(brk,unsigned long ,brk) { unsigned long retval; unsigned long newbrk,oldbrk,origbrk; struct mm_struct mm = current->mm; struct vm_area_struct next; unsigned long min_brk; bool populate; bool downgraded = false; LIST_HEAD(uf); if (down_write_killable(&mm->mmap_sem)) return -EINTR; origbrk = mm->brk; ...... }
linux 系统当中有两个方法可以创建堆:
   brk()是系统调用,实际是设置进程数据段的结束地址,将数据段的结束地址向高地址移动,
   mmap() 向操作系统申请一段虚拟地址空间(使用映射到某个文件)。当不用此空间来映射到某个文件时,这块空间成为匿名空间可以用来作为堆空间。

per-CPU 计数器,引入它用来加速SMP系统上计数器操作,部分源码如下:

ifdef CONFIG_SMP

struct percpu_counter { raw_spinlock_t lock; s64 count; #ifdef CONFIG_HOTPLUG_CPU struct list_head list; #endif s32 __percpu *counters; }

## 多核调度分析 
SMP 是多核处理器最常见 ,主要是讲一个计算机上集中一组处理器,各个处理器是对等以其系统总线和内存子系统。
1. 根据处理器实际物理属性,cpu域可以分为超线程,多核。
    a. 超线程(SMT) linux 内核分类 CONFIG_SCHED_SMT;
    b. 多核(MC):linux 内核分类 CONFIG_SCHED_MC 

2. linux 内核对cpu 管理主要是通过bitmap 进行实现,并且定义四种状态:possible,online ,active 和present 具体如下:

extern struct cpumask __cpu_possible_mask; extern struct cpumask __cpu_online_mask; extern struct cpumask __cpu_present_mask; extern struct cpumask __cpu_active_mask;

define cpu_possible_mask((const struct cpumask *) & __cpu_possible_mask)//有多少个可以执行的cpu核心

define cpu_online_mask ((const struct cpumask *) & __cpu_online_mask) // 有多少个正在处于运行状态的cpu核心

define cpu_present_mask((const struct cpumask *) &__cpu_present_mask) //有多少个具备online条件的cpu核心,他们不一定处于online有的cpu 可能被热插拔

define cpu_active_mask((const struct cpumask *) & __cpu_active_mask) //有多少个活跃的cpu 核心

``` linux 内核把所有同一个级别的CPU归纳为一个调度组,然后把同一个级别的调度组组成一个调度域。

调度组和调度域

处理器有一个基本的调度域,它是硬件线程调度域,向上一次是核调度域,处理器调度域和NUMA节点调度域。

  1. 处理器拓扑结构:NUMA 和SMP。 a 核(core):一个处理器包含多个核,每个核有独立的一级缓存,所有核共享二级缓存。 b.硬件线程:也可以叫做虚拟处理器(或者叫做逻辑处理器),一个处理器或者核包含多个硬件线程,硬件线程共享一级缓存和二级缓存。

  2. 调度域和调度组 软件看到的处理器是最底层的处理器。linux 内核按照处理拓扑层次划分为调度域层次,每个调度域包含多个调度组, 每一个调度域对应一个NUMA 节点,每一个调度组对应numa节点处理器,所以每一个处理器有一个基本的硬件线程调度域