__mt_alloc源码分析(4)

__pool<false>是mtallocator中最核心的单线程内存池类,本文详细介绍了其成员变量、初始化过程及内部结构,帮助读者深入理解其运作机制。
class __pool<false>

终于,我们要开始研究mt allocator里最核心的东西了。__pool是实际上的内存池类,以前我们介绍的那么多类,都是在它的基础上建立起来的“上层建筑”。

 

188    template<bool _Thread>

189      class __pool;

 

这是__pool的原型,模板参数_Thread表示是否支持多线程。接着分别对_Threadfalsetrue的情况,进行了特化实现。由于有一部分__pool的实现代码在GCC源码中的“libstdc++-v3/src/ mt_allocator.cc”(以下简称mt_allocator.cc)里,所以读者请注意代码所属的文件,没有标明的情况下默认为mt_allocator.h

 

191    /// Specialization for single thread.

192    template<>

193      class __pool<false> : public __pool_base

 

单线程下的__pool<false>,它的基类是__pool_base__pool_base同时也是多线程下__pool<true>的基类,它的作用就是提炼出线程无关的部分。

 

194      {

195      public:

196        union _Block_record

197        {

198     // Points to the block_record of the next free block.

199     _Block_record* volatile         _M_next;

200        };

 

_Block_record是每个块的头信息,在单线程下只有一个成员,即指向下一个块的指针。对于分配给用户使用的块,_M_next是没有用的。但是为了与多线程下的头信息结构兼容,单线程的_M_next也不会被用户数据占用。

 

202        struct _Bin_record

203        {

204     // An "array" of pointers to the first free block.

205     _Block_record** volatile        _M_first;

206 

207     // A list of the initial addresses of all allocated blocks.

208     _Block_address*              _M_address;

209        };

 

_Bin_recordbin的内部结构,下图虽然在前面出现过,但现在仍是最好的注释。在单线程情况下,只有_M_first[0]会真正被使用到,所以__pool<false>当然不会傻到给_M_first分配4097个指针空间。后面可以看到,__pool<false>里所有bin_M_first数组的长度是1

_M_address是一个内存块链表,每当__poolOS申请到一块新内存,都会加入到这个链表中去。

 

127      struct _Block_address

128      {

129        void*         _M_initial;

130        _Block_address*       _M_next;

131      };

 

成员变量_M_initial指向当前申请到的新内存,_M_next指向下一个。

 

__pool<false>成员变量

__pool<false>的成员变量包括2部分:自己的,和从__pool_base继承来的。

我们先介绍它自己的。

 

245        // An "array" of bin_records each of which represents a specific

246        // power of 2 size. Memory to this "array" is allocated in

247        // _M_initialize().

248        _Bin_record* volatile _M_bin;

 

显然,这就是上图中的bin数组。

 

250        // Actual value calculated in _M_initialize().

251        size_t                      _M_bin_size; 

 

这是bin数组的长度。在__pool<false>对象构造的时候,它并不知道bin数组会有多长,因为这与其他参数有关,实际上_M_bin_size的值是在初始化函数_M_initialize里计算出来的,当然只计算一次。

 

__pool_base成员变量

__pool_base继承来的成员变量有:

 

172      // Configuration options.

173      _Tune                _M_options;

 

我们以前介绍过的可调参数,包括7个:

l   字节对齐:空闲块里数据区的偏移

l   多少字节以上的内存直接用new分配

l   可分配的最小的数据区大小

l   每次从OS申请的内存块的大小

l   可支持的最多线程数目

l   单个线程能保存的空闲块的百分比(超过的空闲块会归还给全局空闲链表)

l   是否直接使用newdelete

 

175      _Binmap_type*       _M_binmap;

 

字节数到bin索引的映射数组。_M_binmap的作用就是最快的找到用户申请的内存大小应该归哪个bin管理

 

177      // Configuration of the pool object via _M_options can happen

178      // after construction but before initialization. After

179      // initialization is complete, this variable is set to true.

180      bool            _M_init;

 

记录__pool是否已经初始化过了。注释的意思是,对于_M_options的更改可能发生在__pool构造完,但是还没有初始化的时候,所以用_M_init标记。当初始化工作做完了,_M_init会设置成true

需要补充的一点是,_Binmap_type的类型:

 

50       // Using short int as type for the binmap implies we are never

51       // caching blocks larger than 32768 with this allocator.

52       typedef unsigned short int _Binmap_type;

 

注释里说,由于_M_binmap数组元素的类型是short int,所以分配的块最大不能超过32768

说实话对这个结论我不太理解。因为据我所知,_M_binmap数组元素的意义是bin数组的索引,所以short int最大值为32768只是表示bin数组最大长度为32768,而232768已经是很大的一个内存块了。不过暂时我保留自己的看法,等研究到后面的代码再说。

 

 

初始化

__pool<false>的初始化工作包括2部分,构造函数和初始化函数。

 

238        explicit __pool()

239        : _M_bin(NULL), _M_bin_size(1) { }

240 

241        explicit __pool(const __pool_base::_Tune& __tune)

242        : __pool_base(__tune), _M_bin(NULL), _M_bin_size(1) { }

 

除了默认构造函数,__pool<false>还可以用调节参数__tune来构造。有一点注意的是,_M_bin_size被初始化为1

函数_M_initialize__pool<false>用来初始化内部参数的,它的实现代码位于mt_allocator.cc里面。

 

<mt_allocator.cc>

155    void

156    __pool<false>::_M_initialize()

157    {

158      // _M_force_new must not change after the first allocate(), which

159      // in turn calls this method, so if it's false, it's false forever

160      // and we don't need to return here ever again.

161      if (_M_options._M_force_new)

162        {

163     _M_init = true;

164     return;

165        }

 

可调参数_M_force_new表示是否直接使用newdelete进行内存分配和释放。如果它为true,那么__pool<false>无需任何初始化,allocatedeallocate函数直接调用newdelete。这里只把_M_init设置为true,然后返回。

 

167      // Create the bins.

168      // Calculate the number of bins required based on _M_max_bytes.

169      // _M_bin_size is statically-initialized to one.

170      size_t __bin_size = _M_options._M_min_bin;

 

可调参数_M_min_bin可分配的最小的数据区大小,也是bin管理的最小字节数。

 

171      while (_M_options._M_max_bytes > __bin_size)

 

可调参数_M_max_bytes表示“多少字节以上的内存直接用new分配”,那么就是bin管理的最大字节数。

 

172        {

173     __bin_size <<= 1;

174     ++_M_bin_size;

 

这里计算出了从_M_min_bin_M_max_bytes之间需要多少个bin,每个bin管理的字节数是指数递增的。注意_M_bin_size初始值为1

 

175        }

176       

177      // Setup the bin map for quick lookup of the relevant bin.

178      const size_t __j = (_M_options._M_max_bytes + 1) * sizeof(_Binmap_type);

 

计算_M_binmap数组的字节数。_M_binmap数组的长度是“_M_max_bytes + 1”,每个元素的类型是_Binmap_type

 

179      _M_binmap = static_cast<_Binmap_type*>(::operator new(__j));

 

分配足够的空间给_M_binmap

 

180      _Binmap_type* __bp = _M_binmap;

181      _Binmap_type __bin_max = _M_options._M_min_bin;

182      _Binmap_type __bint = 0;

183      for (_Binmap_type __ct = 0; __ct <= _M_options._M_max_bytes; ++__ct)

184        {

185     if (__ct > __bin_max)

186       {

187         __bin_max <<= 1;

188         ++__bint;

189       }

190     *__bp++ = __bint;

191        }

 

这段代码设置_M_binmap每个元素的值,使其等于对应的bin索引。首先注意到__bp__ct,初始值分别是_M_binmap0,在for循环里是同步增长的。然后看到__bint,它表示bin索引,初始值为0,每当“__ct > __bin_max”的时候加1。而__bin_max则表示每个bin管理的字节数,从_M_min_bin开始,在设置完前面的所有_M_binmap元素的时候,“__bin_max <<= 1”变成下一个bin管理的字节数。

如果读者看懂了这段代码,我就提出几个问题:

1._M_min_bin_M_max_bytes的类型都是size_t,在mt_allocator.h7681行可以找到它们的定义。为什么__bin_max__ct的类型是_Binmap_typeunsigned short int?

2.代码里唯一给_M_binmap数组元素赋值的语句是“*__bp++ = __bint”,而__bint作为_Binmap_type类型变量只在“__bin_max <<= 1”的时候会加1。即使__bin_maxsize_t类型的,__bint的值也不会超过3232位机器,或64,在64位机器上),也就是说_M_binmap数组元素的最大值不会超过64,那么何必要用unsigned short int类型表示?

 

 

193      // Initialize _M_bin and its members.

194      void* __v = ::operator new(sizeof(_Bin_record) * _M_bin_size);

195      _M_bin = static_cast<_Bin_record*>(__v);

 

分配bin数组的内存,_M_bin_size在前面已经计算出来了。

 

196      for (size_t __n = 0; __n < _M_bin_size; ++__n)

197        {

198     _Bin_record& __bin = _M_bin[__n];

199     __v = ::operator new(sizeof(_Block_record*));

200     __bin._M_first = static_cast<_Block_record**>(__v);

 

从这里可以看出来,__pool<false>里每个bin_M_first数组只有一个元素。

 

201     __bin._M_first[0] = NULL;

202     __bin._M_address = NULL;

203        }

204      _M_init = true;

 

_M_init设置为true,表示初始化工作完成了。

 

205    }

<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`)。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值