address
表示的是线性地址,linux0.11采用的时二级页表,二级页表的知识在这里
当引发缺页中断时,share_page
可能会被do_no_page
被调用。
mm/memory.c
static int share_page(unsigned long address)
{
struct task_struct ** p;
if (!current->executable)
return 0;
if (current->executable->i_count < 2)
return 0;
// 否则搜索任务数组中所有任务。寻找与当前进程可共享页面的进程,即运行相同的
// 执行文件的另一个进程,并尝试对指定地址的页面进行共享。如果找到某个进程p,
// 其executable字段值与当前进程的相同,则调用try_to_share()尝试页面共享。若
// 共享操作成功,则函数返回1。否则返回0,表示共享页面操作失败.
for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
if (!*p)
continue;
if (current == *p)
continue;
// 如果executable不等,表示运行的不是与当前进程相同的执行文件,因此也继续
// 寻找。
if ((*p)->executable != current->executable)
continue;
if (try_to_share(address,*p))
return 1;
}
return 0;
}
try_to_share
先检查P进程中对应的页帧是否存在,不存在就返回0。然后查看这个页帧是否干净,不干净也返回0,然后查看这个地址有没有在主内存中,到这里已经满足一半的条件。接着检查当前进程对应的页帧是否存在,因为这里是缺页异常,页帧一定不存在,否咋就会出问题,然后申请一个页帧,修改进程P对应页表项的标志位RW为0,表示只读,然后当前进程复制进程p对应页表项数据,刷新页变换高速缓冲,因为该页被共享,引用次数被增加了,所以还要在mem_map对象的页使用次数加1。
参数address
表示的是页地址,不含有偏移地址。当前进程共享进程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); //获取address在页目录中页目录项PDE的地址
from_page += ((p->start_code>>20) & 0xffc); // 获取p进程首地址的PDE地址
to_page += ((current->start_code>>20) & 0xffc); //获取当前进程首地址的PDE地址
from = *(unsigned long *) from_page; //页表项内容
if (!(from & 1)) //查看P进程from_page的页是不是存在,不存在,何来共享?直接退出
return 0;
from &= 0xfffff000; // 页表项地址
from_page = from + ((address>>10) & 0xffc); //页表项地址 + 偏移地址 = 物理地址
phys_addr = *(unsigned long *) from_page;
if ((phys_addr & 0x41) != 0x01) //phys_addr & 0100 0001 ,0x41对应页表项中的D(dirty)和P(present)标志
return 0;
phys_addr &= 0xfffff000;
//它不应该超过机器最大物理地址值,也不应该小于内存低端(1 MB).
if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
return 0;
to = *(unsigned long *) to_page; //获取页目录项内容
if (!(to & 1)) { // p == 0 ,页不存在
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; //把R/W标志位置位0,表示该页只读
*(unsigned long *) to_page = *(unsigned long *) from_page; //复制页表项
invalidate(); //页变换高速缓冲
phys_addr -= LOW_MEM; // 减去低端内存地址
phys_addr >>= 12; //转换为页号
mem_map[phys_addr]++; //页帧使用次数加1
return 1;
}
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");
__asm__("std ; repne ; scasb\n\t" // 置方向位,al(0)与对应每个页面的(di)内容比较
"jne 1f\n\t" // 如果没有等于0的字节,则跳转结束(返回0).
"movb $1,1(%%edi)\n\t" // 1 => [1+edi],将对应页面内存映像bit位置1.
"sall $12,%%ecx\n\t" // 页面数*4k = 相对页面起始地址
"addl %2,%%ecx\n\t" // 再加上低端内存地址,得页面实际物理起始地址
"movl %%ecx,%%edx\n\t" // 将页面实际其实地址->edx寄存器。
"movl $1024,%%ecx\n\t" // 寄存器ecx置计数值1024
"leal 4092(%%edx),%%edi\n\t" // 将4092+edx的位置->edi(该页面的末端地址)
"rep ; stosl\n\t" // 将edi所指内存清零(反方向,即将该页面清零)
"movl %%edx,%%eax\n" // 将页面起始地址->eax(返回值)
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
);
return __res; // 返回空闲物理页面地址(若无空闲页面则返回0).
}
get_free_page
就是在mem_map
数组中找到一个值为0的位置,因为数组的每一项代表主存中的每一页,所以可以根据数组中的位置乘以4KB就可以得到一个相对内存位置,再加上低端内存地址(LOW_MEM)就可以得出物理地址,然后把刚才申请的一页的内存全部初始化为0。把申请的地址返回给调用者。如果申请不到就返回0。
要想看详细的代码分析,就看下面的一行一行分析,你可以选择看或者不看😵
一行一行代码分析:
汇编代码执行前寄存器的状态:
register unsigned long __res;
定义一个寄存器变量,如果想指定哪个寄存器就可以register unsigned long __res asm("ax");
。"=a" (__res)
表示结果输出到__res
,即eax
寄存器中。"0" (0)
第一个0表示用上一次的约束,即用eax
寄存器,第二个0是把值0给eax
,"c" (PAGING_PAGES)
表示把PAGING_PAGES给ecx
"D" (mem_map+PAGING_PAGES-1)
表示em_map+PAGING_PAGES-1给edi
开始执行寄存器中的代码:
-
std
指令将DF标志置1,DF=0表示正向操作,DF=1表示反向操作。cld
指令将DF标志清零,正向操作是指传送操作的方向是从内存区域的低地址端到高地址端,反向操作则正好相反。 -
SCASB
、SCASW
和SCASD
指令分别将AL
/AX
/EAX
中的值与EDI
寻址的一个字节 / 字 / 双字进行比较。这些指令可用于在字符串或数组中寻找一个数值。结合REPE
(或REPZ
)前缀,当 ECX > 0 且 AL/AX/EAX 的值等于内存中每个连续的值时,不断扫描字符串或数组。REPNE 前缀也能实现扫描,直到 AL/AX/EAX 与某个内存数值相等或者 ECX = 0。
repne ; scasb
:重复执行scasb
指令,scasb
指令的作用就是把al中的值(现在是0)与es:edi中的值比较,edi中初始化的值是mem_map
的最后一项的地址,因为使用了std
,每次scasb
后,edi
都会自动减去1(因为这里用的是scasb,b表示byte),并且修改标志寄存器中的一些标志位。
repne
:要想停下来要么是ecx中的循环次数等于0,要么是ZF位等于1。
jne 1f
向前跳转到标号1处,标号1没有执行代码,直接返回。movb $1,1(%%edi)
表示edi + 1的位置给1,sall $12,%%ecx
:ecx之前存的是主存总页数(linux0.11最大支持16MB,内存规划是内核、缓冲区、虚拟盘、主存、高端内存),现在是页面数 * 4KB表示主内存的字节数。movl %%ecx,%%edx
:主内存的字节数 -> edxmovl $1024,%%ecx
:1024 -> ecxleal 4092(%%edx),%%edi
:由于依照4字节对齐,所以每项占用4字节,取当前物理页最后一项4096 = 4096-4 = 4092,rep ; stosl
:stosl指令相当于将EAX中的值保存到ES:EDI指向的地址中,若设置了EFLAGS中的方向位置位(即在stosl指令前使用STD指令)则EDI自减4,否则(使用CLD指令)EDI自增4,从ecx+4092处开始,反方向,步进4,反复1024次,将该物理页1024项所有填入eax寄存器的值(0)。movl %%edx,%%eax
:将该物理页面起始地址放入eax寄存器中,用于返回
参考:
英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A