首页 > 系统相关 >linux内核源码阅读-mm

linux内核源码阅读-mm

时间:2024-07-23 17:42:17浏览次数:13  
标签:mm unsigned long mem 地址 源码 linux page 页面

 

总体功能:

在Intel 80X86 CPU中,程序在寻址过程中使用的是由段和偏移值构成的地址。该地址并不能直接用来寻址物理内存地址,因此被称为虚拟地址。为了能寻址物理内存,就需要一种地址变换机制将虚拟地址映射或变换到物理内存中,这种地址变换机制就是内存管理的主要功能之一(内存管理的另外一个主要功能是内存的寻址保护机制。由于篇幅所限,本章不对其进行讨论)。虚拟地址通过段管理机制首先变换成一种中间地址形式一CPU32位的线性地址,然后使用分页管理机制将此线性地址映射到物理地址。为了弄清 Linux内核对内存的管理操作方式,我们需要了解内存分页管理的工作原理,了解其寻址的机制。分页管理的目的是将物理内存页面映射到某一线性地址处。在分析本章的内存管理程序时,需

明确区分清楚给定的地址是指线性地址还是实际物理内存的地址。

 

来自:

https://in1t.top/2020/04/27/linux%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB-mm/

mm 是 Linux 0.11 内存管理的模块,一共两个文件 memory.c 与 page.s。开篇先来“再续前缘”,继续探讨写时复制技术的后半部分。

写时复制之页错误

上一篇文章提到了,当父/子进程其中之一对只读的内存页面进行写操作时,会产生页错误的异常,该异常处理程序负责将共享的内存页面复制到新内存页中,并重新构建该页表项,使其指向新内存页并可写。实际上,页错误异常不仅由写保护引发,还有可能是缺页引起的。页错误异常就定义在 page.s 中,该文件也就只有 page_fault 的代码:

PLAINTEXT  
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
.globl page_fault
/* 引发页错误的线性地址保存在控制寄存器 CR2 中 */
page_fault:
xchgl %eax,(%esp) /* 将出错码取到 eax 中 */
pushl %ecx
pushl %edx
push %ds
push %es
push %fs /* 保存现场 */
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
mov %dx,%fs /* 修改段寄存器,指向内核数据段 */
movl %cr2,%edx /* 将引起页错误的线性地址放到 edx 中 */
pushl %edx
pushl %eax /* 压参(页错误线性地址与错误码) */
testl $1,%eax /* 页存在 P 位如果不为 0,表明不是由缺页引起的异常 */
jne 1f /* 而是由写保护引发的异常,跳去调用 do_wp_page */
call do_no_page /* 如果是缺页引发的异常,则调用 do_no_page */
jmp 2f
1: call do_wp_page
2: addl $8,%esp /* 栈平衡 */
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax /* 还原现场 */
iret

先来看由写保护引起的异常处理函数 do_wp_page(之后涉及的函数都在 memory.c 中)

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
27
28
29
30
31
32
33
34
35
36
37
38
// Line 248
void do_wp_page(unsigned long error_code,unsigned long address)
{
#if 0
if (CODE_SPACE(address)) // 这段代码本意是如果线性地址位于进程代码空间中,
do_exit(SIGSEGV); // 则终止程序,但 #if 0 表示该段代码不起作用
#endif
un_wp_page((unsigned long *) // 实际通过 un_wp_page 实现,参数是线性地址 address
(((address>>10) & 0xffc) + (0xfffff000 & // 对应的页面在页表中的页表项指针
*((unsigned long *) ((address>>20) &0xffc)))));

}

// Line 222
void un_wp_page(unsigned long * table_entry)
{
unsigned long old_page,new_page;

old_page = 0xfffff000 & *table_entry; // 获取页面物理地址
// 判断该页面是否在主内存区(LOW_MEM 值为 1MB,1MB 以上为主内存区),并且没有被共享
if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
*table_entry |= 2; // 加上写属性
invalidate(); // 刷新 TLB
return;
}
// 否则在主内存区申请空闲页给执行写操作的进程单独使用
if (!(new_page=get_free_page())) // 如果没有空闲页则调用 oom 报错退出,oom 定义在 33 行
oom(); // out of memory
if (old_page >= LOW_MEM) // 如果页面物理在主内存区,且被共享了
mem_map[MAP_NR(old_page)]--; // 将映射的数量减 1
*table_entry = new_page | 7; // 设置新页面可读写、存在
invalidate(); // 刷新 TLB
copy_page(old_page,new_page); // 拷贝旧页面内容到新页面中
}

// Line 54
#define copy_page(from,to) \
__asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024))

于是,写时复制的全貌就展现完毕了。由缺页引发的页错误处理涉及到块设备的知识,之后再做记录。

 

mem_map数组

之前涉及内存管理的代码都或多或少地有 mem_map 数组的影子,这个字符数组就是 Linux 用于判断 1MB 以上物理内存使用情况的,每个字节描述一个物理页面的占用状态,该字节的数值表示该页面被占用的次数,0 代表该页面空闲,100 代表该页面已被完全占用,不能再被分配/共享。Linux 0.11 的物理内存区域划分如下:

mem_map

 

mm 模块中的几类函数

释放内存

接着来看 memory.c 中还剩下的一些函数,可根据功能分为几类,首先是释放内存:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Line 106
// 该函数释放从 from 开始,长度为 size 字节的线性地址空间
int free_page_tables(unsigned long from,unsigned long size)
{
unsigned long *pg_table;
unsigned long * dir, nr;

if (from & 0x3fffff) // 检查 from 是否是 4MB 对齐
panic("free_page_tables called with wrong alignment");
if (!from) // 如果 from 为 0,则不允许释放内核空间
panic("Trying to free up swapper memory space");
size = (size + 0x3fffff) >> 22; // 对 size 进行 4MB 舍入,右移 22 位求出涉及到的页目录项数
dir = (unsigned long *) ((from>>20) & 0xffc); // 计算起始的页目录项地址
for ( ; size-->0 ; dir++) { // 遍历涉及到的页目录项
if (!(1 & *dir)) // 如果该项不存在(P 位为 0),则跳过
continue;
pg_table = (unsigned long *) (0xfffff000 & *dir); // 页表基址
for (nr=0 ; nr<1024 ; nr++) { // 遍历所有页表项
if (1 & *pg_table) // 如果该页存在
free_page(0xfffff000 & *pg_table); // 释放该页所占用的空间
*pg_table = 0; // 页表项置空
pg_table++;
}
free_page(0xfffff000 & *dir); // 释放该页表所占用的空间
*dir = 0;
}
invalidate(); // 刷新 TLB
return 0;
}

// Line 90
// 释放一页空间实际上就是将 mem_map 数组对应项的映射数减 1
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; // 求出在 mem_map 数组中的索引
if (mem_map[addr]--) return; // 如果映射数不为 0,则减 1 后返回
mem_map[addr]=0; // 否则置 0 并死机
panic("trying to free free page");
}

 

获取空闲页面

第二类有关获取空闲页面

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
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
// Line 275
// 实际通过 get_free_page 与 put_page 实现
void get_empty_page(unsigned long address)
{
unsigned long tmp;

if (!(tmp=get_free_page()) || !put_page(tmp,address)) {
free_page(tmp);
oom();
}
}

// Line 63
// PAGING_PAGES 为页面总数,输入为 ax = 0,cx = PAGING_PAGES,
// edi = mem_map+PAGING_PAGES-1(即 mem_map 最后一项的地址)
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");
// std: 置方向位,从高到低
// repne: repeat if not equal,即下一条指令如果不能使 ZF 标志位为 0,则重复该指令,最多 ecx 次
// scasb: scas 用于比较字符串,加个 b 表示一次比较一字符,比较一次,di 自动递减(std 设置了方向)
// 每次操作比较 es:[di] 和 al 是否相等,这里从 mem_map 最后一项开始寻找映射数为 0 的项
//
__asm__("std ; repne ; scasb\n\t"
"jne 1f\n\t" // 如果遍历完 mem_map 还是没有找到空闲页面,则返回
"movb $1,1(%%edi)\n\t" // 到这里说明找到了空闲页,将 mem_map 对应项置 1
"sall $12,%%ecx\n\t" // 索引 * 4K 得到页面的相对起始地址
"addl %2,%%ecx\n\t" // 加上 LOW_MEM 得到页面实际物理起始地址
"movl %%ecx,%%edx\n\t" // 起始地址赋给 edi
"movl $1024,%%ecx\n\t" // 循环 1024 次
"leal 4092(%%edx),%%edi\n\t" // 当前页面末端 4 字节地址赋给 edi
"rep ; stosl\n\t" // stosl 将 eax 中的内容赋值给 es:[edi]
"movl %%edx,%%eax\n\t" // 返回值为页面物理起始地址
"1:"
"cld\n\t" // 复位方向位
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
);
return __res;
}

// Line 198
// 该函数将内存页面物理地址 page 映射到指定线性地址 address 处
unsigned long put_page(unsigned long page,unsigned long address)
{
unsigned long tmp, *page_table;
// page 应该在 LOW_MEM 与 HIGH_MEMORY 之间
if (page < LOW_MEM || page >= HIGH_MEMORY)
printk("Trying to put page %p at %p\n",page,address);
if (mem_map[(page-LOW_MEM)>>12] != 1) // 检查 page 是否是已经申请的页面
printk("mem_map disagrees with %p at %p\n",page,address);
page_table = (unsigned long *) ((address>>20) & 0xffc); // 线性地址对应页目录项指针
if ((*page_table)&1) // 如果页目录存在,直接获取页表地址
page_table = (unsigned long *) (0xfffff000 & *page_table);
else { // 否则申请一空内存页,存放页表
if (!(tmp=get_free_page()))
return 0;
*page_table = tmp|7; // 设置权限等
page_table = (unsigned long *) tmp; // 获取页表地址
}
page_table[(address>>12) & 0x3ff] = page | 7; // 修改线性地址对应的页表项,指向给定的物理地址
return page; // 无需刷新 TLB,直接返回
}

 

共享内存

第三类有关共享内存,share_page 函数仅被缺页处理函数 do_no_page 调用。这里引入一个新概念——页面逻辑地址,意为该页面地址是以进程的代码/数据起始地址算起的页面地址。以下是 do_no_page 部分代码:

C  
1
2
3
4
5
6
7
8
// Line 373
// address 是产生页错误的线性地址
// current->start_code 是当前进程的线性地址空间基址(64MB 对齐)
// address - current->start_code 求出地址对应的页面逻辑地址
address &= 0xfffff000; // 取得线性地址所在的线性页面的地址
tmp = address - current->start_code;
if (share_page(tmp))
return;

share_page 的具体实现:

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
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
// Line 345
// 寻找运行相同执行文件的进程,并尝试与之共享页面
// 参数 address 是页面逻辑地址
static int share_page(unsigned long address)
{
struct task_struct ** p;

if (!current->executable) // 如果当前进程没有执行文件,则返回
return 0;
// 如果当前进程正在执行的文件引用数小于 2,说明只有当前进程在运行该文件,直接返回
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)) // 调用 try_to_share 尝试共享页面
return 1;
}
return 0;
}

// Line 293
// 尝试与目标任务 p 共享内存
// address 为产生页错误的线性地址所在的线性页面地址对应的页面逻辑地址(有点绕)
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);
from_page += ((p->start_code>>20) & 0xffc); // 计算进程 p 的 address 对应的页目录项地址
to_page += ((current->start_code>>20) & 0xffc); // 计算当前进程的 address 对应的页目录项地址
from = *(unsigned long *) from_page; // 获得进程 p 页目录项
if (!(from & 1)) // 如果进程 p 该页表不存在,返回 0
return 0;
from &= 0xfffff000; // 否则取得页表基址
from_page = from + ((address>>10) & 0xffc); // 计算页表项地址
phys_addr = *(unsigned long *) from_page; // 获得页表项内容
if ((phys_addr & 0x41) != 0x01) // 0x41 对应 dirty 与 P 位,判断页面是否干净且存在
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); // 计算页表项地址
if (1 & *(unsigned long *) to_page) // 如果当前进程的该页已有(我们本意是想共享内存),则死机
panic("try_to_share: to_page already exists");
*(unsigned long *) from_page &= ~2; // 取消页的写权限
*(unsigned long *) to_page = *(unsigned long *) from_page; // 建立映射
invalidate(); // 刷新 TLB
phys_addr -= LOW_MEM;
phys_addr >>= 12;
mem_map[phys_addr]++; // 物理地址在 mem_map 数组中对应项的映射数 + 1
return 1;
}

 

初始化函数

第四类是 main.c 中调用的 mem_init 初始化函数:

C  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Line 400
void mem_init(long start_mem, long end_mem)
{
int i;

HIGH_MEMORY = end_mem; // 设置内存最高地址(16MB)
for (i=0 ; i<PAGING_PAGES ; i++) // 将 mem_map 所有项都赋值为 100,表示已占用
mem_map[i] = USED;
i = MAP_NR(start_mem); // 获取主内存开始地址对应的索引
end_mem -= start_mem; // 计算主内存区域大小
end_mem >>= 12; // 计算在 mem_map 数组中一共有多少项
while (end_mem-->0) // 将 mem_map 从后往前清零
mem_map[i++]=0;
}

 

其他

最后是一些杂项:

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
27
28
29
30
31
32
33
34
35
// Line 262 
// 写页面的验证,address 是页面的线性地址
void write_verify(unsigned long address)
{
unsigned long page;

if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1)) // 页表不存在则返回
return;
page &= 0xfffff000; // 取得页表基址
page += ((address>>10) & 0xffc); // 计算页表项地址
if ((3 & *(unsigned long *) page) == 1) // 如果对应的页只读、存在
un_wp_page((unsigned long *) page); // 则执行复制页面、构建新映射的操作
return;
}

// Line 414
// 显示当前内存信息
void calc_mem(void)
{
int i,j,k,free=0;
long * pg_tbl;

for(i=0 ; i<PAGING_PAGES ; i++)
if (!mem_map[i]) free++; // 计算主内存区中有多少空闲页面并打印
printk("%d pages free (of %d)\n\r",free,PAGING_PAGES);
for(i=2 ; i<1024 ; i++) { // 遍历所有页目录项
if (1&pg_dir[i]) { // 如果对应页表存在
pg_tbl=(long *) (0xfffff000 & pg_dir[i]); // 获取页表基址
for(j=k=0 ; j<1024 ; j++) //遍历页表项
if (pg_tbl[j]&1) // 如果对应物理页存在,计数变量 k 加 1
k++;
printk("Pg-dir[%d] uses %d pages\n",i,k); // 打印页目录中有多少正在使用的页
}
}
}

 

 

 

 

 

参考:https://in1t.top/2020/04/27/linux%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB-mm/

 

൘ Intel 80X86 CPU ѝˈ〻ᒿ൘ራ൰䗷〻ѝ֯⭘Ⲵᱟ⭡⇥઼ٿ〫٬ᶴᡀⲴൠ൰DŽ䈕ൠ൰ᒦн㜭ⴤ᧕⭘ ᶕራ൰⢙⨶޵ᆈൠ൰ˈഐ↔㻛〠Ѫ㲊ᤏൠ൰DŽѪҶ㜭ራ൰⢙⨶޵ᆈˈቡ䴰㾱а⿽ൠ൰ਈᦒᵪࡦሶ㲊ᤏൠ ൰᱐ሴᡆਈᦒࡠ⢙⨶޵ᆈѝˈ䘉⿽ൠ൰ਈᦒᵪࡦቡᱟ޵ᆈ㇑⨶Ⲵѫ㾱࣏㜭ѻа˄޵ᆈ㇑⨶Ⲵਖཆањѫ 㾱࣏㜭ᱟ޵ᆈⲴራ൰؍ᣔᵪࡦDŽ⭡Ҿㇷᑵᡰ䲀ˈᵜㄐнሩަ䘋㹼䇘䇪˅ DŽ㲊ᤏൠ൰䙊䗷⇥㇑⨶ᵪࡦ俆ݸਈ ᦒᡀа⿽ѝ䰤ൠ൰ᖒᔿ —CPU 32 սⲴ㓯ᙗൠ൰ˈ❦ਾ֯⭘࠶亥㇑⨶ᵪࡦሶ↔㓯ᙗൠ൰᱐ሴࡠ⢙⨶ൠ൰DŽ ѪҶᔴ␵ Linux ޵Ṩሩ޵ᆈⲴ㇑⨶᫽֌ᯩᔿˈᡁԜ䴰㾱Ҷ䀓޵ᆈ࠶亥㇑⨶Ⲵᐕ֌৏⨶ˈҶ䀓ަራ൰ ⲴᵪࡦDŽ࠶亥㇑⨶ⲴⴞⲴᱟሶ⢙⨶޵ᆈ亥䶒᱐ሴࡠḀа㓯ᙗൠ൰༴DŽ൘࠶᷀ᵜㄐⲴ޵ᆈ㇑⨶〻ᒿᰦˈ䴰 ᰾⺞४࠶␵ᾊ㔉ᇊⲴൠ൰ᱟᤷ㓯ᙗൠ൰䘈ᱟᇎ䱵⢙⨶޵ᆈⲴൠ൰DŽ

标签:mm,unsigned,long,mem,地址,源码,linux,page,页面
From: https://www.cnblogs.com/rebrobot/p/18319139

相关文章

  • linux进程
      Linux下有3个特殊的进程,idle进程(PID=0),init进程(PID=1)和kthreadd(PID=2)*idle进程由系统自动创建,运行在内核态idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换*ini......
  • 500个计算机毕业设计项目分享(源码+论文+PPT)
    计算机毕业设计项目分享(源码+论文+PPT)需要链接请私信我哦!部分选题参考基于生鲜仓储管理系统的设计与实现基于Java的电竞酒店管理系统设计与实现基于Javaweb的电动车租借的信息管理系统的设计与实现基于微信小程序的小说阅读系统基于Android平台的广州二手手机商城设计......
  • Linux——DNS服务搭建
    (一)搭建nginx1.首先布置基本环境要求能够ping通外网,有yum源2.安装nginxyum-yinstallnginx然后查看验证3.修改网页配置文件修改文件,任意编写内容,然后去物理机测试(二)创建一台客户端1.模拟一下客户,用母机克隆一台作为我们的客户端然后只需修改地址,保证能够ping通......
  • linux内核源码阅读-初始化主程序
     来自:https://in1t.top/2020/03/26/linux%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB-%E5%88%9D%E5%A7%8B%E5%8C%96%E4%B8%BB%E7%A8%8B%E5%BA%8F/main.c功能描述之前setup在0x90000~0x901FF保存了一些重要的机器参数,其中包括主内存区的开始地址,内存大小和......
  • ytb_dlp源码解析
    源代码#-*-coding:utf-8-*-fromyt_dlpimportYoutubeDLimportpickle,osos.environ["http_proxy"]="http://127.0.0.1:10809"os.environ["https_proxy"]="http://127.0.0.1:10809"withopen('../urls.pkl',�......
  • 第一作者解读|我们这篇Nature Communication背后的故事
    2024年7月16日,大暑将至,立秋不远。我们基于Python的转录组学全分析框架的文章——"OmicVerse:aframeworkforbridginganddeepeninginsightsacrossbulkandsingle-cellsequencing"——正式在NatureCommunication上发表了,这是我们课题组第一个里程碑意义的成果,也是我第一......
  • ArchLinux使用笔记
    {%post_linkDistro/'免启动盘安装ArchLinux'%}{%post_linkDistro/'ArchLinux-TLP'%}安装NVIDIA驱动官方完整教程:https://wiki.archlinux.org/title/NVIDIA只要卡不是太老,一般情况下,如果用的是stable内核(linux),就安装nvidia,如果用的是LTS内核(linux-lts),就安装nvidia-lt......
  • 使用PHP实现悲观锁的最佳实践。里面包含源码
    在数据库编程中,确保数据的一致性和完整性是非常重要的。当多个用户或线程同时访问和修改同一条数据记录时,可能会出现并发问题,比如读写冲突、数据丢失等。为了解决这些问题,我们可以使用并发控制机制,其中一种常见的方法就是悲观锁。什么是悲观锁?悲观锁是一种并发控制策......
  • linux 相关基础操作
    df-Th这个命令用于显示文件系统的磁盘空间占用情况。选项 -T 表示显示文件系统类型,-h 表示以人类可读的格式(如KB、MB、GB)显示大小。执行这个命令后,你会看到各个已挂载文件系统的总大小、已用空间、可用空间、已用百分比以及挂载点等信息。这对于检查磁盘空间使用情况非常有......
  • linux 内核版本
     来自:https://blog.csdn.net/qq_23084801/article/details/78795870有了这个Linux内核版本发布时间表(0.00到3.19,当然没有包含全部的版本),大家就可以看看自己用的版本是何时发布的了!做内核维护查看相关patchlog时大致做个参考。 版本号时间发展史0.001991.2-4......