修改linux进程映射加载地址,linux地址映射机制

6.4.3

内存映射

当某个程序的映象

开始执行时,可执行映象

必须装入到进程

的虚拟地址空间。如果该进程用到了任何一个共享库,则共享库也必须装入到进程

的虚拟地址空间。由此可看出,Linux

并不将映象

装入到物理内存,相反,可执行文件只是被连接到进程

的虚拟地址空间中。随着程序的运行,被引用的程序部分会由操作系统装入到物理内存,这种将映象

链接到进程地址空间的方法被称为“内存映射”。

当可执行映象

映射到进程

的虚拟地址空间时,将产生一组vm_area_struct

结构来描述虚拟内存区间的起始点和终止点,每个vm_area_struct

结构代表可执行映象

的一部分,可能是可执行代码,也可能是初始化的变量或未初始化的数据,这些都是在函数do_mmap()

中来实现的。随着vm_area_struct

结构的生成,这些结构所描述的虚拟内存区间上的标准操作函数也由Linux

初始化。但要明确在这一步还没有建立从虚拟内存到物理内存的影射,也就是说还没有建立页表页目录。

为了对上面的原理进行具体的说明,我们来看一下do_mmap()

的实现机制。

函数do_mmap()

为当前进程创建并初始化一个新的虚拟区,如果分配成功,就把这个新的虚拟区与进程已有的其他虚拟区进行合并,do_mmap()

在include/linux/mm.h

中定义如下:

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;

}

函数中参数的含义如下:

file

:表示要映射的文件,file

结构将在第八章文件系统中进行介绍;

off

:文件内的偏移量,因为我们并不是一下子全部映射一个文件,可能只是映射文件的一部分,off

就表示那部分的起始位置;

len

:要映射的文件部分的长度

addr

:虚拟空间中的一个地址,表示从这个地址开始查找一个空闲的虚拟区;

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

它们的含义与表6.2

中所列出标志的含义相同。

MAP_SHARED

和MAP_PRIVATE

前一个标志指定虚拟区中的页可以

被许多进程共享;后一个标志作用相反。这两个标志都涉及vm_area_struct

中的VM_SHARED

标志。

MAP_ANONYMOUS

表示这个虚拟区是匿名的,与任何文件无关。

MAP_FIXED

这个区间的起始地址必须是由参数addr

所指定的。

MAP_NORESERVE

函数不必预先检查空闲页面的数目。

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;

unsigned

int vm_flags;

int

correct_wcount = 0;

int

error;

rb_node_t ** rb_link, * rb_parent;

if

(file && (!file->f_op ||

!file->f_op->mmap))

return

-ENODEV;

if

((len = PAGE_ALIGN(len)) == 0)

return

addr;

if

(len >

TASK_SIZE)

return

-EINVAL;

/*

offset overflow? */

if

((pgoff + (len >> PAGE_SHIFT)) <

pgoff)

return

-EINVAL;

/* Too

many mappings? */

if

(mm->map_count > MAX_MAP_COUNT)

return

-ENOMEM;

函数首先检查参数的值是否正确,所提的请求是否能够被满足,如果发生以上情况中的任何一种,do_mmap()

函数都终止并返回一个负值。

/* Obtain

the

address to map to. we

verify (or select) it and ensure

* that

it represents a valid section of the address space.

*/

addr

= get_unmapped_area(file, addr, len, pgoff, flags);

if

(addr & ~PAGE_MASK)

return

addr;

调用get_unmapped_area

()函数在当前进程的用户空间中获得一个未映射区间的起始地址。PAGE_MASK

的值为0xFFFFF000

,因此,如果“addr & ~PAGE_MASK

”为非0

,说明addr

最低12

位非0

,addr

就不是一个有效的地址,就以这个地址作为返回值;否则,addr

就是一个有效的地址(最低12

位为0

),继续向下看:

/* 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.

*/

vm_flags = calc_vm_flags(

prot,flags)

| mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

/* mlock MCL_FUTURE? */

if

(vm_flags & VM_LOCKED) {

unsigned

long locked = mm->locked_vm

<< PAGE_SHIFT;

locked

+= len;

if

(locked >

current->rlim[RLIMIT_MEMLOCK].rlim_cur)

return

-EAGAIN;

}

如果

flag

参数指定的新虚拟区中的页必须

锁在内存,且进程

加锁页的总数超过了保存在进程的task_struct

结构rlim[RLIMIT_MEMLOCK].rlim_cur

域中的上限值,则返回一个负值,继续:

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(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;

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 {

vm_flags |= VM_SHARED | VM_MAYSHARE;

switch

(flags & MAP_TYPE) {

default

:

return

-EINVAL;

case

MAP_PRIVATE:

vm_flags &= ~(

VM_SHARED | VM_MAYSHARE);

/* fall through */

case

MAP_SHARED:

break

;

}

}

如果file

结构指针为0

,则目的仅在于创建虚拟区间,或者说,并没有真正的映射发生;如果file

结构指针不为0

,则目的在于建立从文件到虚拟区间的映射,那就要根据标志指定的映射种类,把为文件设置的访问权考虑进去:

·

如果所请求的内存映射是共享可写的,就要检查要映射的文件是为写入而打开的,而不是以追加模式打开的,还要检查文件上没有强制锁。

·

对于任何种类的内存映射,都要检查文件是为读操作而打开的。

如果以上条件都不满足,就返回一个错误码。

/* 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) {

if

(do_munmap(mm, addr, len))

return

-ENOMEM;

goto

munmap_back;

}

函数find_vma_prepare

()与find_vma

()基本相同,它扫描当前进程地址空间的vm_area_struct

结构所形成的红黑树,试图找到结束地址高于addr

的第一个区间;如果找到了一个虚拟区,说明addr

所在的虚拟区已经在使用,也就是已经有映射存在,因此要调用do_munmap

()把这个老的虚拟区从进程

地址空间中撤销,如果撤销不成功,就返回一个负数;如果撤销成功,就继续查找,直到在红黑树中找不到addr

所在的虚拟区,并继续下面的检查;

/* Check against address space limit. */

if

((mm->total_vm << PAGE_SHIFT) + len

> current->rlim[

RLIMIT_AS].rlim_cur)

return

-ENOMEM;

total_vm

是表示进程地址空间的页面数,如果把文件映射到进程

地址空间后,其长度超过了保存在当前进程rlim[RLIMIT_AS].rlim_cur

中的上限值,则返回一个负数。

/*

Private writable mapping? Check memory availability..

*/

if

((vm_flags & (VM_SHARED | VM_WRITE)) ==

VM_WRITE &&

!(

flags & MAP_NORESERVE)

&&!vm_enough_memory(len >> PAGE_SHIFT))

return

-ENOMEM;

如果flags

参数中没有设置MAP_NORESERVE

标志,新的虚拟区含有私有的可写页,空闲页面数小于要映射的虚拟区的大小;则函数终止并返回一个负数;其中函数vm_enough_memory

()用来检查一个进程的地址空间中是否有足够的内存来进行一个新的映射。

/*

Can we just expand an old anonymous mapping? */

if

(!file && !(vm_flags & VM_SHARED) &&

rb_parent)

if

(vma_merge(mm, prev, rb_parent, addr, addr +

len, vm_flags))

goto

out;

如果是匿名映射(file

为空),并且这个虚拟区是非共享的,则可以把这个虚拟区和与它紧挨的前一个虚拟区进行合并;虚拟区的合并是由vma_merge()

函数实现的。如果合并成功,则转out

处,请看后面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.

*/

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;

vma

->vm_page_prot

= protection_map[vm_flags & 0x0f];

vma

->vm_ops = NULL;

vma

->vm_pgoff = pgoff;

vma->vm_file = NULL;

vma->vm_private_data = NULL;

vma->vm_raend

= 0;

经过以上各种检查后

现在必须为新的虚拟区分配一个

vm_area_struct

结构。这是通过调用Slab

分配函数kmem_cache_alloc

()来实现的,然后就对这个结构的各个域进行了初始化。

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 (flags & MAP_SHARED) {

error

= shmem_zero_setup(vma);

if

(error)

goto

free_vma;

}

free_vma:

kmem_cache_free(

vm_area_cachep, vma);

return

error;

}

如果建立的是从文件到虚存区间的映射,则:

·

当参数

flags

中的

VM_GROWSDOWN

VM_GROWSUP

标志位为

1

时,说明这个区间可以向低地址或高地址扩展,但从文件映射的区间不能进行扩展,因此转到

free_vma

,释放给

vm_area_struct

分配的

Slab

,并返回一个错误;

·

flags

中的

VM_DENYWRITE

标志位为

1

时,就表示不允许通过常规的文件操作访问该文件,所以要调用

deny_write_access

()排斥常规的文件操作(参见第八章)。

·

get_file

()函数的主要作用是递增

file

结构中的共享计数;

·

每个文件系统都有个

fiel_operation

数据结构,其中的函数指针

mmap

提供了用来建立从该类文件到虚存区间进行映射的操作,这是最具有实质意义的函数;对于大部分文件系统,这个函数为

generic_file_mmap( )

函数实现的,该函数执行以下操作:

(1)

初始化

vm_area_struct

结构中的

vm_ops

域。如果

VM_SHARED

标志为

1

,就把该域设置成

file_shared_mmap

,否则就把该域设置成

file_private_mmap

。从某种意义上说,这个步骤所做的事情类似于打开一个文件并初始化文件对象的方法。

(2)

从索引节点的

i_mode

域(参见第八章)检查要映射的文件是否是一个常规文件。如果是其他类型的文件(例如目录或套接字),就返回一个错误代码。

(3)

从索引节点的

i_op

域中检查是否定义了

readpage( )

的索引节点操作。如果没有定义,就返回一个错误代码。

(4)

调用

update_atime( )

函数把当前时间存放在该文件索引节点的

i_atime

域中,并将这个索引节点标记成脏。

·

如果

flags

参数中的

MAP_SHARED

标志位为

1

,则调用

shmem_zero_setup

()进行共享内存的映射。

继续看

do_mmap()

中的代码;

/* Can addr have changed??

*

* Answer: Yes, several device drivers can do it in their

*

f_op->mmap method.

-DaveM

*/

addr

= vma->vm_start;

源码作者给出了解释,意思是说,

addr

有可能已被驱动程序改变,因此,把新虚拟区的起始地址赋给

addr

vma_link(

mm, vma, prev, rb_link, rb_parent);

if

(correct_wcount)

atomic_inc(

&file->f_dentry->d_inode->i_writecount);

此时,应该把新建的虚拟区插入到进程

的地址空间,这是由函数

vma_link

()完成的,该函数具有三方面的功能:

(1)

vma

插入到虚拟区链表中

(2)

vma

插入到虚拟区形成的红黑树中

(3)

vam

插入到索引节点(

inode

)共享链表中

函数

atomic_inc

(

x

)给

*x

1

,这是一个原子操作。在内核代码中,有很多地方调用了以

atomic

为前缀的函数。所谓原子操作,就是在操作过程中不会被中断。

out

:

mm

->total_vm += len >> PAGE_SHIFT;

if

(vm_flags & VM_LOCKED) {

mm

->locked_vm += len >> PAGE_SHIFT;

make_pages_present(

addr, addr + len);

}

return

addr;

do_mmap()

函数准备从这里退出,首先增加进程地址空间的长度,然后看一下对这个区间是否加锁,如果加锁,说明准备访问这个区间,就要调用

make_pages_present

()函数,建立虚拟页面到物理页面的映射,也就是完成文件到物理内存的真正调入。返回一个正数,说明这次映射成功。

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. */

zap_page_range(

mm, vma->vm_start,

vma->vm_end - vma->vm_start);

如果对文件的操作不成功,则解除对该虚拟区间的页面映射,这是由

zap_page_range

()函数完成的。

当你读到这里时可能感到困惑,页面的映射到底在何时建立?实际上,

generic_file_mmap(

)

就是真正进行映射的函数。因为这个函数的实现涉及很多文件系统的内容,我们在此不进行深入的分析,当读者了解了文件系统的有关内容后,可自己进行分析。

这里要说明的是,文件到虚存的映射仅仅是建立了一种映射关系,也就是说,虚存页面到物理页面之间的映射还没有建立。当

某个可

执行映象

映射到进程

虚拟内存中并开始执行时,因为只有很少一部分虚拟内存区间装入到了物理内存,可能会遇到所访问的数据不在物理内存。这时,处理器将向Linux

报告一个页故障及其对应的故障原因,于是就用到了请页机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值