Linux 内存管理器实现了写时复制的按需分页策略,依赖于 386 的分页支持。一个进程从其父进程(在fork()期间)获取其页表,其中条目标记为只读或已交换。然后,如果进程尝试写入该内存空间,并且该页是写时复制页,则该页会被复制,并且该页会被标记为读写。一个exec()操作会导致从可执行文件读取一个或多个页面。然后,进程会调入它需要的任何其他页面。
每个进程都有一个页目录,这意味着它可以访问 1 KB 的页表,这些页表指向 1 MB 的 4 KB 页面,即 4 GB 的内存。进程的页目录在 fork 期间由copy_page_tables()初始化。空闲进程的页目录在初始化序列期间初始化。
每个用户进程都有一个本地描述符表,其中包含代码段和数据-堆栈段。这些用户段从 0 扩展到 3 GB (0xc0000000)。在用户空间中,线性地址和逻辑地址是相同的。
在 80386 上,线性地址从 0GB 运行到 4GB。线性地址指向此空间内的特定内存位置。线性地址不是物理地址 - 它是虚拟地址。逻辑地址由选择器和偏移量组成。选择器指向段,偏移量指示地址位于该段中的距离)。
内核代码和数据段是在全局描述符表中定义的特权段,范围从 3 GB 到 4 GB。交换器页目录 (swapper_page_dir)被设置成使得逻辑地址和物理地址在内核空间中是相同的。
3 GB 以上的空间在进程的页目录中显示为指向内核页表的指针。此空间在用户模式下对进程不可见,但是当进入特权模式(例如,处理系统调用)时,映射变得相关。管理模式是在当前进程的上下文中进入的,因此地址转换是相对于进程的页目录发生的,但是使用内核段。这与使用swapper_pg_dir和内核段产生的映射相同,因为两个页目录在此空间中使用相同的页表。只有task[0](空闲任务,由于历史原因,有时称为交换器任务,即使它与 Linux 实现中的交换没有任何关系) 直接使用swapper_pg_dir。
用户堆栈位于用户数据段的顶部并向下增长。内核堆栈不是一个漂亮的可以指向的结构或段,我不能用“那边是内核堆栈”来描述。一个kernel_stack_frame(一个页面)与每个新创建的进程相关联,并在内核在该进程的上下文中运行时使用。如果内核堆栈增长到其当前堆栈帧以下,则会发生不好的事情。[内核堆栈放在哪里?我知道每个进程都有一个,但是当它不被使用时存储在哪里?]
用户页面可以被窃取或交换。用户页面是在用户页表中的 3 GB 以下映射的页面。此区域不包含页目录或页表。只有脏页会被交换。
在某些地方需要进行细微的更改(例如,进程内存限制的测试),以提供对程序员定义的段的支持。
[现在有一个 modify_ldt() 系统调用,被 dosemu、Wine、TWIN 和 Wabi 用来创建任意段。]
以下是执行任何用户进程之前的物理内存映射。左侧的列给出了项目的起始地址,斜体数字是近似值。中间的列命名项目。最右边的列给出了相关的例程或变量名,或解释条目。
0x110000 | FREE | memory_end或high_memory |
mem_map | mem_init() | |
inode_table | inode_init() | |
设备数据 | device_init()* | |
0x100000 | 更多pg_tables | paging_init() |
0x0A0000 | RESERVED | |
0x060000 | FREE | |
low_memory_start | ||
0x006000 | 内核代码 + 数据 | |
floppy_track_buffer | ||
bad_pg_table bad_page | 被page_fault_handler使用,以便在内存不足时优雅地杀死进程。 | |
0x002000 | pg0 | 第一个内核页表。 |
0x001000 | swapper_pg_dir | 内核页目录。 |
0x000000 | 空页面 |
请注意,所有未标记为 FREE 的内存都被 RESERVED (mem_init)。RESERVED 页面属于内核,永远不会被释放或交换。
0xc0000000 | 不可见的内核 | reserved |
初始堆栈 | ||
堆栈增长空间 | 4 页 | |
0x60000000 | 共享库 | |
brk | 未使用 | |
malloc 内存 | ||
end_data | 未初始化数据 | |
end_code | 已初始化数据 | |
0x00000000 | 文本 |
代码段和数据段都从 0x00 一直延伸到 3 GB。目前,页面错误处理程序do_wp_page检查以确保进程不会写入其代码空间。但是,通过捕获SEGV信号,可以写入代码空间,导致发生写时复制。处理程序do_no_page确保进程获得的任何新页面都属于可执行文件、共享库、堆栈或位于brk值内。
用户进程可以通过调用brk值sbrk()来重置其malloc()在需要时会这样做。文本和数据部分分配在单独的页面上,除非选择-N编译器选项。共享库加载地址目前取自共享镜像本身。该地址介于 1.5 GB 和 3 GB 之间,特殊情况除外。
用户进程内存分配
可交换 | 可共享 | |
---|---|---|
一些代码页 | Y | Y |
一些数据页 | Y | N? |
堆栈 | Y | N |
pg_dir | N | N |
代码/数据page_table | N | N |
堆栈page_table | N | N |
task_struct | N | N |
kernel_stack_frame | N | N |
shlibpage_table | N | N |
一些 shlib 页面 | Y | Y? |
堆栈、shlibs 和数据彼此相距太远,无法由一个页表跨越。所有内核page_table都由所有进程共享,因此它们不在列表中。只有脏页会被交换。干净的页面会被窃取,因此进程可以根据需要从可执行文件中将其读回。大多数情况下,只有干净的页面是共享的。脏页最终会在 fork 期间共享,直到父进程或子进程选择再次写入它。
以下是进程表中保存的用于内存管理的一些数据的摘要
在start_kernel()(main.c) 中有 3 个与内存初始化相关的变量
memory_start | 开始于 1 MB。由设备初始化更新。 |
memory_end | 物理内存的结尾:8 MB、16 MB 或其他。 |
low_memory_start | 最初加载的内核代码和数据的结尾。 |
每个设备初始化通常占用memory_start并返回一个更新后的值(如果它在memory_start分配空间)。paging_init()初始化 {\tt swapper_pg_dir} 中的页表(从 0xc0000000 开始)以覆盖从memory_start到memory_end的所有物理内存。实际上,前 4 MB 在startup_32(head.S) 中完成。memory_start如果添加了任何新的page_table,则会递增。第一个页面被清零以捕获内核中的空指针引用。
在sched_init()的ldt和tss描述符在 GDT 中设置,并加载到 TR 和 LDTR 中(这是唯一一次显式完成)。为task[0]system_call()设置陷阱门 (0x80)。嵌套任务标志被关闭,为进入用户模式做准备。计时器已打开。 的为task_struct的task[0]task[0]的.
mem_mapsched.h>mem_init()然后通过
mem_init()构建以反映物理页面的当前使用情况。这是上一节的物理内存映射中反映的状态。然后 Linux 使用iret, 进入用户模式,在推送当前sstask[0]esp
task[0]:
= 用户代码,base=0xc0000000,size = 640Kexec()LDT[2]= 用户数据,base=0xc0000000,size = 640K第一个
将fork():
将exec():
Interrupts and traps are handled within the context of the current task. In particular, the page directory of the current process is used in address translation. The segments, however, are kernel segments so that all linear addresses point into kernel memory. For example, assume a user process invokes a system call and the kernel wants to access a variable at address 0x01. The linear address is 0xc0000001 (using kernel segments) and the physical address is 0x01. The later is because the process' page directory maps this range exactly as (中断和陷阱在当前任务的上下文中处理。特别是,当前进程的页目录用于地址转换。然而,段是内核段,因此所有线性地址都指向内核内存。例如,假设一个用户进程调用一个系统调用,内核想要访问地址 0x01 的一个变量。线性地址是 0xc0000001(使用内核段),物理地址是 0x01。后者是因为进程的页目录以如下方式映射这个范围:)page_pg_dir (page_pg_dir).
The kernel space (0xc0000000 + (内核空间 (0xc0000000 +)high_memory) is mapped by the kernel page tables which are themselves part of the RESERVED memory. They are therefore shared by all processes. During a fork () 由内核页表映射,内核页表本身是 RESERVED 内存的一部分。因此,它们被所有进程共享。在 fork 期间)copy_page_tables()treats RESERVED page tables differently. It sets pointers in the process page directories to point to kernel page tables and does not actually allocate new page tables as it does normally. As an example the (以不同的方式对待 RESERVED 页表。它在进程页目录中设置指针以指向内核页表,并且不像通常那样实际分配新的页表。例如,)kernel_stack_page(which sits somewhere in the kernel space) does not need an associated ((位于内核空间中的某个位置)不需要关联的)page_tableallocated in the process' (在进程中分配的)pg_dirto map it. (来映射它。)
The interrupt instruction sets the stack pointer and stack segment from the privilege 0 values saved in the tss of the current task. Note that the kernel stack is a really fragmented object--it's not a single object, but rather a bunch of stack frames each allocated when a process is created, and released when it exits. The kernel stack should never grow so rapidly within a process context that it extends below the current frame. (中断指令从当前任务的 TSS 中保存的特权 0 值设置堆栈指针和堆栈段。请注意,内核堆栈是一个非常分散的对象 - 它不是单个对象,而是一堆堆栈帧,每个堆栈帧在创建进程时分配,并在进程退出时释放。内核堆栈在进程上下文中永远不应增长得如此之快,以至于它扩展到当前帧以下。)
When any kernel routine wants memory it ends up calling (当任何内核例程需要内存时,它最终会调用)get_free_page() (get_free_page()). This is at a lower level than (。这比...更低级)kmalloc() (kmalloc())(in fact ((事实上)kmalloc() (kmalloc())uses (使用)get_free_page() (get_free_page())when it needs more memory). (当需要更多内存时)。)
get_free_page() (get_free_page())takes one parameter, a priority. Possible values are (接受一个参数,即优先级。可能的值是)GFP_BUFFER (GFP_BUFFER), GFP_KERNEL (GFP_KERNEL), GFP_NFS (GFP_NFS), and (,以及)GFP_ATOMIC (GFP_ATOMIC). It takes a page off of the (。它从...中取出一页)free_page_list (free_page_list), updates (,更新)mem_map, zeroes the page and returns the physical address of the page (note that (,将页面清零并返回页面的物理地址(请注意,)kmalloc() (kmalloc())returns a physical address. The logic of the mm depends on the identity map between logical and physical addresses). (返回一个物理地址。mm 的逻辑取决于逻辑地址和物理地址之间的标识映射)。)
That itself is simple enough. The problem, of course, is that the (这本身很简单。当然,问题是)free_page_list (free_page_list)may be empty. If you did not request an atomic operation, at this stage, you enter into the realm of page stealing which we'll go into in a moment. As a last resort (and for atomic requests) a page is torn off from the (可能为空。如果您没有请求原子操作,在此阶段,您将进入页面窃取的领域,我们稍后将讨论。作为最后的手段(对于原子请求),从...中撕下一页)secondary_page_list (secondary_page_list)(as you may have guessed, when pages are freed, the ((正如您可能已经猜到的,当页面被释放时,)secondary_page_list (secondary_page_list)gets filled up first). (首先被填满)。)
The actual manipulation of the (对...的实际操作)page_list (page_list)s and (和)mem_mapoccurs in this mysterious macro called (发生在这个神秘的宏中,称为)REMOVE_FROM_MEM_QUEUE() (REMOVE_FROM_MEM_QUEUE())which you probably never want to look into. Suffice it to say that interrupts are disabled. [I think that this should be explained here. It is not that hard...] (你可能永远不想深入研究它。 够了,中断被禁用。 [我认为应该在这里解释一下。这并不那么难...])
Now back to the page stealing bit. (现在回到页面窃取部分。)get_free_page() (get_free_page())calls (调用)try_to_free_page() (try_to_free_page())which repeatedly calls (它重复调用)shrink_buffers() (shrink_buffers())和swap_out() (swap_out())in that order until it is successful in freeing a page. The priority is increased on each successive iteration so that these two routines run through their page stealing loops more often. (按该顺序,直到成功释放一个页面。每次迭代都会提高优先级,以便这两个例程更频繁地运行它们的页面窃取循环。)
Here's one run through (这是一次运行)swap_out() (swap_out()):
try_to_swap_out() (try_to_swap_out())scans the page tables of all user processes and enforces the stealing policy (扫描所有用户进程的页表并强制执行窃取策略)
Of these actions, 6 and 7 will stop the process as they result in the actual freeing of a physical page. Action 5 results in one of the processes losing an unshared clean page that was not accessed recently (decrement (在这些操作中,6 和 7 将停止进程,因为它们会导致实际释放物理页面。操作 5 导致其中一个进程丢失一个最近未访问的非共享干净页面(减少)Q->rss (Q->rss)) which is not all that bad, but the cumulative effects of a few iterations can slow down a process considerably. At present, there are 6 iterations, so a page shared by 6 processes can get stolen if it is clean. ()这并不是那么糟糕,但是几次迭代的累积效应会大大降低进程的速度。目前,有 6 次迭代,因此如果一个页面是干净的,则可以被 6 个进程共享。)
Page table entries are updated and the TLB invalidated. (页表条目已更新,TLB 已失效。)
The actual work of freeing the page is done by (释放页面的实际工作由...完成)free_page() (free_page()), the complement of (,是...的补充)get_free_page() (get_free_page()). It ignores RESERVED pages, updates (。它忽略 RESERVED 页面,更新)mem_map, then frees the page and updates the (,然后释放页面并更新)page_list (page_list)s if it is unmapped. For swapping (in 6 above), (s,如果它未映射。对于交换(在上面的 6 中),)write_swap_page() (write_swap_page())gets called and does nothing remarkable from the memory management perspective. (被调用,并且从内存管理的角度来看,没有什么特别之处。)
The details of (...的详细信息)shrink_buffers() (shrink_buffers())would take us too far afield. Essentially it looks for free buffers, then writes out dirty buffers, then goes at busy buffers and calls (会使我们走得太远。 本质上,它寻找空闲缓冲区,然后写出脏缓冲区,然后处理繁忙缓冲区并调用)free_page() (free_page())when its able to free all the buffers on a page. (当它能够释放页面上的所有缓冲区时。)
Note that page directories and page tables along with RESERVED pages do not get swapped, stolen or aged. They are mapped in the process page directory through reserved page tables. They are freed only on exit from the process. (请注意,页目录和页表以及 RESERVED 页面不会被交换、窃取或老化。它们通过保留的页表映射到进程页目录中。它们仅在进程退出时释放。)
When a process is created via fork, it starts out with a page directory and a page or so of the executable. So the page fault handler is the source of most of a processes' memory. (当通过 fork 创建进程时,它从页目录和可执行文件的一个页面左右开始。因此,页面错误处理程序是进程的大部分内存的来源。)
The page fault handler (页面错误处理程序)do_page_fault() (do_page_fault())retrieves the faulting address from the register cr2. The error code (retrieved in sys_call.S) differentiates user/supervisor access and the reason for the fault--write protection or a missing page. The former is handled by (从寄存器 cr2 中检索导致错误的地址。错误代码(在 sys_call.S 中检索)区分用户/主管访问以及导致错误的原因 - 写保护或缺少页面。前者由...处理)do_wp_page() (do_wp_page())and the latter by (后者由...处理)do_no_page() (do_no_page()).
If the faulting address is greater than TASK_SIZE the process receives a SIGKILL. [Why this check? This can only happen in kernel mode because of segment level protection.] (如果导致错误的地址大于 TASK_SIZE,则进程会收到 SIGKILL。 [为什么进行此检查?这只能在内核模式下发生,因为有段级别保护。])
These routines have some subtleties as they can get called from an interrupt. You can't assume that it is the ``current'' task that is executing. (这些例程有一些微妙之处,因为它们可以从中断中调用。您不能假设正在执行的是“当前”任务。)
do_no_page() (do_no_page())handles three possible situations (处理三种可能的情况)
In all cases (在所有情况下)get_empty_pgtable() (get_empty_pgtable())is called first to ensure the existence of a page table that covers the faulting address. In case 3 (首先被调用以确保存在覆盖导致错误的地址的页表。在案例 3 中)get_empty_page() (get_empty_page())is called to provide a page at the required address and in case of the swapped page, (被调用以在所需地址提供一个页面,对于交换的页面,)swap_in() (swap_in())is called. (被调用。)
In case 2, the handler calls (在案例 2 中,处理程序调用)share_page() (share_page())to see if the page is shareable with some other process. If that fails it reads in the page from the executable or library (It repeats the call to (以查看该页面是否可与某些其他进程共享。如果失败,它会从可执行文件或库中读取页面(它重复调用)share_page() (share_page())in case another process did the same meanwhile). Any portion of the page beyond the brk value is zeroed. (以防另一个进程同时执行了相同的操作)。超出 brk 值的页面的任何部分都将被清零。)
A page read in from the disk is counted as a major fault ( (从磁盘读取的页面被计为主要错误()maj_flt (maj_flt)). This happens with a ()。这发生在...的情况下)swap_in() (swap_in())or when it is read from the executable or a library. Other cases are deemed minor faults ( (或者当从可执行文件或库中读取时。其他情况被认为是次要错误()min_flt (min_flt)).
When a shareable page is found, it is write-protected. A process that writes to a shared page will then have to go through (找到可共享页面时,它会受到写保护。写入共享页面的进程然后将不得不通过)do_wp_page() (do_wp_page())which does the copy-on-write. (,它执行写时复制。)
do_wp_page() (do_wp_page())does the following (执行以下操作)
Paging is swapping on a page basis rather than by entire processes. We will use swapping here to refer to paging, since Linux only pages, and does not swap, and people are more used to the word ``swap'' than ``page.'' Kernel pages are never swapped. Clean pages are also not written to swap. They are freed and reloaded when required. The swapper maintains a single bit of aging info in the (分页是基于页面的交换,而不是整个进程的交换。我们将在此处使用交换来指代分页,因为 Linux 仅分页而不交换,并且人们更习惯于使用“交换”而不是“页面”。内核页面永远不会被交换。干净页面也不会写入交换空间。它们在需要时被释放并重新加载。交换器在...中维护单个老化信息位)PAGE_ACCESSED (PAGE_ACCESSED)bit of the page table entries. [What are the maintainance details? How is it used?] (页表条目的位。[维护细节是什么?如何使用?])
Linux supports multiple swap files or devices which may be turned on or off by the swapon and swapoff system calls. Each swapfile or device is described by a (Linux 支持多个交换文件或设备,这些文件或设备可以通过 swapon 和 swapoff 系统调用打开或关闭。每个交换文件或设备都由...描述)struct swap_info_struct (struct swap_info_struct)(swap.c). (swap.c)。
static struct swap_info_struct { unsigned long flags; struct inode * swap_file; unsigned int swap_device; unsigned char * swap_map; char * swap_lockmap; int lowest_bit; int highest_bit; } swap_info[MAX_SWAPFILES];
The flags field ( (flags 字段()SWP_USED (SWP_USED)或SWP_WRITEOK (SWP_WRITEOK)) is used to control access to the swap files. When () 用于控制对交换文件的访问。当)SWP_WRITEOK (SWP_WRITEOK)is off space will not be allocated in that file. This is used by swapoff when it tries to unuse a file. When swapon adds a new swap file it sets (关闭时,不会在该文件中分配空间。swapoff 在尝试取消使用文件时会使用它。当 swapon 添加新的交换文件时,它会设置)SWP_USED (SWP_USED). A static variable (。一个静态变量)nr_swapfiles (nr_swapfiles)stores the number of currently active swap files. The fields (存储当前活动的交换文件的数量。字段)lowest_bit (lowest_bit)和highest_bit (highest_bit)bound the free region in the swap file and are used to speed up the search for free swap space. (绑定交换文件中的空闲区域,并用于加速搜索空闲交换空间。)
The user program mkswap initializes a swap device or file. The first page contains a signature (` (用户程序 mkswap 初始化交换设备或文件。第一页包含签名(`)SWAP-SPACE (SWAP-SPACE)swap_map 位于最后 10 个字节,并持有一个位图。 最初,位图中的 0 表示坏页。 位图中的 “1” 表示相应的页面是空闲的。 此页面永远不会被分配,因此只需要初始化一次。
系统调用swapon()通常由用户程序 swapon 从 /etc/rc 调用。 几页内存被分配给swap_map和swap_lockmap.
swap_map为交换文件中的每个页面保存一个字节。 它从位图初始化,包含 0 表示可用页面,128 表示不可用页面。 它用于维护交换文件中每个页面上的交换请求计数。swap_lockmap为每个页面保存一位,用于确保读取或写入交换文件时的互斥。
当需要换出一个内存页面时,通过调用get_swap_page()获得交换位置的索引。 然后,该索引存储在页表条目的第 1-31 位中,以便交换的页面可以通过页面错误处理程序找到,do_no_page() (do_no_page())在需要时。
索引的高 7 位给出交换文件(或设备),低 24 位给出该设备上的页码。 这使得多达 128 个交换文件,每个文件有大约 64 GB 的空间,但由于swap_map的空间开销将很大。 相反,交换文件大小被限制为 16 MB,因为swap_map然后占用 1 页。
函数swap_duplicate()被copy_page_tables()使用,以便子进程在 fork 期间继承交换的页面。 它只是增加swap_map中维护的该页面的计数。 每个进程在访问页面时,都将交换到该页面的单独副本中。
swap_free()递减swap_map中维护的计数。 当计数降至 0 时,该页面可以被get_swap_page()重新分配。 它在每次交换的页面被读入内存时 (swap_in() (swap_in())) 或当页面要被丢弃时 (free_one_table()等) 被调用。
版权 (C) 1992, 1993, 1996 Michael K. Johnson, johnsonm@redhat.com.
版权 (C) 1992, 1993 Krishna Balasubramanian 和 Douglas Johnson