free源码分析---1

free源码分析—__libc_free

本章继续之前的glibc中的《malloc源码分析》系列开始分析free的源代码,malloc的源码分析可以查看博客里同类别文章下的《malloc源码分析—1》到《malloc源码分析—5》,因此free的源码中有一些结构和malloc相似的地方就不会做过多的介绍了。

首先在glibc的malloc.c中有如下定义,

strong_alias( __libc_free, __cfree)
weak_alias( __libc_free, cfree)
strong_alias( __libc_free, __free)
strong_alias( __libc_free, free)

因此,free__libc_free的别名,实际执行的是__libc_free函数,下面来看,

void __libc_free(void *mem) {
    mstate ar_ptr;
    mchunkptr p;

    void (*hook)(void *, const void *) = atomic_forced_read (__free_hook);
    if (__builtin_expect(hook != NULL, 0)) {
        (*hook)(mem, RETURN_ADDRESS(0));
        return;
    }

    if (mem == 0)
        return;

    p = mem2chunk(mem);

    if (chunk_is_mmapped(p)){
        if (!mp_.no_dyn_threshold
                && p->size
                        > mp_.mmap_threshold&& p->size <= DEFAULT_MMAP_THRESHOLD_MAX) {
            mp_.mmap_threshold = chunksize(p);
            mp_.trim_threshold = 2 * mp_.mmap_threshold;
            LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
                    mp_.mmap_threshold, mp_.trim_threshold);
        }
        munmap_chunk(p);
        return;
    }

    ar_ptr = arena_for_chunk(p);
    _int_free(ar_ptr, p, 0);
}

__libc_free首先查看是否有__free_hook函数,如果有就直接调用,这里假设没有默认函数可用。接下来通过mem2chunk将虚拟内存的指针mem转换为对应的chunk指针p

#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))

因为一个使用中的chunk结构体只使用其prev_sizesize字段,因此这里只需要减去2*SIZE_SZ
接下来,chunk_is_mmapped用来检查size最低三位中的标志位,判断该chunk是否是由mmap分配的,如果是,就调用munmap_chunk释放该chunk并返回,在调用munmap_chunk之前,需要更新全局的mmap阀值和收缩阀值。
再往下,如果该chunk不是由mmap分配的,就通过arena_for_chunk获得分配区指针ar_ptr,并调用_int_free释放内存。_int_free放在下一章分析,本章重点分析munmap_chunk函数。

munmap_chunk

munmap_chunk用来释放由mmap分配的chunk,下面来看,

static void internal_function munmap_chunk(mchunkptr p) {
    INTERNAL_SIZE_T size = chunksize(p);

    assert(chunk_is_mmapped (p));

    uintptr_t block = (uintptr_t) p - p->prev_size;
    size_t total_size = p->prev_size + size;

    if (__builtin_expect(((block | total_size) & (GLRO(dl_pagesize) - 1)) != 0,
            0)) {
        malloc_printerr(check_action, "munmap_chunk(): invalid pointer",
                chunk2mem(p), NULL);
        return;
    }

    atomic_decrement(&mp_.n_mmaps);
    atomic_add(&mp_.mmapped_mem, -total_size);

    __munmap((char *) block, total_size);
}

首先获得前一个chunk的指针block,计算这两个chunk的size之和至total_size,接着对全局结构mp_进行相应的设置后,就通过__munmap释放这两个chunk。根据malloc的源码可知,由mmap分配的chunk是独立的,大部分情况下,p->prev_size为0,因此这里还是释放一个chunk,特殊情况下需要释放两个chunk,特殊情况请参考_int_malloc中的代码。
__munmap再往下就是系统调用了,定义在linux内核代码的mmap.c中,

SYSCALL_DEFINE2(munmap, unsigned long, addr, size_t, len){

    profile_munmap(addr);
    return vm_munmap(addr, len);
}

profile_munmap为空函数,下面看vm_munmap,

int vm_munmap(unsigned long start, size_t len){

    int ret;
    struct mm_struct *mm = current->mm;

    down_write(&mm->mmap_sem);
    ret = do_munmap(mm, start, len);
    up_write(&mm->mmap_sem);
    return ret;
}

这里就是信号量的操作,最主要是执行do_munmap释放内存,为了方便分析和查看,只复制了do_munmap的关键代码,

int do_munmap(struct mm_struct *mm, unsigned long start, size_t len){

    unsigned long end;
    struct vm_area_struct *vma, *prev, *last;

    if ((start & ~PAGE_MASK) || start > TASK_SIZE || len > TASK_SIZE-start)
        return -EINVAL;
    len = PAGE_ALIGN(len);

    vma = find_vma(mm, start);
    prev = vma->vm_prev;
    end = start + len;
    if (vma->vm_start >= end)
        return 0;

    if (start > vma->vm_start) {
        int error;

        if (end < vma->vm_end && mm->map_count >= sysctl_max_map_count)
            return -ENOMEM;

        error = __split_vma(mm, vma, start, 0);
        if (error)
            return error;
        prev = vma;
    }

    last = find_vma(mm, end);
    if (last && end > last->vm_start) {
        int error = __split_vma(mm, last, end, 1);
        if (error)
            return error;
    }
    vma = prev ? prev->vm_next : mm->mmap;

    detach_vmas_to_be_unmapped(mm, vma, prev, end);
    unmap_region(mm, vma, prev, start, end);
    arch_unmap(mm, vma, start, end);
    remove_vma_list(mm, vma);

    return 0;
}

首先对传入的参数进行检查,需要释放的虚拟内存的开始地址start和长度len必须按页对齐,且不能释放内核空间的内存。
接着通过find_vma在进程的管理内存的AVL树上查找第一个结束地址大于start的虚拟内存vma,如果vma->vm_start >= end,说明需要释放的虚拟内存本来就不存在,因此什么也不做返回;如果start > vma->vm_start,则表示找到的vma包含了需要释放的内存,这时候通过__split_vma函数将该vma根据start地址划分成两块,因此需要判断虚拟内存的数量是否超过了系统的限制sysctl_max_map_count。为了方便分析,下面只给出了__split_vma的几行关键代码,

static int __split_vma(struct mm_struct *mm, struct vm_area_struct *vma,
          unsigned long addr, int new_below){

    struct vm_area_struct *new;
    new = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);
    *new = *vma;

    if (new_below)
        new->vm_end = addr;
    else {
        new->vm_start = addr;
        new->vm_pgoff += ((addr - vma->vm_start) >> PAGE_SHIFT);
    }

    if (new_below)
        err = vma_adjust(vma, addr, vma->vm_end, vma->vm_pgoff +
            ((addr - new->vm_start) >> PAGE_SHIFT), new);
    else
        err = vma_adjust(vma, vma->vm_start, addr, vma->vm_pgoff, new);

    if (!err)
        return 0;
}

首先分配一个vm_area_struct结构体new,然后将vma中的所有内容拷贝到new中,new_below决定将原vma按照addr决定的地址分割成两个后,vma中保存低地址部分还是高地址部分。do_munmap第一次进入__split_vmanew_below为0,因此返回的vma保存低地址部分。然后调用vma_adjust对低地址部分的vma进行相应的设置,主要是更改其end变量为addr,并将高地址部分插入进程内存的管理树中。

回到do_munmap中,find_vma(mm, end)获得最尾部的last,如果该last包含了需要释放的虚拟内存,就继续将其拆成两部分,这时候由于new_below为1,因此返回的last为高地址部分。返回后,vma将指向低地址部分。

结合前面的分析,在执行detach_vmas_to_be_unmapped之前,原来的vma被拆成如下所示
| prev | vma | … | vma | last |
mm->mmap的赋值是在vma_adjust中,其实就是拆分后低地址处那块虚拟内存。
接下来detach_vmas_to_be_unmapped用于将所有和要释放的内存有交集的vma从红黑树中删除,并形成一个以vma为链表头的链表。根据刚刚vma被拆开成的结果,其实就是取数组中所有除了prevlast的元素构成一个链表。即
| prev | vma | … | vma | last |
经过detach_vmas_to_be_unmapped后变成,
| prev| last |
| vma | … | vma |
往下就是要释放第二部分。

static void detach_vmas_to_be_unmapped(struct mm_struct *mm, struct vm_area_struct *vma,
    struct vm_area_struct *prev, unsigned long end){

    struct vm_area_struct **insertion_point;
    struct vm_area_struct *tail_vma = NULL;

    insertion_point = (prev ? &prev->vm_next : &mm->mmap);
    vma->vm_prev = NULL;
    do {
        vma_rb_erase(vma, &mm->mm_rb);
        mm->map_count--;
        tail_vma = vma;
        vma = vma->vm_next;
    } while (vma && vma->vm_start < end);
    *insertion_point = vma;
    if (vma) {
        vma->vm_prev = prev;
        vma_gap_update(vma);
    } else
        mm->highest_vm_end = prev ? prev->vm_end : 0;
    tail_vma->vm_next = NULL;

    vmacache_invalidate(mm);
}

回到do_munmap中,unmap_region就是用于释放内存了。下面来看,

static void unmap_region(struct mm_struct *mm,
        struct vm_area_struct *vma, struct vm_area_struct *prev,
        unsigned long start, unsigned long end){

    struct vm_area_struct *next = prev ? prev->vm_next : mm->mmap;
    struct mmu_gather tlb;

    lru_add_drain();
    tlb_gather_mmu(&tlb, mm, start, end);
    update_hiwater_rss(mm);
    unmap_vmas(&tlb, vma, start, end);
    free_pgtables(&tlb, vma, prev ? prev->vm_end : FIRST_USER_ADDRESS,
                 next ? next->vm_start : USER_PGTABLES_CEILING);
    tlb_finish_mmu(&tlb, start, end);
}

lru_add_drain用于将percpu变量pagevec对应的每个page放回其对应的zonelru链表中,因为马上要解映射了,这些缓存的page变量由可能被改变。
tlb_gather_mmu构造了一个mmu_gather变量并初始化。
接下来的unmap_vmas用于解映射,即释放存在物理页面映射的虚拟内存,

void unmap_vmas(struct mmu_gather *tlb, struct vm_area_struct *vma, unsigned long start_addr, unsigned long end_addr){

    struct mm_struct *mm = vma->vm_mm;

    mmu_notifier_invalidate_range_start(mm, start_addr, end_addr);
    for ( ; vma && vma->vm_start < end_addr; vma = vma->vm_next)
        unmap_single_vma(tlb, vma, start_addr, end_addr, NULL);
    mmu_notifier_invalidate_range_end(mm, start_addr, end_addr);
}

这里开始遍历vma链表,对每个vma调用unmap_single_vma进行释放,

static void unmap_single_vma(struct mmu_gather *tlb, struct vm_area_struct *vma,                        unsigned long start_addr, unsigned long end_addr, struct zap_details *details){
    unsigned long start = max(vma->vm_start, start_addr);
    unsigned long end;

    if (start >= vma->vm_end)
        return;
    end = min(vma->vm_end, end_addr);
    if (end <= vma->vm_start)
        return;

    if (vma->vm_file)
        uprobe_munmap(vma, start, end);

    if (unlikely(vma->vm_flags & VM_PFNMAP))
        untrack_pfn(vma, 0, 0);

    if (start != end) {
        if (unlikely(is_vm_hugetlb_page(vma))) {
            if (vma->vm_file) {
                i_mmap_lock_write(vma->vm_file->f_mapping);
                __unmap_hugepage_range_final(tlb, vma, start, end, NULL);
                i_mmap_unlock_write(vma->vm_file->f_mapping);
            }
        } else
            unmap_page_range(tlb, vma, start, end, details);
    }
}

这里主要就是通过unmap_page_range进行释放。再往下因为涉及太多linux内核内存管理的知识,这里就不深入分析了,最后就是通过虚拟地址找到页表pte,解开和物理页面之间的映射,并设置一些page结构。
由于unmap_vmas后,一些页表里没有了相对应的物理页面,free_pgtables将这些页表释放。

void free_pgtables(struct mmu_gather *tlb, struct vm_area_struct *vma,
        unsigned long floor, unsigned long ceiling){

    while (vma) {
        struct vm_area_struct *next = vma->vm_next;
        unsigned long addr = vma->vm_start;

        unlink_anon_vmas(vma);
        unlink_file_vma(vma);

        if (is_vm_hugetlb_page(vma)) {
            hugetlb_free_pgd_range(tlb, addr, vma->vm_end,
                floor, next? next->vm_start: ceiling);
        } else {

            while (next && next->vm_start <= vma->vm_end + PMD_SIZE
                   && !is_vm_hugetlb_page(next)) {
                vma = next;
                next = vma->vm_next;
                unlink_anon_vmas(vma);
                unlink_file_vma(vma);
            }
            free_pgd_range(tlb, addr, vma->vm_end,
                floor, next? next->vm_start: ceiling);
        }
        vma = next;
    }
}

这里主要是调用free_pgd_range。该函数中,假设要释放的虚拟内存为vma,其前一个vma为prev,后一个为last,如果释放完vma后,prev->vm_endlast->vm_start大于一个pgd管理的内存大小(32位系统下为4MB),就释放pgd里的所有页表,如果小于4MB,就什么也不做返回。

再回到do_munmap中,arch_unmap是一些体系结构相关的操作,不管它。remove_vma_list释放每个vma对应的vm_area_struct结构至slab分配器中。

static void remove_vma_list(struct mm_struct *mm, struct vm_area_struct *vma){
    unsigned long nr_accounted = 0;

    update_hiwater_vm(mm);
    do {
        long nrpages = vma_pages(vma);

        if (vma->vm_flags & VM_ACCOUNT)
            nr_accounted += nrpages;
        vm_stat_account(mm, vma->vm_flags, vma->vm_file, -nrpages);
        vma = remove_vma(vma);
    } while (vma);
    vm_unacct_memory(nr_accounted);
    validate_mm(mm);
}

主要的函数是remove_vma,该函数通过kmem_cache_free释放对应的vma,并返回链表上的下一个vma

static struct vm_area_struct *remove_vma(struct vm_area_struct *vma){

    struct vm_area_struct *next = vma->vm_next;

    might_sleep();
    if (vma->vm_ops && vma->vm_ops->close)
        vma->vm_ops->close(vma);
    if (vma->vm_file)
        fput(vma->vm_file);
    mpol_put(vma_policy(vma));
    kmem_cache_free(vm_area_cachep, vma);
    return next;
}
  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值