ARM64内存管理十七:内存池

前沿

往篇回顾

在上一篇中,着重分析了匿名页RMAP的机制及建立过程

  • 在malloc触发缺页中断、写时复制等场景下产生匿名页面的同时建立新页的RMAP,主要有下面几个步骤

    1. 通过anon_vma_prepare()创建新页对应vma的av,avc;并使用anon_vma_chain_link初始化结构体数据,建立该vma的RMAP基础框架

    2. 从Buddy sys中申请到新页面后,使用page_add_new_anon_rmap为该页面添加RMAP;主要处理struct page,struct av,struct avc成员变量初始化

  • 在fork后,要更新进程的RMAP框架,要让页面能反向找到子进程,关键子进程对父进程各个vma的复制

    1. dup_mmap()函数中,遍历所有父进程所有非VM_DONTCOPY的vma,使用anon_vma_fork复制对应的子进程vma

    2. anon_vma_fork中首先使用anon_vma_clone对挂在vma->anon_vma_chain中的所有avc进行复制,并使用anon_vma_chain_link使复制的avc加入子进程RMAP,当然要加入父进程的av红黑树

    3. 在avc的复制后,在anon_vma_fork再建立完全属于子进程的av,avc

匿名页的诞生

  • malloc/mmap(共享)分配内存,发生缺页中断,调用do_anonymous_page()产生匿名页面

  • 发生写时复制,缺页中断出现写保护错误

    1. do_wp_page(): fork后访问页面触发、页面swap in触发的写时复制

    2. do_cow_page(): 共享文件匿名页(do_fault_page->do_cow_page)

  • do_swap_page(),从swap分去读回数据时会新分配匿名页

  • 迁移页面

内核内存池简介

  • 内存池是用于预先申请一些内存用于备用,当系统内存不足无法从伙伴系统和slab中获取内存时,会从内存池中获取预留的那些内存;

  • 内核里使用mempool_create()创建一个内存池,使用mempool_destroy()销毁一个内存池,使用mempool_alloc()申请内存和mempool_free()释放内存

本篇主要内容

内核内存池的使用流程分析

代码分析

mempool_create_node

mempool_create->mempool_create_node

/*
 * 新建一个mempool,需要用户自己传入内存申请及释放函数
 * 初始化mempool_t管理结构体,过程清晰简单
 */
mempool_t *mempool_create_node(int min_nr, mempool_alloc_t *alloc_fn,
			       mempool_free_t *free_fn, void *pool_data,
			       gfp_t gfp_mask, int node_id)
{
	mempool_t *pool;
	pool = kzalloc_node(sizeof(*pool), gfp_mask, node_id);
	if (!pool)
		return NULL;
	pool->elements = kmalloc_node(min_nr * sizeof(void *),
				      gfp_mask, node_id);
	if (!pool->elements) {
		kfree(pool);
		return NULL;
	}
	spin_lock_init(&pool->lock);
	pool->min_nr = min_nr;
	pool->pool_data = pool_data;
	init_waitqueue_head(&pool->wait);
	pool->alloc = alloc_fn;
	pool->free = free_fn;

	/*
	 * First pre-allocate the guaranteed number of buffers.
	 */
	while (pool->curr_nr < pool->min_nr) {
		void *element;
		/* 调用pool->alloc函数min_nr次 */
		element = pool->alloc(gfp_mask, pool->pool_data);
		/* 如果申请不到element,则直接销毁此内存池 */
		if (unlikely(!element)) {
			mempool_destroy(pool);
			return NULL;
		}
		/* 添加到elements指针数组中 */
		add_element(pool, element);
	}
	/* 返回内存池结构体 */
	return pool;
}

mempool_destroy

/*
 * 销毁一个内存池
 * 初始化mempool_t管理结构体
 */
void mempool_destroy(mempool_t *pool)
{
	while (pool->curr_nr) {
	/* 销毁elements数组中的所有对象 */
		void *element = remove_element(pool);
		pool->free(element, pool->pool_data);
	}
	/* 销毁elements指针数组 */
	kfree(pool->elements);
	/* 销毁内存池结构体 */
	kfree(pool);
}

mempool_alloc

/*
 * 从mempool_t中申请内存:
 * 1. 首先从pool->pool_data中申请元素对象(即从伙伴系统、slub cache中申请)
 * 2. 再尝试从mempool中申请
 * 3. mempool申请失败,且分配允许等待;则进程加入pool->wait等待队列
		a. 在init_wait中初始化wait_queue_t wait结构体
		b. 在prepare_to_wait中将本进程的wait结构体加入pool->wait
		c. 在io_schedule_timeout中本进入睡眠状态5s,并触发进程调度	
 * 4. 在finish_wait中触发本进程恢复执行,并再次执行1,2,3,4尝试分配内存
 */
void * mempool_alloc(mempool_t *pool, gfp_t gfp_mask)
{
	void *element;
	unsigned long flags;
	wait_queue_t wait;
	gfp_t gfp_temp;

	VM_WARN_ON_ONCE(gfp_mask & __GFP_ZERO);
	/* 如果有__GFP_WAIT标志,则会先阻塞,切换进程 */
	might_sleep_if(gfp_mask & __GFP_WAIT);
	/* 不使用预留内存 */
	gfp_mask |= __GFP_NOMEMALLOC;	/* don't allocate emergency reserves */
	/* 分配页时如果失败则返回,不进行重试 */
	gfp_mask |= __GFP_NORETRY;	/* don't loop in __alloc_pages */
	/* 分配失败不提供警告 */
	gfp_mask |= __GFP_NOWARN;	/* failures are OK */
	/* gfp_temp等于gfp_mask去除__GFP_WAIT和__GFP_IO的其他标志 */
	gfp_temp = gfp_mask & ~(__GFP_WAIT|__GFP_IO);

repeat_alloc:
	/* 使用内存池中的alloc函数进行分配对象,实际上就是从伙伴系统或者slab缓冲区获取内存对象 */
	element = pool->alloc(gfp_temp, pool->pool_data);
	/* 在内存富足的情况下,一般是能够获取到内存的 */
	if (likely(element != NULL))
		return element;
	/* 在内存不足的情况,造成从伙伴系统或slab缓冲区获取内存失败,则会执行到这 */
	/* 给内存池上锁,获取后此段临界区禁止中断和抢占 */
	spin_lock_irqsave(&pool->lock, flags);
	/* 如果当前内存池中有空闲数量,就是初始化时获取的内存数量保存在curr_nr中 */
	if (likely(pool->curr_nr)) {
		/* 从内存池中获取内存对象 */
		element = remove_element(pool);
		spin_unlock_irqrestore(&pool->lock, flags);
		/* 写内存屏障,保证之前的写操作已经完成 */
		smp_wmb();
		/* 用于debug */
		kmemleak_update_trace(element);
		return element;
	}

	/* 这里是内存池中也没有空闲内存对象的时候进行的操作 */
	/* gfp_temp != gfp_mask说明传入的gfp_mask允许阻塞等待,但是之前已经阻塞等待过了,所以这里立即重新获取一次 */
	if (gfp_temp != gfp_mask) {
		spin_unlock_irqrestore(&pool->lock, flags);
		gfp_temp = gfp_mask;
		goto repeat_alloc;
	}

	/* 传入的参数gfp_mask不允许阻塞等待,分配不到内存则直接退出 */
	if (!(gfp_mask & __GFP_WAIT)) {
		spin_unlock_irqrestore(&pool->lock, flags);
		return NULL;
	}

	/* Let's wait for someone else to return an element to @pool */
	init_wait(&wait);
	/* 将当前进程wait结构体加入到内存池的等待队列中,并把状态设置为只有wake_up信号才能唤醒的状态
	 * 1. 当内存池有新释放内存时,会主动唤醒(wake_up)等待队列中的第一个进程
	 * 2. 等待超时,定时器自动唤醒 
	 */
	prepare_to_wait(&pool->wait, &wait, TASK_UNINTERRUPTIBLE);

	spin_unlock_irqrestore(&pool->lock, flags);

	/*
	 * FIXME: this should be io_schedule().  The timeout is there as a
	 * workaround for some DM problems in 2.6.18.
	 */
	 /* 阻塞等待5秒 */
	io_schedule_timeout(5*HZ);
	/* 从内存池的等待队列删除此进程 */
	finish_wait(&pool->wait, &wait);
	/* 跳转到repeat_alloc,重新尝试获取内存对象 */
	goto repeat_alloc;
} 

mempool_free

/*
 * 释放内存
 * 1. 如果内存池中水位低于min_nr,则将释放的内存加入内存池中,并wake_up wait队列中任务
 * 2. 否则直接释放内存
 */
void mempool_free(void *element, mempool_t *pool)
{
	unsigned long flags;
	/* 传入的对象为空,则直接退出 */
	if (unlikely(element == NULL))
		return;

	/* 读内存屏障 */
	smp_rmb();

	/* pool->curr_nr < pool->min_nr,优先把释放的对象加入到内存池空闲数组中 */
	if (unlikely(pool->curr_nr < pool->min_nr)) {
		spin_lock_irqsave(&pool->lock, flags);
		if (likely(pool->curr_nr < pool->min_nr)) {
			add_element(pool, element);
			spin_unlock_irqrestore(&pool->lock, flags);
			/* 唤醒(default_wake_function)等待队列中的第一个进程 */
			wake_up(&pool->wait);
			return;
		}
		spin_unlock_irqrestore(&pool->lock, flags);
	}
	/* 直接调用释放函数 */
	pool->free(element, pool->pool_data);
}

内核中mempool例子可以搜索mempool_create函数被调用的地方

附录

mempool_s

typedef struct mempool_s {
	spinlock_t lock;
	/* 最大元素个数,也是初始个数
	 * 当内存池被创建时,会调用alloc函数申请此变量相应数量的slab放到elements指向的指针数组中 
	 */
	int min_nr;		
	int curr_nr;		/* 当前元素个数 */
	/* 指向一个数组,在mempool_create中会分配内存,数组中保存指向元素指针 */
	void **elements;
	/* 内存池的拥有者的私有数据结构,当元素是slab中的对象时,这里保存的是slab缓存描述符 */
	void *pool_data;
	/* 当元素是slab中的对象时,会使用方法mempool_alloc_slab()和mempool_free_slab() */
	/* 分配一个元素的方法 */
	mempool_alloc_t *alloc;
	/* 释放一个元素的方法 */
	mempool_free_t *free;
	/* 当内存池为空时使用的等待队列
	 * 当内存池中空闲内存对象为空时,获取函数会将当前进程阻塞,直到超时或者有空闲内存对象时才会唤醒 */
	wait_queue_head_t wait;
} mempool_t;

参考资料

tolimit-内存池