美文网首页
iOS底层原理探究03-calloc探究

iOS底层原理探究03-calloc探究

作者: superFool | 来源:发表于2021-07-27 17:46 被阅读0次

    通过对《iOS底层原理探究01-alloc底层原理》我们知道了OC对象alloc的过程中实际上是都是走到_class_createInstanceFromZone方法生成的对象,而该方法中又是调用calloc为对象分配内存空间的,今天我们就来深入了解一下这个函数

    在研究calloc方法时定位到这个方法已经不在libobjc库里了而是在libmalloc库里,具体定位方法参考前面文章里的三种方法,libmalloc这里可以下载到源码

    拿到源码开始玩儿,我们在main函数里直接调用calloc函数跟流程

    main函数里直接调用calloc
    1. calloc里调用了_malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX)函数,再来看这个函数
      image.png
    • 这个 default_zone 其实是一个“假的”zone,它存在的目的就是要引导程序进入一个创建真正的 zone 的流程。
    1. _malloc_zone_calloc函数中1556行是关键代码ptr = zone->calloc(zone, num_items, size)

      image.png
      然而我们点进去只能看到calloc的函数声明并没有实现,这样显然是找不到后续流程的,我们得想点其他办法了
      image.png
      我们在这里打个断点运行起来停在这个断点后 p 一下 zone->calloc 发现这个实际调用的是default_zone_calloc函数,继续跟进这个函数
      image.png
    2. 看下default_zone_calloc方法的具体实现

      image.png
    • 引导创建真正的 zone
    • 使用真正的 zone 进行 calloc
      系统有一套创建zone的策略,runtime_default_zone()函数就是创建zone的入口函数接下来我们看下调用链,最终调用_malloc_initialize,为了流程的连贯这个函数我们放到后面研究,继续看后续流程
      创建zone函数调用链
      这里的zone->calloc(zone, num_items, size);用之前的方法po一下看实际调用的函数为nano_calloc
      image.png
      继续来看nano_calloc
    static void *
    nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
    {
        size_t total_bytes;
    
        if (calloc_get_size(num_items, size, 0, &total_bytes)) {
            return NULL;
        }
        // 如果要开辟的空间小于 NANO_MAX_SIZE 则进行nanozone_t的malloc。
        if (total_bytes <= NANO_MAX_SIZE) {
            void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
            if (p) {
                return p;
            } else {
                /* FALLTHROUGH to helper zone */
            }
        }
        //否则就进行helper_zone的流程
        malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
        return zone->calloc(zone, 1, total_bytes);
    }
    
    • 如果要开辟的空间小于 NANO_MAX_SIZE 则调用_nano_malloc_check_clear执行后续内存分配流程
    • 否则就进行 helper_zone 的流程
    image.png
    这里可以看到NANO_MAX_SIZE的值为256B

    这里内存分配的的重点是_nano_malloc_check_clea函数,深入了解一下她

    static void *
    _nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
    {
        MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
    
        void *ptr;
        size_t slot_key;
        // 获取16字节对齐之后的大小,slot_key非常关键,为slot_bytes/16的值,也是数组的二维下下标
        size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
        //根据_os_cpu_number经过运算获取 mag_index(meta_data的一维索引)
        mag_index_t mag_index = nano_mag_index(nanozone);
        //确定当前cpu对应的mag和通过size参数计算出来的slot,去对应metadata的链表中取已经被释放过的内存区块缓存
        nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
        //检测是否存在已经释放过,可以直接拿来用的内存,已经被释放的内存会缓存在 chained_block_s 链表
        //每一次free。同样会根据 index 和slot 的值回去 pMeta,然后把slot_LIFO的指针指向释放的内存。
        ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
        if (ptr) {
        
        ...省略无关代码
        
        //如果缓存的内存存在,这进行指针地址检查等异常检测,最后返回
        //第一次调用malloc时,不会执行这一块代码。
        } else {
        //没有释放过的内存,所以调用函数 获取内存
            ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
        }
    
        if (cleared_requested && ptr) {
            memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
        }
        return ptr;
    }
    

    该方法主要是通过 cpuslot 确定 index,从chained_block_s 链表中找出是否存在已经释放过的缓存。如果存在则进行指针检查之后返回,否则进入查询 meta data 或者开辟 band

    • slot_bytes 是通过segregated_size_to_fit()16字节对齐算法对齐后的大小
    • slot_keyslot_bytes/16的值,也是数组的二维下下标
    • 通过上面的参数去取已经被释放过的内存区块缓存
    • 取到缓存会对指针检测之后返回
    • 没有取到缓存调用segregated_next_block函数分配内存

    segregated_next_block函数深入了解

    static MALLOC_INLINE void *
    segregated_next_block(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
    {
        while (1) {
            //当前这块pMeta可用内存的结束地址
            uintptr_t theLimit = pMeta->slot_limit_addr; // Capture the slot limit that bounds slot_bump_addr right now
            //原子的为pMeta->slot_bump_addr添加slot_bytes的长度,偏移到下一个地址
            uintptr_t b = OSAtomicAdd64Barrier(slot_bytes, (volatile int64_t *)&(pMeta->slot_bump_addr));
            //减去添加的偏移量,获取当前可以获取的地址
            b -= slot_bytes; // Atomic op returned addr of *next* free block. Subtract to get addr for *this* allocation.
            
            if (b < theLimit) {   // Did we stay within the bound of the present slot allocation?
                //如果地址还在范围之内,则返回地址
                return (void *)b; // Yep, so the slot_bump_addr this thread incremented is good to go
            } else {
                //已经用尽了
                if (pMeta->slot_exhausted) { // exhausted all the bands availble for this slot?
                    pMeta->slot_bump_addr = theLimit;
                    return 0;                 // We're toast
                } else {
                    // One thread will grow the heap, others will see its been grown and retry allocation
                    _malloc_lock_lock(&nanozone->band_resupply_lock[mag_index]);
                    // re-check state now that we've taken the lock
                    //多线程的缘故,重新检查是否用尽
                    if (pMeta->slot_exhausted) {
                        _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
                        return 0; // Toast
                    } else if (b < pMeta->slot_limit_addr) {
                        //如果小于最大限制地址,当重新申请一个新的band后,重新尝试while
                        _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
                        continue; // ... the slot was successfully grown by first-taker (not us). Now try again.
                    } else if (segregated_band_grow(nanozone, pMeta, slot_bytes, mag_index)) {
                        //申请新的band成功,重新尝试while
                        _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
                        continue; // ... the slot has been successfully grown by us. Now try again.
                    } else {
                        pMeta->slot_exhausted = TRUE;
                        pMeta->slot_bump_addr = theLimit;
                        _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
                        return 0;
                    }
                }
            }
        }
    }
    

    该函数作用就是分配内存还是很重要的所以详细的讲一下,原理通俗一点说就像找座位一样,看当前座位有没有人,有人就看下一个座位有没有人 没人就坐下(分配内存)有人就继续找下一个,没有到最后一个座位之前就一直重复往后找,直到找到座位坐下(分配内存成功),或者直到最后一个座位都有人坐(分配内存失败)。

    • theLimit = pMeta->slot_limit_addr 当前这块pMeta可用内存的结束地址
    • b = OSAtomicAdd64Barrier(slot_bytes, (volatile int64_t *)&(pMeta->slot_bump_addr));原子的为pMeta->slot_bump_addr添加slot_bytes的长度,偏移到下一个地址
    • b -= slot_bytes;减去添加的偏移量,获取当前可以获取的地址如果地址还在范围之内,则返回地址return (void *)b;
    • 否则 pMeta->slot_exhausted 检查是否用尽已用尽直接返回return 0;分配失败
    • 如果小于最大限制地址,重新尝试 while循环 分配内存
    • 如果大于最大限制地址 segregated_band_grow(nanozone, pMeta, slot_bytes, mag_index) 申请新的band,申请成功重新尝试while循环
    • 申请band失败Meta->slot_exhausted = TRUE; pMeta->slot_bump_addr = theLimit;设置用尽标志 return 0;
      其中配合了加锁解锁保证线程安全

    如果是第一次调用 segregated_next_block 函数,band 不存在,缓存也不会存在,所以会调用segregated_band_grow。来开辟新的 band

    segregated_band_grow深入了解

    boolean_t
    segregated_band_grow(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
    {
        用来计算slot_current_base_addr 的联合体
        nano_blk_addr_t u; // the compiler holds this in a register
        uintptr_t p, s;
        size_t watermark, hiwater;
    
        if (0 == pMeta->slot_current_base_addr) { // First encounter?
            //利用nano_blk_addr_t 来计算slot_current_base_addr。
            u.fields.nano_signature = NANOZONE_SIGNATURE;
            u.fields.nano_mag_index = mag_index;
            u.fields.nano_band = 0;
            u.fields.nano_slot = (slot_bytes >> SHIFT_NANO_QUANTUM) - 1;
            u.fields.nano_offset = 0;
            
            //根据设置的属性计算 slot_current_base_addr 
            p = u.addr;
            pMeta->slot_bytes = (unsigned int)slot_bytes;
            pMeta->slot_objects = SLOT_IN_BAND_SIZE / slot_bytes;
        } else {
            p = pMeta->slot_current_base_addr + BAND_SIZE; // Growing, so stride ahead by BAND_SIZE
    
            u.addr = (uint64_t)p;
            if (0 == u.fields.nano_band) { // Did the band index wrap?
                return FALSE;
            }
    
            assert(slot_bytes == pMeta->slot_bytes);
        }
        pMeta->slot_current_base_addr = p;
    //BAND_SIZE = 1 << 21 = 2097152 = 256kb
        mach_vm_address_t vm_addr = p & ~((uintptr_t)(BAND_SIZE - 1)); // Address of the (2MB) band covering this (128KB) slot
        if (nanozone->band_max_mapped_baseaddr[mag_index] < vm_addr) {
        //如果最大能存储的地址 仍然小于目标地址,则小开辟新的band
    #if !NANO_PREALLOCATE_BAND_VM
            // Obtain the next band to cover this slot
            //// mac 和模拟器 或重新使用
            // Obtain the next band to cover this slot
            //重新申请新的 band,调用mach_vm_map  从pmap 转换。
            kern_return_t kr = mach_vm_map(mach_task_self(), &vm_addr, BAND_SIZE, 0, VM_MAKE_TAG(VM_MEMORY_MALLOC_NANO),
                    MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_DEFAULT, VM_PROT_ALL, VM_INHERIT_DEFAULT);
    
            void *q = (void *)vm_addr;
            if (kr || q != (void *)(p & ~((uintptr_t)(BAND_SIZE - 1)))) { // Must get exactly what we asked for
                if (!kr) {
                    mach_vm_deallocate(mach_task_self(), vm_addr, BAND_SIZE);
                }
                return FALSE;
            }
    #endif
            nanozone->band_max_mapped_baseaddr[mag_index] = vm_addr;
        }
    
        // Randomize the starting allocation from this slot (introduces 11 to 14 bits of entropy)
        if (0 == pMeta->slot_objects_mapped) { // First encounter?
            pMeta->slot_objects_skipped = (malloc_entropy[1] % (SLOT_IN_BAND_SIZE / slot_bytes));
            pMeta->slot_bump_addr = p + (pMeta->slot_objects_skipped * slot_bytes);
        } else {
            pMeta->slot_bump_addr = p;
        }
    
        pMeta->slot_limit_addr = p + (SLOT_IN_BAND_SIZE / slot_bytes) * slot_bytes;
        pMeta->slot_objects_mapped += (SLOT_IN_BAND_SIZE / slot_bytes);
    
        u.fields.nano_signature = NANOZONE_SIGNATURE;
        u.fields.nano_mag_index = mag_index;
        u.fields.nano_band = 0;
        u.fields.nano_slot = 0;
        u.fields.nano_offset = 0;
        s = u.addr; // Base for this core.
    
        // Set the high water mark for this CPU's entire magazine, if this resupply raised it.
        watermark = nanozone->core_mapped_size[mag_index];
        hiwater = MAX(watermark, p - s + SLOT_IN_BAND_SIZE);
        nanozone->core_mapped_size[mag_index] = hiwater;
    
        return TRUE;
    }
    
    • nano_blk_addr_t u 用来计算 slot_current_base_addr 的联合体
    • 利用 nano_blk_addr_t 来计算 slot_current_base_addr
    • 根据设置的属性计算 slot_current_base_addr
    • 如果最大能存储的地址 仍然小于目标地址,则小开辟新的band
    • mac 和模拟器 或重新使用
    • 重新申请新的 band,调用 mach_vm_mappmap 转换。
      当进入 segregated_band_grow 时,如果当前的 band 不够用,则使用 mach_vm_map 经由 pmap 重新映射物理内存到虚拟内存。

    至此malloc主流程全部分析完了,其中还剩余两个方法没有深入了解下面我们就了解一下她们
    segregated_size_to_fit16字节对齐算法

    //这两个宏定义也贴在这里方便下面分析用
    #define SHIFT_NANO_QUANTUM      4
    #define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16
    
    static MALLOC_INLINE size_t
    segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
    {
        size_t k, slot_bytes;
    
        if (0 == size) {
            size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
        }
        k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
        slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
        *pKey = k - 1;                                                  // Zero-based!
    
        return slot_bytes;
    }
    
    
    • 首先看上面的宏定义SHIFT_NANO_QUANTUM的值是4 NANO_REGIME_QUANTA_SIZE(1 << SHIFT_NANO_QUANTUM) 也就是1 << 4 等于 16
    • if (0 == size) { size = NANO_REGIME_QUANTA_SIZE; // Historical behavior }
      传进来的size如果等于0 size赋值为16
    • k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
      带入值得到k = (size + 15) >> 4;
      slot_bytes = k << SHIFT_NANO_QUANTUM;
      带入值得到slot_bytes = k <<4;
      (size + 15)先右移4位再左移4位,这样操作相当对(size + 15)的低四位抹零,而size + 15会让size低4位小于16大于0的值变成16,也就是说这样一通操作之后size就变成了16的整数倍且多余的余数也会进位变成16

    _malloc_initialize深入了解

    static void
    _malloc_initialize(void *context __unused)
    {
        ...... - 省略多余代码
        //创建helper_zone,
        malloc_zone_t *helper_zone = create_scalable_zone(0, malloc_debug_flags);
        //创建 nano zone
        if (_malloc_engaged_nano == NANO_V2) {
        zone = nanov2_create_zone(helper_zone, malloc_debug_flags);
        } else if (_malloc_engaged_nano == NANO_V1) {
        zone = nano_create_zone(helper_zone, malloc_debug_flags);
        }
        //如果上面的if else if 成立,这进入 nonazone
        if (zone) {
        malloc_zone_register_while_locked(zone);
        malloc_zone_register_while_locked(helper_zone);
    
        // Must call malloc_set_zone_name() *after* helper and nano are hooked together.
        malloc_set_zone_name(zone, DEFAULT_MALLOC_ZONE_STRING);
        malloc_set_zone_name(helper_zone, MALLOC_HELPER_ZONE_STRING);
        } else {
        //使用helper_zone分配内存
        zone = helper_zone;
        malloc_zone_register_while_locked(zone);
        malloc_set_zone_name(zone, DEFAULT_MALLOC_ZONE_STRING);
        }
        //缓存default_zone
        initial_default_zone = zone;
        .....    
    }
    

    这个函数还是挺简单的

    • 创建 helper_zone
    • 创建 nano zone
    • 如果上面的 if else if 成立,这进入nonazone
    • 使用 helper_zone 分配内存
    • 缓存 default_zone

    打完收工!

    参考资料:iOS 高级之美(六)—— malloc分析

    相关文章

      网友评论

          本文标题:iOS底层原理探究03-calloc探究

          本文链接:https://www.haomeiwen.com/subject/dikomltx.html