在Linux中,用户内存和内核内存是独立的,并在单独的地址空间中实现。地址空间被虚拟化,这意味着地址是从物理内存中抽象出来的(通过一个简短的过程)。因为地址空间是虚拟化的,所以可以存在许多地址空间。实际上,内核本身驻留在一个地址空间中,每个进程驻留在自己的地址空间中。这些地址空间由虚拟内存地址组成,允许具有独立地址空间的许多进程引用相当小的物理地址空间(计算机中的物理内存)。这不仅方便,而且是安全的,因为每个地址空间都是独立的、孤立的,因此是安全的。
但这种安全是有代价的。因为每个进程(和内核)都可以有相同的地址引用不同的物理内存区域,所以不可能立即共享内存。幸运的是,有一些解决方案。用户进程可以通过便携式UNIX操作系统接口(POSIX)共享内存机制(shmem),请注意,每个进程可能有一个不同的虚拟地址,该地址引用相同的物理内存区域。
虚拟内存到物理内存的映射是通过页面表实现的,这些表是在底层硬件中实现的(参见图1)。硬件本身提供映射,但是内核管理表及其配置。注意,如这里所示,一个进程可能有一个大的地址空间,但是它是稀疏的,这意味着地址空间的小区域(页面)通过页面表引用物理内存。这允许一个进程拥有一个只为任何给定时间所需的页面定义的大量地址空间。
图1.页表提供了从虚拟地址到物理地址的映射
具有为进程稀疏定义内存的能力意味着底层物理内存可能会被过度提交。通过一个叫做寻呼(虽然在Linux中,它通常被称为互换),将较少使用的页面动态移动到较慢的存储设备(如磁盘),以容纳需要访问的其他页面(请参见图2)。这种行为允许计算机中的物理内存为应用程序更容易需要的页面提供服务,同时将不需要的页面迁移到磁盘以提高物理内存的利用率。请注意,有些页面可以引用文件,在这种情况下,如果数据是脏的(通过页面缓存),则可以刷新数据,或者,如果页面是干净的,则可以简单地丢弃数据。
图2.交换允许更好地使用物理内存空间,方法是将使用较少的页迁移到速度较慢、成本较低的存储区。
无MMU体系结构
并不是所有处理器都有MMU。因此,uClinux发行版(微控制器Linux)支持单一地址空间的操作。这种架构缺乏MMU提供的保护,但允许Linux在另一类处理器上运行。
选择要交换到存储的页的过程称为页面替换算法并且可以使用许多算法(例如最近使用的最少的算法)来实现。当请求其页不在内存中的存储器位置(在存储器管理单元[MMU]中不存在映射)时,可发生此处理。此事件称为页面故障并由硬件(MMU)检测,然后在出现页面故障中断后由固件进行管理。看见图3有关此堆栈的说明。
Linux提供了一个有趣的交换实现,它提供了一些有用的特性。Linux交换系统允许创建和使用多个交换分区和优先级,这允许具有不同性能特征的交换设备(例如,固态磁盘上的一级交换空间[ssd]和较慢存储设备上更大的二级交换空间)的交换层。将更高的优先级附加到SSD交换区允许将其使用到耗尽;只有这样,页面才会被写入优先级较低(较慢)的交换分区。
图3.虚拟地址到物理地址映射的地址空间和元素
并不是所有的页面都是交换的候选页面。考虑响应中断的内核代码或管理页表和交换逻辑的代码。这些都是显而易见的页面,不应该被交换掉,因此被钉住了,或者永久存在于记忆中。虽然内核页不是交换的候选对象,但用户空间页可以通过mlock(或mlockall)函数来锁定页面。这就是用户空间内存访问函数背后的目的。如果内核假定用户传递的地址是有效的和可访问的,则最终会发生内核恐慌(例如,因为用户页被交换,导致内核中出现页面错误)。这个应用程序编程接口(API)确保正确处理这些角落的情况。
现在,让我们探索用于操作用户内存的内核API。请注意,这包括内核和用户空间界面,但下一节将探讨其他一些内存API。表1列出了要研究的用户空间内存访问函数。
表1.用户空间内存访问API
正如您所预期的,这些函数的实现可以是依赖于体系结构的。对于x86体系结构,您可以找到在
./linux/arch/x86/include/asm/uaccess.h中定义的这些函数和符号,源代码位于./linux/arch/x86/lib/userCopy_32.c和userCop64.c。
数据移动函数的作用如图4所示,因为它与复制所涉及的类型(简单的和聚合的)有关。
图4.使用用户空间内存访问API进行数据移动
使用access_ok函数检查要访问的用户空间中指针的有效性。调用方提供指针,指针引用数据块的开始、块的大小和访问类型(无论该区域是要读写的)。功能原型定义为:
access_ok( type, addr, size );
这个type参数可以指定为VERIFY_READ或VERIFY_WRITE。这个VERIFY_WRITE符号还标识内存区域是否可读和可写。如果该区域可能是可访问的,则该函数返回非零(尽管访问仍可能导致-EFAULT)。这个函数只是检查地址是否可能在用户空间中,而不是在内核中。
若要从用户空间读取简单变量,请使用get_user功能。此函数用于简单类型,如char和int,但是更大的数据类型(如结构)必须使用copy_from_user取而代之的是功能。原型接受一个变量(用于存储数据)和读取操作的用户空间中的地址:
get_user( x, ptr );
这个get_user函数映射到两个内部函数中的一个。在内部,此函数确定要访问的变量的大小(基于为存储结果提供的变量),并通过以下方式形成内部调用:__get_user_x。此函数在成功时返回零。一般来说,get_user和put_user函数比它们的块副本更快,如果移动了小类型,就应该使用它。
使用put_user函数将一个简单的变量从内核写入用户空间。喜欢get_user,它接受一个变量(包含要写入的值)和一个用户空间地址作为写入目标:
put_user( x, ptr );
喜欢get_user,put_user函数内部映射到put_user_x函数并在成功时返回0或-EFAULT关于错误。
这个clear_user函数用于使用户空间中的内存块为零。此函数在用户空间中接受一个指针,大小为零,以字节为单位定义:
clear_user( ptr, n );
内部,clear_user函数首先检查用户空间指针是否可写(通过access_ok),然后调用内部函数(以内联程序集编码)来执行清除操作。这个函数被优化为一个非常紧的循环,使用带有重复前缀的字符串指令。如果操作成功,则返回不可清除的字节数或零字节数。
这个copy_to_user函数将数据块从内核复制到用户空间。此函数接受指向用户空间缓冲区的指针、指向内核缓冲区的指针以及以字节为单位定义的长度。该函数在成功时返回零,或非零返回,以指示未传输的字节数。
copy_to_user( to, from, n );
在检查写入用户缓冲区的能力之后(通过access_ok),内部功能__copy_to_user调用,然后调用__copy_from_user_inatomic(见
./linux/arch/x86/include/asm/uaccess)XX。H,其中_XX视架构而定是32或64)。此函数(在确定是否执行1、2或4字节副本之后)最终调用复制_to_user_ll`,这是完成实际工作的地方。在损坏的硬件中(在i 486之前,WP位不能从监控模式中获得),页表可能在任何时候发生更改,需要将所需的页面固定到内存中,以便在寻址时不能换掉它们。POST i 486,进程只不过是一个优化的副本。
这个copy_from_user函数将数据块从用户空间复制到内核缓冲区。它接受一个目标缓冲区(在内核空间中)、一个源缓冲区(来自用户空间)和一个以字节为单位的长度。同.一样copy_to_user,该函数在成功时返回零,而非零返回指示复制某些字节数失败的非零。
copy_from_user( to, from, n );
该函数首先检查用户空间中从源缓冲区读取数据的能力(access_ok),然后调用__copy_from_user最终__copy_from_user_ll。从这里开始,根据体系结构的不同,调用将用户缓冲区复制到具有零(不可用字节)的内核缓冲区。优化的装配功能包括管理能力。
这个strnlen_user函数的用法就像strnlen但假设缓冲区在用户空间中可用。这个strnlen_user函数有两个参数:用户空间缓冲区地址和要检查的最大长度。
strnlen_user( src, n );
这个strnlen_user函数首先检查用户缓冲区是否可通过调用access_ok。如果可以访问,则strlen函数,并且max length争论被忽略了。
这个strncpy_from_user函数将字符串从用户空间复制到内核缓冲区,给定用户空间源地址和最大长度。
strncpy_from_user( dest, src, n );
作为来自用户空间的副本,此函数首先检查缓冲区是否可通过access_ok。类似于copy_from_user,该函数被实现为一个优化的程序集函数(在
./linux/arch/x86/lib/userCopy_XX._c中)。
上一节探讨了在内核和用户空间之间移动数据的方法(内核启动了操作)。Linux提供了许多其他方法,您可以在内核和用户空间中使用这些方法来进行数据移动。虽然这些方法可能不一定提供用户空间存储器访问函数所描述的相同功能,但它们在地址空间之间映射内存的能力是相似的。
在用户空间中,请注意,由于用户进程出现在单独的地址空间中,因此必须通过某种形式的进程间通信机制在它们之间移动数据。Linux提供了多种方案(例如消息队列),但最值得注意的是POSIX共享内存(shmem)。此机制允许进程创建内存区域,然后与一个或多个进程共享该区域。请注意,每个进程都可以将共享内存区域映射到各自地址空间中的不同地址。因此,需要相对偏移寻址。
这个mmap函数允许用户空间应用程序在虚拟地址空间中创建映射。这种功能在某些类型的设备驱动程序中很常见(为了性能),允许将物理设备内存映射到进程的虚拟地址空间。在司机内部,mmap函数通过remap_pfn_range内核函数,它提供设备内存到用户地址空间的线性映射。
本文探讨了Linux中内存管理的主题(以达到分页之后的位置),然后探讨了使用这些概念的用户空间内存访问函数。在用户空间和内核之间移动数据并不像看起来那么简单,但是Linux包含一组简单的API,这些API为您跨平台管理这项任务的复杂性。