linux之 内存管理(3)-kernel 启动时 内存初始化 sparse memory

一、物理内存 model

1.1 UMA

Uniform-Memory-Access,一致内存访问

物理存储器被所有CPU均匀共享。所有处理机对所有存储字具有相同的存取时间

1.2 NUMA

Non-uniform Memory Access, 非一致内存访问

每个 CPU具有独立的内存和 I/O,由于其节点之间可以通过互联模块(如称为Crossbar Switch)进行连接和信息交互,因此每个CPU可以访问整个系统的内存,但是访问自己内存速度远远快于远端内存。

二、linux内存模型

linux内核支持三种内存模型。

平坦内存(Flat Memory):内存的物理地址空间是连续的,没有空洞 

不连续内存(Discontinuous Memory):内存的物理地址空间存在空洞 

稀疏内存(Sparse Memory):内存的物理地址空间存在空洞,或者如果要支持内存热插拔,就只能选择稀疏内存模型,并且以section为单位进行内存管理。

 

 还记得上一篇文章中,memblock 数据结构中有数组:

struct memblock_type {

    unsigned long cnt;
    
    unsigned long max;

    phys_addr_t total_size;

    struct memblock_region *regions;

    char *name;

};

三、内核的内存管理的数据结构

linux内存管理系统在用节点(node),区域(zone)和页(page)三级结构来管理整个物理内存。

3.1 节点(node)

一般情况下,对于NUMA(非一致内存访问),根据处理器和内存的距离划分为多个node。而对于UMA系统,就只有一个node。 内存节点使用一个pglist_data结构体来描述内存布局。

typedef struct pglist_data {

    struct zone node_zones[MAX_NR_ZONES];

    struct zonelist node_zonelists[MAX_ZONELISTS];

    int nr_zones; 

#ifdef CONFIG_FLAT_NODE_MEM_MAP 

    struct page *node_mem_map;

#ifdef CONFIG_PAGE_EXTENSION

    struct page_ext *node_page_ext;

#endif

#endif

    unsigned long node_start_pfn;

    unsigned long node_present_pages;

    unsigned long node_spanned_pages;

    int node_id;

    ...

} pg_data_t;
  • 成员node_id是节点标识符;
  • node_zones是内存区域数组,nr_zones是内存节点包含的内存区域的数量;
  • node_start_pfn是起始物理帧号
  • node_present_pages是实际存在的物理页的总数
  • node_spanned_pages是包括空洞的物理页总数
  • node_mem_map是指向页描述的数组。

3.2 内存区域(zone)

一个内存节点又分为多个内存区域,内存区域的类型定义如下:

enum zone_type {

#ifdef CONFIG_ZONE_DMA
    ZONE_DMA,
#endif

#ifdef CONFIG_ZONE_DMA32
    ZONE_DMA32,
#endif

    ZONE_NORMAL,

#ifdef CONFIG_HIGHMEM
    ZONE_HIGHMEM,
#endif

    ZONE_MOVABLE,

#ifdef CONFIG_ZONE_DEVICE
    ZONE_DEVICE,
#endif

    __MAX_NR_ZONES

};

ZONE_DMA:DMA区域,有些设备不能直接访问所有内存,需要使用DMA区域;

ZONE_DMA32:64位系统,如果既要支持只能直接 访问16MB以下内存的设备,又要支持只能直接访问4GB以下内存的32 位设备,那么必须使用DMA32区域;

ZONE_NORMAL:普通区域,直接映射到内核虚拟地址空间的内存区域;

ZONE_HIGHMEM:32位系统下,内核地址空间一般只有1GB,不能把1G以上的内存直接映射到内核地址空间,把不能直接映射的内存划分到高端内存区域。64位系统下一般不再需要高端内存区域;

ZONE_MOVABLE:用于防止内存碎片;

ZONE_DEVICE:为支持持久内存热插拔增加的内存区域。

从上面定义可知,大部分都是可选的,就是根据系统需要情况进行自定义。

同样,zone也采用一个数据结构zone来描述,主要成员如下:

struct zone {

    unsigned long _watermark[NR_WMARK];

    unsigned long watermark_boost;

    unsigned long nr_reserved_highatomic;

    long lowmem_reserve[MAX_NR_ZONES];

    struct pglist_data  *zone_pgdat;

    struct per_cpu_pageset __percpu *pageset;

    unsigned long       zone_start_pfn;

    atomic_long_t       managed_pages;

    unsigned long       spanned_pages;

    unsigned long       present_pages;

    const char      *name;

    int initialized;

    ZONE_PADDING(_pad1_)


    struct free_area    free_area[MAX_ORDER];

    unsigned long       flags;

    spinlock_t      lock;


    ZONE_PADDING(_pad2_)

    unsigned long percpu_drift_mark;

    bool            contiguous;


    ZONE_PADDING(_pad3_)

    atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];

    atomic_long_t       vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];

} ____cacheline_internodealigned_in_smp;

这里注意到zone这个结构是要求L1 Cache对齐的,因为这个结构是会经常被访问到的,这样有助于提升效率。

  • _watermark:页分配器使用的水位线
  • lowmem_reserved:zone中预留的内存
  • zone_pgdat:指向内存节点
  • node pageset:Per-CPU上的页集合
  • zone_start_pfn:zone中开始页面的页帧号
  • managed_pages:zone中被伙伴系统管理的页面数量
  • spanned_pages:zone包含的页面数量
  • present_pages:zone里实际管理的页面数量
  • free_area:管理空闲区域的数组

3.3 物理页(page)

每一个物理页对应了一个page结构

struct page {

    unsigned long flags;


    union {

        struct {  

            struct list_head lru;

            struct address_space *mapping;

            pgoff_t index; 

            unsigned long private;

        };


        struct {  

            union {

                struct list_head slab_list;

                struct { 

                    struct page *next;

                    int pages; 

                    int pobjects;

                };

            };

            struct kmem_cache *slab_cache; 

            void *freelist; 

            union {

                void *s_mem;

                unsigned long counters;

                struct {

                    unsigned inuse:16;

                    unsigned objects:15;

                    unsigned frozen:1;

                };

            };

        };

        struct { 

            unsigned long compound_head; 

            unsigned char compound_dtor;

            unsigned char compound_order;

            atomic_t compound_mapcount;

            unsigned int compound_nr;

        };

        struct { 

            unsigned long _compound_pad_1;

            atomic_t hpage_pinned_refcount;

            struct list_head deferred_list;

        };

        struct { 

            unsigned long _pt_pad_1;

            pgtable_t pmd_huge_pte; 

            unsigned long _pt_pad_2; 

            union {

                struct mm_struct *pt_mm; 

                atomic_t pt_frag_refcount; 

            };

        };

        struct { 

            struct dev_pagemap *pgmap;

            void *zone_device_data;

        };

        struct rcu_head rcu_head;

    };



    union {

        atomic_t _mapcount;

        unsigned int page_type;

        unsigned int active; 

        int units; 

    };


    atomic_t _refcount;

} _struct_page_alignment;
  • flags:标识符,包括此页属于的node节点号,zone号和页的属性;
  • lru:用于将此页描述符放入相应的链表;
  • freelist:用于SLAB描述符,指向第一个空闲对象地址
  • _refcount:表示该物理页的引用次数

四、内存初始化

mem_section初始化

ARM64内存组织采用的是Sparse内存模型,这种模型设计了一种比page更大的内存管理粒度mem_section。这里简要介绍下mem_section机制。

一个mem_section的大小,在AMR64下,为1GB,mem_section的总共数量由宏NR_MEM_SECTION定义:

#define MAX_PHYSMEM_BITS    CONFIG_ARM64_PA_BITS
#define SECTION_SIZE_BITS   30
#define SECTIONS_SHIFT  (MAX_PHYSMEM_BITS - SECTION_SIZE_BITS)
#define NR_MEM_SECTIONS     (1UL << SECTIONS_SHIFT)

如果是48位地址系统,那么NR_MEM_SECTIONS的大小就是(1<<18)=262144。

这些section内核中通过一个动态的二维数组来维护

#define SECTIONS_PER_ROOT       (PAGE_SIZE / sizeof (struct mem_section))
#define NR_SECTION_ROOTS    DIV_ROUND_UP(NR_MEM_SECTIONS, SECTIONS_PER_ROOT)
struct mem_section **mem_section;

这里,为了这个二维数组能够节省空间,采用的是一个二级指针的方式,然后在初始化的时候动态分配,并且在mem_section的上面又划分了一个层级——讲SECTIONS_PER_ROOT个struct mem_section划分为一个SECTION_ROOT。

mem_section的初始化主要在memblocks_present函数完成,看下这个函数的调用情况:

路径: mm/sparse.c   中有sparse_init()。

setup_arch()->bootmem_init(void)->sparse_init()->memblocks_present()

static void __init memblocks_present(void)

{
    unsigned long start, end;
    int i, nid;


    for_each_mem_pfn_range(i, MAX_NUMNODES, &start, &end, &nid)
        memory_present(nid, start, end);
}

static void __init memory_present(int nid, unsigned long start, unsigned long end)

{
    unsigned long pfn;

    if (unlikely(!mem_section)) {

        unsigned long size, align;

        size = sizeof(struct mem_section*) * NR_SECTION_ROOTS;

        align = 1 << (INTERNODE_CACHE_SHIFT);

        mem_section = memblock_alloc(size, align);

    }


    start &= PAGE_SECTION_MASK;

    mminit_validate_memmodel_limits(&start, &end);

    for (pfn = start; pfn < end; pfn += PAGES_PER_SECTION) {

        unsigned long section = pfn_to_section_nr(pfn);

        struct mem_section *ms;


        sparse_index_init(section, nid);

        set_section_nid(section, nid);


        ms = __nr_to_section(section);

        if (!ms->section_mem_map) {

            ms->section_mem_map = sparse_encode_early_nid(nid) |

                            SECTION_IS_ONLINE;

            section_mark_present(ms);

        }
    }
}

memblock_alloc函数给二级指针mem_section分配空间;

mminit_validate_memmodel_limits函数检查物理地址范围;

pfn_to_section_nr根据物理页获得section索引;

sparse_index_init为二级指针指向的mem_section数组分配空间;

__nr_to_section函数根据section索引获得当前mem_section结构指针;

最后标识section_mem_map。

这里对section_mem_map做个说明,它主要存储该mem_section的struct page数组的指针,还包含其他一些信息。

#define SECTION_MARKED_PRESENT  (1UL<<0)
#define SECTION_HAS_MEM_MAP (1UL<<1)
#define SECTION_IS_ONLINE   (1UL<<2)
#define SECTION_IS_EARLY    (1UL<<3)
#define SECTION_MAP_LAST_BIT    (1UL<<4)
#define SECTION_MAP_MASK    (~(SECTION_MAP_LAST_BIT-1))
#define SECTION_NID_SHIFT   3

section_mem_map处于不同的阶段其划分不同,在处于early早期阶段,划分如下:

在early阶段,还没有为mem_map分配物理内存,为了节省空间将后面的字段用于记录node id,sparse_init完成后,会为mem_map分配物理内存,然后section_mem_map会变成如下划分:

sparse_init

SPARSEMEM模型的初始化主要由sparse_init()函数完成,上面的mem_section初始化也是sparse_init()函数的一部分

void __init sparse_init(void)

{
    unsigned long pnum_end, pnum_begin, map_count = 1;
    int nid_begin;

    memblocks_present();


    pnum_begin = first_present_section_nr();
    nid_begin = sparse_early_nid(__nr_to_section(pnum_begin));


    set_pageblock_order();


    for_each_present_section_nr(pnum_begin + 1, pnum_end) {

        int nid = sparse_early_nid(__nr_to_section(pnum_end));

        if (nid == nid_begin) {

            map_count++;

            continue;

        }

        sparse_init_nid(nid_begin, pnum_begin, pnum_end, map_count);

        nid_begin = nid;

        pnum_begin = pnum_end;

        map_count = 1;

    }

    sparse_init_nid(nid_begin, pnum_begin, pnum_end, map_count);

    vmemmap_populate_print_last();

}

 first_present_section_nr函数获取到第一个mem_section id即pnum_begin;

sparse_early_nid函数获取node id;

set_pageblock_order,如果开启了CONFIG_HUGETLB_PAGE_SIZE_VARIABLE功能,则设置pageblock_order,这里我们系统配置没有开启;

下面开始遍历各个section,这里我们只有一个node,所以循环里只是统计这个node的section个数map_count;

sparse_init_nid函数初始化section。

static void __init sparse_init_nid(int nid, unsigned long pnum_begin,

                   unsigned long pnum_end,

                   unsigned long map_count)

{
    struct mem_section_usage *usage;
    unsigned long pnum;
    struct page *map;


    usage = sparse_early_usemaps_alloc_pgdat_section(NODE_DATA(nid),
            mem_section_usage_size() * map_count);

    sparse_buffer_init(map_count * section_map_size(), nid);

    for_each_present_section_nr(pnum_begin, pnum) {

        unsigned long pfn = section_nr_to_pfn(pnum);

        if (pnum >= pnum_end)
            break;


        map = __populate_section_memmap(pfn, PAGES_PER_SECTION,
                nid, NULL);

        check_usemap_section_nr(nid, usage);

        sparse_init_one_section(__nr_to_section(pnum), pnum, map, usage,

                SECTION_IS_EARLY);

        usage = (void *) usage + mem_section_usage_size();

    }

    sparse_buffer_fini();

}

 sparse_early_usemaps_alloc_pgdat_section函数为mem_section中的mem_section_usage申请内存;

sparse_buffer_init函数提前将一个节点内的物理内存管理结构mem_map申请好,后续每个mem_section的mem_map直接从sparsemap_buf中进行划分;

__populate_section_memmap函数将page结构与vmemmap建立起映射;

sparse_init_one_section初始化一个mem_section

static void __meminit sparse_init_one_section(struct mem_section *ms,

        unsigned long pnum, struct page *mem_map,

        struct mem_section_usage *usage, unsigned long flags)

{

    ms->section_mem_map &= ~SECTION_MAP_MASK;

    ms->section_mem_map |= sparse_encode_mem_map(mem_map, pnum)

        | SECTION_HAS_MEM_MAP | flags;

    ms->usage = usage;

}

 完成了sparse memory model初始化后,就可以进行zone的初始化了。

<think>嗯,用户问的是如何透彻理解芯片手册、外设协议、内存管理、中断机制这些硬件交互细节。首先,我需要确定用户的具体背景。可能是一个刚入门的嵌入式开发工程师或者学生,想要提升自己的硬件和驱动开发能力。他们可能已经有一些基础,但需要更系统的学习方法。 接下来,我得考虑用户的需求是什么。他们想从“做题者”变成“出题者”,也就是从执行者转变为设计者。所以需要深入理解底层技术,而不仅仅是应用现有的知识。这需要系统的学习路径和实践方法。 然后,我要分解问题中的各个部分:芯片手册、外设协议、内存管理、中断机制。每个部分都需要详细的方法论。比如芯片手册,用户可能不知道如何高效阅读和提取关键信息。外设协议如PCIe、USB等,可能需要参考官方文档和实际代码分析。内存管理和中断机制则涉及操作系统层面的知识,需要结合理论和调试实践。 还要考虑用户可能遇到的困难,比如文档过于复杂、缺乏实践环境或调试工具的使用技巧。这时候需要推荐工具和资源,比如使用QEMU模拟硬件,或者利用示波器、逻辑分析仪等设备进行调试。 另外,用户可能希望有具体的例子和步骤,这样更容易理解和操作。比如在SPI协议中,可以给出一个简单的驱动代码示例,说明如何配置寄存器,并处理数据传输。同时,强调通过实际项目来巩固知识,比如参与开源项目或自己设计硬件模块。 还要注意学习路径的循序渐进。从基础到高级,逐步深入。比如先学会读芯片手册,再学习协议规范,然后结合代码分析和调试,最后进行项目实践。同时,加入社区和讨论组可以帮助解决疑难问题,获取最新的技术动态。 最后,确保回答的结构清晰,分点说明,每个部分都有具体的方法、工具和实践建议。还要提供相关问题,帮助用户进一步深入思考和探索。</think>要透彻理解芯片手册、外设协议、内存管理、中断机制等硬件交互细节,需结合**文档精读、代码验证、工具调试、系统性实践**四层方法论,以下是具体路径: --- ### **1. 芯片手册:从信息挖掘到反向验证** #### **方法论** - **结构化阅读**: 以ARM Cortex-M系列为例,先定位`Memory Map`(内存映射)、`Register Summary`(寄存器列表)、`Interrupts`(中断向量表)等关键章节。 **示例**:STM32F4参考手册中,`RCC`(复位和时钟控制)寄存器的配置直接影响外设时钟使能,需结合时钟树图理解。 - **反向推导**: 通过现有驱动代码反查手册。例如分析Linux内核中的`arch/arm64/boot/dts/freescale/fsl-ls1021a.dtsi`设备树,对照手册验证GPIO控制器寄存器基地址和中断触发模式。 #### **工具辅助** - **交叉索引工具**:使用`grep`或`Global`工具在内核源码中搜索寄存器名称(如`UARTx_CR`),快速定位配置代码。 - **仿真验证**:通过QEMU模拟器运行裸机程序,观察寄存器行为。例如用QEMU调试ARMv7-M架构时,可实时查看NVIC(嵌套向量中断控制器)寄存器状态。 --- ### **2. 外设协议:从协议规范到物理层抓包** #### **核心协议学习路径** - **PCIe**: - 精读PCI-SIG官方规范(如`PCIe Base Specification 5.0`),重点理解TLP(事务层数据包)格式、链路训练流程。 - **实践**:用`lspci -vvv`查看设备能力寄存器(CAP),用Wireshark捕获PCIe事务层流量(需硬件支持,如LeCroy Protocol Analyzer)。 - **USB**: - 分析`usbutils`工具链输出(如`lsusb -v`)与USB 2.0规范的对应关系。 - **代码验证**:跟踪Linux内核`drivers/usb/core/hub.c`中的枚举逻辑,理解设备描述符请求(GET_DESCRIPTOR)的交互时序。 - **SPI**: - 通过逻辑分析仪(如Saleae Logic Pro)捕获SPI时钟极性(CPOL)和相位(CPHA)组合,验证手册中时序图。 - **驱动对比**:对比不同SoC的SPI控制器驱动(如Rockchip的`spi-rk3x.c`与Qualcomm的`spi-msm.c`),总结通用配置模式。 #### **协议栈分层调试** ```c // 示例:I2C适配器驱动中的消息传输函数 static int my_i2c_xfer(struct i2c_adapter *adap, u32 flags) { // 模拟SCL时钟生成,符合I2C规范的400kHz速率限制 for (bit = 0; bit < 8; bit++) { set_sda((data >> (7 - bit)) & 0x1); udelay(1); // 根据手册计算时钟高/低电平时间 set_scl(1); udelay(1); set_scl(0); } } ``` --- ### **3. 内存管理:从地址映射到MMU配置** #### **关键步骤** 1. **物理地址空间划分**: 在设备树中定义内存映射(如`memory@0x80000000`),验证内核启动日志中的`memblock`分配结果。 **示例**: ```dts memory@80000000 { device_type = "memory"; reg = <0x80000000 0x20000000>; // 512MB内存 }; ``` 2. **页表与缓存一致性**: - 在ARM64架构中,通过`set_memory`函数修改页表属性(如`PGprot`),观察DMA缓冲区的缓存一致性问题(如`dma_cache_maint`调用)。 - **调试**:使用`perf`监控TLB miss事件,优化页表映射粒度。 #### **实战案例** 设计DMA缓冲区时,需确保物理地址连续且符合缓存对齐要求: ```c dma_addr_t dma_handle; void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL); if (!buf) return -ENOMEM; // 验证dma_handle是否符合设备地址宽度限制(如32位DMA mask) WARN_ON(dma_handle + size > 0xFFFFFFFF); ``` --- ### **4. 中断机制:从向量表到GIC配置** #### **系统级分析** - **中断控制器编程**: 在ARM GIC(通用中断控制器)中,通过`ICC_IAR1_EL1`寄存器获取中断号,需结合`GICD_ISPENDRn`寄存器验证中断触发模式(电平/边沿触发)。 **示例**: ```c // 配置SPI中断为下降沿触发(对应GICD_ICFGRn寄存器位域) writel(0x2, base + GICD_ICFGRn(irq)); ``` - **中断嵌套与优先级**: 在RTOS中实现中断抢占时,需设置GIC优先级寄存器(`GICD_IPRIORITYRn`)并验证上下文保存效率。 #### **调试技巧** - **中断风暴排查**:使用`irqtop`工具监控中断频率,结合`cat /proc/interrupts`定位异常源。 - **硬件触发测试**:通过GPIO模拟外部中断信号,验证驱动的中断处理路径。 --- ### **5. 系统性实践建议** 1. **逆向工程经典驱动**: 分析Linux内核中的`stmmac`以太网驱动,理解如何通过MDIO总线读取PHY芯片状态寄存器(`MII_BMSR`)。 2. **构建最小系统验证**: 使用FPGA开发板(如Xilinx Zynq)编写裸机程序,手动配置DDR控制器、中断路由,观察硬件行为。 3. **工具链全栈掌握**: - **静态分析**:用`Sparse`检查驱动中的资源泄漏。 - **动态追踪**:通过`ftrace`跟踪中断上下文切换延迟。 - **硬件级调试**:使用JTAG调试器(如OpenOCD)单步执行初始化代码。 --- ### **关键资源推荐** - **文档**: - 《ARM Architecture Reference Manual》(ARMv8-A) - PCIe官方规范文档([PCI-SIG](https://pcisig.com/)- **工具**: - QEMU(全系统模拟)、Wireshark(协议分析)、Saleae逻辑分析仪 - **开源项目**: - Zephyr OS(轻量级RTOS,含完整驱动示例) - Barebox(嵌入式bootloader,贴近硬件层) ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值