linux0.11内核源码阅读

基本上每天一个文件吧

RTFSC

有makefile https://github.com/cyysu/linux0.11/tree/master
无makefile https://github.com/beride/linux0.11-1

可编译调试的 https://github.com/yuan-xy/Linux-0.11

从服务器启动qemu无图形界面: -curses(在教室开虚拟机太耗电了,只能开云服务器,学校的网还卡艹)

  • alt 2切换到监视 alt 1切换会模拟界面
  • esc 2和esc 1 同样效果

vscode配置

launch.json

{
"version": "0.2.0",
"configurations": [
{
"name": "debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/tools/system",
"miDebuggerServerAddress": "127.0.0.1:1234",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"preLaunchTask": "run"
}
]
}

tasks.json

{
"tasks": [
{
"label": "run",
"type": "shell",
"command": "make ; and make debug ",
"isBackground": true
}
],
"version": "2.0.0"
}

boot

bootsect.s

8086汇编 ljmp 0x7c0:5将cs=0x7c0 ip=5 跳转到新的代码段和偏移地址

ok1_read:
;// 计算和验证当前磁道需要读取的扇区数,放在ax寄存器中。
;// 根据当前磁道还未读取的扇区数以及段内数据字节开始偏移位置,计算如果全部读取这些
;// 未读扇区,所读总字节数是否会超过64KB段长度的限制。若会超过,则根据此次最多能读
;// 入的字节数(64KB - 段内偏移位置),反算出此次需要读取的扇区数。
;// seg cs
mov ax,cs:sectors ;// 取每磁道扇区数。
sub ax,sread ;// 减去当前磁道已读扇区数。
mov dx,ax ;// ax = 当前磁道未读扇区数。
mov cl,9
shl dx,cl ;// dx = ax * 512 字节。
add dx,bx ;// dx = dx + 段内当前偏移值(bx)
;// = 此次读操作后,段内共读入的字节数。
jnc ok2_read ;// 若没有超过64KB字节,则跳转至ok2_read处执行。
je ok2_read
xor ax,ax ;// 若加上此次将读磁道上所有未读扇区时会超过64KB,则计算
sub ax,bx ;// 此时最多能读入的字节数(64KB - 段内读偏移位置),再转换
shr ax,cl ;// 成需要读取的扇区数。

因为实模式下寄存器最多寻址64kb,所以需要判断是否需要修改段寄存器,这里我觉得比较有意思的是直接用0减去段内偏移,再把结果当作无符号数做运算得到需要读取扇区数

sub ax,bxax为0时相当于得到bx的补码也就相当于做了无符号运算0x1 0000-bx,由于寄存器位数限制无法直接操作0x1 0000,感觉这种方法确实好

setup.s

准备进入保护模式前先关了中断,这是因为此时idtr寄存器指向0,即bios的中断向量表,后面setup把system移动到0x0处,会覆盖掉这个表,中断也就g了

然后加载中断描述符表和全局描述符表

lidt fword ptr idt_48            ;// 加载中断描述符表(idt)寄存器,idt_48 是6 字节操作数的位置
;// 前2 字节表示idt 表的限长,后4 字节表示idt 表所处的基地址。
lgdt fword ptr gdt_48 ;// 加载全局描述符表(gdt)寄存器,gdt_48 是6 字节操作数的位置

lidt lgdt 前两个字节是描述符表的长度限制,后四个字节是描述符表的线性基地址

idt_48:
dw 0 ;// idt limit=0
dw 0,0 ;// idt base=0L
gdt_48:
dw 800h ;// 全局表长度为2k 字节,因为每8 字节组成一个段描述符项 所以表中共可有256 项。
dw 512+gdt,9h ;// 小端序,4个字节构成的内存线性地址:0009<<16 + 0200+gdt 也即90200 + gdt标号。
gdt:
dw 0,0,0,0 ;// 第0 个描述符,不用。
;// 这里在gdt 表中的偏移量为08,当加载代码段寄存器(段选择符)时,使用的是这个偏移值。
dw 07FFh ;// 8Mb - limit=2047 (2048*4096=8Mb)
dw 0000h ;// base address=0
dw 9A00h ;// code read/exec
dw 00C0h ;// granularity=4096, 386
;// 这里在gdt 表中的偏移量是10,当加载数据段寄存器(如ds 等)时,使用的是这个偏移值。
dw 07FFh ;// 8Mb - limit=2047 (2048*4096=8Mb)
dw 0000h ;// base address=0
dw 9200h ;// data read/write
dw 00C0h ;// granularity=4096, 386

第1个描述符00C0 9A00 0000 07FF 代码段描述符 基地址:0x0000 0000 DPL=0 非一致性代码段

第2个描述符00C0 9200 0000 07FF 数据段描述符 基地址:0x0000 0000 DPL=0 向上扩展的数据段

lmsw来给cr0的PE赋值启用保护模式标志,lmsw是为了兼容80286,386及以上推荐用mov cr0

mov    ax,0001h	;// 保护模式比特位(PE)。
lmsw ax ;// 就这样加载机器状态字
jmp 8:0 ;// 跳转至cs 段8,偏移0 处。执行system 中的代码

cs段选择子为8即000000001 0 00

  • TI位为0 表示是全局描述符里的1个描述符
  • RPL=0

跳转到0地址处system模块执行

head.s

程序所在物理地址为0,而且此时还没开启分页

lss esp,_stack_start    ;// 表示_stack_start -> ss:esp,设置系统堆栈。

_stack_start定义在kernel/sched.c

long user_stack[PAGE_SIZE >> 2];    // 定义系统堆栈指针,4K。指针指在最后一项。
stack_start = {&user_stack[PAGE_SIZE >> 2], 0x10};

lss指令把stack_start 前四字节装入ESP寄存器,后两字节装入SS

重新设置了idt表和gdt表后(只是把表长变为了16M),因为前面0地址处的代码已经执行完了就没用了,所以用来做了页目录表

_pg_dir:        ;// 页目录将会存放在这里。

并且在后面设置了4个页表来描述16M的物理内存

org 1000h        ;// 从偏移0x1000 处开始是第1 个页表(偏移0 开始处将存放页表目录)。
pg0:

org 2000h
pg1:

org 3000h
pg2:

org 4000h
pg3:

stosb, stosw, stosd。这三个指令把al ax eax的内容存储到edi指向的内存单元中,同时edi的值根据方向标志的值增加或者减少

设置页目录项和页表具体操作是先把这5个表清零,然后把页目录表项前四项填上值

mov eax,_pg_dir
mov [eax],pg0+7 ;页目录项,前20位表示页表基址,后面的12位为7表示了属性:在物理内存 普通用户可读可写
mov [eax+4],pg1+7
mov [eax+8],pg2+7
mov [eax+12],pg3+7

设置页表项时倒叙从pag3最后一项开始

mov edi,pg3+4092        ;// edi -> 最后一页的最后一项。
mov eax,00fff007h ;/* 16Mb - 4096 + 7 (r/w user,p) */
;4092-4096是最后一个页表项,此处填写00fff007h来描述16M物理地址最后一个4k属性为在物理内存 普通用户可读可写

把页目录的前四项这样去映射说明了这个任务的前16M线性地址等于物理地址

最后设置cr3指向物理地址0即页目录,然后设置cr0 pg位开启分页保护 ret到main函数

image-20231123112231893

init

main.c

linux0.11管理的最大物理内存是16M,对于不同大小的内存的区域划分是不一样的,以16M物理内存来说

image-20231123215714223

mem_map用来管理除了内核外的15M内存

//内存映射字节图(1 字节代表1 页内存),每个页面对应的字节用于标志页面当前被引用(占用)次数。
static unsigned char mem_map [ PAGING_PAGES ] = {0,};

mem_init函数过后把3M缓冲区和虚拟盘所在的mem_map字节置为USED宏表示页面被占用标志,主内存区mem_map置为0

trap_init函数初始化了中断描述符表,中断描述符TYPE位均为1111即为陷阱门,目标代码段描述符选择子均为8(RPL=0,TI=0,索引=1),,其中3-5号中断描述符的dpl设置为3,其余为0

image-20231123215220376

调用INT3 int n INTO等指令产生的中断或异常检测要求

  • 先检测当前特权级CPL<=中断描述符里的DPL
  • 再检测中断描述符里代码段描述符选择子的RPL < 当前特权级CPL

由硬件产生的中断或处理器检测到的异常检测要求

  • 中断描述符里代码段描述符选择子的RPL < 当前特权级CPL

陷阱门和中断门唯一区别就是中断门处理时会把IF位清零(关中断),陷阱门不会

sched_init初始化了任务0在全局描述符表gdt的tss和ldt分别为0x00008901f4480068 0x00008201f4300068

0x00008901f4480068: S=0 TYPE:1001 为tss段描述符

image-20231124110911039

0x00008201f4300068 : S=0 TYPE:0010 为 ldt描述符

tss和ldt描述符的基地址都是该任务task_struct结构体里的成员的地址

struct task_struct
{
..............
struct desc_struct ldt[3];
/* tss for this task */
struct tss_struct tss;
};

然后把task数组除了第一个任务都清空,再把其余gdt表项都清空,把eflags寄存器的NT位置零,防止后面在iret到用户态时时进行任务切换,把tr寄存器设置为任务0的tss

对于寻找tss的宏一般人都会这样写#define _TSS2(n) (FIRST_TSS_ENTRY+2*n)<<3

linus的写法#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))感觉对于当时性能可能会高些,但是现在编译器优化的bt程度可能性能上没有差别

ltr val指令会把gdt里索引val tss描述符基地址和段限长加载,并把TYPE的B置1,表示任务正忙

设置ldtr寄存器指向任务0的ldt

设置并允许时钟中断处理,设置系统调用中断0x80(陷阱门,DPL=3)

TODO

利用iret中断返回切换到用户态

image-20231124170448393

init_task的ldt为INIT_TASK宏里ldt[1]=0x00c0fa000000009f ldt[2]=0x00c0f2000000009f

/* ldt[3]*/    {{0, 0}, \
{0x9f, 0xc0fa00}, /* 代码长640K,基址0x0,G=1,D=1,DPL=3,P=1 TYPE=0x0a*/ \
{ 0x9f, 0xc0f200},}, /* 数据长640K,基址0x0,G=1,D=1,DPL=3,P=1 TYPE=0x02*/ \

堆栈段选择符SS:0x17:00010 1 11 代码段选择符为0xf:1 1 11分别对应ldt里的第二项和第一项

kernel

asm.s

at&t语法差别

xchgl %eax,(%esp) eax和esp栈顶里的值交互
lea 44(%esp),%edx edx=esp+44
call *%eax 调用eax寄存器里的地址 eip=eax,相当于intel的call eax
* https://stackoverflow.com/questions/62813954/warning-indirect-call-without

对于无错误码的中断压入0作为默认错误码,这样统一起来处理比较简单(保存现场,传递两个参数old_esp就是cpu帮我们压入几个寄存器的eip的位置,和error_code)

trap.s

疑惑的点大多是gcc的“语法”和一些不常见的asm inst

#define get_seg_long(seg,addr) ({ \
register unsigned long __res; \
__asm__("push %%fs;mov %%ax,%%fs;movl %%fs:%2,%%eax;pop %%fs" \
:"=a" (__res) \ //表示asm块执行完后把eax值给到__res变量
:"0" (seg),"m" (*(addr))); \
//0表示使用与上一个位置相同的寄存器但是seg的编号仍然是%1,所以"0" (seg)表示asm块里语句执行前把seg加载进eax
//"m" (*(addr)) 表示一个内存偏移地址值
__res;})//返回值为__res

首先就是({…})的语句块,语义上这个语句块等于一条语句,语句块的局部变量随语句块结束失效,语句块最后一个表达式就是这个语句块的返回值

int v = x+({puts("gr");z+3;})+y 等价于 v= x+z+3+y

lsl指令 load segment limit

str指令 store task regist

TODO 暂时没读懂的点: die里str(i)如何获取任务号

system_call.s

syscall的时候fs寄存器保持了三环状态,可以用来访问用户数据

用push和jmp模拟可指定返回地址的call

pushl $ret_from_sys_call
jmp schedule

sys_execve和sys_fork是个系统调用不是中断,用了ret而不是iret,(可能是不想单独再开个asm文件写)

sched.c

**show_task()**函数

要理解j = 4096-sizeof(struct task_struct);需要看下面的联合体 可以看出j就是栈的大小

union task_union
{ // 定义任务联合(任务结构成员和stack 字符数组程序成员)。
struct task_struct task; // 因为一个任务数据结构与其堆栈放在同一内存页中,所以
char stack[PAGE_SIZE]; // 从堆栈段寄存器ss 可以获得其数据段选择符。
};

后面就是(p + 1)也就是跳过这个结构体,去得到未使用的栈大小,因为栈是从高地址往低地址长的

i = 0;
while (i < j && !((char *) (p + 1))[i]) // 检测指定任务数据结构以后等于0 的字节数。
i++;

**schedule()**函数

里首先比较了每个任务的task_struct->alarm和全局的滴答数字jiffies,小于则设置SIGALRM信号,jiffies是从开机到现在的滴答数,10ms一次的时钟中断用sys_call.s里的_timer_interrupt中断处理会把jiffies+1

if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
_BLOCKABLE宏展开是0xfffbfeff 再&上block取非 再 &信号 相当于取了信号位图中除被阻塞的信号外的其它信号

然后比较得到任务中最大的时间片task->count,switch_to函数切换到该任务(使用ljmp tss切换的,会把当前寄存器现场写到当前tss段(也就是说switch_to函数在下次该任务调度前不会返回),然后恢复ljmp指定tss的现场执行)

如果时间片都为0则用counter = counter /2 + priority赋值时间片,再次循环

sleep_on函数

void sleep_on (struct task_struct **p)//*p是因为正在等待某个资源而睡眠的任务的队列链表的头部
{
struct task_struct *tmp;//栈上形成的临时节点,就相当于链表的next指针啦

// 若指针无效,则退出。(指针所指的对象可以是NULL,但指针本身不会为0)。
if (!p)
return;
if (current == &(init_task.task)) // 如果当前任务是任务0,则死机(impossible!)。
panic ("task[0] trying to sleep");
tmp = *p; // 让tmp 指向已经在等待队列上的任务(如果有的话)。
*p = current; // 当前任务调用了,所有将睡眠队列头的等待指针指向当前任务。
current->state = TASK_UNINTERRUPTIBLE; // 将当前任务置为不可中断的等待状态。
schedule (); // 重新调度。
// 只有当这个等待任务被唤醒时,调度程序才又返回到这里,则表示进程已被明确地唤醒。
// 既然大家都在等待同样的资源,那么在资源可用时,就有必要唤醒所有等待该资源的进程。该函数
// 嵌套调用,也会嵌套唤醒所有等待该资源的进程。然后系统会根据这些进程的优先条件,重新调度
// 应该由哪个进程首先使用资源。也即让这些进程竞争上岗。
if (tmp) // 若还存在等待的任务,则也将其置为就绪状态(唤醒)。
tmp->state = TASK_RUNNING;
}

p可以类似于是static struct task_struct *wait_motor[4] = { NULL, NULL, NULL, NULL };,这个数组就相当于四个链表头

*p和当前任务的栈上的tmp指针就会形成下面等待某一个资源的任务链表image-20231130125348019

当资源可以被抢占后,上图中的*p指向的”当前任务“返回执行调度前的代码,就可以从当前栈上访问到tmp了

if (tmp)            // 若还存在等待的任务,则也将其置为就绪状态(唤醒)。
tmp->state = TASK_RUNNING;

也就是把task2的state置为可调度状态,task2被调度执行时,task2重复“当前任务”上面的过程,把task1的state置为可调度,task1的tmp=0,说明他是第一个等待该资源的任务,没用上一个了

interruptible_sleep_on()函数

repeat:    current->state = TASK_INTERRUPTIBLE;
schedule();
if (*p && *p != current) {//先唤醒当前任务进入等待队列链表之后进入的任务
(**p).state = TASK_RUNNING;
goto repeat;
}
*p=tmp;//执行到这里说明*p=current,下面几句代码就可以把等待链表里的任务FIFO的方式被调度
if (tmp)
tmp->state=TASK_RUNNING;

signal.c

首先是signal.h里的一个难理解的函数声明

void (*signal(int _signr, void (*_handle)(int)))(int);
//这里void (*和后面的(int)是声明signal的返回值是一个void(*func)(int)类型的函数指针

写成下面的方式更容易理解

typedef void sigfun(int)
sigfun *signal(int signr,sigfun *handle)

do_signal()函数

do_signal函数是在system_call.s中ret_from_sys_call检测到current->signal有信号时被调用,do_signal的参数也是在那里压入栈的

do_signal里巧妙处理

*(&eip) = sa_handler;//改掉进入中断时处理器帮助我们压入的eip,这样返回到ret_from_sys_call里iret时就会执行用户态的信号处理函数
longs = (sa->sa_flags & SA_NOMASK)?7:8;//和下面if判断有关
*(&esp) -= longs;//构造堆栈控制执行流
verify_area(esp,longs*4);
tmp_esp=esp;
put_fs_long((long) sa->sa_restorer,tmp_esp++);//相当于sa_handler的返回地址
put_fs_long(signr,tmp_esp++);//sa_handler的第一个参数
if (!(sa->sa_flags & SA_NOMASK))//没有设置SA_NOMASK即不允许信号自己的处理句柄收到信号自己
put_fs_long(current->blocked,tmp_esp++);
put_fs_long(eax,tmp_esp++);//保护系统调用返回值
put_fs_long(ecx,tmp_esp++);//保护寄存器
put_fs_long(edx,tmp_esp++);//保护寄存器
put_fs_long(eflags,tmp_esp++);//保护寄存器
put_fs_long(old_eip,tmp_esp++);//处理完信号用来返回到用户程序处继续执行
current->blocked |= sa->sa_mask;

此时栈的情况

image-20231202124200694

要理解if (!(sa->sa_flags & SA_NOMASK))//没有设置SA_NOMASK即允许信号自己的处理句柄收到信号自己需要看下面的sys_sigaction函数里的内容

if (current->sigaction[signum - 1].sa_flags & SA_NOMASK)
current->sigaction[signum - 1].sa_mask = 0;
else
current->sigaction[signum - 1].sa_mask |= (1 << (signum - 1));//没有设置SA_NOMASK会设置屏蔽码
return 0;

do_signal最后一句current->blocked |= sa->sa_mask;设置阻塞当前信号,这里put_fs_long(current->blocked,tmp_esp++);保存主要是为了信号处理完后恢复阻塞,从而避免了传统sys_signal的问题

sa_restorer也根据是否设置SA_NOMASK分为下面两种情况

image-20231202124403747

image-20231202125100060

此时设置了SA_NOMASK会调用signal.c的sys_ssetmask (int newmask)恢复

int sys_ssetmask (int newmask)
{
int old = current->blocked;

current->blocked = newmask & ~(1 << (SIGKILL - 1));
return old;
}

再进行add esp的外平栈,再pop,再ret到old_eip

exit.c

TODO do_exit里文件,目录和tty相关的没读懂,

sys_close (i);

iput (current->pwd);

tty_table[current->tty].pgrp = 0;

fork.c

复习下内存管理,每个任务逻辑空间是64M

image-20231204095432853

所有操作系统管理所有任务共用0地址处的页目录表即可

verify_area()函数用来copy on write,在写某个地址addr之前调用,addr可写就直接返回了,否则会复制该页到新物理页

void verify_area (void *addr, int size)
{
unsigned long start;
start = (unsigned long) addr;//此时start是当前进程空间中的虚拟地址。
size += start & 0xfff;//加上之前没有4k对齐的大小
start &= 0xfffff000;//start 调整为其所在页的左边界开始位置
start += get_base (current->ldt[2]);// 此时start 变成系统整个线性空间中的地址位置。4k对齐
while (size > 0)//依次验证每一个页是否可写
{
size -= 4096;
// 写页面验证。若页面不可写,则复制页面。(mm/memory.c)
write_verify (start);
start += 4096;
}
}
void write_verify(unsigned long address)//address线性地址
{
unsigned long page;

// 判断指定地址所对应页目录项的页表是否存在(P),若不存在(P=0)则返回。
if (!((page = *((unsigned long*)((address >> 20) & 0xffc))) & 1))//本来要>>22,再乘以页目录项大小4也就是<<2,这里直接右移20和前面>>22再<<2的区别是会取到页表的前两bit可能不为0,所有&0xffc把低两位置零 页目录表在物理地址0处,head.s里_pg_dir定义的
return;
// 取页表的地址,加上指定地址的页面在页表中的页表项偏移值,得对应物理页面的页表项指针。
page &= 0xfffff000;//现在的page是页表在物理内存的基址
page += ((address>>10) & 0xffc);//和前面>>20一样,这里是把取到的页内偏移前两bit置零
// 如果该页面不可写(标志R/W 没有置位),则执行共享检验和复制页面操作(写时复制)。
if ((3 & *(unsigned long *) page) == 1) /* 0 1说明 non-writeable, present */
un_wp_page((unsigned long *) page);//参数page就是页表项基地址,改变他就可以改变映射
return;
}

copy_process() find_empty_process()函数

读之前先看一下fork的系统调用实现

_sys_fork:
call _find_empty_process ;// 调用find_empty_process()(kernel/fork.c,135)。
test eax,eax
js l2
push gs
push esi
push edi
push ebp
push eax
call _copy_process ;// 调用C 函数copy_process()(kernel/fork.c,68)。
add esp,20 ;// 丢弃这里所有压栈内容。
l2: ret

find_empty_process里

repeat:
if ((++last_pid) < 0)//当last_pid++到overflow时,置为1
last_pid = 1;
for (i = 0; i < NR_TASKS; i++)
if (task[i] && task[i]->pid == last_pid)//得到一个没人用的pid
goto repeat;
for (i = 1; i < NR_TASKS; i++) // 任务0 排除在外。
if (!task[i])//得到一个空闲的tastk位置
return i;

_copy_process里

p->tss.esp0 = PAGE_SIZE + (long) p;    // 内核堆栈指针(由于是给任务结构p 分配了1 页新内存,所以此时esp0 正好指向该页顶端)。
类似于sched.c里下面的union结构体
union task_union
{ // 定义任务联合(任务结构成员和stack 字符数组程序成员)。
struct task_struct task; // 因为一个任务数据结构与其堆栈放在同一内存页中,所以
char stack[PAGE_SIZE]; // 从堆栈段寄存器ss 可以获得其数据段选择符。
};
p->tss.eax = 0; //子进程返回0的原因

copy_mem函数把nr*64M处线性地址给ldt基址,并把这部分线性基址页表映射到当前进程的物理内存即父进程的物理内存

sys.c

sys_setregid函数

if (egid>0) {
if ((current->gid == egid) ||//相当于把current->egid=current->gid
(current->egid == egid) ||//相当于把current->egid=egid
(current->sgid == egid) ||//current->egid =current->sgid
suser())
current->egid = egid;
else
return(-EPERM);
}

vsprintf.c

skip_atoi函数

while (is_digit (**s))
i = i * 10 + *((*s)++) - '0';//这里i*10,当后面还有数字时会把前面收集到的数字*10

do_div函数

#define do_div(n,base) ({ \
int __res; \
__asm__( "divl %4" \
: \
"=a" (n), \ 执行后把div的商写回eax
"=d" (__res)\执行后把div的余数写回__res
: \
"0" (n), \ eax=n
"1" (0), \ edx=0
"r" (base)); \
__res; })//函数返回res 余数

number函数

while (num != 0)//do_div()num写回商 返回余数
tmp[i++] = digits[do_div (num, base)];//这里转换进制,do_div取出余数,得到倒叙的转换结果

kernel blk_drv

mm

memory.c

free_page

if (mem_map[addr]--) return;
mem_map[addr]=0;//
panic("trying to free free page");//mem_map[addr]原本为0 直接panci

int copy_page_tables(unsigned long from,unsigned long to,long size) fork中被调用

//其中from和to都是线性地址 size单位为字节,读的时候要铭记0.11所以进程共用4G的线性地址空间
if (1 & *to_dir)//目标页目录项的页表存在panic
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()))//get_free_page()找一个空闲物理页当作新页表
*to_dir = ((unsigned long) to_page_table) | 7;//页目录项填上新页表位置并把 U/S R/W P 置1 表示用户可读可写且存在物理页表

for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2;//R/W位置0 只读,为了COW
*to_page_table = this_page;//把P位存在的修改过后的源页表项复制到新页表
if (this_page > LOW_MEM) {
*from_page_table = this_page;//如果是用户主内存把源页表项也置位只读
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}

unsigned long put_page(unsigned long page,unsigned long address)

//page是主内存区物理地址,address是线性地址
主要功能就是把address对应的页目录下里的页表里的页表项里填入物理地址page

void un_wp_page(unsigned long * table_entry)

table_entry是页表项的物理地址
主要功能就是对于非共享页面(mem_map[idx]==1)给予写权限,共享页面申请一个新物理页,填入页表项,并把原本数据复制到新物理页

void do_wp_page(unsigned long error_code,unsigned long address)

//address线性地址
un_wp_page((unsigned long *)
(((address>>10) & 0xffc) + (0xfffff000 &//address页表项在页表里的偏移物理地址
*((unsigned long *) ((address>>20) &0xffc)))));//取出address所在页表的物理地址

TODO:do_no_page从磁盘加载缺页的内容

page.s

通过判断page fault error code 的最低位P判断是由缺页还是页写保护引起的缺页异常,决定调用do_no_page还是do_wp_page

image-20231228231131848

fs

buffer.c

remove_from_queues

if (!(bh->b_prev_free) || !(bh->b_next_free))
panic("Free block list corrupted");

看不懂的话要好好看一下书这里LRU链表得描述

image-20240227200219711

buffer_init

while ( (b = (char*)b - BLOCK_SIZE) >= ((void *) (h+1)) ) {

头尾相向而行,直到摩擦碰撞,b = (char*)b - BLOCK_SIZE) 是下一个缓冲块得位置,h+1是该缓冲块对应得缓冲头