Linux 0.11 内存管理-代码阅读记录 Linux 0.11 MMU Code Reading Record

Linux 0.11 内存管理代码阅读记录

MMU(Memory Management Unit) 代码主要是在mm/memory.c中

此代码文件包含的函数有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void do_exit(long code); //非主要的函数
static inline void oom(void) //非主要的函数
#define invalidate()//非主要的函数
#define copy_page(from,to)
unsigned long get_free_page(void)
void free_page(unsigned long addr)
int free_page_tables(unsigned long from,unsigned long size)
int copy_page_tables(unsigned long from,unsigned long to,long size)
unsigned long put_page(unsigned long page,unsigned long address)
void un_wp_page(unsigned long * table_entry)
void do_wp_page(unsigned long error_code,unsigned long address)
void write_verify(unsigned long address)
void get_empty_page(unsigned long address)
static int try_to_share(unsigned long address, struct task_struct * p)
static int share_page(unsigned long address)
void do_no_page(unsigned long error_code,unsigned long address)
void mem_init(long start_mem, long end_mem) // 内存初始化函数
void calc_mem(void) // 任何地方没有被调用过的函数

函数分类

这么多函数,为了便于理解,先对以上的函数做一个分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//主要操作物理内存地址的函数,与线性地址无关
unsigned long get_free_page(void)
void free_page(unsigned long addr)

//和上面的相反,这里的两个函数是与线性地址相关的,释放和复制线性地址对应的物理地址范围
int free_page_tables(unsigned long from,unsigned long size)
int copy_page_tables(unsigned long from,unsigned long to,long size)

//与写时复制相关的函数
void un_wp_page(unsigned long * table_entry)
void do_wp_page(unsigned long error_code,unsigned long address)
void write_verify(unsigned long address)

//与pags.s:page_fault 相关的函数
//且他们的调用关系是: do_no_page -- share_page -- try_to_share
static int try_to_share(unsigned long address, struct task_struct * p)
static int share_page(unsigned long address)
void do_no_page(unsigned long error_code,unsigned long address)

//其他类:
/*内存初始化函数,在kernel_init的时候会调用*/
void mem_init(long start_mem, long end_mem)

/*get_empty_page与get_free_page有什么区别?
get_empty_page:会将空闲的物理地址映射到线性地址
get_free_page: 不会映射到线性地址
*/
void get_empty_page(unsigned long address)

/*get_empty_page会调用put_page()*/
unsigned long put_page(unsigned long page,unsigned long address)

main函数与物理内存空间的规划

linux/init/main.c

main() 中关于物理内存空间的规划。

内存初始化示意图

内存初始化示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// main.c 部分代码
#define EXT_MEM_K (*(unsigned short *)0x90002) // 1MB以后的扩展内存大小(KB)
static long memory_end = 0; // 机器具有的物理内存容量(B)
static long buffer_memory_end = 0; // 高速缓冲区末端地址
static long main_memory_start = 0; // 主内存开始的位置
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO;
memory_end = (1<<20) + (EXT_MEM_K<<10); // 内存大小=1MB+扩展内存(K)*1024字节
memory_end &= 0xfffff000; // 忽略不到4KB的内存数,因为一个内存页4KB,小于4KB利用不上
if (memory_end > 16*1024*1024) // 如果内存超过16MB,则按照16MB计
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024; // 设置缓冲区末端地址
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end; // 主内存起始位置=缓冲区末端
#ifdef RAMDISK_SIZE
main_memory_start += rd_init(main_memory_start, RAMDISK_SIZE*1024); // 占用主内存空间,定义内存虚拟盘
#endif
mem_init(main_memory_start,memory_end);
}

head.s 执行完毕后就会跳转到 main.c 继续执行,main.c 文件主要做了内核初始化的工作,包括了块设备、字符设备等,以及人工设置第一个任务的工作。上面的代码展现了 main() 中关于内存的规划部分。主要内容是规范内存大小、确定主存区起始位置、设置虚拟盘空间和调用 memory.c 中的主内存初始化函数。

物理内存空间的规划步骤:

  1. 规范内存大小。代码第 14 行,求出内存大小,通过 1MB 内核区域+扩展内存区域方式求解。扩展内存(EXT_MEM_K,定义在代码第 2 行)大小为 0x90002,内存大小(memory_end,定义在代码第 3 行)计算结果为 0x241007ff。代码第 15 行,忽略不到 4K 的内存数,求解出结果 0x24100000。代码第 16~17 行,通过分支语句,判断出内存容量超过了16MB,将内存大小记为 16MB。

  2. 确定主存起始位置。代码第 18~23 行,通过内存大小,来设置高速缓冲区末端地址(buffer_memory_end,定义在代码第4行),具体分支判断过程可参考下方图片。代码第 24 行,将高速缓冲区末端地址赋值给主内存起始地址(main_memory_start,定义在代码第 5 行),至此,完成主内存区域始址的标记工作。

  3. 设置虚拟盘空间。代码第 25~27 行,通过 kernel/blk_drv/ramdisk.c 文件中的变量,来设置虚拟盘所占用的空间。

  4. 调用 memory.c 中的主内存初始化函数。代码第 28 行,调用 mm/memory.c 程序中的函数 mem_init,进一步将主内存区初始化,调用形式为 mem_init(main_memory_start,memory_end);

mem_init对全局变量mem_map[] 的初始化

mem_init() 函数主要针对主内存区域进行管理分配。mem_map[] 数据结构则表示了物理内存页面的占用状态,数组元素中的值表示被占用的次数,0 表示物理内存空闲,当申请一页物理内存时,就将对应的字节值变为1。

memory.c 部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// memory.c 部分代码
/*总结初始化流程
S1:15M PAGING_MEMORY 主内存,每个页都设置为USED ,通过>>12 (4K)来计算页数量。4K 为每页的大小。 按照道理mem_map只是一个内存状态的映射
S2:计算start_mem 所在的页数位置 ;
S3:start_mem到end_mem之间的页都设置为零,表示为空闲;
*/
#define LOW_MEM 0x100000 //大小1MB
#define PAGING_MEMORY (15*1024*1024)
#define PAGING_PAGES (PAGING_MEMORY>>12)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12) //LOW_MEM表示内存的低位
#define USED 100
static unsigned char mem_map [ PAGING_PAGES ] = {0,};
void mem_init(long start_mem, long end_mem)
{
int i;
HIGH_MEMORY = end_mem; //HIGH_MEMORY表示的高位
for (i=0 ; i<PAGING_PAGES ; i++) // PAGING_PAGES = 15x1024x1024 / 2^12 = (15x1024x1024) / 4096 (4K)= 3840 (PAGES)
mem_map[i] = USED; //unsigned char 0 - 65535
//#define 当作函数来使用。
i = MAP_NR(start_mem); //计算start_mem在mem_map[]位置的索引i
end_mem -= start_mem;
end_mem >>= 12; // 计算end_mem在mem_map[]位置的索引
// >>= 等价于 end_mem = end_mem >> 12
while (end_mem-->0) //等价于 (end_mem --) > 0
mem_map[i++]=0; // start_mem 到end_mem之间范围
}

该变量的初始化过程为:

  1. 计算非内核空间内存所需要的页面数(PAGING_PAGES,代码第 2 行)。
  2. 将高速缓冲区域以及虚拟盘区域(如果有)全部初始化为 100(代码 11~12 行)。
  3. 将主内存区域的项清零(代码 16~17 行)。

以 16MB 内存大小为例。除去内核空间 1MB,mem_map 需要管理剩余 15MB 空间的页面,一共有(16MB-1MB)/4KB=3840 项,即 PAGING_PAGES 为 3840,主内存区域具有(16MB-4.5MB)/4KB=2944 项(此 4.5MB 空间还包括了高速缓冲区域及虚拟盘区域),故前 896 项在数组 mem_map 中均被置为 100,而剩余 2944 项均被置为 0,等待内存分页管理程序的分配,如图展示了 mem_map 初始化结果。

mem_map 初始化 示意图

mem_map 初始化

free_page & get_free_page

这两个函数主要是操作物理内存地址的函数,与线性地址无关,与线性地址不产生映射。

get_free_page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
* Get physical address of first (actually last :-) free page, and mark it
* used. If no free pages left, return 0.
* process steps : (mainly meanings , Refrence:
* https://topic.alibabacloud.com/tc/a/linux-011-kernel-memory-management-get_free_page--function-analysis_1_16_30220847.html)
* 0. mem map = memory map
* 1. find where are free pages
* 2. set it's mem map to 1 , and make it empty ,and return it .
*
*/
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");

__asm__("std ; repne ; scasb\n\t"
"jne 1f\n\t"
"movb $1,1(%%edi)\n\t"
"sall $12,%%ecx\n\t"
"addl %2,%%ecx\n\t"
"movl %%ecx,%%edx\n\t"
"movl $1024,%%ecx\n\t"
"leal 4092(%%edx),%%edi\n\t"
"rep ; stosl\n\t"
" movl %%edx,%%eax\n"
"1: cld"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
);
return __res;
}

这段代码主要由汇编构成,具体每个成分的意思可以参考Linux-0.11核心記憶體管理get_free_page()函數分析)

这里简述此函数主要的工作内容:

  1. 从物理地址空间中寻址空闲页

  2. 如果找到空闲页,该页对应的mem map 为1(表示即将要使用该页) ,并设置清空对应的物理内存,并返回该地址。 如果没有空闲页,则返回0。

  • 这里注意的是mem_map[page] = 1 表示被占用; mem_map[page] = 2 表示被共享。

free_page

此函数主要是根据给定的物理地址,去释放该地址的内存值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* Free a page of memory at physical address 'addr'. Used by
* 'free_page_tables()'
*/
void free_page(unsigned long addr) 、、
{
if (addr < LOW_MEM) return;
if (addr >= HIGH_MEMORY)
panic("trying to free nonexistent page");
addr -= LOW_MEM;
addr >>= 12; // addr = addr >> 12 . means divide 2^12 = 4K for calculating
// the number of page .
if (mem_map[addr]--) return; // if (a--) first condite a then -- ;
// bug if the mem_map bigger than 1 (e.g == 3 ) ,is it meaningful ?
// mem_map[addr]>=2 : page is shared
// mem_map[addr]>=1 : page is used
mem_map[addr]=0;
panic("trying to free free page"); //error
}

这里需要注意的是if (mem_map[addr]–) 先判断mem_map[addr]是否大于等于1,如果条件为真,则– 并返回 ; 如果条件为假,则置零,并且报错。

但是,如果mem_map[addr]大于等于2 时,并不能完全free掉内存的占用,因为mem_map[addr]不同值代表不同的含义,应该是多次执行此函数,才可以完全free掉内存的占用。

free_page_tables & copy_page_tables

这里的两个是与线性地址相关的函数,释放和复制范围线性地址对应的物理地址内存

free_page_tables

函数作用: 释放连续的块页表,但是只处理与4M对齐的块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/*
* This function frees a continuos block of page tables, as needed
* by 'exit()'. As does copy_page_tables(), this handles only 4Mb blocks.
* 函数功能: 释放一块连续的物理内存
* process: 工作过程
* 1. begin addr + size (only aligned to 4M ,so begin address could be 0 4M 8M 12M )
* 2. confirm dir : need dir location ;
* which page table : need page table location .
* if LINEAR ADDRESS , only need it's DIR offset address.
* 3. free each FRAME PAGE that relative to page table entries .
* from : 线性地址
* size : 长度(页表个数)
*
* 总结: 从一个入参线性地址from 确定对应的页目录项, 页表 . 根据size大小确定要释放的范围.
* 从页目录到页表 逐级遍历释放掉PAGE FRAME , 最后释放掉页表
* 再刷新缓存区
* 完毕.
*/
int free_page_tables(unsigned long from,unsigned long size)
{
unsigned long *pg_table;
unsigned long * dir, nr;

if (from & 0x3fffff) /* 3fffff = 4M . 计算过程: 3fffff/ffffff = 1/4 ; 0xffffff = 2^24 = 16M ; 0xffffff * 1/4 = 4M
& 位与; && 逻辑与
只有当from 是4M 或4M的倍数的时候,条件才为假 . 学习表达式. if (from & 0xff)
表示判断某个数 from 是否在 (0xff+0x1)的边界上,如果条件为否,表示在边界上.是说明不在边界上.
linus 编程也太牛了.*/
/*
The expression from & 0x3fffff checks if the least significant 22 bits of the from variable are non-zero.
If the result is non-zero, the if condition evaluates to true.
Therefore, the if statement will be false when from has a value that has all 22 least significant bits set to zero.
In other words, if from is a multiple of 4M (0x400000).
*/
panic("free_page_tables called with wrong alignment");
if (!from) // 判断是否为地址零,条件否,from非零;条件是,from为零,死机
panic("Trying to free up swapper memory space");
size = (size + 0x3fffff) >> 22;/* 计算size是有多少个页表,避免最后结果为零,+4M(0x3fffff)
左移多少位代表什么含义?表示除2^22的大小 . 2^22 = 4M . 除4M . 计算得到多少页表个数. 一个页表能包含的额内存范围是4M
所以在 linux 0.11 中 ,主物理内存大小是16M ,所以用4个页表表示足够了.
*/
dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_ dir = 0 ;
dir指的是目录项, 计算的应该是从哪个页目录项*/
/* 一个线性地址32bit;
DIR(31-22)|PAGE(21-12)|OFFSET(11-0)

回忆一下页目录项的地址: 分别是0x0000 ; 0x0004 ; 0x0008 ;0x0012

from 从上面的条件判断只能是4M或4M的倍数.
所以得到的值会是4;8;12

0xffc:The purpose of this operation is to mask the lower bits
and ensure that the resulting value is aligned to a 4KB boundary (the size of a page directory entry).
注意这里的mask. 0xffc = 1111 1111 1100 . 所以这里是为了mask掉最后 2bits.

unsigned long * dir : dir是一个地址. *dir 就是该地址的值.
例如 在调试debug的时候, 0x0004 -- 0x002027 --0x40a06700
x 0x0004 : 0x002027
x 0x002027 : 0x40a06700
x (*0x0004): 0x40a06700
所以 *0x0004能够取到地址0x0004的内容.
x 0x002027 与 x (*0x0004)是等价的.
*/
for ( ; size-->0 ; dir++) { // 换个写法 : for( ; size>0 ; size-- , dir ++ )
if (!(1 & *dir)) // *dir 就是某一个page table的基地址, 如果*dir无效 continue
continue;
pg_table = (unsigned long *) (0xfffff000 & *dir); //取pa_table的基地址 mask掉最后三位无关地址的内容
for (nr=0 ; nr<1024 ; nr++) { //释放掉page table中的每一个entry
if (1 & *pg_table) // *pa_table 指的是某一个FRAME_PAGE(4K)的地址. 如果这个地址有效call free_page ;
// 如果无效(指已经是零,或者无法映射到物理内存FRAME PAGE), 令FRAME_PAGE =0 ()
free_page(0xfffff000 & *pg_table); // 0xfffff000 & *pg_table : 取地址
*pg_table = 0;
pg_table++;
}
free_page(0xfffff000 & *dir); // page table 自己的mem map 置为空闲状态
*dir = 0; // page table 指向 PAGE FRAME 的地址 清零
}
invalidate(); //刷新缓存
return 0;
}

copy_page_tables

看懂free_page_tables函数之后,再看copy_page_tables就会简单很多,这两个函数有很多共通之处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*
* Well, here is one of the most complicated functions in mm. It
* copies a range of linerar addresses by copying only the pages.
* Let's hope this is bug-free, 'cause this one I don't want to debug :-)
*
* Note! We don't copy just any chunks of memory - addresses have to
* be divisible by 4Mb (one page-directory entry), as this makes the
* function easier. It's used only by fork anyway.
*
* NOTE 2!! When from==0 we are copying kernel space for the first
* fork(). Then we DONT want to copy a full page-directory entry, as
* that would lead to some serious memory waste - we just copy the
* first 160 pages - 640kB. Even that is more than we need, but it
* doesn't take any more memory - we don't copy-on-write in the low
* 1 Mb-range, so the pages can be shared with the kernel. Thus the
* special case for nr=xxxx.
* 这里注意: 最后是将from_page_table复制给to_page_table
,并且两者指向的是同一块pageframe,复制的不是page frame .
函数名称也是copy_page_tables 不是copy_pages。

*/
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
unsigned long * from_page_table;
unsigned long * to_page_table;
unsigned long this_page;
unsigned long * from_dir, * to_dir;
unsigned long nr;


if ((from&0x3fffff) || (to&0x3fffff))
panic("copy_page_tables called with wrong alignment");
from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
to_dir = (unsigned long *) ((to>>20) & 0xffc);
size = ((unsigned) (size+0x3fffff)) >> 22;
for( ; size-->0 ; from_dir++,to_dir++) {
if (1 & *to_dir) //end page_tables
panic("copy_page_tables: already exist");
if (!(1 & *from_dir))
continue;
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
if (!(to_page_table = (unsigned long *) get_free_page()))
return -1; /* Out of memory, see freeing */
*to_dir = ((unsigned long) to_page_table) | 7;
nr = (from==0)?0xA0:1024; //0xA0=160(DEC) 只能允许复制一部分
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2; //设置只读 ? // ~2 =~10 = 01  // this_page = this_page & ~2 //为什么可以设置成只读?4K page frame的数据格式是如何定义的?
*to_page_table = this_page;
if (this_page > LOW_MEM) {
*from_page_table = this_page; //变更属性后重新赋予
this_page -= LOW_MEM;
this_page >>= 12; //this_page的物理地址,计算索引
mem_map[this_page]++; /*设置共享。mem_map[page] =1 表示被占用;
mem_map[page] >=2 表示被占用且共享 */
}
}
}
invalidate();
return 0;
}

以下是示意图,帮助理解

内存页复制示意图

与写时复制相关的函数

写时复制

“当进程A使用系统调用fork创建一个子进程B时,由于子进程B实际上时父进程A的一个拷贝,因此会拥有与父进程相同的物理页面。为了达到节约内存和加快创建速度的目标,fork()函数会让子进程B以只读的方式共享父进程A的物理页面。同时,将父进程A对这些物理页面的访问权限也设置成只读。详见copy_page_tables()函数。这样一来,当父进程A或子进程B任何一方对这些以共享的物理页面执行写操作时,都会产生页面出错异常(page_fault int14)中断,此时CPU会执行系统提供的异常处理函数do_wp_page()来试图解决这个异常。” — 《Linux 0.11 内核完全剖析》

do_wp_page() 对写入异常中断的物理页面进行取消共享操作(调用un_wp_page()函数),为写进程复制一新的物理页面,让父进程A和子进程B各自拥有一块内容相同的物理页面。并且把将要执行写入操作的这块物理页面标记成可以写访问的。最后,从异常处理函数中返回时,CPU就会重新执行刚才导致异常的写入的操作指令,让其能够继续执行下去。

写时复制,通俗点来说,就是在存在写入时,内存才真正进行复制内存页,如果没有写入的操作,内存只是做映射共享而已。

另外,进程调用某个’系统调用‘时,会提前调用内存页面中是否有共享页面存在,如果存在,则进行写时复制。所以,这也解释了,我们日常电脑使用经验,当相同的程序再执行第二遍的时候,往往比第一遍时间要短。

do_wp_page

当父进程A或子进程B任何一方对这些以共享的物理页面执行写操作时,都会产生页面出错异常(page_fault int14)中断,此时CPU会执行系统提供的异常处理函数do_wp_page()来试图解决这个异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
* This routine handles present pages, when users try to write
* to a shared page. It is done by copying the page to a new address
* and decrementing the shared-page counter for the old page.
*
* If it's in code space we exit with a segment error.
*/

// The page_fault() call this function
// 这个函数还是取消写保护. 因为page fault调用的时候,是因为程序想要写入共享页,但是共享页只读.
void do_wp_page(unsigned long error_code,unsigned long address)
{
#if 0
/* we cannot do this yet: the estdio library writes to code space */
/* stupid, stupid. I really want the libc.a from GNU */
if (CODE_SPACE(address))
do_exit(SIGSEGV);
#endif
un_wp_page((unsigned long *)
(((address>>10) & 0xffc) + (0xfffff000 & //((address>>10) & 0xffc) 保留 DIR|PAGE . 表示某个页表项的偏移地址
*((unsigned long *) ((address>>20) &0xffc)))));/* S1: ((address>>20) &0xffc) 保留 DIR .
S2: *DIR 取地址以后 就是某个page table .
S3: 0xfffff000 & page table获得page table所存的地址,这个地址指向某个PAGE FRAME

这两个为什么要加呢? 前者指的页表项的的偏移地址
后者指的是某个页表项的基地址
基地址+偏移地址 ,正好表示某个页表项
整条命令等价于 : up_wp_page(某个页表项具体地址);
*/

}

un_wp_page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//un-write protect page  取消写保护,入参是某个页表项
//
void un_wp_page(unsigned long * table_entry)
{
unsigned long old_page,new_page;

old_page = 0xfffff000 & *table_entry; //去掉页表项属性
//页面不能写的状态有两种,占用or共享
//后者判断页面是否处于共享状态;== 1 则表示 未处于共享状态,但是处于占用状态
if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
*table_entry |= 2;// |2 取消写保护 r/w bit . 1 =w ; 0=r
invalidate();
return;
}
//处于共享状态,不能直接写入,如果写入会破坏原进程
if (!(new_page=get_free_page()))//如果申请的新页面为空
oom();
if (old_page >= LOW_MEM)//申请到new_page,判断old_page是否大于LOW_MEM
mem_map[MAP_NR(old_page)]--;//从2减1,取消共享标记
*table_entry = new_page | 7; // | 7 表示可读可写,让*table_entry指向new_page(物理页的地址)
invalidate();
copy_page(old_page,new_page);// 将old_page复制给new_page
}

write_verify

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*

write_verify() : why need to verify .写页面之前做验证,目的是尝试是否可写
in : linear address

*/
void write_verify(unsigned long address)
{
unsigned long page;

if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1)) // twice get value : equal to ** (address >> 22)
return;
page &= 0xfffff000; //去掉属性
page += ((address>>10) & 0xffc); // page = page + address ( DIR | PAGE )
if ((3 & *(unsigned long *) page) == 1) /* non-writeable, present */
/* 3(dec) = 11(bin)
3 & 1011 = 3;
3 & 1001 = 1;
bit write/read , 0 non-writeable
The Present bit indicates whether a page table
entry can be used in address translation.
P=1 indicates that the entry can be used.
*/
un_wp_page((unsigned long *) page);
return;
}

与pags.s:page_fault 相关的函数

他们的调用关系是: do_no_page – share_page – try_to_share

share_page

尝试寻找一个进程,可以与当前进程共享页面。address是期望共享页面的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* share_page() tries to find a process that could share a page with
* the current one. Address is the address of the wanted page relative
* to the current data space.
*
* We first check if it is at all feasible by checking executable->i_count.
* It should be >1 if there are other tasks sharing this inode.
*/
static int share_page(unsigned long address)
{
struct task_struct ** p;

if (!current->executable)
return 0;
if (current->executable->i_count < 2) // ?
return 0;
for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
if (!*p)
continue;
if (current == *p)
continue;
if ((*p)->executable != current->executable)
continue;
if (try_to_share(address,*p)) // address 期望共享的页面地址;p某一个进程
return 1;
}
return 0;
}

try_to_share

将期望共享的页面共享给当前进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*
* try_to_share() checks the page at address "address" in the task "p",
* to see if it exists, and if it is clean. If so, share it with the current
* task.
*
* NOTE! This assumes we have checked that p != current, and that they
* share the same executable.
*/
static int try_to_share(unsigned long address, struct task_struct * p)
{
unsigned long from;
unsigned long to;
unsigned long from_page;
unsigned long to_page;
unsigned long phys_addr;

from_page = to_page = ((address>>20) & 0xffc); // DIR
from_page += ((p->start_code>>20) & 0xffc); // 加上相对p进程的DIR
to_page += ((current->start_code>>20) & 0xffc);// 加上相对current进程的DIR
/* is there a page-directory at from? */
from = *(unsigned long *) from_page; //from(page table entry)
if (!(from & 1))
return 0;
from &= 0xfffff000; // 去掉属性
from_page = from + ((address>>10) & 0xffc); //得到相对进程p的page_table_entry
phys_addr = *(unsigned long *) from_page;
/* is the page clean and present? */
if ((phys_addr & 0x41) != 0x01) //0x41 对应表项中的Dirty和Present标志
return 0;
phys_addr &= 0xfffff000;
if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
return 0;
to = *(unsigned long *) to_page;
if (!(to & 1)) {
if ((to = get_free_page()))
*(unsigned long *) to_page = to | 7;
else
oom();
}
to &= 0xfffff000;
to_page = to + ((address>>10) & 0xffc);//得到相对进程current的page_table_entry
if (1 & *(unsigned long *) to_page)
panic("try_to_share: to_page already exists");
/* share them: write-protect */
*(unsigned long *) from_page &= ~2; // 设置写保护
// 共享,to_page 和from_page指向相同的物理页地址
*(unsigned long *) to_page = *(unsigned long *) from_page;
invalidate();
phys_addr -= LOW_MEM;
phys_addr >>= 12;
mem_map[phys_addr]++; // 共享标记
return 1;
}

do_no_page

此函数是在当进程运行时,内存分页不够用的时候,产生中断后调用此函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/// @brief 这个是在当进程运行时,内存分页不够用的时候该做什么事情. 
/*
1. 进程动态申请内存页面 映射一页物理页
2. 尝试与已加载的相同文件进行页面共享
3. 从文件中读取所缺的数据页面到指定线性地址处
当进程要使用内存页时,在线性地址空间寻找时,发现内存页不够了. 于是page_default调用do_no_page函数
*/
/// @param error_code
/// @param address 缺页的线性地址
void do_no_page(unsigned long error_code,unsigned long address)
{
int nr[4];
unsigned long tmp;
unsigned long page;
int block,i;

address &= 0xfffff000;
tmp = address - current->start_code; // 进程线性地址空间对应偏移地址 . 从进程的start_code开始计算
if (!current->executable || tmp >= current->end_data) {
get_empty_page(address); // no_page的处理方式1 , 获取一个free page 并且与address 建立映射.
return;
}
if (share_page(tmp)) // no_page 的处理方式2
return;
if (!(page = get_free_page()))
oom();
/* remember that 1 block is used for header */
block = 1 + tmp/BLOCK_SIZE;
for (i=0 ; i<4 ; block++,i++)
nr[i] = bmap(current->executable,block);
bread_page(page,current->executable->i_dev,nr);
i = tmp + 4096 - current->end_data;
tmp = page + 4096;
while (i-- > 0) {
tmp--;
*(char *)tmp = 0;
}
if (put_page(share_page,address))
return;
free_page(page);
oom();
}

get_empty_page & put_page

get_empty_page

此函数让一个线性地址与空闲页建立映射,这个线性地址存在一个空页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// @brief get free page and map to address 
// the difference between get_empty_page and get_free_page ?
// get_free_page : not relative to linear addres
// get_empty_page : map free page and linear address. this page called empty page.
/// @param address : if successed , param address map to empty page .
void get_empty_page(unsigned long address)
{
unsigned long tmp;

if (!(tmp=get_free_page()) || !put_page(tmp,address)) {
free_page(tmp); /* 0 is ok - ignored */
oom();
}
}

put_page

此函数是让内存页面和想要的线性地址进行映射,建立关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*
* This function puts a page in memory at the wanted address.
* It returns the physical address of the page gotten, 0 if
* out of memory (either when trying to access page-table or
* page.)
*
* 入参: page(4K)-- > PAGE FRAME
* 入参: address linear address
* put page to linear address
*
* 总结:实际上这个函数表达的意思是, 用入参address得到的page基地址 映射到 入参page基地址
* 也就是变更了线性地址中的某个页表所指向的PAGEFRAME
*
*
* 有几个问题 1. page_table 是数组吗? 如果不是 怎么可以page_table[] 进行操作 ,如果是,unsigned long page_tables 不是定义数组
* 答 : 数组的本质是指针 , 所以指针可以用数组表示,他们可以互相表示.
* 2. page_table是局部变量 ,这样的映射是否有效,在函数之外?
* 答 : 其实实际上操作的是内存中的值, 所以在函数之外是有效的. 对dir;page_table;pageframe操作,这些都是在内存中.
*
*/
unsigned long put_page(unsigned long page,unsigned long address)
{
unsigned long tmp, *page_table;

/* NOTE !!! This uses the fact that _pg_dir=0 */

if (page < LOW_MEM || page >= HIGH_MEMORY) // page variable is page table or page entry ? page pointer ? page
printk("Trying to put page %p at %p\n",page,address);
if (mem_map[(page-LOW_MEM)>>12] != 1)
printk("mem_map disagrees with %p at %p\n",page,address);
page_table = (unsigned long *) ((address>>20) & 0xffc); // 目录项偏移地址, 前面变量名应该为dir才对
// (address>>20) & 0xffc: extract DIR(10bits) of LINER ADDRESS
// 0xffc = 1111 1111 1100 . 所以这里是为了mask掉最后 2bits.
if ((*page_table)&1) //指向pagetable的DIR 是否有效
page_table = (unsigned long *) (0xfffff000 & *page_table); // 接上面 (unsigned long *) (0xfffff000 & *dir)
// *dir 等价于page_table
// 0xfffff000 & page_table 只保留page table 中的frame page address,也就是指向某个page frame
else { // 如果无效
if (!(tmp=get_free_page()))
return 0;
*page_table = tmp|7;
page_table = (unsigned long *) tmp;
}
page_table[(address>>12) & 0x3ff] = page | 7; /*
address >> 12 保留DIR|PAGE| ; OFFSET 被移出
0x3ff = 0011 1111 1111
(address>>12) & 0x3ff : 保留10bits , 其他mask , get PAGE(10bits)
page_table[(address>>12) & 0x3ff] 等价于 page_table[PAGE] ,
等价于page_table的基地址+偏移地址PAGE 可以计算得到一个page frame的起始地址
总结:实际上这个函数表达的意思是, 用入参address得到的page基地址 映射到 入参page基地址
也就是变更了线性地址中的某个页表所指向的PAGEFRAME
*/
/* no need for invalidate */
return page;
}

附:内存寻址全局图

参考资料

Linux 内核完全注释

Linux 内核设计的艺术

segmentation和保护模式(二)

深入理解Linux内核虚拟内存原理与实现 - 掘金

【Linux 0.11】第十三章 内存管理 - 掘金

I386 Datasheet | Intel - Datasheetspdf.com


Linux 0.11 内存管理-代码阅读记录 Linux 0.11 MMU Code Reading Record
https://pans0ul.github.io/2024/01/02/Linux-0-11-MMU-Code-Reading-Record/
Author
pans0ul
Posted on
January 2, 2024
Licensed under