linux内存管理-系统调用mmap()

一个进程可以通过系统调用mmap,将一个已打开文件的内容映射到它的用户空间,其用户界面为:

void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);

参数fd代表着一个已打开文件,offset为文件中的起点,而addr为映射到用户空间中的起始地址,length则为长度。还有两个参数prot和flags,前者用于对映射区间的访问模式,如可写、可执行等等,后者则用于其他控制目的。从应用程序设计的角度来说,比之常规的文件操作,如read、write、lseek等等,将文件映射到用户空间后像访问内存一样访问文件显然要方便得多(读者不妨设想一下对数据库文件的访问)。

在阅读本博客之前,读者应先看一下sys_brk的代码和有关说明,并且在阅读的过程中注意与sys_brk互相参照比较。有些内容可能要到阅读了后面的博客以后,再回过来阅读才能弄懂。

在2.4.0版本的内核中实现这个调用的函数sys_mmap2,但是老一些的版本中另有一个函数old_mmap,这两个函数对应着不同的系统调用号。为保持对老版本的兼容,2.4.0版本中仍保留老的系统调用号和old_mmap,由不同版本的C语言库程序决定采用哪一个系统调用号。二者的代码都在sys_i386.c中:

asmlinkage long sys_mmap2(unsigned long addr, unsigned long len,
	unsigned long prot, unsigned long flags,
	unsigned long fd, unsigned long pgoff)
{
	return do_mmap2(addr, len, prot, flags, fd, pgoff);
}





asmlinkage int old_mmap(struct mmap_arg_struct *arg)
{
	struct mmap_arg_struct a;
	int err = -EFAULT;

	if (copy_from_user(&a, arg, sizeof(a)))
		goto out;

	err = -EINVAL;
	if (a.offset & ~PAGE_MASK)
		goto out;

	err = do_mmap2(a.addr, a.len, a.prot, a.flags, a.fd, a.offset >> PAGE_SHIFT);
out:
	return err;
}

可见,二者的区别仅在于传递参数的方式,它们的主体都是do_mmap2,其代码在同一个文件中:

sys_mmap2=>do_mmap2

/* common code for old and new mmaps */
static inline long do_mmap2(
	unsigned long addr, unsigned long len,
	unsigned long prot, unsigned long flags,
	unsigned long fd, unsigned long pgoff)
{
	int error = -EBADF;
	struct file * file = NULL;

	flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);
	if (!(flags & MAP_ANONYMOUS)) {
		file = fget(fd);
		if (!file)
			goto out;
	}

	down(&current->mm->mmap_sem);
	error = do_mmap_pgoff(file, addr, len, prot, flags, pgoff);
	up(&current->mm->mmap_sem);

	if (file)
		fput(file);
out:
	return error;
}

一般而言,系统调用mmap将已打开文件映射到用户空间。但是有个例外,那就是可以在调用参数flags中把标志位MAP_ANONYMOUS设成1,表示没有文件,实际上只是用来圈地,即在指定的位置上分配空间。除此之外,操作的主体就是do_mmap_pgoff。

内核中还有个inline函数do_mmap,是供内核自己用的,它也是将已打开文件映射到当前进程的用户空间。以后,在阅读系统调用sys_execve的代码时,在函数load_aout_binary中可以看到通过do_mmap将可执行程序(二进制代码)映射到当前进程的用户空间。此外,do_mmap还用来创建作为进程间通信手段的共享内存区。这个inline函数的定义如下:

static inline unsigned long do_mmap(struct file *file, unsigned long addr,
	unsigned long len, unsigned long prot,
	unsigned long flag, unsigned long offset)
{
	unsigned long ret = -EINVAL;
	if ((offset + PAGE_ALIGN(len)) < offset)
		goto out;
	if (!(offset & ~PAGE_MASK))
		ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
	return ret;
}

与do_mmap2做一下比较,就可发现二者基本上相同,都是通过do_mmap_pgoff完成操作。不同的只是do_mmap不支持MAP_ANONYMOUS;另一方面由于在进入do_mmap之前已经在临界区内,所以也不再需要通过信号量操作down和up加以保护。

函数do_mmap_pgoff的代码如下:

sys_mmap2=>do_mmap2=>do_mmap_pgoff

unsigned long do_mmap_pgoff(struct file * file, unsigned long addr, unsigned long len,
	unsigned long prot, unsigned long flags, unsigned long pgoff)
{
	struct mm_struct * mm = current->mm;
	struct vm_area_struct * vma;
	int correct_wcount = 0;
	int error;

	if (file && (!file->f_op || !file->f_op->mmap))
		return -ENODEV;

	if ((len = PAGE_ALIGN(len)) == 0)
		return addr;

	if (len > TASK_SIZE || addr > TASK_SIZE-len)
		return -EINVAL;

	/* offset overflow? */
	if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
		return -EINVAL;

	/* Too many mappings? */
	if (mm->map_count > MAX_MAP_COUNT)
		return -ENOMEM;

	/* 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;
	}

	/* Do simple checking here so the lower-level routines won't have
	 * to. we assume access permissions have been handled by the open
	 * of the memory object, so we don't do any here.
	 */
	if (file != NULL) {
		switch (flags & MAP_TYPE) {
		case MAP_SHARED:
			if ((prot & PROT_WRITE) && !(file->f_mode & FMODE_WRITE))
				return -EACCES;

			/* Make sure we don't allow writing to an append-only file.. */
			if (IS_APPEND(file->f_dentry->d_inode) && (file->f_mode & FMODE_WRITE))
				return -EACCES;

			/* make sure there are no mandatory locks on the file. */
			if (locks_verify_locked(file->f_dentry->d_inode))
				return -EAGAIN;

			/* fall through */
		case MAP_PRIVATE:
			if (!(file->f_mode & FMODE_READ))
				return -EACCES;
			break;

		default:
			return -EINVAL;
		}
	}

首先对文件和区间两方面都做些检查,包括起始地址与长度、已映射的次数等等。指针file非0表示映射的是具体的文件(而不是MAP_ANONYMOUS),所以相应file结构中的指针f_op必须指向一个file_operations数据结构,其中的函数指针mmap又必须指向具体文件系统所提供的mmap操作(详见文件系统)。从某种意义上说,do_mmap2和do_mmap提供的只是一个高层的框架,底层的文件操作是由具体的文件系统提供的。

此外,还要对文件和区间的访问权限进行检查,二者必须相符。读者可以在阅读了文件系统的博客后回过头来仔细看这些代码。这里我们继续往下看:

sys_mmap2=>do_mmap2=>do_mmap_pgoff

	/* Obtain the address to map to. we verify (or select) it and ensure
	 * that it represents a valid section of the address space.
	 */
	if (flags & MAP_FIXED) {
		if (addr & ~PAGE_MASK)
			return -EINVAL;
	} else {
		addr = get_unmapped_area(addr, len);
		if (!addr)
			return -ENOMEM;
	}

调用do_mmap_pgoff时的参数基本上就是系统调用mmap的参数,如果参数flags中的标志位MAP_FIXED为0,就便是指定的映射地址只是个 参考值,不能满足时可以由内核给分配一个。所以,就通过get_unmapped_area在当前进程的用户空间中分配一个起始地址。其代码在mm/mmap.c中:

sys_mmap2=>do_mmap2=>do_mmap_pgoff=>get_unmapped_area

/* Get an address range which is currently unmapped.
 * For mmap() without MAP_FIXED and shmat() with addr=0.
 * Return value 0 means ENOMEM.
 */
#ifndef HAVE_ARCH_UNMAPPED_AREA
unsigned long get_unmapped_area(unsigned long addr, unsigned long len)
{
	struct vm_area_struct * vmm;

	if (len > TASK_SIZE)
		return 0;
	if (!addr)
		addr = TASK_UNMAPPED_BASE;
	addr = PAGE_ALIGN(addr);

	for (vmm = find_vma(current->mm, addr); ; vmm = vmm->vm_next) {
		/* At this point:  (!vmm || addr < vmm->vm_end). */
		if (TASK_SIZE - len < addr)
			return 0;
		if (!vmm || addr + len <= vmm->vm_start)
			return addr;
		addr = vmm->vm_end;
	}
}
#endif

读者自行阅读这段代码应该不会有困难,常数TASK_UNMAPPED_BASE是在include/asm-i386/processor.h中定义的:


/* This decides where the kernel will search for a free chunk of vm
 * space during mmap's.
 */
#define TASK_UNMAPPED_BASE	(TASK_SIZE / 3)

也就是说,当给定的目的地址为0时,内核从(TASK_SIZE / 3)即1GB处开始向上在当前进程的虚存空间中寻找一块足以容纳给定长度的区间。而当给定的目标地址不为0时,则从给定的地址开始向上寻找。函数find_vma在当前进程已经映射的虚存空间中找到第一个满足vmm->vm_end大于给定地址的区间。如果找不到这么一个区间,那就说明给定的地址尚未映射,因而可以使用。

至此,只要返回的地址非0,addr就已经是一个符合各种要求的虚存地址了。我们回到do_mmap_pgoff中继续往下看:

sys_mmap2=>do_mmap2=>do_mmap_pgoff

	/* Determine the object being mapped and call the appropriate
	 * specific mapper. the address has already been validated, but
	 * not unmapped, but the maps are removed from the list.
	 */
	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 = vm_flags(prot,flags) | mm->def_flags;

	if (file) {
		VM_ClearReadHint(vma);
		vma->vm_raend = 0;

		if (file->f_mode & FMODE_READ)
			vma->vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
		if (flags & MAP_SHARED) {
			vma->vm_flags |= VM_SHARED | VM_MAYSHARE;

			/* This looks strange, but when we don't have the file open
			 * for writing, we can demote the shared mapping to a simpler
			 * private mapping. That also takes care of a security hole
			 * with ptrace() writing to a shared mapping without write
			 * permissions.
			 *
			 * We leave the VM_MAYSHARE bit on, just to get correct output
			 * from /proc/xxx/maps..
			 */
			if (!(file->f_mode & FMODE_WRITE))
				vma->vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
		}
	} else {
		vma->vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
		if (flags & MAP_SHARED)
			vma->vm_flags |= VM_SHARED | VM_MAYSHARE;
	}
	vma->vm_page_prot = protection_map[vma->vm_flags & 0x0f];
	vma->vm_ops = NULL;
	vma->vm_pgoff = pgoff;
	vma->vm_file = NULL;
	vma->vm_private_data = NULL;

	/* Clear old maps */
	error = -ENOMEM;
	if (do_munmap(mm, addr, len))
		goto free_vma;

	/* Check against address space limit. */
	if ((mm->total_vm << PAGE_SHIFT) + len
	    > current->rlim[RLIMIT_AS].rlim_cur)
		goto free_vma;

	/* Private writable mapping? Check memory availability.. */
	if ((vma->vm_flags & (VM_SHARED | VM_WRITE)) == VM_WRITE &&
	    !(flags & MAP_NORESERVE)				 &&
	    !vm_enough_memory(len >> PAGE_SHIFT))
		goto free_vma;

每个逻辑区间都要有个vm_area_struct数据结构,所以通过kmem_cache_alloc为待映射的区间分配一个,并加以设置。我们不妨与do_brk的代码作比较,在那里只是在新增的区间不能与已有的区间合并时,才分配了一个vm_area_struct数据结构,而这里却是无条件的。以前我们提到过,属性不同的区段不能共存于同一逻辑区间中,而映射到一个特定的文件也是一种属性,所以总是要为之单独建立一个逻辑区间。

如果调用do_mmap_pgoff时的file结构指针为0,则目的仅在于创建虚存区间,或者说仅在于建立从物理空间到虚存空间的映射。而如果目的在于建立从文件到虚存区间的映射,那就要把为文件设置的访问权限考虑进去(见275-296行)。

注意代码中的303行将参数pgoff设置到vm_area_struct数据结构中的vm_pgoff字段。这个参数代表着所映射内核在文件中的起点。有了这个起点,发生缺页异常时就可以根据虚存地址计算出相应页面在文件中的位置。所以,当断开映射时,对于文件映射页面不需要像普通换入、换出页面那样在页面表项中指明其去向。另一方面,这也说明了为什么这样的区间必须是独立的。

至此,代表着我们所需虚存区间的数据结构已经创建了,只是尚未插入代表当前进程虚存空间的mm_struct结构中。可是,在某些条件下却还不得不将它撤销。为什么呢?这里调用了一个函数do_munmap。它检查目标地址在当前进程的虚存空间是否已经在使用,如果已经在使用就要将老的映射撤销。要是这个操作失败,那当然不能重复映射同一个目标地址,所以就得转移到free_vma标号处,把已经分配的vm_area_struct数据结构撤销。我们已经在前面讲过了do_munmap的代码。也许读者会感到奇怪,这个区间不是在前面调用get_unmapped_area找到的吗?怎么会原来就已经映射呢?回过头去注意看一下就可知道,那只是当调用参数flags中的标志位MAP_FIXED为0时,而当该标志位为1时则尚未对此加以检查。除此之外,还有两个情况也会导致撤销已经分配的vm_area_struct数据结构:一个是如果当前进程对虚存空间的使用超出了为其设置的上限;另一个是在要求建立由当前进程专用的可写区间,而物理页面的数量已经(暂时)不足。

读者也许还要问,为什么不把所有条件的检验放在分配vm_area_struct数据结构之前呢?问题在于,在通过kmem_cache_alloc分配vm_area_struct数据结构的过程中,有可能会发生供这种数据结构专用的slab已经用完,而不得不分配更多物理页面的情况。而分配物理页面的过程,则又有可能因一时不能满足要求而只好先调度别的进程运行。这样,由于可能已经有别的进程或线程,特别是由本进程clone出来的线程(见后面的进程管理博客)运行过了,就不能排除这些条件已经改变的情况。所以,读者在内核中常常可以看到先分配某项资源,然后检测条件,如果条件不符再将资源释放(而不是先检测条件,后分配资源)的情景。关键在于分配资源的过程是否有可能发生调度,以及其他进程或线程的运行有否可能改变这些条件。以这里的第三个条件为例,如果发生调度,那就明显是有可能改变的。

继续往下看do_mmap_pgoff的代码:

sys_mmap2=>do_mmap2=>do_mmap_pgoff

	if (file) {
		if (vma->vm_flags & VM_DENYWRITE) {
			error = deny_write_access(file);
			if (error)
				goto free_vma;
			correct_wcount = 1;
		}
		vma->vm_file = file;
		get_file(file);
		error = file->f_op->mmap(file, vma);
		if (error)
			goto unmap_and_free_vma;
	} else if (flags & MAP_SHARED) {
		error = shmem_zero_setup(vma);
		if (error)
			goto free_vma;
	}

	/* Can addr have changed??
	 *
	 * Answer: Yes, several device drivers can do it in their
	 *         f_op->mmap method. -DaveM
	 */
	flags = vma->vm_flags;
	addr = vma->vm_start;

	insert_vm_struct(mm, vma);
	if (correct_wcount)
		atomic_inc(&file->f_dentry->d_inode->i_writecount);
	
	mm->total_vm += len >> PAGE_SHIFT;
	if (flags & VM_LOCKED) {
		mm->locked_vm += len >> PAGE_SHIFT;
		make_pages_present(addr, addr + len);
	}
	return addr;

unmap_and_free_vma:
	if (correct_wcount)
		atomic_inc(&file->f_dentry->d_inode->i_writecount);
	vma->vm_file = NULL;
	fput(file);
	/* Undo any partial mapping done by a device driver. */
	flush_cache_range(mm, vma->vm_start, vma->vm_end);
	zap_page_range(mm, vma->vm_start, vma->vm_end - vma->vm_start);
	flush_tlb_range(mm, vma->vm_start, vma->vm_end);
free_vma:
	kmem_cache_free(vm_area_cachep, vma);
	return error;
}

如果建立的是从文件到虚存区间的映射,而在调用do_munmap时的参数flags中的VM_DENYWRITE标志位为1(这个标志位在前面273行引用的宏操作vm_flags中转换成VM_DENYWRITE),那就表示不允许通过常规的文件操作访问该文件,所以要调用deny_write_access排斥常规的文件操作,详见文件系统的博客。至于get_file,其作用是递增file结构中的共享计数。

我们在这里暂时不关心为共享内存区而建立的映射,所以跳过335-339行,将来在讲到共享内存区时,还要回过来看shmem_zero_setup的代码。

每种文件系统都有个file_operations数据结构,其中的函数指针mmap提供了用来建立从该类文件到虚存区间的映射的操作。那么,具体到linux的ext2文件系统,这个函数是什么呢?我们来看ext2文件系统的file_operations数据结构ext2_file_operations:

/*
 * We have mostly NULL's here: the current defaults are ok for
 * the ext2 filesystem.
 */
struct file_operations ext2_file_operations = {
	llseek:		ext2_file_lseek,
	read:		generic_file_read,
	write:		generic_file_write,
	ioctl:		ext2_ioctl,
	mmap:		generic_file_mmap,
	open:		ext2_open_file,
	release:	ext2_release_file,
	fsync:		ext2_sync_file,
};

当打开一个文件时,如果所打开的文件在一个ext2文件系统中,内核就会将file结构中的指针f_op设置成指向这个数据结构,所以上面322行的file->f_op->mmap就指向generic_file_mmap,这个函数的代码如下:

sys_mmap2=>do_mmap2=>do_mmap_pgoff=>generic_file_mmap


/* This is used for a general mmap of a disk file */

int generic_file_mmap(struct file * file, struct vm_area_struct * vma)
{
	struct vm_operations_struct * ops;
	struct inode *inode = file->f_dentry->d_inode;

	ops = &file_private_mmap;
	if ((vma->vm_flags & VM_SHARED) && (vma->vm_flags & VM_MAYWRITE)) {
		if (!inode->i_mapping->a_ops->writepage)
			return -EINVAL;
		ops = &file_shared_mmap;
	}
	if (!inode->i_sb || !S_ISREG(inode->i_mode))
		return -EACCES;
	if (!inode->i_mapping->a_ops->readpage)
		return -ENOEXEC;
	UPDATE_ATIME(inode);
	vma->vm_ops = ops;
	return 0;
}

这个函数很简单,实质性的操作就是1723行将虚存区间控制结构中的指针vm_ops设置成ops。至于ops,则根据映射为专有或共享而分别指向数据结构file_private_mmap或file_shared_mmap。这两个结构的定义如下:

/*
 * Shared mappings need to be able to do the right thing at
 * close/unmap/sync. They will also use the private file as
 * backing-store for swapping..
 */
static struct vm_operations_struct file_shared_mmap = {
	nopage:		filemap_nopage,
};

/*
 * Private mappings just need to be able to load in the map.
 *
 * (This is actually used for shared mappings as well, if we
 * know they can't ever get write permissions..)
 */
static struct vm_operations_struct file_private_mmap = {
	nopage:		filemap_nopage,
};

数据结构的初始化也是gcc对C语言所做的改进之一。这里表示具体vm_operations_struct结构中除nopage以外,所有成分的初始化均是0或NULL,而nopage的初始值则为filemap_nopage。

两个结构其实是一样的,都只是为缺页异常提供了nopage操作。此外,在generic_file_mmap中还检验了用于页面读、写的函数是否存在(见1714和1720行)。这两个函数应该由文件的inode数据结构间接地提供。在inode结构中有个指针i_mapping,它指向一个address_space数据结构,读者应该回到前面的linux内存管理-物理页面的使用与周转博客中看一下它的定义。我们这里关心的是address_space结构中的指针a_ops,它指向一个address_space_operations数据结构。不同的文件系统(页面交换设备可以看作是一种特殊的文件系统)有不同的address_space_operations结构。对于ext2文件系统ext2_aops,定义如下:

struct address_space_operations ext2_aops = {
	readpage: ext2_readpage,
	writepage: ext2_writepage,
	sync_page: block_sync_page,
	prepare_write: ext2_prepare_write,
	commit_write: generic_commit_write,
	bmap: ext2_bmap
};

这个数据结构提供了用来读写ext2文件页面的函数ext2_readpage和ext2_writepage。这些有关的数据结构和指针也是在打开文件时设置好了的。

完成了这些检查和处理,把新建立的vm_area_struct结构插入到当前进程的mm_struct结构中,就基本完成了do_mmap_pgoff的操作,仅在要求对区间加锁时才调用make_pages_present,建立起初始的页面映射,这个函数的代码已经在linux内存管理-系统调用brk()看到过了。

读者也许感到困惑,在文件与虚存区间之间建立映射难道就这么简单?而且我们根本就没有看到页面映射的建立!其实,具体的映射时非常动态、经常在变的。所谓文件与虚存区间之间的映射包含着两个环节,一是物理页面与文件映像之间的换入、换出,二是物理页面与虚存页面之间的映射。这二者都是动态的。所以,重要的并不是建立起一个特定的映射,而是建立起一套机制,使得一旦需要时就可以根据当时的具体情况建立起新的映射。另一方面,在计算机技术中有一个称为lazy computation的概念,就是说有些为将来作某种准备而进行的操作(计算)可能并无必要,所以应该推迟到真正需要时才进行。这是因为实际运行中的情况千变万化,有时候花了老大的劲才完成的准备,实际上却根本没有用到或者只用到了很小一部分,从而造成了浪费。就以这里的文件映射来说,也许映射了100个页面,而实际上在相当长的时间里只用到了其中的一个页面,而映射99个页面的开销却是不能忽略不计的。何况,长期不用的页面还得费劲把它们换出来。考虑到这些因素,还不如到真正需要用到一个页面时再来建立该页面的映射,用到几个页面就映射几个页面。当然,那样很可能会因为分散处理而使具体映射每一个页面的开销增加。所以这里有个利弊权衡的问题,具体的决定往往要建立在统计数据的基础上。这里正是运用了这个概念,把具体页面的映射推迟到真正需要的时候才进行。具体地,就是为映射的建立、物理页面的换入换出(以及映射的拆除)分别准备一些函数,这就是filemap_nopage、ext2_readpage以及ext2_writepage。

那么,什么时候,由谁来调用这些函数呢?

  1. 首先,当这个区间中的一个页面首次受到访问时,会由于页面无法映射而发生缺页异常,相应的异常处理程序为do_no_page。对于ext2文件系统,do_no_page会通过ext2_readpage分配一个空闲内存页面并从文件读入相应的页面,然后建立起映射。
  2. 建立起映射以后,对页面的写操作使页面变脏,但是页面的内容并不立即写回文件中,而由内核线程bdflush周期性运行时通过page_launder间接地调用ext2_writepage,将页面的内容写入文件。如果页面很长时间没有受到访问,则页面会耗尽它的寿命,从而在一次try_to_swap_out中被解除映射而转入不活跃状态。如果页面是脏的,则也会在page_launder中调用ext2_writepage。我们在try_to_swap_out的代码中曾经看到,对用于文件映射的页面与普通的换入换出页面有不同的处理。对于前者是解除页面映射,把页面表项设置成0;而对后者是断开页面映射,使页面表项指向盘上页面。
  3. 解除了映射的页面在再次受到访问时又会发生缺页异常,仍旧因页面无映射而进入do_no_page,而不像换入换出页面那样进入do_swap_page。

除mmap以外,linux内核还提供了几个与之有关的系统调用,作为对mmap的补充。

Guess you like

Origin blog.csdn.net/guoguangwu/article/details/120930251