linux内核内存管理-brk系统调用

news2024/9/25 3:17:01

【推荐阅读】

深入linux内核架构--进程&线程

浅谈linux 内核网络 sk_buff 之克隆与复制

浅析linux内核网络协议栈--linux bridge

尽管可见度不高,brk也许是最常使用的系统调用了,用户进程通过它向内核申请空间。人们常常并不意识到在调用brk,原因在于很少有人会直接使用系统调用brk向系统申请空间,而总是通过像malloc一类的C语言库函数(或语言成分,如C++中的new)间接地调用brk。如果把malloc想象成零售,brk则是批发。库函数malloc为用户进程(malloc本身就是该进程的一部分)维持一个小仓库,当进程需要使用更多的内存空间时就向小仓库要,小仓库中存量不足时就通过brk向内核批发。

前面讲过,每个进程拥有3GB字节的用户虚存空间。但是,这并不意味着用户进程在这3GB字节的范围里可以任意使用,因为虚存空间最终得映射到某个物理存储空间(内存或磁盘空间),才真正可以使用,而这种映射的建立和管理则由内核处理。所谓向内核申请一块空间,是指请求内核分配一块虚存空间和相应的若干物理页面,并建立起映射关系。由于每个进程的虚存空间都很大(3GB),而实际需要使用的又很小,内核不可能在创建进程时就为整个虚存空间都分配好相应的物理空间并建立映射,而只能是需要用多少才分配多少。

那么,内核怎样管理每个进程的3G字节虚存空间呢?粗略地说,用户程序经过编译、链接形成的映像文件中有一个代码段和一个数据段(包括data段和bss段),其中代码段在下,数据段在上。数据段中包括了所有静态分配的数据空间,包括全局变量和说明为static的局部变量。这些空间是进程所必须的基本要求,所以内核在建立一个进程的运行映象时就分配好些空间,包括虚存地址区间和物理页面,并建立好二者间的映射。除此之外,堆栈空间安置在虚存空间的顶部,运行时由顶向下延伸;代码段和数据段则在底部(注意,不要与X86系统结构中由段寄存器建立的代码段及数据段相混淆);在运行时并不向上伸展。而从数据段的顶部end_data到堆栈段地址的下沿这个中间区域则是一个巨大的空洞,这就是可以在运行时动态分配的空间。最初,这个动态分配空间是从进程的end_data开始的,这个地址为内核和进程所共知。以后,每次动态分配一块内存,这个边界就往上推进一段距离,同时内核和进程都要记下当前的边界在哪里。在进程这一边由malloc或类似的库函数管理,而在内核则将当前的边界记录在进程的mm_struct结构中。具体地说,mm_struct结构中有一个成分brk,表示动态分配区当前的底部。当一个进程需要分配内存时,将要求的大小与其当前的动态分配区底部边界相加,所得的就是所要求的的新边界,也就是brk调用时的参数brk。当内核能满足要求时,系统调用brk返回0,此后新旧两个边界之间的虚存地址就都可以使用了。当内核发现无法满足要求(例如物理空间已经分配完),或者发现新的边界已经过于逼近设于顶部的堆栈时,就拒绝分配而返回-1。

系统调用brk在内核中的实现为sys_brk,其代码在mm/mmap.c中,这个函数既可以用来分配空间,即把动态分配区底部的边界往上推;也可以用来释放,即归还空间。因此,它的代码也大致上可以分成两部分。我们先读第一部分:

sys_brk

/*
 *  sys_brk() for the most part doesn't need the global kernel
 *  lock, except when an application is doing something nasty
 *  like trying to un-brk an area that has already been mapped
 *  to a regular file.  in this case, the unmapping will need
 *  to invoke file system routines that need the global lock.
 */
asmlinkage unsigned long sys_brk(unsigned long brk)
{
	unsigned long rlim, retval;
	unsigned long newbrk, oldbrk;
	struct mm_struct *mm = current->mm;
 
	down(&mm->mmap_sem);
 
	if (brk < mm->end_code)
		goto out;
	newbrk = PAGE_ALIGN(brk);
	oldbrk = PAGE_ALIGN(mm->brk);
	if (oldbrk == newbrk)
		goto set_brk;
 
	/* Always allow shrinking brk. */
	if (brk <= mm->brk) {
		if (!do_munmap(mm, newbrk, oldbrk-newbrk))
			goto set_brk;
		goto out;
	}

参数brk表示所要求的新边界,这个边界不能低于代码段的终点,并且必须与页面大小对齐。如果新边界低于老边界,那就不是申请分配空间,而是释放空间,所以通过do_munmap解除一部分区间的映射,这是个重要的函数。其代码如下:

sys_brk=>do_munmap

/* Munmap is split into 2 main parts -- this part which finds
 * what needs doing, and the areas themselves, which do the
 * work.  This now handles partial unmappings.
 * Jeremy Fitzhardine <[email]jeremy@sw.oz.au[/email]>
 */
int do_munmap(struct mm_struct *mm, unsigned long addr, size_t len)
{
	struct vm_area_struct *mpnt, *prev, **npp, *free, *extra;
 
	if ((addr & ~PAGE_MASK) || addr > TASK_SIZE || len > TASK_SIZE-addr)
		return -EINVAL;
 
	if ((len = PAGE_ALIGN(len)) == 0)
		return -EINVAL;
 
	/* Check if this memory area is ok - put it on the temporary
	 * list if so..  The checks here are pretty simple --
	 * every area affected in some way (by any overlap) is put
	 * on the list.  If nothing is put on, nothing is affected.
	 */
	mpnt = find_vma_prev(mm, addr, &prev);
	if (!mpnt)
		return 0;
	/* we have  addr < mpnt->vm_end  */
 
	if (mpnt->vm_start >= addr+len)
		return 0;
 
	/* If we'll make "hole", check the vm areas limit */
	if ((mpnt->vm_start < addr && mpnt->vm_end > addr+len)
	    && mm->map_count >= MAX_MAP_COUNT)
		return -ENOMEM;

函数find_vma_prev的作用于以前在linux内存管理-几个重要的数据结构和函数博客中读过的find_vma基本相同,它扫描当前进程用户空间的vm_area_struct结构链表或AVL树,试图找到结束地址高于address的第一个区间,如果找到,则函数返回该区间的vm_area_struct结构指针。不同的是,它同时还通过参数prev返回其前一区间结构的指针。等一下我们就将看到为什么需要这个指针。如果返回的指针为0,或者该区间的起始地址也高于addr+len,那就表示想要解除映射的那部分空间原来就没有映射,所以直接返回0,。如果这部分空间落在某个区间的中间,则在解除这部分空间的映射以后会造成一个空洞而使原来的区间一分为二。可是,一个进程可以拥有的虚存区间的数量是有限制的,所以若这个数量达到了上限MAX_MAP_COUNT,就不再允许这样的操作。

sys_brk=>do_munmap

/*
	 * We may need one additional vma to fix up the mappings ... 
	 * and this is the last chance for an easy error exit.
	 */
	extra = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
	if (!extra)
		return -ENOMEM;
 
	npp = (prev ? &prev->vm_next : &mm->mmap);
	free = NULL;
	spin_lock(&mm->page_table_lock);
	for ( ; mpnt && mpnt->vm_start < addr+len; mpnt = *npp) {
		*npp = mpnt->vm_next;
		mpnt->vm_next = free;
		free = mpnt;
		if (mm->mmap_avl)
			avl_remove(mpnt, &mm->mmap_avl);
	}
	mm->mmap_cache = NULL;	/* Kill the cache. */
	spin_unlock(&mm->page_table_lock);

由于解除一部分空间的映射有可能使原来的区间一分为二,所以这里先分配好一个空白的vm_area_struct结构extra。另一方面,要解除映射的那部分空间也有可能跨越好几个区间,所以通过一个for循环把所有涉及的区间都转移到一个临时队列free中,如果建立了AVL树,则也要把这些区间的vm_area_struct结构从AVL树中删除。以前讲过,mm_struct结构中的指针mmap_cache指向上一次find_vma操作的对象,因为对虚存区间的操作往往是有连续性的(见find_vma的代码),而现在用户空间的结构有了变化,多半已经打破了这种连续性,所以把它清成0。至此,已经完成了所有的准备,下面就要具体解除映射了。

sys_brk=>do_munmap

/* Ok - we have the memory areas we should free on the 'free' list,
	 * so release them, and unmap the page range..
	 * If the one of the segments is only being partially unmapped,
	 * it will put new vm_area_struct(s) into the address space.
	 * In that case we have to be careful with VM_DENYWRITE.
	 */
	while ((mpnt = free) != NULL) {
		unsigned long st, end, size;
		struct file *file = NULL;
 
		free = free->vm_next;
 
		st = addr < mpnt->vm_start ? mpnt->vm_start : addr;
		end = addr+len;
		end = end > mpnt->vm_end ? mpnt->vm_end : end;
		size = end - st;
 
		if (mpnt->vm_flags & VM_DENYWRITE &&
		    (st != mpnt->vm_start || end != mpnt->vm_end) &&
		    (file = mpnt->vm_file) != NULL) {
			atomic_dec(&file->f_dentry->d_inode->i_writecount);
		}
		remove_shared_vm_struct(mpnt);
		mm->map_count--;
 
		flush_cache_range(mm, st, end);
		zap_page_range(mm, st, size);
		flush_tlb_range(mm, st, end);
 
		/*
		 * Fix the mapping, and free the old area if it wasn't reused.
		 */
		extra = unmap_fixup(mm, mpnt, st, size, extra);
		if (file)
			atomic_inc(&file->f_dentry->d_inode->i_writecount);
	}
 
	/* Release the extra vma struct if it wasn't used */
	if (extra)
		kmem_cache_free(vm_area_cachep, extra);
 
	free_pgtables(mm, prev, addr, addr+len);
 
	return 0;
}

这里通过一个while循环逐个处理所涉及的区间,这些区间的vm_area_struct结构都链接在一个临时的队列free中。在下一篇博客中读者将看到,一个进程可以通过系统调用mmap将一个文件的内容映射到其用户空间的某个区间,然后就像访问内存一样来访问这个文件。但是,如果这个文件同时又被别的进程打开,并通过常规的文件操作访问,则在二者对此文件的两种不同形式的写操作之间要加以互斥。如果要解除映射的只是这样的区间的一部分(735-737行),那就相当于对此区间的写操作,所以要递减该文件的inode结构中的一个计数器i_writecount,以保证互斥,到操作完成以后再予以恢复(751-752行)。同时,还要通过remove_shared_vm_struct看看所处理的区间是否是这样的区间,如果是,就将其vm_area_struct结构从目标文件的inode结构内的i_mapping队列中脱链。

代码中的zap_page_range解除若干连续页面的映射,并且释放所映射的内存页面,或对交换设备上物理页面的引用,这才是我们在这里所主要关心的。其代码如下:

sys_brk=>do_munmap=>zap_page_range

/*
 * remove user pages in a given range.
 */
void zap_page_range(struct mm_struct *mm, unsigned long address, unsigned long size)
{
	pgd_t * dir;
	unsigned long end = address + size;
	int freed = 0;
 
	dir = pgd_offset(mm, address);
 
	/*
	 * This is a long-lived spinlock. That's fine.
	 * There's no contention, because the page table
	 * lock only protects against kswapd anyway, and
	 * even if kswapd happened to be looking at this
	 * process we _want_ it to get stuck.
	 */
	if (address >= end)
		BUG();
	spin_lock(&mm->page_table_lock);
	do {
		freed += zap_pmd_range(mm, dir, address, end - address);
		address = (address + PGDIR_SIZE) & PGDIR_MASK;
		dir++;
	} while (address && (address < end));
	spin_unlock(&mm->page_table_lock);
	/*
	 * Update rss for the mm_struct (not necessarily current->mm)
	 * Notice that rss is an unsigned long.
	 */
	if (mm->rss > freed)
		mm->rss -= freed;
	else
		mm->rss = 0;
}

这个函数解除一块虚存区间的页面映射。首先通过pgd_offset在第一层页面目录中找到起始地址所属的目录项,然后通过一个do-while循环从这个目录项开始处理涉及的所有目录项。

/* to find an entry in a page-table-directory. */
#define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))
 
#define __pgd_offset(address) pgd_index(address)
 
#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))

对于涉及的每一个目录项,通过zap_pmd_range处理第二层的中间目录项。

sys_brk=>do_munmap=>zap_page_range=>zap_pmd_range

static inline int zap_pmd_range(struct mm_struct *mm, pgd_t * dir, unsigned long address, unsigned long size)
{
	pmd_t * pmd;
	unsigned long end;
	int freed;
 
	if (pgd_none(*dir))
		return 0;
	if (pgd_bad(*dir)) {
		pgd_ERROR(*dir);
		pgd_clear(dir);
		return 0;
	}
	pmd = pmd_offset(dir, address);
	address &= ~PGDIR_MASK;
	end = address + size;
	if (end > PGDIR_SIZE)
		end = PGDIR_SIZE;
	freed = 0;
	do {
		freed += zap_pte_range(mm, pmd, address, end - address);
		address = (address + PMD_SIZE) & PMD_MASK; 
		pmd++;
	} while (address < end);
	return freed;
}

同样,先通过pmd_offset,在第二层目录表中找到起始目录项。对于采用二级映射的i386结构,中间目录表这一层是空的。定义如下:

extern inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
{
	return (pmd_t *) dir;
}

可见,pmd_offset把指向第一层目录项的指针原封不动地作为指向中间目录项的指针返回来了,也就是说把第一层目录当成了中间目录。所以,对于二级映射,zap_pmd_range在某种意义上只是把zap_page_range所做的事情重复了一遍。不过,这一次重复调用的是zap_pte_range,处理的是底层的页面映射表了。

sys_brk=>do_munmap=>zap_page_range=>zap_pmd_range=>zap_pte_range

static inline int zap_pte_range(struct mm_struct *mm, pmd_t * pmd, unsigned long address, unsigned long size)
{
	pte_t * pte;
	int freed;
 
	if (pmd_none(*pmd))
		return 0;
	if (pmd_bad(*pmd)) {
		pmd_ERROR(*pmd);
		pmd_clear(pmd);
		return 0;
	}
	pte = pte_offset(pmd, address);
	address &= ~PMD_MASK;
	if (address + size > PMD_SIZE)
		size = PMD_SIZE - address;
	size >>= PAGE_SHIFT;
	freed = 0;
	for (;;) {
		pte_t page;
		if (!size)
			break;
		page = ptep_get_and_clear(pte);
		pte++;
		size--;
		if (pte_none(page))
			continue;
		freed += free_pte(page);
	}
	return freed;
}

还是先找到在给定页面表中的起始表项,与pte_offset有关的定义如下:

/* Find an entry in the third-level page table.. */
#define __pte_offset(addr)	(((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pte_offset(dir, addr)	((pte_t *)pmd_page(*(dir)) + __pte_offset(addr))

然后就是在一个for循环中,对需要解除映射的页面调用ptep_get_and_clear将页面表项清成0:

#define ptep_get_and_clear(xp)	__pte(xchg(&(xp)->pte_low, 0))

最后通过free_pte解除对内存页面以及盘上页面的使用,这个函数的代码在mm/memory.c中:

sys_brk=>do_munmap=>zap_page_range=>zap_pmd_range=>zap_pte_range=>free_pte

/*
 * Return indicates whether a page was freed so caller can adjust rss
 */
static inline int free_pte(pte_t pte)
{
	if (pte_present(pte)) {
		struct page *page = pte_page(pte);
		if ((!VALID_PAGE(page)) || PageReserved(page))
			return 0;
		/* 
		 * free_page() used to be able to clear swap cache
		 * entries.  We may now have to do it manually.  
		 */
		if (pte_dirty(pte) && page->mapping)
			set_page_dirty(page);
		free_page_and_swap_cache(page);
		return 1;
	}
	swap_free(pte_to_swp_entry(pte));
	return 0;
}

如果页面表项表明在解除映射前页面就已不在内存,则当前进程对该内存页面的使用已经解除,所以只需调用swap_free解除对交换设备上的盘上页面的使用。当然,swap_free首先是递减盘上页面的使用计数,只有当这个计数达到0时才真正地释放了这个盘上页面。如果当前进程是这个盘上页面的最后一个用户(或惟一的用户),则该计数递减后为0。反之,则要通过free_page_and_swap_cache解除对盘上页面和内存页面二者的使用。此外,如果页面在最近一次try_to_swap_out以后已被写过,则还要通过set_page_dirty设置该页面page结构中的PG_dirty标志位,并在相应的address_space结构中将其移入dirty_pages队列。函数free_page_and_swap_cache的代码在mm/swap_state.c中:

sys_brk=>do_munmap=>zap_page_range=>zap_pmd_range=>zap_pte_range=>free_pte=>free_page_and_swap_cache

/* 
 * Perform a free_page(), also freeing any swap cache associated with
 * this page if it is the last user of the page. Can not do a lock_page,
 * as we are holding the page_table_lock spinlock.
 */
void free_page_and_swap_cache(struct page *page)
{
	/* 
	 * If we are the only user, then try to free up the swap cache. 
	 */
	if (PageSwapCache(page) && !TryLockPage(page)) {
		if (!is_page_shared(page)) {
			delete_from_swap_cache_nolock(page);
		}
		UnlockPage(page);
	}
	page_cache_release(page);
}

【文章福利】小编在群文件上传了一些个人觉得比较好得学习书籍、视频资料,有需要的可以进群【977878001】领取!!!额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车(腾讯课堂免费报名):Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

以前讲过,一个由用户空间映射、可换出的内存页面(确切地说是它的page数据结构),同时在三个队列中。一是通过其队列头list链入某个换入、换出队列,即相应address_space结构中的clean_pages、dirty_pages以及locked_pages三个队列之一;二是通过其队列头lru链入某个LRU队列,即active_list、inactive_dirty_list或者某个inactive_clean_list之一;最后就是通过指针next_hash链入一个杂凑队列。当一个页面在某个换入、换出队列中时,其page结构中的PG_swap_cache标志位为1,如果当前进程是这个页面的最后一个用户(或唯一用户),此时便要调用delete_from_swap_cache_nolock将页面从上述队列中脱离出来。

sys_brk=>do_munmap=>zap_page_range=>zap_pmd_range=>zap_pte_range=>free_pte=>free_page_and_swap_cache=>delete_from_swap_cache_nolock

/*
 * This will never put the page into the free list, the caller has
 * a reference on the page.
 */
void delete_from_swap_cache_nolock(struct page *page)
{
	if (!PageLocked(page))
		BUG();
 
	if (block_flushpage(page, 0))
		lru_cache_del(page);
 
	spin_lock(&pagecache_lock);
	ClearPageDirty(page);
	__delete_from_swap_cache(page);
	spin_unlock(&pagecache_lock);
	page_cache_release(page);
}

先通过block_flushpage把页面的内容冲刷到块设备上,不过实际上这种冲刷仅在页面来自一个映射到用户空间的文件时才进行,因为对于交换设备上的页面,此时的内容已经没有意义了。完成了冲刷以后,就通过lru_cache_del将页面从其所在的LRU队列中脱离出来。然后,再通过__delete_from_swap_cache,使页面脱离其他两个队列。

sys_brk=>do_munmap=>zap_page_range=>zap_pmd_range=>zap_pte_range=>free_pte=>free_page_and_swap_cache=>delete_from_swap_cache_nolock=>__delete_from_swap_cache

/*
 * This must be called only on pages that have
 * been verified to be in the swap cache.
 */
void __delete_from_swap_cache(struct page *page)
{
	swp_entry_t entry;
 
	entry.val = page->index;
 
#ifdef SWAP_CACHE_INFO
	swap_cache_del_total++;
#endif
	remove_from_swap_cache(page);
	swap_free(entry);
}

这里的remove_from_swap_cache将页面的page结构从换入、换出队列和杂凑队列中脱离出来。然后,也是通过swap_free释放盘上页面,回到delete_from_swap_cache_nolock。最后是page_cache_release,即递减page结构中的使用计数。由于当前进程是页面的最后一个用户,并且在解除映射之前页面在内存中(见上面free_pte中的264行),所以页面的使用计数应该是2,这里(119行)调用了一次page_cache_release就变成了1。再返回到free_page_and_swap_cache中,这里(149行)又调用了一次page_cache_release,这一次就使其变成了0,于是就最终把页面释放,让它回到了空闲页面队列中。

当回到do_munmap中的时候,已经完成了对一个虚存区间的操作。此时,一方面要对虚存区间的vm_area_struct数据结构和进程的mm_struct数据结构作出调整,以反映已经发生的变化,如果整个区间都解除了映射,则要释放原有的vm_area_struct数据结构。这些操作是由unmap_fixup完成的。其代码如下:

sys_brk=>do_munmap=>unmap_fixup

/* Normal function to fix up a mapping
 * This function is the default for when an area has no specific
 * function.  This may be used as part of a more specific routine.
 * This function works out what part of an area is affected and
 * adjusts the mapping information.  Since the actual page
 * manipulation is done in do_mmap(), none need be done here,
 * though it would probably be more appropriate.
 *
 * By the time this function is called, the area struct has been
 * removed from the process mapping list, so it needs to be
 * reinserted if necessary.
 *
 * The 4 main cases are:
 *    Unmapping the whole area
 *    Unmapping from the start of the segment to a point in it
 *    Unmapping from an intermediate point to the end
 *    Unmapping between to intermediate points, making a hole.
 *
 * Case 4 involves the creation of 2 new areas, for each side of
 * the hole.  If possible, we reuse the existing area rather than
 * allocate a new one, and the return indicates whether the old
 * area was reused.
 */
static struct vm_area_struct * unmap_fixup(struct mm_struct *mm, 
	struct vm_area_struct *area, unsigned long addr, size_t len, 
	struct vm_area_struct *extra)
{
	struct vm_area_struct *mpnt;
	unsigned long end = addr + len;
 
	area->vm_mm->total_vm -= len >> PAGE_SHIFT;
	if (area->vm_flags & VM_LOCKED)
		area->vm_mm->locked_vm -= len >> PAGE_SHIFT;
 
	/* Unmapping the whole area. */
	if (addr == area->vm_start && end == area->vm_end) {
		if (area->vm_ops && area->vm_ops->close)
			area->vm_ops->close(area);
		if (area->vm_file)
			fput(area->vm_file);
		kmem_cache_free(vm_area_cachep, area);
		return extra;
	}
 
	/* Work out to one of the ends. */
	if (end == area->vm_end) {
		area->vm_end = addr;
		lock_vma_mappings(area);
		spin_lock(&mm->page_table_lock);
	} else if (addr == area->vm_start) {
		area->vm_pgoff += (end - area->vm_start) >> PAGE_SHIFT;
		area->vm_start = end;
		lock_vma_mappings(area);
		spin_lock(&mm->page_table_lock);
	} else {
	/* Unmapping a hole: area->vm_start < addr <= end < area->vm_end */
		/* Add end mapping -- leave beginning for below */
		mpnt = extra;
		extra = NULL;
 
		mpnt->vm_mm = area->vm_mm;
		mpnt->vm_start = end;
		mpnt->vm_end = area->vm_end;
		mpnt->vm_page_prot = area->vm_page_prot;
		mpnt->vm_flags = area->vm_flags;
		mpnt->vm_raend = 0;
		mpnt->vm_ops = area->vm_ops;
		mpnt->vm_pgoff = area->vm_pgoff + ((end - area->vm_start) >> PAGE_SHIFT);
		mpnt->vm_file = area->vm_file;
		mpnt->vm_private_data = area->vm_private_data;
		if (mpnt->vm_file)
			get_file(mpnt->vm_file);
		if (mpnt->vm_ops && mpnt->vm_ops->open)
			mpnt->vm_ops->open(mpnt);
		area->vm_end = addr;	/* Truncate area */
 
		/* Because mpnt->vm_file == area->vm_file this locks
		 * things correctly.
		 */
		lock_vma_mappings(area);
		spin_lock(&mm->page_table_lock);
		__insert_vm_struct(mm, mpnt);
	}
 
	__insert_vm_struct(mm, area);
	spin_unlock(&mm->page_table_lock);
	unlock_vma_mappings(area);
	return extra;
}

我们把这段代码留给读者。最后,当循环结束之时,由于已经解除了一些页面的映射,有些页面映射表可能整个都已经空白,对于这样的页面表(所占的页面)也要加以释放。这是由free_pgtables完成的。我们把它代码留给读者。

sys_brk=>do_munmap=>free_pgtables

/*
 * Try to free as many page directory entries as we can,
 * without having to work very hard at actually scanning
 * the page tables themselves.
 *
 * Right now we try to free page tables if we have a nice
 * PGDIR-aligned area that got free'd up. We could be more
 * granular if we want to, but this is fast and simple,
 * and covers the bad cases.
 *
 * "prev", if it exists, points to a vma before the one
 * we just free'd - but there's no telling how much before.
 */
static void free_pgtables(struct mm_struct * mm, struct vm_area_struct *prev,
	unsigned long start, unsigned long end)
{
	unsigned long first = start & PGDIR_MASK;
	unsigned long last = end + PGDIR_SIZE - 1;
	unsigned long start_index, end_index;
 
	if (!prev) {
		prev = mm->mmap;
		if (!prev)
			goto no_mmaps;
		if (prev->vm_end > start) {
			if (last > prev->vm_start)
				last = prev->vm_start;
			goto no_mmaps;
		}
	}
	for (;;) {
		struct vm_area_struct *next = prev->vm_next;
 
		if (next) {
			if (next->vm_start < start) {
				prev = next;
				continue;
			}
			if (last > next->vm_start)
				last = next->vm_start;
		}
		if (prev->vm_end > first)
			first = prev->vm_end + PGDIR_SIZE - 1;
		break;
	}
no_mmaps:
	/*
	 * If the PGD bits are not consecutive in the virtual address, the
	 * old method of shifting the VA >> by PGDIR_SHIFT doesn't work.
	 */
	start_index = pgd_index(first);
	end_index = pgd_index(last);
	if (end_index > start_index) {
		clear_page_tables(mm, start_index, end_index - start_index);
		flush_tlb_pgtables(mm, first & PGDIR_MASK, last & PGDIR_MASK);
	}
}

回到sys_brk的代码中,我们已经完成了通过sys_brk释放空间的情景分析。

如果新边界高于老边界,就表示要分配空间,这就是sys_brk的后一部分。我们继续往下看:

sys_brk

/* Check against rlimit.. */
	rlim = current->rlim[RLIMIT_DATA].rlim_cur;
	if (rlim < RLIM_INFINITY && brk - mm->start_data > rlim)
		goto out;
 
	/* Check against existing mmap mappings. */
	if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
		goto out;
 
	/* Check if we have enough memory.. */
	if (!vm_enough_memory((newbrk-oldbrk) >> PAGE_SHIFT))
		goto out;
 
	/* Ok, looks good - let it rip. */
	if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
		goto out;
set_brk:
	mm->brk = brk;
out:
	retval = mm->brk;
	up(&mm->mmap_sem);
	return retval;
}

首先检查对进程的资源限制,如果所要求的新边界使数据段的大小超过了对当前进程的限制,就拒绝执行。此外,还要通过find_vma_intersection,检查所要求的那部分空间是否与已经存在的某一区间相冲突,这个inline函数的代码如下:

sys_brk=>find_vma_intersection

/* Look up the first VMA which intersects the interval start_addr..end_addr-1,
   NULL if none.  Assume start_addr < end_addr. */
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
{
	struct vm_area_struct * vma = find_vma(mm,start_addr);
 
	if (vma && end_addr <= vma->vm_start)
		vma = NULL;
	return vma;
}

这里的start_addr是老边界,如果find_vma返回一个非0指针,就表示在它之上已经有了一个已经映射区间,因此有冲突的可能。此时新的边界end_addr必须落在这个区间的起点之下,也就是让从start_addr到end_addr这个空间落在空洞中,否则便是有了冲突。在查明了不存在冲突以后,还要通过vm_enough_memory看看系统中是否有足够的空闲内存页面。

sys_brk=>vm_enough_memory

/* Check that a process has enough memory to allocate a
 * new virtual mapping.
 */
int vm_enough_memory(long pages)
{
	/* Stupid algorithm to decide if we have enough memory: while
	 * simple, it hopefully works in most obvious cases.. Easy to
	 * fool it, but this should catch most mistakes.
	 */
	/* 23/11/98 NJC: Somewhat less stupid version of algorithm,
	 * which tries to do "TheRightThing".  Instead of using half of
	 * (buffers+cache), use the minimum values.  Allow an extra 2%
	 * of num_physpages for safety margin.
	 */
 
	long free;

        /* Sometimes we want to use more memory than we have. */
	if (sysctl_overcommit_memory)
	    return 1;
 
	free = atomic_read(&buffermem_pages);
	free += atomic_read(&page_cache_size);
	free += nr_free_pages();
	free += nr_swap_pages;
	return free > pages;
}

通过了这些检查,接着就是操作的主体do_brk了。这个函数的代码在mm/mmap.c中:

sys_brk=>do_brk

/*
 *  this is really a simplified "do_mmap".  it only handles
 *  anonymous maps.  eventually we may be able to do some
 *  brk-specific accounting here.
 */
unsigned long do_brk(unsigned long addr, unsigned long len)
{
	struct mm_struct * mm = current->mm;
	struct vm_area_struct * vma;
	unsigned long flags, retval;
 
	len = PAGE_ALIGN(len);
	if (!len)
		return addr;
 
	/*
	 * mlock MCL_FUTURE?
	 */
	if (mm->def_flags & VM_LOCKED) {
		unsigned long locked = mm->locked_vm << PAGE_SHIFT;
		locked += len;
		if (locked > current->rlim[RLIMIT_MEMLOCK].rlim_cur)
			return -EAGAIN;
	}
 
	/*
	 * Clear old maps.  this also does some error checking for us
	 */
	retval = do_munmap(mm, addr, len);
	if (retval != 0)
		return retval;
 
	/* Check against address space limits *after* clearing old maps... */
	if ((mm->total_vm << PAGE_SHIFT) + len
	    > current->rlim[RLIMIT_AS].rlim_cur)
		return -ENOMEM;
 
	if (mm->map_count > MAX_MAP_COUNT)
		return -ENOMEM;
 
	if (!vm_enough_memory(len >> PAGE_SHIFT))
		return -ENOMEM;
 
	flags = vm_flags(PROT_READ|PROT_WRITE|PROT_EXEC,
				MAP_FIXED|MAP_PRIVATE) | mm->def_flags;
 
	flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

 
	/* Can we just expand an old anonymous mapping? */
	if (addr) {
		struct vm_area_struct * vma = find_vma(mm, addr-1);
		if (vma && vma->vm_end == addr && !vma->vm_file && 
		    vma->vm_flags == flags) {
			vma->vm_end = addr + len;
			goto out;
		}
	}
 
 
	/*
	 * create a vma struct for an anonymous mapping
	 */
	vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
	if (!vma)
		return -ENOMEM;
 
	vma->vm_mm = mm;
	vma->vm_start = addr;
	vma->vm_end = addr + len;
	vma->vm_flags = flags;
	vma->vm_page_prot = protection_map[flags & 0x0f];
	vma->vm_ops = NULL;
	vma->vm_pgoff = 0;
	vma->vm_file = NULL;
	vma->vm_private_data = NULL;
 
	insert_vm_struct(mm, vma);
 
out:
	mm->total_vm += len >> PAGE_SHIFT;
	if (flags & VM_LOCKED) {
		mm->locked_vm += len >> PAGE_SHIFT;
		make_pages_present(addr, addr + len);
	}
	return addr;
}

参数addr为需要建立映射的新区间的起点,len则为区间的长度。前面我们已经看到find_vma_intersection对冲突的检查,可是不知读者是否注意到,实际上检查的只是新区间的高端,对于其低端的冲突则并未检查。例如,老的边界是否恰好是一个已映射区间的终点呢?如果不是,那就说明在低端有了冲突。不过,对于低端的冲突是允许的,解决的方法是以新的映射为准,先通过do_munmap把原有的映射解除(见803行),再来建立新的映射,读者大概要问了,为什么对新区间的高端和低端有如此不同的容忍程度和对待呢?读者最好先想一想,然后再往下看。

以前说过,用户空间的顶端是进程的用户空间堆栈。不管什么进程,在那里总是有一个已映射区间存在着的,所以find_vma_intersection中的find_vma其实不会返回0,因为至少用于堆栈的那个区间总是存在的。当然,在堆栈以下也可能还有通过mmap或ioremap建立的映射区间。所以,如果新区间的高端有冲突,那就可能是与堆栈的冲突,而低端的冲突则只能是与数据段的冲突。所以,对于低端可以让进程自己对可能的错误负责,而对于堆栈可就不能采取把原有的映射解除,另行建立新的映射这样的方法了。

建立新的映射时,先看看是否可以跟原有的区间合并,即通过扩展原有区间来覆盖新增的区间(826-831行)。如果不行就得另行建立一个区间(838-852行)。

最后,通过make_pages_present,为新增的区间建立起对内存页面的映射。其代码如下:

sys_brk=>do_brk=>make_pages_present

/*
 * Simplistic page force-in..
 */
int make_pages_present(unsigned long addr, unsigned long end)
{
	int write;
	struct mm_struct *mm = current->mm;
	struct vm_area_struct * vma;
 
	vma = find_vma(mm, addr);
	write = (vma->vm_flags & VM_WRITE) != 0;
	if (addr >= end)
		BUG();
	do {
		if (handle_mm_fault(mm, vma, addr, write) < 0)
			return -1;
		addr += PAGE_SIZE;
	} while (addr < end);
	return 0;
}

这里所用的方法很有趣,那就是对新区间中的每一个页面模拟一次缺页异常。读者不妨想想,当从do_brk返回,进而从sys_brk返回之时,这些页面表项的映射是怎样的?如果进程从新分配的区间中读,读出的内容该是什么?往里面写,情况又会怎样?

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/97912.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python Flask构建微信小程序订餐系统 (三)

&#x1f525; 管理员登录和列表界面 &#x1f525; 账号相关登录界面 1、新建 User 对象文件 2、新建login (登录的方法)、edit (编辑的方法)、reset-pwd(重置密码的方法)方法 3、新建对应的视图层 账户管理相关界面 1、新建 Account 对象文件 2、新建index(账户列表)、set(新…

基于 Spring Cloud 的微服务脚手架

基于 Spring Cloud 的微服务脚手架 作者&#xff1a; Grey 原文地址&#xff1a; 博客园&#xff1a;基于 Spring Cloud 的微服务脚手架 CSDN&#xff1a;基于 Spring Cloud 的微服务脚手架 本文主要介绍了基于 Spring Cloud Finchley 和 Spring Boot 2.0.x 版本的微服务脚…

2006-2019年280个地级市绿色全要素生产率含原始数据和测算结果

2006-2019年280个地级市绿色全要素生产率含原始数据和测算结果 1、时间&#xff1a;2006-2019年 2、来源&#xff1a;原始数据来自各省NJ 城市NJ、各市NJ、各市社会统计GB 3、范围&#xff1a;包括280个地级市 4、指标包括&#xff1a; 投入&#xff1a;地级市市辖区从业人…

Spring Boot热部署配置

⭐️前言⭐️ 在我们进行Spring Boot项目的编写过程中&#xff0c;会有局部的代码&#xff0c;发生一些变动&#xff0c;这时候&#xff0c;我们只有将项目重启&#xff0c;发生变动的代码才能够生效&#xff0c;为了解决这个问题&#xff0c;我们可以设置Spring Boot热部署&a…

ClassLoader 隔离性的基石是namespace,证明给你看

一、背景 朋友&#xff1a;在我知识体系中ClassLoader的双亲委派机制是流畅丝滑的&#xff0c;可是看到通过委派执行类加载来保障这种分治能力&#xff0c;进而达到了类资源的隔离性突然就感觉有点陌生和排斥呢&#xff1f; 我&#xff1a;类的命名空间有了解嘛&#xff1f; …

Tableau可视化设计案例-01Tableau简介,条形图与直方图

文章目录Tableau可视化设计案例Tableau简介&#xff0c;条形图与直方图Tableau界面介绍Tableau绘制条形图2.1条形图1 各地区酒店数量2.2条形图2&#xff1a;各地区酒店均价2.3堆积图&#xff1a;价格等级堆积图Tableau绘制直方图3.1直方图概念与用途3.2创建评分直方图Tableau饼…

python中的split函数

返回数据类型为list # split以空格切片,返回数据类型为list a"I LOVE Python" print(a.split(" ")) print(type(a.split(" ")))运行结果如下&#xff1a; 可指定分隔符 # split以.切片,返回数据类型为list b"www.baidu.com" print…

Python工程师培训要多久?

Python作为目前备受初学者青睐的编程语言&#xff0c;学习的难度与其他语言相比&#xff0c;还是比较容易入门的。当然&#xff0c;一些零基础的初学者想要一两个月就能速成Python&#xff0c;还是不太可能的。尤其是想在学完之后应聘相关岗位&#xff0c;就算是最快的学习方式…

Dubbo 4 Dubbo 高级特性 4.2 Dubbo 常用高级配置 4.2.4 重试 4.2.5 多版本

Dubbo 【黑马程序员Dubbo快速入门&#xff0c;Java分布式框架dubbo教程】 4 Dubbo 高级特性 文章目录Dubbo4 Dubbo 高级特性4.2 Dubbo 常用高级配置4.2.4 重试4.2.5 多版本4.2 Dubbo 常用高级配置 4.2.4 重试 之前我们已经完成 了超时的配置 而且知道 了如果服务提供方 和消…

Linux 中存在太多的垃圾文件?

不知道大家是否也跟我一样&#xff0c;是一只要把的自己电脑文件安排的条理有序&#xff0c;把没用的文件会及时删掉的程序猿呢&#xff1f;如果是的话&#xff0c;那么我们可以愉快地探讨下文章的内容。如果不是的话&#xff0c;你也可以留下来凑凑热闹嘛(>- 下面要介绍的是…

基于java+springmvc+mybatis+vue+mysql的邮票鉴赏系统及实现

项目介绍 随着邮票行业的发展&#xff0c;邮票市场已经有了越来越多的爱好者加入。收藏邮票&#xff0c;也就成了邮票收藏爱好者一个有爱又恨的话题。没错&#xff0c;大量的邮票收藏确实是对知识面的增广和一种成就感的满足。但是面对越来越多的邮票。五花八门各种各样的邮票…

十四、CANdelaStudio入门-DID池

本专栏将由浅入深的展开诊断实际开发与测试的数据库编辑,包含大量实际开发过程中的步骤、使用技巧与少量对Autosar标准的解读。希望能对大家有所帮助,与大家共同成长,早日成为一名车载诊断、通信全栈工程师。 本文介绍CANdelaStudio的DID池,欢迎各位朋友订阅、评论,可以提…

【C++进阶】C++11新特性上篇(万字详解)

&#x1f387;C学习历程&#xff1a;入门 博客主页&#xff1a;一起去看日落吗持续分享博主的C学习历程博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 也许你现在做的事情&#xff0c;暂时看不到成果&#xff0c;但不要忘记&…

Dubbo 4 Dubbo 高级特性 4.2 Dubbo 常用高级配置 4.2.6 负载均衡

Dubbo 【黑马程序员Dubbo快速入门&#xff0c;Java分布式框架dubbo教程】 4 Dubbo 高级特性 文章目录Dubbo4 Dubbo 高级特性4.2 Dubbo 常用高级配置4.2.6 负载均衡4.2 Dubbo 常用高级配置 4.2.6 负载均衡 【举个栗子】 现在 同一个服务 提供者&#xff0c;我们把它 部署在了…

Dijkstra迪杰斯特拉算法

1.场景 用于计算一个节点到其他节点的最短路径&#xff0c;特点是由其实点位中心向外层扩展&#xff08;BFS思想&#xff09;&#xff0c;直至扩展到终点为止 2.认识 https://blog.csdn.net/weixin_57128596/article/details/126982769?ops_request_misc%257B%2522request%…

自动生成changelog

本文主要记录我如何在React项目中优雅的使用TypeScript&#xff0c;来提高开发效率及项目的健壮性。 项目目录及ts文件划分 由于我在实际项目中大部分是使用umi来进行开发项目&#xff0c;所以使用umi生成的目录来做案例。 . ├── README.md ├── global.d.ts ├── mo…

安全分析模型

安全分析模型自动化调优 MLOps&#xff08;Machine Learning Operations&#xff09;是一种人工智能 的工程实践&#xff0c;是面向机器学习项目的研发运营管理体系 。旨在实现 ML 管道的操作、ML 模型的部署和管理标准化&#xff0c;支持ML 模型的发布、激活、监控、性能跟踪…

Pytorch 学习之:关于 GPU 训练你必须知道的几件事

文章目录torchvision 下载的 pretrain 模型路径cuda 版本一定要适配多 cuda 训练 DataParallel 使用须知torchvision 下载的 pretrain 模型路径 使用 torchvision 来直接下载 pretrained 模型&#xff0c;有时候服务器的下载速度很慢&#xff0c;不如直接下载到本地之后传上去…

简单个人静态HTML网页设计作品 基于HTML+CSS+JavaScript仿小米手机网站 html静态在线购物商城网页制作

常见网页设计作业题材有 个人、 美食、 公司、 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 服装、 体育、 化妆品、 物流、 环保、 书籍、 婚纱、 游戏、 节日、 戒烟、 电影、 摄影、 文化、 家乡、 鲜花、 礼品、 汽车、 其他等网页设计题目, A…

一种用于模拟电晕放电的高效半拉格朗日算法(Matlab代码实现)

目录 摘要 1 概述 2 数学模型与方法 3 讲解 3.1 测试1 3.2 测试2 3.3 测试3 3.4 测试4 4 Matlab代码实现 摘要 提出了一种无需通量校正的高效电晕放电模拟算法。称为位置-状态分离 (POSS) 方法的算法用于求解电晕放电建模中通常存在的以对流为主的连续性方程。所…