前面讲了那么多线性区底层分配的细节,现在让我们讨论怎样分配一个新的线性地址区间。为了做到这点,do_mmap()函数为当前进程创建并初始化一个新的线性区。不过,分配成功之后,可以把这个新的线性区与进程已有的其他线性区进行合并。
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_mmap()函数的参数比较多,我们来一一分解:
file和offset:如果新的线性区将把一个文件映射到内存,则使用文件描述符指针file和文件偏移量offset。这个主题将在回收页框的相关专题进行讨论。在这里,我们假定不需要内存映射,所以file和offset都为空(NULL和0)。
addr:这个线性地址指定从何处开始查找一个空闲的区间。
len:线性地址区间的长度。
prot:这个参数指定这个线性区所包含页的访问权限。可能的标志有PROT_READ、PROT_WRITE、PROT_EXEC和PROT_NONE。前三个标志与标志VM_READ、VM_WRITE及VM_EXEC的意义一样。PROT_NONE表示进程没有以上三个访问权限中的任意一个。
flag:这个参数指定线性区的其他标志:
MAP_GROWSDOWN、MAP_LOCKED、MAP_DENYWRITE和MAP_EXECUTABLE
它们的含义与“线性区数据结构”博文中所列出标志的含义相同。
MAP_SHARED和MAP_PRIVATE
前一个标志指定线性区中的页可以被几个进程共享;后一个标志作用和两个标志都指向vm_area_struct描述符中的VM_SHARED标志。
MAX_FIXED
区间的起始地址必须是由参数addr所指定的。
MAX_ANONYMOUS
没有文件与这个线性区相关联。
MAP_NORESERVE
函数不必预先检查空闲页框的数目。
MAP_POPULATE
函数应该为线性区建立的映射提前分配需要的页框。该标志仅对映射文件的线性区和IPC共享的线性区有意义。
MAX_NONBLOCK
只有在MAP_POPULATE标志置位时才有意义:提前分配页框时,函数肯定不阻塞。
我们看到do_mmap()函数对offset的值进行一些初步检查,然后执行do_mmap_pgoff()函数。这里假设新的线性地址区间映射的不是磁盘文件,仅对实现匿名线性区的do_mmap_pgoff()函数进行说明(mm/Mmap.c):
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, * prev;
struct inode *inode;
unsigned int vm_flags;
int correct_wcount = 0;
int error;
struct rb_node ** rb_link, * rb_parent;
int accountable = 1;
unsigned long charged = 0, reqprot = prot;
/* 首先检查参数的值是否正确,所提的请求是否能被满足。尤其是要检查以下不能满足请求的条件:*/
if (file) { /* 如果是映射磁盘文件,则检查: */
if (is_file_hugepages(file)) /* 是否是大块文件 */
accountable = 0;
if (!file->f_op || !file->f_op->mmap) /* 文件必须有自己的mmap方法 */
return -ENODEV;
if ((prot & PROT_EXEC) && /* 文件必须是不可执行的 */
(file->f_vfsmnt->mnt_flags & MNT_NOEXEC))
return -EPERM;
}
/*
* Does the application expect PROT_READ to imply PROT_EXEC?
*
* (the exception is when the underlying filesystem is noexec
* mounted, in which case we dont add PROT_EXEC.)
* 上面的英文解释看不懂,但是可以肯定是跟文件映射相关,不理睬*/
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
if (!(file && (file->f_vfsmnt->mnt_flags & MNT_NOEXEC)))
prot |= PROT_EXEC;
if (!len) /* 检查len,如果是0则错误: */
return -EINVAL;
if (!(flags & MAP_FIXED)) /* 检查flags,如果不是MAP_FIXED则说明区间的起始地址不是由参数addr所指定的: */
addr = round_hint_to_min(addr); /* 此时就需要把addr设置成mmap_min_addr */
error = arch_mmap_check(addr, len, flags); /* 执行一系列处理器相关检查,x86体系为空函数 */
if (error)
return error;
/* Careful about overflows.. 先做个页面对齐调整,在检测防止内存溢出*/
len = PAGE_ALIGN(len);
if (!len || len > TASK_SIZE)
return -ENOMEM;
/* offset overflow? 同样检查最后那个页的页内偏移是否有溢出,这里为0,当然不会啦*/
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
return -EOVERFLOW;
/* Too many mappings? 进程已经映射了过多的线性区,因此mm内存描述符的map_count字段的值可能超过了允许的最大值*/
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;
/* Obtain the address to map to. we verify (or select) it and ensure
* that it represents a valid section of the address space.
* 上面的一系列检查都通过了,那么调用“线性区的底层处理”博文中的函数获得新线性区的线性地址区间。
* 由于是非文件映射,所以最终调用的是current->mm->get_unmapped_area,
* 即执行内存描述符的get_unmapped_area方法
*/
addr = get_unmapped_area_prot(file, addr, len, pgoff, flags, prot & PROT_EXEC);
if (addr & ~PAGE_MASK)
return addr;
/* 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.
* 通过把存放在prot和flags参数中的值进行组合来计算新线性区描述符的标志:
*/
vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
/* 注意,只有在prot中设置了相应的PROT_READ、PROT_WRITE和PROT_EXEC标志,
* calc_vm_prot_bits()函数才在vm_flags中设置VM READ, VM WRITE和VM EXEC标志;
* 只有在flags设置了相应的MAP_GROWSDOWN,MAP_DENYWRITE,MAP_EXECUTABLE和MAP_LOCKED标志,
* calc_vm_flag_bits()也才在VM_flags中设置VM_GROWSDOWN、VM_DENYWRITE、VM_EXECUTABLE和VM_LOCKED标志。
* 在vm_flags中还有几个标志被置为1:VM_MAYREAD、VM_MAYWRITE、VM_MAYEXEC,
* 在mm->def_flags中所有线性区的默认标志,以及如果线性区的页与其他进程共享时的VM_SHARED和VM_MAYSHARE。
*/
if (flags & MAP_LOCKED) {
if (!can_do_mlock())
return -EPERM;
vm_flags |= VM_LOCKED;
}
/* mlock MCL_FUTURE? */
if (vm_flags & VM_LOCKED) {
/* flag参数指定新线性地址区间的页必须被锁在RAM中,
* 但不允许进程创建上锁的线性区,
* 或者进程加锁页的总数超过了保存在进程描述符signal-> rlim[RLIMIT_MEMLOCK].rlim_cur字段中的阈值。
*/
unsigned long locked, lock_limit;
locked = len >> PAGE_SHIFT;
locked += mm->locked_vm;
lock_limit = current->signal->rlim[RLIMIT_MEMLOCK].rlim_cur;
lock_limit >>= PAGE_SHIFT;
if (locked > lock_limit && !capable(CAP_IPC_LOCK))
return -EAGAIN;
}
inode = file ? file->f_dentry->d_inode : NULL;
if (file) { /* 文件映射相关的处理 */
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(inode) && (file->f_mode & FMODE_WRITE))
return -EACCES;
/*
* Make sure there are no mandatory locks on the file.
*/
if (locks_verify_locked(inode))
return -EAGAIN;
vm_flags |= VM_SHARED | VM_MAYSHARE;
if (!(file->f_mode & FMODE_WRITE))
vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
/* fall through */
case MAP_PRIVATE:
if (!(file->f_mode & FMODE_READ))
return -EACCES;
break;
default:
return -EINVAL;
}
} else {
switch (flags & MAP_TYPE) {
case MAP_SHARED:
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
case MAP_PRIVATE:
/*
* Set pgoff according to addr for anon_vma.
*/
pgoff = addr >> PAGE_SHIFT;
break;
default:
return -EINVAL;
}
}
error = security_file_mmap_addr(file, reqprot, prot, flags, addr, 0);
if (error)
return error;
/* Clear old maps */
error = -ENOMEM;
munmap_back:
/* 确定处于新区间之前的线性区对象的位置,以及在红-黑树中新线性区的位置: */
vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
if (vma && vma->vm_start < addr + len) {
/* find_vmaprepare()函数也检查是否还存在与新区间重叠的线性区。
* 这种情况发生在函数返回一个非空的地址,这个地址指向一个线性区,而该区的起始位置位于新区间结束地址之前的时候。
* 在这种情况下,调用do_munmap()删除新的区间,然后重复整个步骤。
*/
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}
/* Check against address space limit. */
if (!may_expand_vm(mm, len >> PAGE_SHIFT))
/* 检查插人新的线性区是否引起进程地址空间的大小
* (mm->total_vm<<PAGE_SHIFT) + len超过存放在进程描述符signal->rlim[RLIMIT_AS].rlim_cur字段中的阈值。
* 如果是,就返回出错码-ENOMEM。注意,这个检查只在这里进行,而不在前面与其他检查一起进行,
* 因为一些线性区可能在刚才调用do_munmap()时候被删除。
*/
return -ENOMEM;
if (accountable && (!(flags & MAP_NORESERVE) ||
sysctl_overcommit_memory == OVERCOMMIT_NEVER)) {
/* 如果在flags参数中没有设置MAP_NORESERVE标志,新的线性区包含私有可写页,
* 并且没有足够的空闲页框,则返回出错码-ENOMEM;这最后一个检查是由security_vm_enough_memory()函数实现的。
*/
if (vm_flags & VM_SHARED) {
/* Check memory availability in shmem_file_setup? */
vm_flags |= VM_ACCOUNT;
} else if (vm_flags & VM_WRITE) {
/*
* Private writable mapping: check memory availability
*/
charged = len >> PAGE_SHIFT;
if (security_vm_enough_memory(charged))
return -ENOMEM;
vm_flags |= VM_ACCOUNT;
}
}
/*
* Can we just expand an old private anonymous mapping?
* The VM_SHARED test is necessary because shmem_zero_setup
* will create the file object for a shared anonymous map below.
* 如果新区间是私有的(没有设置VM_SHARED),且映射的不是磁盘上的一个文件,
* 那么,调用vma_merge()检查前一个线性区是否可以以这样的方式进行扩展来包含新的区间。
* 当然,前一个线性区必须与在vm_flags局部变量中存放标志的那些线性区具有完全相同的标志。
* 如果前一个线性区可以扩展,那么,vma_merge()也试图把它与随后的线性区进行合并
* (这发生在新区间填充两个线性区之间的空洞,且三个线性区全部具有相同的标志的时候)。
* 万一在扩展前一个线性区时获得成功,则跳到out。
*/
if (!file && !(vm_flags & VM_SHARED) &&
vma_merge(mm, prev, addr, addr + len, vm_flags,
NULL, NULL, pgoff, NULL))
goto out;
/*
* 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.
* 当然,如果没有合并,则调用slab分配函数kmem_cache_alloc()为新的线性区分配一个vm_area_struct数据结构。
*/
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
/* 初始化新的线性区对象(由vma指向): */
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = protection_map[vm_flags &
(VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)];
vma->vm_pgoff = pgoff;
if (file) {
error = -EINVAL;
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
goto free_vma;
if (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 (vm_flags & VM_SHARED) {
/* 如果MAP_SHARED标志被设置(以及新的线性区不映射磁盘上的文件),则该线性区是一个共享匿名区:
* 调用shmem_zero_setup()对它进行初始化。共享匿名区主要用于进程间通信。
*/
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
}
/* We set VM_ACCOUNT in a shared mapping's vm_flags, to inform
* shmem_zero_setup (perhaps called through /dev/zero's ->mmap)
* that memory reservation must be checked; but that reservation
* belongs to shared memory object, not to vma: so now clear it.
*/
if ((vm_flags & (VM_SHARED|VM_ACCOUNT)) == (VM_SHARED|VM_ACCOUNT))
vma->vm_flags &= ~VM_ACCOUNT;
/* Can addr have changed??
*
* Answer: Yes, several device drivers can do it in their
* f_op->mmap method. -DaveM
*/
addr = vma->vm_start;
pgoff = vma->vm_pgoff;
vm_flags = vma->vm_flags;
if (vma_wants_writenotify(vma))
vma->vm_page_prot =
protection_map[vm_flags & (VM_READ|VM_WRITE|VM_EXEC)];
if (!file || !vma_merge(mm, prev, addr, vma->vm_end,
vma->vm_flags, NULL, file, pgoff, vma_policy(vma))) {
file = vma->vm_file;
/* 调用vma_link()把新线性区插人到线性区链表和红-黑树中(参见前面“线性区的底层处理函数”博文)。*/
vma_link(mm, vma, prev, rb_link, rb_parent);
if (correct_wcount)
atomic_inc(&inode->i_writecount);
} else {
if (file) {
if (correct_wcount)
atomic_inc(&inode->i_writecount);
fput(file);
}
mpol_free(vma_policy(vma));
kmem_cache_free(vm_area_cachep, vma);
}
out:
/* 增加存放在内存描述符total_vm字段中的进程地址空间的大小。 */
mm->total_vm += len >> PAGE_SHIFT;
vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
if (vm_flags & VM_LOCKED) {
/* 如果设置了VM_LOCKED标志,就调用make_pages_present()连续分配线性区的所有页,并把它们锁在RAM中:*/
mm->locked_vm += len >> PAGE_SHIFT;
make_pages_present(addr, addr + len);
/* make_pages_present()函数按如下方式调用get_user_pages():
* write = (vma->vm_flags & VM_WRITE) != 0;
* get_user_pages(current, current->mm, addr, len, write, 0, NULL, NULL);
* get_user_pages()函数在addr和addr+len之间页的所有起始线性地址上循环;
* 对于其中的每个页,该函数调用follow_page()检查在当前页表中是否有到物理页的映射。
* 如果没有这样的物理页存在,则get_user_pages()调用handle_mm_fault(),
* 以后我们会看到,后一个函数分配一个页框并根据内存描述符的vm_flags字段设置它的页表项。
*/
}
if (flags & MAP_POPULATE) {
up_write(&mm->mmap_sem);
sys_remap_file_pages(addr, len, 0,
pgoff, flags & MAP_NONBLOCK);
down_write(&mm->mmap_sem);
}
/* 最后,函数通过返回新线性区的线性地址而终止。 */
return addr;
unmap_and_free_vma:
if (correct_wcount)
atomic_inc(&inode->i_writecount);
vma->vm_file = NULL;
fput(file);
/* Undo any partial mapping done by a device driver. */
unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
charged = 0;
free_vma:
kmem_cache_free(vm_area_cachep, vma);
unacct_error:
if (charged)
vm_unacct_memory(charged);
return error;
}