调用malloc()函数之后,内核发生了什么?附malloc()和free()实现的源代码

特此声明:本文参照了另外一篇文章和一个帖子,再结合自己的理解总结了malloc()函数的实现机制。

我们经常会在C程序中调用malloc()函数动态分配一块连续的内存空间并使用它们。那么,这些用户空间发生的事会引发内核空间什么样的反应呢?

malloc()是一个API,这个函数在库中封装了系统调用brk。因此如果调用malloc,那么首先会引发brk系统调用执行的过程。brk()在内核中对应的系统调用服务例程为SYSCALL_DEFINE1(brk,
unsigned long, brk),参数brk用来指定heap段新的结束地址,也就是重新指定mm_struct结构中的brk字段。

brk系统调用服务例程首先会确定heap段的起始地址min_brk,然后再检查资源的限制问题。接着,将新老heap地址分别按照页大小对齐,对齐后的地址分别存储在newbrk和okdbrk中。

brk()系统调用本身既可以缩小堆大小,又可以扩大堆大小。缩小堆这个功能是通过调用do_munmap()完成的。如果要扩大堆的大小,那么必须先通过find_vma_intersection()检查扩大以后的堆是否与已经存在的某个虚拟内存重合,如何重合则直接退出。否则,调用do_brk()进行接下来扩大堆的各种工作。

SYSCALL_DEFINE1(brk, unsigned long, brk)
{
        unsigned long rlim, retval;
        unsigned long newbrk, oldbrk;
        struct mm_struct *mm = current->mm;
        unsigned long min_brk;

        down_write(&mm->mmap_sem);

#ifdef CONFIG_COMPAT_BRK
        min_brk = mm->end_code;
#else
        min_brk = mm->start_brk;
#endif
        if (brk < min_brk)
                goto out;

        rlim = rlimit(RLIMIT_DATA);
        if (rlim < RLIM_INFINITY && (brk - mm->start_brk) +
                        (mm->end_data - mm->start_data) > rlim)

        newbrk = PAGE_ALIGN(brk);
        oldbrk = PAGE_ALIGN(mm->brk);
        if (oldbrk == newbrk)
                goto set_brk;
        if (brk brk) {
                if (!do_munmap(mm, newbrk, oldbrk-newbrk))
                        goto set_brk;
                goto out;
        }

        if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
                goto out;

        if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
                goto out;
set_brk:
        mm->brk = brk;
out:
        retval = mm->brk;
        up_write(&mm->mmap_sem);
        return retval;
}

brk系统调用服务例程最后将返回堆的新结束地址。

用户进程调用malloc()会使得内核调用brk系统调用服务例程,因为malloc总是动态的分配内存空间,因此该服务例程此时会进入第二条执行路径中,即扩大堆。do_brk()主要完成以下工作:

1.通过get_unmapped_area()在当前进程的地址空间中查找一个符合len大小的线性区间,并且该线性区间的必须在addr地址之后。如果找到了这个空闲的线性区间,则返回该区间的起始地址,否则返回错误代码-ENOMEM;

2.通过find_vma_prepare()在当前进程所有线性区组成的红黑树中依次遍历每个vma,以确定上一步找到的新区间之前的线性区对象的位置。如果addr位于某个现存的vma中,则调用do_munmap()删除这个线性区。如果删除成功则继续查找,否则返回错误代码。

3.目前已经找到了一个合适大小的空闲线性区,接下来通过vma_merge()去试着将当前的线性区与临近的线性区进行合并。如果合并成功,那么该函数将返回prev这个线性区的vm_area_struct结构指针,同时结束do_brk()。否则,继续分配新的线性区。

4.接下来通过kmem_cache_zalloc()在特定的slab高速缓存vm_area_cachep中为这个线性区分配vm_area_struct结构的描述符。

5.初始化vma结构中的各个字段。

6.更新mm_struct结构中的vm_total字段,它用来同级当前进程所拥有的vma数量。

7.如果当前vma设置了VM_LOCKED字段,那么通过mlock_vma_pages_range()立即为这个线性区分配物理页框。否则,do_brk()结束。

可以看到,do_brk()主要是为当前进程分配一个新的线性区,在没有设置VM_LOCKED标志的情况下,它不会立刻为该线性区分配物理页框,而是通过vma一直将分配物理内存的工作进行延迟,直至发生缺页异常。

经过上面的过程,malloc()返回了线性地址,如果此时用户进程访问这个线性地址,那么就会发生缺页异常(Page Fault)。整个缺页异常的处理过程非常复杂,我们这里只关注与malloc()有关的那一条执行路径。

当CPU产生一个异常时,将会跳转到异常处理的整个处理流程中。对于缺页异常,CPU将跳转到page_fault异常处理程序中。异常处理程序会调用do_page_fault()函数,该函数通过读取CR2寄存器获得引起缺页的线性地址,通过各种条件判断以便确定一个合适的方案来处理这个异常。

do_page_fault()函数:

该函数通过各种条件来检测当前发生异常的情况,但至少do_page_fault()会区分出引发缺页的两种情况:由编程错误引发异常,以及由进程地址空间中还未分配物理内存的线性地址引发。对于后一种情况,通常还分为用户空间所引发的缺页异常和内核空间引发的缺页异常。

内核引发的异常是由vmalloc()产生的,它只用于内核空间内存的分配。显然,我们这里需要关注的是用户空间所引发的异常情况。这部分工作从do_page_fault()中的good_area标号处开始执行,主要通过handle_mm_fault()完成。

dotraplinkage void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
…… ……
good_area:
        write = error_code & PF_WRITE;

        if (unlikely(access_error(error_code, write, vma))) {
                bad_area_access_error(regs, error_code, address);
                return;
        }
        fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0);
}

handle_mm_fault()函数:

该函数的主要功能是为引发缺页的进程分配一个物理页框,它先确定与引发缺页的线性地址对应的各级页目录项是否存在,如何不存在则分进行分配。具体如何分配这个页框是通过调用handle_pte_fault()完成的。

int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, unsigned int flags)
{
        pgd_t *pgd;
        pud_t *pud;
        pmd_t *pmd;
        pte_t *pte;
        …… ……
        pgd = pgd_offset(mm, address);
        pud = pud_alloc(mm, pgd, address);
        if (!pud)
                return VM_FAULT_OOM;
        pmd = pmd_alloc(mm, pud, address);
        if (!pmd)
                return VM_FAULT_OOM;
        pte = pte_alloc_map(mm, pmd, address);
        if (!pte)
                return VM_FAULT_OOM;
          return handle_pte_fault(mm, vma, address, pte, pmd, flags);
}

handle_pte_fault()函数:

该函数根据页表项pte所描述的物理页框是否在物理内存中,分为两大类:

请求调页:被访问的页框不再主存中,那么此时必须分配一个页框。

写时复制:被访问的页存在,但是该页是只读的,内核需要对该页进行写操作,此时内核将这个已存在的只读页中的数据复制到一个新的页框中。

用户进程访问由malloc()分配的内存空间属于第一种情况。对于请求调页,handle_pte_fault()仍然将其细分为三种情况:

1.如果页表项确实为空(pte_none(entry)),那么必须分配页框。如果当前进程实现了vma操作函数集合中的fault钩子函数,那么这种情况属于基于文件的内存映射,它调用do_linear_fault()进行分配物理页框。否则,内核将调用针对匿名映射分配物理页框的函数do_anonymous_page()。

2.如果检测出该页表项为非线性映射(pte_file(entry)),则调用do_nonlinear_fault()分配物理页。

3.如果页框事先被分配,但是此刻已经由主存换出到了外存,则调用do_swap_page()完成页框分配。

由malloc分配的内存将会调用do_anonymous_page()分配物理页框。

static inline int handle_pte_fault(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, pte_t *pte, pmd_t *pmd, unsigned int flags)
{
        …… ……
        if (!pte_present(entry)) {
                if (pte_none(entry)) {
                        if (vma->vm_ops) {
                                if (likely(vma->vm_ops->fault))
                                        return do_linear_fault(mm, vma, address,
                                                pte, pmd, flags, entry);
                        }
                        return do_anonymous_page(mm, vma, address,
                                                 pte, pmd, flags);
                }
                if (pte_file(entry))
                        return do_nonlinear_fault(mm, vma, address,
                                        pte, pmd, flags, entry);
                return do_swap_page(mm, vma, address,
                                        pte, pmd, flags, entry);
        }
…… ……
}

do_anonymous_page()函数:

此时,缺页异常处理程序终于要为当前进程分配物理页框了。它通过alloc_zeroed_user_highpage_movable()来完成这个过程。我们层层拨开这个函数的外衣,发现它最终调用了alloc_pages()。

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
                unsigned long address, pte_t *page_table, pmd_t *pmd,
                unsigned int flags)
{
…… ……
        if (unlikely(anon_vma_prepare(vma)))
                goto oom;
        page = alloc_zeroed_user_highpage_movable(vma, address);
        if (!page)
                goto oom;
…… ……
}

经过这样一个复杂的过程,用户进程所访问的线性地址终于对应到了一块物理内存。

下面附上我自认为比较完善的malloc()和free()函数源代码:

#include <unistd.h>
#include <stdlib.h>
//块首
union header
{
	struct{
		union header *next;//指向下一空闲快的指针
		unsigned int size;//空闲块的大小
	}s;
	long x;//对齐
};
typedef union header Header;

#define NALLOC 1024;//请求的最小单位数,每页大小为1KB
static Header* moreSys(unsigned int num);//向系统申请一块内存
void* userMalloc(unsigned int nbytes);//从用户管理区申请内存
void userFree(void *ap);//释放内存,放入到用户管理区

static Header base;//定义空闲链表头
static Header *free_list = NULL;//空闲链表的起始查询指针

void* userMalloc(unsigned int nbytes)
{
	Header *p;
	Header *prev;
	unsigned int unitNum;
	//将申请的字节数nbytes转换成unitNum个块首单位,多计算一个作为管理块首
	unitNum = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;
	if ((prev = free_list) == NULL)//如果无空闲链表,定义空闲链表
	{
		base.s.next = free_list = prev = &base;
		base.s.size = 1;
	}
	for (p = prev->s.next; ; p = p->s.next, prev = p)
	{
		if (p->s.size >= unitNum)//空闲块足够大
		{
			if (p->s.size <= (unitNum + 1))
			{
				prev->s.next = p->s.next;
			}
			else//偏大,切出需要的一块
			{
				p->s.size = unitNum;
				p += p->s.size;
				p->s.size = unitNum;
		    }
			free_list = prev;
			return (void *)(p+1);
		}
		if (p == free_list)
		{
			if ((p = moreSys(unitNum)) == NULL)//无合适块,向系统申请
			{
				return NULL;
			}
		}
	}
}

static Header* moreSys(unsigned int num)
{
	char *cp;
	Header *up;

	if(num < NALLOC)
		num = NALLOC;//向系统申请的最小量
	cp = sbrk(num * sizeof(Header));
	if (cp == (char *)-1)
	{
		return NULL;//无空闲页面,返回空地址
	}
	up = (Header *)cp;
	up->s.size = num;
	userFree(up + 1);
	return free_list;
}
//回收内存到空闲链上
void Free(void *ap)
{
Header *bp, *p;
bp = (Header *)ap - 1;	 //指向块首

for(p = free_list; !(bp>p && bp<p->s.next); p = p->s.next)	//按地址定位空闲块在链表
//中的位置
if(p>=p->s.next && (bp>p || bp<p->s.next))
break;	 //空闲块在两端
if(bp + bp->s.size == p->s.next) {	 //看空闲块是否与已有的块相邻,相邻就合并
bp->s.size += p->s.next->s.size;
bp->s.next = p->s.next->s.next;
}
else
bp->s.next = p->s.next;

if(p + p->s.size == bp) {
p->s.size += bp->s.size;
p->s.next = bp->s.next;
}
else
p->s.next = bp;

free_list = p;
}
时间: 10-06

调用malloc()函数之后,内核发生了什么?附malloc()和free()实现的源代码的相关文章

malloc()之后,内核发生了什么?【转】

转自:http://blog.csdn.net/qianlong4526888/article/details/9042835 [-] 1brk系统调用服务例程 2扩大堆 3缺页异常的处理过程 31do_page_fault 32handle_mm_fault 33handle_pte_fault 34do_anonymous_page 考虑这样一种常见的情况:用户进程调用malloc()动态分配了一块内存空间,再对这块内存进行访问.这些用户空间发生的事会引发内核空间的那些反映?本文将简单为您解

malloc 函数工作机制(转)

malloc()工作机制 malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表.调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块.然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节).接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上.调用free函数时,它将用户释放的内存块连接到空闲链上.到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空

sizeof运算符、malloc函数及free函数

一.sizeof运算符的用法 1.sizeof运算符给出某个类型或变量在内存中所占据的字节数. int a;  sizeof(a)=4;  //sizeof(int)=4; double b;  sizeof(b)=8;  //sizeof(double)=8; 2.数组的sizeof值等于数组所占用的内存总字节数. 如:char a[] = "yes";  sizeof(a);  // 结果为4,字符末尾还存在一个NULL终止符 int a[3];  sizeof(a);  // 结

转让malloc()该功能后,发生了什么事内核?附malloc()和free()实现源

特此声明:在本文中,引用另一篇文章和帖子,结合的概括的理解malloc()函数的实现机制. 我们常常会在C程序中调用malloc()函数动态分配一块连续的内存空间并使用它们.那么,这些用户空间发生的事会引发内核空间什么样的反应呢? malloc()是一个API,这个函数在库中封装了系统调用brk.因此假设调用malloc,那么首先会引发brk系统调用运行的过程. brk()在内核中相应的系统调用服务例程为SYSCALL_DEFINE1(brk, unsigned long, brk).參数brk

malloc 函数详解

很多学过C的人对malloc都不是很了解,知道使用malloc要加头文件,知道malloc是分配一块连续的内存,知道和free函数是一起用的.但是但是: 一部分人还是将:malloc当作系统所提供的或者是C的关键字,事实上:malloc只是C标准库中提供的一个普通函数 而且很多很多人都对malloc的具体实现机制不是很了解. 1,关于malloc以及相关的几个函数 #include <stdlib.h>(Linux下) void *malloc(size_t size);        voi

在子函数中应用malloc函数

一般在主函数中(main)使用malloc函数,然后在通过free函数进行释放内存,但有时候如果必须在子函数长调用malloc函数该怎样进行内存释放呢?在这里进行一下总结: 首先我们先来了解一下malloc函数的含义: malloc函数是为指针变量或数组分配某个可用空间的首地址,所以当分配一个首地址给一个指针变量时,这个指针变量指向的内容就发生了改变,指向了由malloc分配的那一块空间.如果要想改变一个指针变量的指向,在子函数中就要传递这个指针变量的地址,而不是它指向的空间. 有一种不合理的解

linux下的系统调用函数到内核函数的追踪

使用的 glibc : glibc-2.17  使用的 linux kernel :linux-3.2.07 系统调用是内核向用户进程提供服务的唯一方法,应用程序调用操作系统提供的功能模块(函数).用户程序通过系统调用从用户态(user mode)切换到核心态(kernel mode ),从而可以访问相应的资源.这样做的好处是:为用户空间提供了一种硬件的抽象接口,使编程更加容易.有利于系统安全.有利于每个进程度运行在虚拟系统中,接口统一有利于移植.             运行模式.地址空间.上

RTX——第19章 SVC 中断方式调用用户函数(后期补历程)

本章节为大家讲解如何采用 SVC 中断方式调用用户函数. 当用户将 RTX 任务设置为工作在非特权级模式时,任务中是不允许访问特权级寄存器的,这个时候使用 SVC 中断,此问题就迎刃而解了. SVC 功能介绍SVC 用于产生系统函数的调用请求.例如,操作系统通常不让用户程序直接访问硬件,而是通过提供一些系统服务函数,让用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件.因此,当用户程序想要控制特定的硬件时,它就要产生一个 SVC 异常,然后操作系统提供的SVC

第 13 条:使用立即调用的函数表达式创建局部作用域

第 13 条:使用立即调用的函数表达式创建局部作用域这段程序(Bug 程序)输出什么? function wrapElements(a) { var result = [], i, n; for (i = 0, n = a.length; i < n; i++) { result[i] = function() { return a[i]; }; } return result; } var wrapped = wrapElements([10, 20, 30, 40, 50]); var f