iOS 高級之美(六)—— malloc分析

前言: iOS 高級之美 是本人總結了一些工作實際開發(fā)研究以及面試重點梁丘,圍繞底層進行 源碼分析 - LLDB 調(diào)試 - 源碼斷點 - 匯編調(diào)試童叠,讓讀者真正感受 Runtime底層之美~??
目錄如下:

iOS 高級之美(一)—— iOS_objc4-756.2 最新源碼編譯調(diào)試
iOS 高級之美(二)—— OC對象底層上篇
iOS 高級之美(三)—— OC對象底層下篇
iOS 高級之美(四)—— isa原理分析
iOS 高級之美(五)—— 類結構分析
iOS 高級之美(六)—— malloc分析

image

我們前面分析了對象的創(chuàng)建饼煞,其中一個非常重要的點:申請內(nèi)存空間!
然而 obj = (id)calloc(1, size) 這一段代碼所在位置不再是 libObjc4,它定位到了 libmalloc , 至于愈合定位的大家可以參考筆者前面的文章儒恋。這個篇章我們針對 malloc 展開而分析

那么 calloc 方法做了什么呢湖笨,讓我們來一探究竟!

一剥悟、malloc_zone_t 分析

這個家伙是一個非常重要的家伙灵寺,我們先來看看 malloc_zone_t 的結構

typedef struct _malloc_zone_t {
void    *reserved1;    /* RESERVED FOR CFAllocator DO NOT USE */
    void    *reserved2;    /* RESERVED FOR CFAllocator DO NOT USE */
    size_t     (* MALLOC_ZONE_FN_PTR(size))(struct _malloc_zone_t *zone, const void *ptr); /* returns the size of a block or 0 if not in this zone; must be fast, especially for negative answers */
    void     *(* MALLOC_ZONE_FN_PTR(malloc))(struct _malloc_zone_t *zone, size_t size);
    void     *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
    void     *(* MALLOC_ZONE_FN_PTR(valloc))(struct _malloc_zone_t *zone, size_t size); /* same as malloc, but block returned is set to zero and is guaranteed to be page aligned */
    void     (* MALLOC_ZONE_FN_PTR(free))(struct _malloc_zone_t *zone, void *ptr);
    void     *(* MALLOC_ZONE_FN_PTR(realloc))(struct _malloc_zone_t *zone, void *ptr, size_t size);
    void     (* MALLOC_ZONE_FN_PTR(destroy))(struct _malloc_zone_t *zone);
    const char    *zone_name;

    unsigned    (* MALLOC_ZONE_FN_PTR(batch_malloc))(struct _malloc_zone_t *zone, size_t size, void **results, unsigned num_requested);

    struct malloc_introspection_t    * MALLOC_INTROSPECT_TBL_PTR(introspect);
    unsigned    version;

    void *(* MALLOC_ZONE_FN_PTR(memalign))(struct _malloc_zone_t *zone, size_t alignment, size_t size);

    void (* MALLOC_ZONE_FN_PTR(free_definite_size))(struct _malloc_zone_t *zone, void *ptr, size_t size);

    size_t     (* MALLOC_ZONE_FN_PTR(pressure_relief))(struct _malloc_zone_t *zone, size_t goal);

    boolean_t (* MALLOC_ZONE_FN_PTR(claimed_address))(struct _malloc_zone_t *zone, void *ptr);

} malloc_zone_t;

malloc_zone_t 是一個非常基礎結構区岗,里面包含一堆函數(shù)指針略板,用來存儲一堆相關的處理函數(shù)的具體實現(xiàn)的地址,例如malloc慈缔、free叮称、realloc等函數(shù)的具體實現(xiàn)。后續(xù)會基于malloc_zone_t進行擴展胀糜。

二去件、calloc 的流程

2.1 calloc -> malloc_zone_calloc 的流程

void * calloc(size_t num_items, size_t size)
{
    void *retval;
    retval = malloc_zone_calloc(default_zone, num_items, size);
    if (retval == NULL) {
        errno = ENOMEM;
    }
    return retval;
}
  • 這個 default_zone 其實是一個“假的”zone乍构,同時它也是malloc_zone_t類型杨伙。它存在的目的就是要引導程序進入一個創(chuàng)建真正的 zone 的流程护盈。
  • 下面來看一下 default_zone 的引導流程也糊。

2.2 default_zone 引導

void * malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

    void *ptr;
    if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
        internal_check();
    }

    ptr = zone->calloc(zone, num_items, size);
    
    if (malloc_logger) {
        malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
                (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
    }

    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
    return ptr;
}
  • ptr = zone->calloc(zone, num_items, size)
  • 此時傳進來的 zone 的類型是 上面 calloc 傳入的 defaultzone辫封,所以 zone->calloc的調(diào)用實現(xiàn)要看defaultzone 的定義砖第。

2.3 defaultzone 的定義

static virtual_default_zone_t virtual_default_zone
__attribute__((section("__DATA,__v_zone")))
__attribute__((aligned(PAGE_MAX_SIZE))) = {
    NULL,
    NULL,
    default_zone_size,
    default_zone_malloc,
    default_zone_calloc,
    default_zone_valloc,
    default_zone_free,
    default_zone_realloc,
    default_zone_destroy,
    DEFAULT_MALLOC_ZONE_STRING,
    default_zone_batch_malloc,
    default_zone_batch_free,
    &default_zone_introspect,
    10,
    default_zone_memalign,
    default_zone_free_definite_size,
    default_zone_pressure_relief,
    default_zone_malloc_claimed_address,
};
  • 從上面的結構可以看出 defaultzone->calloc 實際的函數(shù)實現(xiàn)為 default_zone_calloc荷腊。
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    zone = runtime_default_zone();
    
    return zone->calloc(zone, num_items, size);
}
  • 引導創(chuàng)建真正的 zone
  • 使用真正的 zone 進行 calloc

2.4 zone分析

在創(chuàng)建正在的 zone時绍移,其實系統(tǒng)是有對應的一套創(chuàng)建策略的悄窃。在跟蹤 runtime_default_zone 方法后,最終會進入如下調(diào)用

image

static void
_malloc_initialize(void *context __unused)
{
    ...... - 省略多余代碼
    //創(chuàng)建helper_zone,
    malloc_zone_t *helper_zone = create_scalable_zone(0, malloc_debug_flags);
    //創(chuàng)建 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分配內(nèi)存
    zone = helper_zone;
    malloc_zone_register_while_locked(zone);
    malloc_set_zone_name(zone, DEFAULT_MALLOC_ZONE_STRING);
    }
    //緩存default_zone
    initial_default_zone = zone;
    .....    
}
  • 創(chuàng)建 helper_zone
  • 創(chuàng)建 nano zone
  • 如果上面的 if else if 成立轧抗,這進入 nonazone
  • 使用 helper_zone 分配內(nèi)存
  • 緩存 default_zone

在這里 會存在兩種 zone

    1. nanozone_t
    1. scalable_zone

2.5 nanozone_t 分析

typedef struct nano_meta_s {
 OSQueueHead            slot_LIFO MALLOC_NANO_CACHE_ALIGN;
    unsigned int        slot_madvised_log_page_count;
    volatile uintptr_t        slot_current_base_addr;
    volatile uintptr_t        slot_limit_addr;
    volatile size_t        slot_objects_mapped;
    volatile size_t        slot_objects_skipped;
    bitarray_t            slot_madvised_pages;
    // position on cache line distinct from that of slot_LIFO
    volatile uintptr_t        slot_bump_addr MALLOC_NANO_CACHE_ALIGN;
    volatile boolean_t        slot_exhausted;
    unsigned int        slot_bytes;
    unsigned int        slot_objects;
} *nano_meta_admin_t;

    // vm_allocate()'d, so page-aligned to begin with.
typedef struct nanozone_s {
    // first page will be given read-only protection
    malloc_zone_t        basic_zone;
    uint8_t            pad[PAGE_MAX_SIZE - sizeof(malloc_zone_t)];

    // remainder of structure is R/W (contains no function pointers)
    // page-aligned
    // max: NANO_MAG_SIZE cores x NANO_SLOT_SIZE slots for nano blocks {16 .. 256}
    //以Mag、Slot為維度瞬测,維護申請的band內(nèi)存部分 slot 的范圍為 1~16
    struct nano_meta_s        meta_data[NANO_MAG_SIZE][NANO_SLOT_SIZE];//
    _malloc_lock_s            band_resupply_lock[NANO_MAG_SIZE];
    uintptr_t           band_max_mapped_baseaddr[NANO_MAG_SIZE];
    size_t            core_mapped_size[NANO_MAG_SIZE];
    unsigned            debug_flags;
    uintptr_t            cookie;
    malloc_zone_t        *helper_zone;
} nanozone_t;
  • nanozone_t 同樣是 malloc_zone_t 類型横媚。在nano_create_zone 函數(shù)內(nèi)部會完成對 calloc等函數(shù)的重新賦值纠炮。

2.6 nano_create_zone 分析

malloc_zone_t *
nano_create_zone(malloc_zone_t *helper_zone, unsigned debug_flags)
{
    nanozone_t *nanozone;
    int i, j;
    //構造nano zone
    /* Note: It is important that nano_create_zone resets _malloc_engaged_nano
     * if it is unable to enable the nanozone (and chooses not to abort). As
     * several functions rely on _malloc_engaged_nano to determine if they
     * should manipulate the nanozone, and these should not run if we failed
     * to create the zone.
     */
//     MALLOC_ASSERT(_malloc_engaged_nano == NANO_V1);

    /* get memory for the zone. */
    nanozone = nano_common_allocate_based_pages(NANOZONE_PAGED_SIZE, 0, 0, VM_MEMORY_MALLOC, 0);
    if (!nanozone) {
        _malloc_engaged_nano = NANO_NONE;
        return NULL;
    }
    //構造對zone 的一些函數(shù)進行重新賦值
    /* set up the basic_zone portion of the nanozone structure */
    nanozone->basic_zone.version = 10;
    nanozone->basic_zone.size = (void *)nano_size;
    nanozone->basic_zone.malloc = (debug_flags & MALLOC_DO_SCRIBBLE) ? (void *)nano_malloc_scribble : (void *)nano_malloc;
    nanozone->basic_zone.calloc = (void *)nano_calloc;
    nanozone->basic_zone.valloc = (void *)nano_valloc;
    nanozone->basic_zone.free = (debug_flags & MALLOC_DO_SCRIBBLE) ? (void *)nano_free_scribble : (void *)nano_free;
    nanozone->basic_zone.realloc = (void *)nano_realloc;
    nanozone->basic_zone.destroy = (void *)nano_destroy;
    nanozone->basic_zone.batch_malloc = (void *)nano_batch_malloc;
    nanozone->basic_zone.batch_free = (void *)nano_batch_free;
    nanozone->basic_zone.introspect = (struct malloc_introspection_t *)&nano_introspect;
    nanozone->basic_zone.memalign = (void *)nano_memalign;
    nanozone->basic_zone.free_definite_size = (debug_flags & MALLOC_DO_SCRIBBLE) ? (void *)nano_free_definite_size_scribble
                                                                                          : (void *)nano_free_definite_size;

    nanozone->basic_zone.pressure_relief = (void *)nano_pressure_relief;
    nanozone->basic_zone.claimed_address = (void *)nano_claimed_address;

    nanozone->basic_zone.reserved1 = 0; /* Set to zero once and for all as required by CFAllocator. */
    nanozone->basic_zone.reserved2 = 0; /* Set to zero once and for all as required by CFAllocator. */

    mprotect(nanozone, sizeof(nanozone->basic_zone), PROT_READ); /* Prevent overwriting the function pointers in basic_zone. */

    /* Nano zone does not support MALLOC_ADD_GUARD_PAGES. */
    if (debug_flags & MALLOC_ADD_GUARD_PAGES) {
        malloc_report(ASL_LEVEL_INFO, "nano zone does not support guard pages\n");
        debug_flags &= ~MALLOC_ADD_GUARD_PAGES;
    }

    /* set up the remainder of the nanozone structure */
    nanozone->debug_flags = debug_flags;

    if (phys_ncpus > sizeof(nanozone->core_mapped_size) /
            sizeof(nanozone->core_mapped_size[0])) {
        MALLOC_REPORT_FATAL_ERROR(phys_ncpus,
                "nanozone abandoned because NCPUS > max magazines.\n");
    }

    /* Initialize slot queue heads and resupply locks. */
    OSQueueHead q0 = OS_ATOMIC_QUEUE_INIT;
    for (i = 0; i < nano_common_max_magazines; ++i) {
        _malloc_lock_init(&nanozone->band_resupply_lock[i]);

        for (j = 0; j < NANO_SLOT_SIZE; ++j) {
            nanozone->meta_data[i][j].slot_LIFO = q0;
        }
    }

    /* Initialize the security token. */
    nanozone->cookie = (uintptr_t)malloc_entropy[0] & 0x0000ffffffff0000ULL; // scramble central 32bits with this cookie

    nanozone->helper_zone = helper_zone;

    return (malloc_zone_t *)nanozone;
}
  • 構造 nano zone
  • 構造對 zone 的一些函數(shù)進行重新賦值
  • Nano zone 不支持 MALLOC_ADD_GUARD_PAGES
  • 建立其余的 nanozone 結構
  • 初始化插槽隊列頭并重新供應鎖
  • 初始化安全令牌。

2.7 nano_calloc 分析

過程參考 defaultzone 灯蝴』挚冢回到上面 default_zone_calloc 函數(shù)內(nèi)。下一步就是使用 nanozone_t 調(diào)用 calloc穷躁。

下面是 nano_calloc 的實現(xiàn)

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 則進行
  • 否則就進行 helper_zone 的流程

2.8 _nano_malloc_check_clear分析

這里我們也可以看出使用 nanozone_t 的限制為不超過256B。繼續(xù)看 _nano_malloc_check_clear

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字節(jié)對齊之后的大小,slot_key非常關鍵问潭,為slot_bytes/16的值猿诸,也是數(shù)組的二維下下標
    size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
    //根據(jù)_os_cpu_number經(jīng)過運算獲取 mag_index(meta_data的一維索引)
    mag_index_t mag_index = nano_mag_index(nanozone);
    //確定當前cpu對應的mag和通過size參數(shù)計算出來的slot,去對應metadata的鏈表中取已經(jīng)被釋放過的內(nèi)存區(qū)塊緩存
    nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
    //檢測是否存在已經(jīng)釋放過狡忙,可以直接拿來用的內(nèi)存,已經(jīng)被釋放的內(nèi)存會緩存在 chained_block_s 鏈表
    //每一次free梳虽。同樣會根據(jù) index 和slot 的值回去 pMeta,然后把slot_LIFO的指針指向釋放的內(nèi)存去枷。
    ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
    if (ptr) {
    
    ...省略無關代碼
    
    //如果緩存的內(nèi)存存在怖辆,這進行指針地址檢查等異常檢測,最后返回
    //第一次調(diào)用malloc時删顶,不會執(zhí)行這一塊代碼竖螃。
    } else {
    //沒有釋放過的內(nèi)存,所以調(diào)用函數(shù) 獲取內(nèi)存
        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;
}
  • 獲取16字節(jié)對齊之后的大小, slot_key 非常關鍵逗余,為slot_bytes/16 的值特咆,也是數(shù)組的二維下下標

  • 根據(jù) _os_cpu_number 經(jīng)過運算獲取 mag_index ( meta_data 的一維索引)

  • 確定當前 cpu 對應的 mag 和通過 size 參數(shù)計算出來的 slot,去對應 metadata 的鏈表中取已經(jīng)被釋放過的內(nèi)存區(qū)塊緩存

  • 檢測是否存在已經(jīng)釋放過录粱,可以直接拿來用的內(nèi)存,已經(jīng)被釋放的內(nèi)存會緩存在chained_block_s鏈表

  • 每一次 free腻格。同樣會根據(jù) indexslot 的值回去 pMeta,然后把 slot_LIFO 的指針指向釋放的內(nèi)存啥繁。

  • 如果緩存的內(nèi)存存在菜职,這進行指針地址檢查等異常檢測,最后返回

  • 沒有釋放過的內(nèi)存旗闽,所以調(diào)用函數(shù) 獲取內(nèi)存

該方法主要是通過 cpuslot 確定 index酬核,從chained_block_s 鏈表中找出是否存在已經(jīng)釋放過的緩存。如果存在則進行指針檢查之后返回适室,否則進入查詢 meta data 或者開辟 band嫡意。

2.9 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可用內(nèi)存的結束地址
        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?
            //如果地址還在范圍之內(nèi)蔬螟,則返回地址
            return (void *)b; // Yep, so the slot_bump_addr this thread incremented is good to go
        } else {
            //已經(jīng)用盡了
            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;
                }
            }
        }
    }
}
  • 當前這塊 pMeta 可用內(nèi)存的結束地址

  • 原子的為 pMeta->slot_bump_addr 添加 slot_bytes 的長度,偏移到下一個地址

  • b -= slot_bytes 減去添加的偏移量菠齿,獲取當前可以獲取的地址

  • 如果地址還在范圍之內(nèi)佑吝,則返回地址 return (void *)b

  • pMeta->slot_exhausted 多線程的緣故,重新檢查是否用盡

  • _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]); 如果小于最大限制地址绳匀,當重新申請一個新的 band 后芋忿,重新嘗試 while

  • _malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);申請新的 band成功,重新嘗試 while

如果是第一次調(diào)用 segregated_next_block 函數(shù)疾棵,band 不存在戈钢,緩存也不會存在,所以會調(diào)用segregated_band_grow是尔。來開辟新的 band

2.10 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 的聯(lián)合體
    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;
        
        //根據(jù)設置的屬性計算 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拟枚,調(diào)用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 的聯(lián)合體

  • 利用 nano_blk_addr_t 來計算 slot_current_base_addr

  • 根據(jù)設置的屬性計算 slot_current_base_addr

  • 如果最大能存儲的地址 仍然小于目標地址,則小開辟新的band

  • mac 和模擬器 或重新使用

  • 重新申請新的 band恩溅,調(diào)用 mach_vm_mappmap 轉換隔箍。

當進入 segregated_band_grow 時,如果當前的 band 不夠用脚乡,則使用 mach_vm_map 經(jīng)由 pmap 重新映射物理內(nèi)存到虛擬內(nèi)存蜒滩。

關于通過 nano_blk_addr_t 的聯(lián)合體結構如下,其每個成員所占的 bit位數(shù) 已經(jīng)寫出奶稠。

struct nano_blk_addr_s {
    uint64_t                                   
    nano_offset:NANO_OFFSET_BITS,              //17 locates the block
    nano_slot:NANO_SLOT_BITS,                  //4  bucket of homogenous quanta-multiple blocks
    nano_band:NANO_BAND_BITS,                  //17
    nano_mag_index:NANO_MAG_BITS,              //6  the core that allocated this block
    nano_signature:NANOZONE_SIGNATURE_BITS;    //   the address range devoted to us.
};

#endif
// clang-format on

typedef union  {
    uint64_t            addr;
    struct nano_blk_addr_s    fields;
} nano_blk_addr_t;

下面通過 LLDB 分析

image
image

free 的階段俯艰,也是使用如上的方式獲取 對應的 slot,mag_index锌订。

下面來梳理下 nana_zone 分配過程:

  • 確定當前 cpu 對應的 mag 和通過 size參數(shù) 計算出來的 slot 竹握,去對應 chained_block_s 的鏈表中取已經(jīng)被釋放過的內(nèi)存區(qū)塊緩存,如果取到檢查指針地址是否有問題辆飘,沒有問題就直接返回涩搓;
  • 初次進行 nano malloc時,nano zon并沒有緩存劈猪,會直接在 nano zone范圍的地址空間上直接分配連續(xù)地址內(nèi)存;
  • 如當前 Band 中當前 Slot 耗盡則向系統(tǒng)申請新的 Band(每個 Band固定大小 2M良拼,容納了16個128k 的槽)战得,連續(xù)地址分配內(nèi)存的基地址、limit地址以及當前分配到的地址由 meta data 結構維護起來庸推,而這些 meta data 則以 Mag常侦、Slot 為維度(Mag個數(shù)是處理器個數(shù)浇冰,Slot是16個)的二維數(shù)組形式,放在 nanozone_tmeta_data字段中聋亡。
    流程如下
image

2.11 scalable zone(helper_zone) 分析

szone 上分配的內(nèi)存包括 tiny肘习、small和large 三大類,其中 tinysmall 的分配坡倔、釋放過程大致相同漂佩,larg類型有自己的方式管理。同樣會通過create_scalable_zone來構造zone罪塔。 這里不在復述create_scalable_zone`投蝉,直接看內(nèi)存的分配策略

2.12 szone_malloc_should_clear 分析

MALLOC_NOINLINE void *
szone_malloc_should_clear(szone_t *szone, size_t size, boolean_t cleared_requested)
{
    void *ptr;
    msize_t msize;
    //64位 <= 1008B  32位<= 496B
    if (size <= SMALL_THRESHOLD) {
        // tiny size: <=1008 bytes (64-bit), <=496 bytes (32-bit)
        // think tiny
        msize = TINY_MSIZE_FOR_BYTES(size + TINY_QUANTUM - 1);
        if (!msize) {
            msize = 1;
        }
        ptr = tiny_malloc_should_clear(&szone->tiny_rack, msize, cleared_requested);
    } else if (size <= szone->large_threshold) {
        //64位 <= 128KB      32位 <= 128KB
        // small size: <=15k (iOS), <=64k (large iOS), <=128k (macOS)
        // think small
        msize = SMALL_MSIZE_FOR_BYTES(size + SMALL_QUANTUM - 1);
        if (!msize) {
            msize = 1;
        }
        ptr = small_malloc_should_clear(&szone->small_rack, msize, cleared_requested);
    } else {
        // large: all other allocations
        size_t num_kernel_pages = round_page_quanta(size) >> vm_page_quanta_shift;
        if (num_kernel_pages == 0) { /* Overflowed */
            ptr = 0;
        } else {
            ptr = large_malloc(szone, num_kernel_pages, 0, cleared_requested);
        }
    }
#if DEBUG_MALLOC
    if (LOG(szone, ptr)) {
        malloc_report(ASL_LEVEL_INFO, "szone_malloc returned %p\n", ptr);
    }
#endif
    /*
     * If requested, scribble on allocated memory.
     */
    if ((szone->debug_flags & MALLOC_DO_SCRIBBLE) && ptr && !cleared_requested && size) {
        memset(ptr, SCRIBBLE_BYTE, szone_size(szone, ptr));
    }
    return ptr;
}

這里以看出在 szone 上分配的內(nèi)存包括 tinysmalllarge 三大類征堪,我們以 tiny為例 開始下面的分析

2.12 tiny_malloc_should_clear 分析

void *
tiny_malloc_should_clear(rack_t *rack, msize_t msize, boolean_t cleared_requested)
{
    void *ptr;
    mag_index_t mag_index = tiny_mag_get_thread_index() % rack->num_magazines;
    //獲取magazine.  magazines 是一個由64個magazine_t組成的數(shù)組
    magazine_t *tiny_mag_ptr = &(rack->magazines[mag_index]);

    MALLOC_TRACE(TRACE_tiny_malloc, (uintptr_t)rack, TINY_BYTES_FOR_MSIZE(msize), (uintptr_t)tiny_mag_ptr, cleared_requested);

#if DEBUG_MALLOC
    if (DEPOT_MAGAZINE_INDEX == mag_index) {
        malloc_zone_error(rack->debug_flags, true, "malloc called for magazine index -1\n");
        return (NULL);
    }

    if (!msize) {
        malloc_zone_error(rack->debug_flags, true, "invariant broken (!msize) in allocation (region)\n");
        return (NULL);
    }
#endif

    SZONE_MAGAZINE_PTR_LOCK(tiny_mag_ptr);

#if CONFIG_TINY_CACHE
    ptr = tiny_mag_ptr->mag_last_free;
    //如果開啟了tiny 的緩存瘩缆。
    if (tiny_mag_ptr->mag_last_free_msize == msize) {
        // we have a winner
        //優(yōu)先查看上次最后釋放的區(qū)塊是否和此次請求的大小剛好相等(都是對齊之后的slot大小)佃蚜,如果是則直接返回庸娱。
        tiny_mag_ptr->mag_last_free = NULL;
        tiny_mag_ptr->mag_last_free_msize = 0;
        tiny_mag_ptr->mag_last_free_rgn = NULL;
        SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
        CHECK(szone, __PRETTY_FUNCTION__);
        if (cleared_requested) {
            memset(ptr, 0, TINY_BYTES_FOR_MSIZE(msize));
        }
#if DEBUG_MALLOC
        if (LOG(szone, ptr)) {
            malloc_report(ASL_LEVEL_INFO, "in tiny_malloc_should_clear(), tiny cache ptr=%p, msize=%d\n", ptr, msize);
        }
#endif
        return ptr;
    }
#endif /* CONFIG_TINY_CACHE */

    while (1) {
        //先從freelist 查找
        ptr = tiny_malloc_from_free_list(rack, tiny_mag_ptr, mag_index, msize);
        if (ptr) {
            SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
            CHECK(szone, __PRETTY_FUNCTION__);
            if (cleared_requested) {
                memset(ptr, 0, TINY_BYTES_FOR_MSIZE(msize));
            }
            return ptr;
        }
        //從一個后備magazine中取出一個可用region,完整地拿過來放到當前magazine谐算,再走一遍上面的步驟熟尉。
        if (tiny_get_region_from_depot(rack, tiny_mag_ptr, mag_index, msize)) {
            //再次嘗試從freelist 中獲取
            ptr = tiny_malloc_from_free_list(rack, tiny_mag_ptr, mag_index, msize);
            if (ptr) {
                SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
                CHECK(szone, __PRETTY_FUNCTION__);
                if (cleared_requested) {
                    memset(ptr, 0, TINY_BYTES_FOR_MSIZE(msize));
                }
                return ptr;
            }
        }

        // The magazine is exhausted. A new region (heap) must be allocated to satisfy this call to malloc().
        // The allocation, an mmap() system call, will be performed outside the magazine spin locks by the first
        // thread that suffers the exhaustion. That thread sets "alloc_underway" and enters a critical section.
        // Threads arriving here later are excluded from the critical section, yield the CPU, and then retry the
        // allocation. After some time the magazine is resupplied, the original thread leaves with its allocation,
        // and retry-ing threads succeed in the code just above.
        if (!tiny_mag_ptr->alloc_underway) {
            //如果沒有正在申請新的的 regin 操作,則進行申請操作
            void *fresh_region;
            
            // time to create a new region (do this outside the magazine lock)
            //設置當前正在申請新的 堆
            tiny_mag_ptr->alloc_underway = TRUE;
            OSMemoryBarrier();
            SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
            //申請新的堆                                 1m             
            fresh_region = mvm_allocate_pages_securely(TINY_REGION_SIZE, TINY_BLOCKS_ALIGN, VM_MEMORY_MALLOC_TINY, rack->debug_flags);
            SZONE_MAGAZINE_PTR_LOCK(tiny_mag_ptr);

            // DTrace USDT Probe
            MAGMALLOC_ALLOCREGION(TINY_SZONE_FROM_RACK(rack), (int)mag_index, fresh_region, TINY_REGION_SIZE);

            if (!fresh_region) { // out of memory!
                tiny_mag_ptr->alloc_underway = FALSE;
                OSMemoryBarrier();
                SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
                return NULL;
            }
            //從最近的一個 region 或者新申請的 region中malloc
            ptr = tiny_malloc_from_region_no_lock(rack, tiny_mag_ptr, mag_index, msize, fresh_region);

            // we don't clear because this freshly allocated space is pristine
            tiny_mag_ptr->alloc_underway = FALSE;
            OSMemoryBarrier();
            SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
            CHECK(szone, __PRETTY_FUNCTION__);
            return ptr;
        } else {
            SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
            yield();
            SZONE_MAGAZINE_PTR_LOCK(tiny_mag_ptr);
        }
    }
    /* NOTREACHED */
}
  • 獲取 magazine.

  • magazines 是一個由 64個magazine_t 組成的數(shù)組

  • 如果開啟了 tiny 的緩存

  • 優(yōu)先查看上次最后釋放的區(qū)塊是否和此次請求的大小剛好相等(都是對齊之后的 slot大小)氯夷,如果是則直接返回臣樱。

  • ptr = tiny_malloc_from_free_list(rack, tiny_mag_ptr, mag_index, msize); 先從 freelist 查找

  • 從一個后備 magazine 中取出一個可用 region,完整地拿過來放到當前 magazine腮考,再走一遍上面的步驟雇毫。

  • void *fresh_region; 如果沒有正在申請新的的 regin 操作,則進行申請操作

  • tiny_mag_ptr->alloc_underway = TRUE; 設置當前正在申請新的 堆

  • fresh_region = mvm_allocate_pages_securely(TINY_REGION_SIZE, TINY_BLOCKS_ALIGN, VM_MEMORY_MALLOC_TINY, rack->debug_flags); 申請新的堆 --- 1M

  • ptr = tiny_malloc_from_region_no_lock(rack, tiny_mag_ptr, mag_index, msize, fresh_region); 從最近的一個 region 或者新申請的 regionmalloc

每次調(diào)用 free 函數(shù)踩蔚,會直接把要釋放的內(nèi)存優(yōu)先放到mag_last_free 指針上棚放,在下次 alloc 時,也會優(yōu)先檢查mag_last_free 是否存在大小相等的內(nèi)存馅闽,如果存在就直接返回飘蚯。

2.14 tiny_malloc_from_free_list & tiny_get_region_from_depot 分析

  • tiny_malloc_from_free_list函數(shù)的作用是從 free_list 中不斷進行各種策略嘗試。

  • 從上面的流程可以看出福也,在查找已經(jīng)釋放的內(nèi)存緩存局骤,會采用2步緩存查找(策略1,2)暴凑,及兩步備用內(nèi)存的開辟(策略3峦甩,4)。

  • free_list 流程仍然找不到可以使用內(nèi)存现喳,就會使用tiny_get_region_from_depot

每一個類型的 rack 指向的 magazines 凯傲,都會在下標為-1 , magazine_t 當做備用:depot犬辰,該方法的作用是從備用的 depot查找出是否有滿足條件的 region 如果存在,更新 depotregion 的關聯(lián)關系冰单,然后在關聯(lián)當前的magazine_tregion幌缝。之后在再次重復 free_list 過程

2.15 mvm_allocate_pages_securely 的分析

  • 走到這一步,就需要申請新的 heap 了诫欠,這里需要理解虛擬內(nèi)存和物理內(nèi)存的映射關系涵卵。

  • 你其實只要記住兩點:vm_map 代表就是一個進程運行時候涉及的虛擬內(nèi)存,pmap 代表的就是和具體硬件架構相關的物理內(nèi)存呕诉。

  • 重新申請的核心函數(shù)為 mach_vm_map ,其概念如圖

image

2.16 tiny_malloc_from_region_no_lock 的分析

重新申請了新的內(nèi)存 (region) 之后缘厢,掛載到當前的 magazine下并分配內(nèi)存。

這個方法的主要作用是把新申請的內(nèi)存地址甩挫,轉換為region贴硫,并進行相關的關聯(lián)。及更新對應的 magazine伊者。整個 scalable_zone 的結構體關系英遭,及流程如下

image

2.17 nano_zone 總結

malloc 庫會檢查指針地址,如果沒有問題亦渗,則以鏈表的形式將這些區(qū)塊按大小存儲起來挖诸。這些鏈表的頭部放在 meta_data數(shù)組 中對應的 [mag][slot]元素中。

其實從緩存獲取空余內(nèi)存和釋放內(nèi)存時都會對指向這篇內(nèi)存區(qū)域的指針進行檢查法精,如果有類似地址不對齊多律、未釋放/多次釋放、所屬地址與預期的 mag搂蜓、slot 不匹配等情況都會以報錯結束狼荞。

2.18 scalable_zone 分析

  • 首先檢查指針指向地址是否有問題。
    如果 last free指針 上沒有掛載內(nèi)存區(qū)塊帮碰,則放到 last free上相味。

  • 如果有 last free ,置換內(nèi)存殉挽,并把 last free 原有內(nèi)存區(qū)塊掛載到 free list上(在掛載的 free list 前丰涉,會先根據(jù) region 位圖檢查前后區(qū)塊是否能合并成更大區(qū)塊,如果能會合并成一個)斯碌。

  • 合并后所在的 region 如果空閑字節(jié)超過一定條件一死,則將把此 region 放到后備的 magazine 中(-1)。

  • 如果整個 region 都是空的傻唾,則直接還給系統(tǒng)內(nèi)核摘符。

三、流程總結

image

四、拓展補充

  • malloc_zone_t 提供了一個模板類逛裤,或者理解為malloc_zone_t 提供一類接口(高度抽象了alloc一個對象所需要的特征),free猴抹,calloc等带族。

  • 由所有拓展的結構體來實現(xiàn)真正的目標函數(shù)。

  • 同上對于上層 Objc蟀给,提供了抽象接口(依賴倒置),這樣就降低了調(diào)用者 (Objc) 與實現(xiàn)模塊間的耦合蝙砌。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市跋理,隨后出現(xiàn)的幾起案子择克,更是在濱河造成了極大的恐慌,老刑警劉巖前普,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肚邢,死亡現(xiàn)場離奇詭異,居然都是意外死亡拭卿,警方通過查閱死者的電腦和手機骡湖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來峻厚,“玉大人响蕴,你說我怎么就攤上這事』萏遥” “怎么了浦夷?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辜王。 經(jīng)常有香客問我劈狐,道長,這世上最難降的妖魔是什么誓禁? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任懈息,我火速辦了婚禮,結果婚禮上摹恰,老公的妹妹穿的比我還像新娘辫继。我一直安慰自己,他們只是感情好俗慈,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布姑宽。 她就那樣靜靜地躺著,像睡著了一般闺阱。 火紅的嫁衣襯著肌膚如雪炮车。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機與錄音瘦穆,去河邊找鬼纪隙。 笑死,一個胖子當著我的面吹牛扛或,可吹牛的內(nèi)容都是我干的绵咱。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼熙兔,長吁一口氣:“原來是場噩夢啊……” “哼悲伶!你這毒婦竟也來了?” 一聲冷哼從身側響起住涉,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤麸锉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后舆声,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體花沉,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年纳寂,在試婚紗的時候發(fā)現(xiàn)自己被綠了主穗。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡毙芜,死狀恐怖忽媒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情腋粥,我是刑警寧澤晦雨,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站隘冲,受9級特大地震影響闹瞧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜展辞,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一奥邮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧罗珍,春花似錦洽腺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至扣唱,卻和暖如春藕坯,著一層夾襖步出監(jiān)牢的瞬間团南,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工炼彪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吐根,地道東北人。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓霹购,卻偏偏與公主長得像佑惠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子齐疙,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容