__mt_alloc源码分析(1)

本文深入研究了mtallocator的内部实现机制,重点介绍了__mt_alloc类及其关键成员函数allocate和deallocate的工作流程,同时还探讨了__mt_alloc_base类以及__common_pool_policy的作用。

本文从源代码级别研究mt allocator的内部实现,使用GCC 4.1.2版本的源码,主要源文件为库文件<ext/mt_allocator.h>GCC源码中的“libstdc++-v3/src/ mt_allocator.cc”。

假定读者对mt allocator的原理已有一定的了解。

 

class __mt_alloc

为避免一开始就深入到复杂的内存分配机制里,我采用从上往下的研究方法。最顶层自然是提供给用户使用的__mt_alloc类:

 

611    template<typename _Tp,

612        typename _Poolp = __common_pool_policy<__pool, __thread_default> >

613      class __mt_alloc : public __mt_alloc_base<_Tp>

 

每行代码前面都加注了所在的行号,方便查阅。如果没有特别注明文件名,所有的代码都摘自<ext/mt_allocator.h>

__mt_alloc2个模板参数,_Tp是管理的对象类型,_Poolp是所用的内存池策略。stl里实现的可用与__mt_alloc的内存池策略有2个,__common_pool_policy__per_type_pool_policy,前者对所有的_Tp类型一视同仁,后者会区别对待每个_Tp类型,这在后面详细研究。__mt_alloc的基类是__mt_alloc_base

根据stl标准,每个allocator都要定义几个固定的typedef类型:

 

616        typedef size_t                       size_type;

617        typedef ptrdiff_t                    difference_type;

618        typedef _Tp*                         pointer;

619        typedef const _Tp*                  const_pointer;

620        typedef _Tp&                         reference;

621        typedef const _Tp&                  const_reference;

622        typedef _Tp                          value_type;

 

__mt_alloc还多出来2个:

 

623        typedef _Poolp                 __policy_type;

624        typedef typename _Poolp::pool_type    __pool_type;

 

__policy_type__mt_alloc采用的内存池策略,而__pool_type则是内存池的类型。

rebind嵌套类也是stl标准规定的allocator成员之一,目的在于把对T1allocator重绑定到T2上。由于__mt_alloc2个模板参数,所以它的rebind类也有2个参数。只要你能看懂模板参数的推导,就能看懂rebind的定义。

__mt_alloc的公有成员函数接口相比stl标准的allocator也多出2个,

 

648        const __pool_base::_Tune

649        _M_get_options()

650        {

651     // Return a copy, not a reference, for external consumption.

652     return __policy_type::_S_get_pool()._M_get_options();

653        }

654       

655        void

656        _M_set_options(__pool_base::_Tune __t)

657        { __policy_type::_S_get_pool()._M_set_options(__t); }

658      };

 

分别是得到__mt_alloc的配置参数和设置配置参数。注意设置函数_M_set_options必须在任何内存分配动作之前调用,因为第一次调用allocate()的时候__mt_alloc会进行一次参数初始化,如果你在初始化之后再_M_set_options,不会有任何效果。

所有成员函数里,最重要的就是allocatedeallocate

allocate

660    template<typename _Tp, typename _Poolp>

661      typename __mt_alloc<_Tp, _Poolp>::pointer

662      __mt_alloc<_Tp, _Poolp>::

663      allocate(size_type __n, const void*)

 

这是allocate函数的原型,参数__n是要分配的对象的个数。

 

664      {

665        if (__builtin_expect(__n > this->max_size(), false))

666     std::__throw_bad_alloc();

 

__builtin_expectgcc的内建函数,原型是:long __builtin_expect(long exp, long c)。其第一个参数exp为一个整型表达式,这个内建函数的返回值也是这个exp,而c为一个编译期常量。这个函数的语义是:你期望exp表达式的值等于常量c,从而GCC为你优化程序,将符合这个条件的分支放在合适的地方。

max_size()函数返回“size_t(-1) / sizeof(_Tp)”,这个值在正常的运行情况下显然比__n大,所以这里用__builtin_expect进行一些优化。

 

668        __policy_type::_S_initialize_once();

 

这里就是__mt_alloc初始化配置参数的地方。从函数名字可以看出来,初始化工作只会进行一次。 _S_initialize_once属于__policy_type,在后面进行研究。

 

670        // Requests larger than _M_max_bytes are handled by operator

671        // new/delete directly.

672        __pool_type& __pool = __policy_type::_S_get_pool();

673        const size_t __bytes = __n * sizeof(_Tp);

674        if (__pool._M_check_threshold(__bytes))

675     {

676       void* __ret = ::operator new(__bytes);

677       return static_cast<_Tp*>(__ret);

678     }

 

得到内存池对象,计算实际需要的内存字节数__bytes,然后检查__bytes是否大于一个阀值。__mt_alloc默认对于128字节以上的内存块,直接调用newdelete进行分配和释放。这个阀值是可以配置的。

 

680        // Round up to power of 2 and figure out which bin to use.

681        const size_t __which = __pool._M_get_binmap(__bytes);

682        const size_t __thread_id = __pool._M_get_thread_id();

 

如果__bytes在阀值以内,那么首先找出它对应的bin索引。由于__mt_alloc只处理2的指数字节的内存块,所以对于其他的数值也要上调至2的指数,然后交给对应的bin来处理。_M_get_binmap使用一个长度为128(可配置)的数组,把字节数映射到bin的索引。__mt_alloc处理的最小的内存块默认为8字节(可配置),于是__mt_alloc总共有5bin,分别对应8163264128字节。这里__which的取值为04,取决于__bytes的大小。

为了在多线程之间管理内存,__mt_alloc给每个线程分配了一个线程id。不同于OS分配的线程id__mt_alloc分配的线程id取值从14096(默认,可以配置),而且可以回收和重新分配给新线程,id0保留给全局使用,不分配给任何线程。_M_get_thread_id函数负责给当前线程分配新的线程id,如果已有,那么直接返回这个id

 

684        // Find out if we have blocks on our freelist.  If so, go ahead

685        // and use them directly without having to lock anything.

686        char* __c;

687        typedef typename __pool_type::_Bin_record _Bin_record;

688        const _Bin_record& __bin = __pool._M_get_bin(__which);

 

由前面算出的__which,得到bin对象引用。

 

689        if (__bin._M_first[__thread_id])

 

每个bin都要维护4097个空闲块链表,其中__thread_id=0对应的是全局的空闲块链表,其他__thread_id对应线程自己的空闲块链表。这些链表的首地址存放在_M_first数组里,自然它的长度是4097。线程向__mt_alloc申请内存块时,首先检查自己的空闲块链表是否有元素,这通过判断_M_first[__thread_id]是否为NULL来完成。

 

690     {

691       // Already reserved.

692       typedef typename __pool_type::_Block_record _Block_record;

693       _Block_record* __block = __bin._M_first[__thread_id];

694       __bin._M_first[__thread_id] = __block->_M_next;

 

如果线程自己的空闲链表有元素,那么取出来到__block里,然后让链表头指向下一个块(如果有的话)。

 

696       __pool._M_adjust_freelist(__bin, __block, __thread_id);

 

在单线程情况下,_M_adjust_freelist什么事情都不做。在多线程情况下,_M_adjust_freelist负责调整_M_free_M_used计数器,然后设置__block_M_thread_id为当前线程的__thread_id

 

697       __c = reinterpret_cast<char*>(__block) + __pool._M_get_align();

 

准备把__block返回给用户。注意用户得到的并不是指向__block的指针,而是加上了一个_M_get_align(),默认情况下返回值为8,表示__block与实际数据区的距离。跳过前面_M_adjust_freelist设置的_M_thread_id,这是每个块都要记录的信息,要保留到deallocate的时候使用。

 

698     }

699        else

700     {

701       // Null, reserve.

702       __c = __pool._M_reserve_block(__bytes, __thread_id);

 

如果线程自己的空闲链表没有有元素,就向内存池申请。_M_reserve_block里的详细内容在后面研究。

 

703     }

704        return static_cast<_Tp*>(static_cast<void*>(__c));

 

最后返回给用户申请到的内存的指针。

 

705      }

 

deallocate

内存块的释放过程与分配过程基本相反,__mt_alloc里实现的代码也比较简单,当然,复杂的内幕都交给内存池去处理了。

 

707    template<typename _Tp, typename _Poolp>

708      void

709      __mt_alloc<_Tp, _Poolp>::

710      deallocate(pointer __p, size_type __n)

 

这是deallocate的函数原型,参数__p为要释放的对象首地址,__n为对象的个数。

 

711      {

712        if (__builtin_expect(__p != 0, true))

713     {

 

显然在多数情况下,__p != 0是成立的。如果__p0,那么deallocate什么都不需要做。

 

714       // Requests larger than _M_max_bytes are handled by

715       // operators new/delete directly.

716       __pool_type& __pool = __policy_type::_S_get_pool();

717       const size_t __bytes = __n * sizeof(_Tp);

718       if (__pool._M_check_threshold(__bytes))

719         ::operator delete(__p);

720       else

721         __pool._M_reclaim_block(reinterpret_cast<char*>(__p), __bytes);

722     }

723      }

 

接下来的代码都很容易理解,计算总共的字节数,检查是否超过了阀值。如果是则用delete释放内存,否则使用_M_reclaim_block归还给内存池。_M_reclaim_block的具体实现在后面研究。

 

class  __mt_alloc_base

__mt_alloc_base __mt_alloc的基类,也许你认为它应该处理了所有__mt_alloc遗留下来的“难题”,但是结果可能令你失望:__mt_alloc_base其实异常简单,连我在初次见到它是时候都有点吃惊。

 

560    /// @brief  Base class for _Tp dependent member functions.

 

这句话定位了__mt_alloc_base的角色,它只处理与_Tp有关的一些成员函数。

 

561    template<typename _Tp>

562      class __mt_alloc_base

563      {

564      public:

565        typedef size_t                    size_type;

566        typedef ptrdiff_t                 difference_type;

567        typedef _Tp*                      pointer;

568        typedef const _Tp*                const_pointer;

569        typedef _Tp&                      reference;

570        typedef const _Tp&                const_reference;

571        typedef _Tp                       value_type;

 

这是基本的typedef类型,与__mt_alloc里的完全一样。

 

573        pointer

574        address(reference __x) const

575        { return &__x; }

576 

577        const_pointer

578        address(const_reference __x) const

579        { return &__x; }

580 

581        size_type

582        max_size() const throw()

583        { return size_t(-1) / sizeof(_Tp); }

 

__mt_allocallocate函数里调用的的max_size()就是这个函数。

 

585        // _GLIBCXX_RESOLVE_LIB_DEFECTS

586        // 402. wrong new expression in [some_] allocator::construct

587        void

588        construct(pointer __p, const _Tp& __val)

589        { ::new(__p) _Tp(__val); }

 

使用placement new操作符在__p指向的内存上建立起值为__val _Tp对象。

 

591        void

592        destroy(pointer __p) { __p->~_Tp(); }

593      };

 

stl里,allocator的一个重要特点就是把内存分配与对象构造区分开来,同时也区分了内存释放与对象析构。函数constructdestroy分别负责构造和析构对象,而allocatedeallocate分别负责分配和释放内存。

 

class __common_pool_policy

虽然只是__mt_alloc的一个模板参数,但是它的意义远大于__mt_alloc_base。当然,__common_pool_policy也不是真正的“幕后主角”,它的作用只是提供_M_rebind,给__mt_alloc::rebind使用。

 

450    /// @brief  Policy for shared __pool objects.

451    template<template <bool> class _PoolTp, bool _Thread>

452      struct __common_pool_policy : public __common_pool_base<_PoolTp, _Thread>

 

模板参数_PoolTp是真正的内存池,_Thread表示是否需要多线程支持。

 

453      {

454        template<typename _Tp1, template <bool> class _PoolTp1 = _PoolTp,

455            bool _Thread1 = _Thread>

 

由于_M_rebind只在__mt_alloc::rebind里使用过,而且只使用了第一个模板参数_Tp1,所以后2个模板参数其实只是为了与__common_pool_policy自己的定义“兼容”,所以这里用了默认值。

 

456          struct _M_rebind

457          { typedef __common_pool_policy<_PoolTp1, _Thread1> other; };

 

other的类型其实只与_PoolTp1_Thread1有关,所以_Tp1模板参数虽然是__mt_alloc::rebind唯一关心的东西,但是到了这里,却发现毫无用处。其实整个_M_rebind都是毫无用处的,如果你回头看__mt_alloc::rebind定义的629行,就会发现pol_type其实就是_Poolp1自己,因为模板参数_Tp1_M_rebind里根本就没有用到过。

 

459        using  __common_pool_base<_PoolTp, _Thread>::_S_get_pool;

460        using  __common_pool_base<_PoolTp, _Thread>::_S_initialize_once;

 

2using的作用我也不太清楚,可能只是为了强调一下吧。因为_S_get_pool_S_initialize_once在基类里本来就是公有的,所以即使去掉这2using语句也可以。

 

461    };

<think> 我们正在讨论的是ARM架构下的内存管理,特别是`arch_reserve_mmu`函数的作用。根据引用[1]中提到的`map_desc`结构体,它用于描述内存映射的区域(虚拟地址、物理页帧号、长度、类型)。在ARM Linux内核启动过程中,会建立初始的页表映射,而`arch_reserve_mmu`函数很可能与这个初始映射的预留有关。 ### `arch_reserve_mmu`函数的作用分析 1. **内存映射预留** 在ARM内核启动的早期阶段,需要为初始页表分配内存。`arch_reserve_mmu`函数通常负责在内存中预留一块区域,用于存放初始页表(即一级页表PGD或段映射表)。这样,在启用MMU之前,页表已经准备就绪。 2. **避免内存覆盖** 初始页表的位置必须位于不会被内核覆盖的区域。通常,这块区域位于内核镜像之后,例如在`.bss`段结束后的位置。通过`arch_reserve_mmu`,我们明确保留这块内存,防止后续的内存分配(如`memblock`分配器)使用它。 3. **与`map_desc`的关系** 引用[1]中的`map_desc`结构体描述了需要映射的物理区域。`arch_reserve_mmu`可能利用这些映射信息,计算页表所需的大小并预留空间。例如: - 每个`map_desc`条目对应一个连续映射区域。 - 页表大小取决于映射的条目数和页表层级(ARMv7使用段映射,每个条目映射1MB)。 4. **具体实现位置** 在ARM Linux内核源码中,`arch_reserve_mmu`可能位于`arch/arm/mm/mmu.c`。其典型实现如下: ```c void __init arch_reserve_mmu(void) { /* 计算初始页表所需大小 */ size_t pgd_size = PTRS_PER_PGD * sizeof(pgd_t); /* 预留内存 */ initial_pg_dir = memblock_alloc(pgd_size, pgd_size); } ``` 5. **启动流程中的调用时机** 如引用[2]所示,在U-Boot或类似bootloader中,启动流程会依次调用初始化函数。在内核启动时: - `start_kernel` → `setup_arch` → `paging_init` → `prepare_page_table` → `map_lowmem`等。 - `arch_reserve_mmu`可能在`setup_arch`的早期被调用,确保页表内存被预留。 ### 总结 | **功能** | **说明** | |-------------------|--------------------------------------------------------------| | 页表内存预留 | 为初始页表分配物理内存,避免被其他模块占用[^1] | | 位置选择 | 通常位于内核镜像之后(如`.bss`段末尾),保证启用MMU前可访问 | | 大小计算 | 根据映射条目数(如`map_desc`数组)计算页表所需空间 | | 与启动流程集成 | 在`setup_arch`中调用,确保后续映射建立时有可用页表内存 | > **关键点**: > `arch_reserve_mmu`的核心目的是**为初始页表安全预留物理内存**,确保MMU启用时页表已就位且不被覆盖。这是ARM架构启动过程中内存管理的关键一步。 §§相关问题§§ 1. ARM Linux内核在启用MMU前如何保证页表内存的物理地址可访问? 2. 如何动态调整`arch_reserve_mmu`预留的内存大小以适应不同的映射需求? 3. 在支持LPAE(大物理地址扩展)的ARMv8系统中,`arch_reserve_mmu`有何变化? [^1]: ARM内存映射描述结构体`map_desc`的作用 [^2]: 系统启动流程中初始化函数的执行顺序 [^3]: 显示相关函数示例(虽不直接相关,但体现系统初始化流程) </think> ### `arch_reserve_mmu`函数的作用分析 `arch_reserve_mmu`是ARM架构中与**内存映射预留**相关的关键函数,主要作用是为MMU(内存管理单元)的初始页表分配保留内存区域。其核心功能如下: #### 1. **页表内存预留** - 在系统启动早期(如U-Boot或内核初始化阶段),为MMU的**初始页表**分配物理内存空间[^1]。 - 通过`memblock`或类似机制保留一块未映射的物理内存,防止被其他模块占用。 - 典型实现逻辑: ```c /* arch/arm/mm/mmu.c */ void __init arch_reserve_mmu(void) { /* 计算页表所需大小(通常为16KB~4MB)*/ size_t pgd_size = PTRS_PER_PGD * sizeof(pgd_t); /* 通过memblock保留物理内存 */ initial_pg_dir = memblock_alloc(pgd_size, pgd_size); } ``` #### 2. **映射描述符准备** - 为后续`map_desc`结构体初始化提供基础(引用[1]): ```c struct map_desc { unsigned long virtual; // 虚拟地址起始 unsigned long pfn; // 物理页帧号 unsigned long length; // 映射长度 unsigned int type; // 内存类型索引 }; ``` - 函数内部会预留内存用于存储这些映射描述符,描述内核关键区域(如代码段、设备内存等)的映射关系[^1]。 #### 3. **启动流程中的调用时机** - 在ARM启动序列中(引用[2]的`init_sequence`),该函数通常在MMU初始化前被调用: ``` start_armboot() → init_sequence[] → arch_reserve_mmu() // 预留页表内存 → mmu_init() // 建立实际页表 ``` - **关键作用**:确保在启用MMU前,页表所需物理内存已被保留且未被污染。 #### 4. **内存类型与属性配置** - 通过参数`type`关联`mem_types[]`数组(引用[1]),决定内存区域的缓存策略和访问权限: ```c static struct mem_type { pteval_t prot_pte; // 页表属性 pteval_t prot_pte_s2; // Hyp模式属性 pmdval_t prot_l1; // 段映射属性 ... } mem_types[]; ``` - 例如:为DDR内存配置`MT_MEMORY`类型(可缓存、可执行),为设备内存配置`MT_DEVICE`类型(不可缓存)。 ### 总结对比表 | **功能** | **具体作用** | **影响范围** | |-------------------|-----------------------------------------------------------|--------------------------| | 页表内存保留 | 分配物理内存存放初始页表 | 确保MMU启用前有有效页表 | | 映射描述符初始化 | 准备`map_desc`结构体,定义虚拟-物理映射关系 | 内核关键区域映射的基础 | | 内存属性配置 | 通过`mem_types`设置缓存/访问权限 | 影响系统性能和稳定性 | | 启动流程保障 | 在MMU初始化前预留资源,防止内存被覆盖 | 系统启动可靠性的关键 | > **关键点**: > `arch_reserve_mmu`的核心价值在于**为MMU建立安全的运行环境**,通过预留物理内存和定义映射规则,确保后续`create_mapping()`等函数能正确建立虚拟地址映射(如将DDR物理地址`0x10000000`映射到`0x80000000`)。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值