__mt_alloc源码分析(11)

探讨了多线程环境下__pool<true>内存池的内存释放机制,详细解析了_M_reclaim_block函数的工作流程及空闲块的转移策略。

__pool<true>的内存释

多线程内存池__pool<true>的内存释放工作主要由函数_M_reclaim_block完成。

 

<mt_allocator.cc>

250    void

251    __pool<true>::_M_reclaim_block(char* __p, size_t __bytes)

 

函数原型,参数__p为用户释放的内存块地址,__bytes为内存块字节大小。

 

252    {

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

254      const size_t __which = _M_binmap[__bytes];

255      const _Bin_record& __bin = _M_bin[__which];

 

找到负责的bin

 

257      // Know __p not null, assume valid block.

258      char* __c = __p - _M_get_align();

259      _Block_record* __block = reinterpret_cast<_Block_record*>(__c);

 

记住用户数据区与内存块的首地址是有偏移的。

 

260      if (__gthread_active_p())

261        {

262     // Calculate the number of records to remove from our freelist:

263     // in order to avoid too much contention we wait until the

264     // number of records is "high enough".

 

根据注释,下面的代码采用了一种“转移策略”,在线程的空闲块个数达到一定“条件”的时候转移若干个块给全局空闲链表。读者可能还记得前面介绍过多线程下mt allocator转移数据块的原理,那里面说要把free计数器控制在used计数器的_S_freelist_headroom %以内,并且每次移动的块数为block_count个,block_count表示_S_chunk_size大小的内存能生成多少个空闲块。但是,现在我告诉你,这个算法已经“过时”了,至少下面的代码不是采用这个算法。

 

265     const size_t __thread_id = _M_get_thread_id();

266     const _Tune& __options = _M_get_options(); 

267     const unsigned long __limit = 100 * (_M_bin_size - __which)

268                               * __options._M_freelist_headroom;

269 

270     unsigned long __remove = __bin._M_free[__thread_id];

271     __remove *= __options._M_freelist_headroom;

272     if (__remove >= __bin._M_used[__thread_id])

273       __remove -= __bin._M_used[__thread_id];

274     else

275       __remove = 0;

276     if (__remove > __limit && __remove > __bin._M_free[__thread_id])

277       {

278         _Block_record* __first = __bin._M_first[__thread_id];

279         _Block_record* __tmp = __first;

280         __remove /= __options._M_freelist_headroom;

281         const unsigned long __removed = __remove;

 

这里计算出来的__removed,就是实际应该从线程里转移的块的个数。关于这个“转移策略”,我想在介绍完这段代码后专门研究,因为这里的空间显然不适合列举那些公式。

 

282         while (--__remove > 0)

283           __tmp = __tmp->_M_next;

284         __bin._M_first[__thread_id] = __tmp->_M_next;

285         __bin._M_free[__thread_id] -= __removed;

 

数出__removed个块,剩余的还由__bin._M_first[__thread_id]保管。

 

287         __gthread_mutex_lock(__bin._M_mutex);

288         __tmp->_M_next = __bin._M_first[0];

289         __bin._M_first[0] = __first;

290         __bin._M_free[0] += __removed;

291         __gthread_mutex_unlock(__bin._M_mutex);

 

__removed个块加入到全局空闲链表里。注意这里需要给bin加锁,因为要修改bin里的全局数据。

 

292       }

293 

294     // Return this block to our list and update counters and

295     // owner id as needed.

296     --__bin._M_used[__block->_M_thread_id];

 

__block指向的是用户现在归还的那个块,现在把分配这个块的线程的used计数器减1。由于内存块可能在线程间传递,所以__block->_M_thread_id可能不是当前线程的id。虽然我不是很肯定,但是这里显然使用了一个假设:第296行代码是原子的执行的。

 

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

299     __bin._M_first[__thread_id] = __block;

300    

301     ++__bin._M_free[__thread_id];

 

把用户释放的块加入当前线程的空闲链表里。

 

302        }

303      else

304        {

305     // Not using threads, so single threaded application - return

306     // to global pool.

307     __block->_M_next = __bin._M_first[0];

308     __bin._M_first[0] = __block;

 

如果不支持多线程,直接把块归还到全局空闲链表里。

 

309        }

310    }

 

空闲块的转移策略

现在我们回头看__pool<true>::_M_reclaim_block里的转移策略是什么。我剔除了所有进行链表操作的代码,而只留下了参数计算的部分,那么代码就变成这样了:

 

<mt_allocator.cc>

267     const unsigned long __limit = 100 * (_M_bin_size - __which)

268                               * __options._M_freelist_headroom;

 

270     unsigned long __remove = __bin._M_free[__thread_id];

271     __remove *= __options._M_freelist_headroom;

272     if (__remove >= __bin._M_used[__thread_id])

273       __remove -= __bin._M_used[__thread_id];

274     else

275       __remove = 0;

276     if (__remove > __limit && __remove > __bin._M_free[__thread_id])

277       {

 

280         __remove /= __options._M_freelist_headroom;

281         const unsigned long __removed = __remove;

 

285         __bin._M_free[__thread_id] -= __removed;

 

290         __bin._M_free[0] += __removed;

291         

292       }

 

根据这部分代码,我可以用数学公式的形式写出转移策略:

1)初始定义:

           Ffree计数器的值;

           Uused计数器的值;

           B_M_bin_size,即bin的个数;

           W__which,即当前bin的索引,取值范围[0, B-1]

           H_M_freelist_headroom,用户可调的参数。

2)公式推导:

计算limit

           L = 100 * (B - W) * H = 100H(B-W)

计算remove

     R = F * H - U = FH-U

根据代码,如果FH < U,则R = 0。这种情况我们可以忽略,因为我们关心的是什么时候需要转移,以及应该转移多少。

接下来根据2个条件判断是否应该转移。

条件

           R > L  =>  FH-U > 100H(B-W)  =>  F > 100(B-W) + U/H = F1

条件

           R > F  =>  FH-U > F  =>  F > U/(H-1) = F2

如果条件都满足,那么,计算需要转移的块的个数removed

           D = R / H = (FH-U)/H = F-U/H

计算剩余块的个数

           L = F - D = U/H

我们首先弄清楚H(也就是可调参数_M_freelist_headroom)到底是什么意义。从剩余块L的计算公式可以看到,无论F是多少,只要发生了块转移,那么最终线程里剩余的块只与Uused计数器)和H有关,而且剩余量是:

           U/H = (100 / H) % * U

这个算法试图把free计数器控制在used计数器值的(100 / _M_freelist_headroom)%以内,而不是以前说的_M_freelist_headroom%以内。

同时我们也看到,块转移并不是在F超过L的时候马上进行的,而是必须满足条件,为什么呢?我们从需要转移的块的个数着手:

根据条件,和D的计算公式,有

           D > F1 - U/H = 100(B-W) + U/H - U/H = 100(B-W) = D1,且

           D > F2 - U/H = U/(H-1) - U/H = U/(H(H-1)) = D2

这说明只有当可转移的块个数达到一定量的时候,转移操作才会进行。D1基本是一个bin的常数,比如8128字节的5bin,各自的D1分别为500400300200100,对于块越小的binD1的值越大。D2是与used计数器和_M_freelist_headroom相关的值。可以看出used有一个阀值,在这个值以下的时候,必须用D1来防止“不必要”的转移。可以算出这个阀值是:

           D1 = D=>  UT = 100(B-W)H(H-1)

U > UT时,条件会起作用,同时D2也会起作用。下表列出了一个计算实例,给读者一些“感官印象”。

U

B-W

H

F1

F2

UT

L

D

0

1

5

100

0

2000

0

100

500

1

5

200

125

2000

100

100

1000

1

5

300

250

2000

200

100

1500

1

5

400

375

2000

300

100

2000

1

5

500

500

2000

400

100

2500

1

5

600

625

2000

500

125

3000

1

5

700

750

2000

600

150

3500

1

5

800

875

2000

700

175

4000

1

5

900

1000

2000

800

200

4500

1

5

1000

1125

2000

900

225

5000

1

5

1100

1250

2000

1000

250

 

从上表可以看出,当_M_freelist_headroomH)为5时,每次转移之后剩余块数L都是used计数器值(U)的20%。在U的值小于UT=2000)的时候,F1的值小于F2,条件起主要作用,此时每次至少转移100=100(B-W))个块;当U大于UT的时候,条件起主要作用,每次转移的块数逐步增多。

<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、付费专栏及课程。

余额充值