Linux 0.11: Init
main() 函数
- 获取机器系统数据 (
0x90000 - 0x90200)- 根设备号
- 驱动信息
- 扩展内存 KB 容量
- 布局内存
- 规定物理内存
- 1MB 基本内存 + 扩展内存
- 最多管理 16 MB 内存,超过 16MB 的忽略不记
- 根据物理内存大小设置硬盘缓冲区区间
buffer_memory_endmemory_end > 12MB: 硬盘缓冲区设置到 4MB6MB < memory_end < 12MB: 硬盘缓冲区设置到 2MBmemory_end < 6MB: 硬盘缓冲区设置到 1MB
- 主存起始地址
main_memory_start设置为硬盘缓冲区末端
- 规定物理内存
- 初始化
- 如果定义了 RAMDISK, 则初始化虚拟盘
rd_init(),主存设置到虚拟盘之后 - 物理内存初始化:
mem_init() - 异常处理初始化:
trap_init() - 块设备初始化:
blk_dev_init() - 字符设备初始化:
chr_dev_init() - 系统时钟初始化:
time_init() - 进程调度初始化:
sched_init() - 硬盘缓冲区初始化:
buffer_init() - 硬盘控制器初始化:
hd_init() - 软盘控制器初始化:
floppy_init()
- 如果定义了 RAMDISK, 则初始化虚拟盘
- 开中断:
sti() - 切换特权级到用户模式:
move_to_user_mode();- 为
iret出栈参数 准备压栈:eip, cs, eflags, esp, ss- ss 为 0x17 用户数据段
- esp 为内核栈
- eflags 当前标志寄存器
- cs 为 Task0 的代码段
- eip 为 1 标号处的代码
iret中断出栈寄存器并跳转到 1 标号处的代码- 将所有 ds, es, fs, gs 全部设置为当前 LDT 的数据段
- 为
- 通过
fork()创建子进程- 对于父进程(进程0) 进入死循环执行
pause()pause()将通过_syscall0执行sys_pause系统调用 inkernel/sched.csys_pause将当前进程(进程 0)挂起(TASK_INTERRUPTIBLE)后开始调度:schedule()
- 对于子进程(进程1):执行
init()
- 对于父进程(进程0) 进入死循环执行
设备初始化
字符设备初始化:
chr_dev_init(): 空tty_init()rs_init()con_init()
块设备初始化
块设备数组,管理所有的块设备
1
2
3
4
5
6
7
8
9 struct blk_dev_struct blk_dev[NR_BLK_DEV] = {
{ NULL, NULL }, /* no_dev */
{ NULL, NULL }, /* dev mem */
{ NULL, NULL }, /* dev fd */
{ NULL, NULL }, /* dev hd */
{ NULL, NULL }, /* dev ttyx */
{ NULL, NULL }, /* dev tty */
{ NULL, NULL } /* dev lp */
};linux 中每个设备的 c 文件在定义 MAJOR_NR 之后 include
blk.h,blk.h将根据MAJOR_NR定义相应的DEVICE_REQUEST
虚拟盘初始化 rd_init()
- 设置 RAMDISK 的请求处理函数
do_rd_request:blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST
块设备初始化:blk_dev_init()
初始化 request 数组: dev 置 -1, next 置空(还未组成链表)
硬盘缓冲区初始化: buffer_init()
in fs/buffer.c
- 如果
buffer_end是 1MB ,则实际可用的 buffer 的末端在 640KB- 因为 640KB - 1MB 区域被内存和 BIOS 占用
硬盘控制器初始化 hd_init()
in kernel/blk_dev/hd.c
- 设置 hd 的请求处理函数
do_hd_request:blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST - 设置 hd 的中断处理函数:
set_intr_gate(0x2E,&hd_interrupt) - 复位 hd 的中断屏蔽位,允许从硬盘控制器发出中断:
outb
软盘控制器初始化 floppy_init()
in kernel/blk_dev/floppy.c
- 设置 fd 的请求处理函数
do_fd_request:blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST - 设置 fd 的中断处理函数:
set_trap_gate(0x26,&floppy_interrupt) - 复位 fd 的中断屏蔽位,允许从软盘控制器发出中断:
outb
物理内存初始化 mem_init()
- 设置主存终点
HIGH_MEMORY - 遍历 1-16MB 的页,设置每个页为 USED
- 0-1MB 用于内核系统,不进行初始化
- mem_map 每个元素管理一个页的进程引用计数,用于分配内存
- USED: 100 表示页面不允许被分配(linux 0.11 最多支持 64 个进程)
- 从起始地址开始,计算可以使用的页面数,从起始页面开始,将相应的 mem_map 元素置 0
- 此时从
start_mem到end_mem之间的页面的引用计数为 0,其他页面为 USED (100).
- 此时从
异常处理初始化:trap_init()
in kernel/traps.c
- 设置 IDT 中的 trap_gate 和 system_gate:
set_trap_gate,set_system_gate- trap_gate 用于异常处理, dpl 为 0
- system_gate 用于系统调用, dpl 为 3
1 |
进程调度初始化: sched_init()
- 补充 GDT :为初始进程 init_task 补充 TSS 和 LDT
- 从 GDT 的第 4 项开始(
FIRST_TSS_ENTRY),每个进程占用一个 TSS 项和一个 LDT 项 init_task属于task_uniontask_union包含task_struct和stack(内核栈) 的联合体, 每个进程有一个task_struct的ldt只包括 3 项: 第 0 项空,第 1 项进程代码段,第 2 项进程数据段
- 从 GDT 的第 4 项开始(
1 | struct task_struct { |
TSS (Task State Segment): 指向一个段,保存进程的上下文
1
2
3
4
5
6
7
8
9
10
11 TSS Descriptor for 32-bit TSS
31 23 15 7 0
+-----------------+-+-+-+-+---------+-+-----+---------+-----------------+
| | | | |A| LIMIT | | | TYPE | |
| BASE 31..24 |G|0|0|V| |P| DPL | | BASE 23..16 | 4
| | | | |L| 19..16 | | |0|1|0|B|1| |
|-----------------+-+-+-+-+---------+-+-----+-+-+-+-+-+-----------------|
| | |
| BASE 15..0 | LIMIT 15..0 | 0
| | |
+-----------------+-----------------+-----------------+-----------------+
init_task task_struct 初始化结构:
1 | /* |
内核初始化完成后将直接使用 INIT_TASK 作为 0 号进程:
- INIT_TASK 的 TSS 中 CR3 设置为
pg_dir,即 INIT_TASK 将使用内核的页表作为其页表。 - INIT_TASK 和内核初始化时使用相同的段基址和段限长,二者的线性地址空间也相同。
- 为剩余进程补充 TSS 和 LDT (linux 0.11 最多支持
NR_TASK个进程) - 加载 0 进程的 TSSR 和 LDTR
ltr(0);lldt(0);
- 设置时钟中断:
set_intr_gate(0x20,&timer_interrupt); - 设置系统调用:
set_system_gate(0x80,&system_call);system_call定义于kernel/system_call.s- 之后系统调用将通过
int 80陷入 IDT 的 system_gate 跳转到_system_call
进程调度 schedule()
in kernel/sched.c
- 检查 alarm ,唤醒所有收到信号的被挂起的进程:
TASK_INTERRUPTIBLE->TASK_RUNNING - 将就绪态的所有进程中 counter 最大的进程选出来(永远不会遍历到进程 0)
- 若所有就绪进程 counter 均为 0, 重新刷新所有进程的 counter
- 切换到调度选出的进程:
switch_to(next)
switch_to 定义于 include/linux/sched.h:
- 比较 next 和 _current: 如果相同,则不需要切换进程,直接返回
- 否则原子性将 _current 切换到 next
fork()
通过 _syscall0(int, fork) 定义
_syscall0macro 定义于include/unistd.h,本质是一条int 80, __NR_fork指令(trap in syscall gate)- 通过
_system_call调用_sys_call_table中的sys_fork
- 判断传入参数是否是系统调用(<
nr_system_calls),否则跳转到bad_sys_call - 将内核数据段(
0x10)赋值 edx, ds, es - 将用户数据段(
0x17)赋值 eax, fs - 调用 _sys_call_table 的第
eax个 entrysys_call_table定义于include/linux/sys.h,存放nr_system_calls个系统调用函数入口:sys_fork()
- 将 eax 压栈(系统调用返回值),将
_current(当前进程的 task struct 指针) 赋值给 eax - 如果当前进程不在就绪状态(state != 0),跳转到
schedule调度进程 - 如果当前进程时间片用完(counter == 0),跳转到
schedule调度进程 - 准备用系统调用中返回
- 出栈相应参数后 iret 返回
sys_fork
in kernel/system_call.S
- 调用
_find_empty_process找到 0 号进程 (inkernel/fork.c)- 从 task 数组中找到空闲的 entry 的 idx
- 压栈 6 个寄存器 : 为
copy_process准备参数 - 调用
_copy_process - 将栈恢复为调用 _sys_fork 之前的状态
- ret 返回
copy_process
int 80硬件压栈(5 个寄存器):eip, cs, eflags, esp, ss_system_call软件压栈(6 个寄存器) :ebx, ecx, edx, fs, es, dscall _sys_call_table会压入一次栈且未返回 :none_sys_fork软件压栈(5 个寄存器):eax(nr), ebp, edi, esi, gs
- 为 nr 进程设置 task 结构体
get_free_page()获取内核中空闲物理内存的空闲页- 从空闲物理内存的高地址开始向低地址分配
- 将 task 数组的第 nr 项指向刚获取的空闲物理页
- 将当前进程(父进程, current)的 task 结构体直接复制到 nr 进程(子进程)的 task 结构体
- 设置子进程 task 结构体状态
- 状态设置为
TASK_UNINTERRUPTIBLE - 设置进程信息
- 设置 tss
- esp0 用户栈顶设置为刚刚获取的空闲页的地址末端
- 将栈上的参数赋值给 tss 中保存的上下文
- eax 表示子进程 fork 返回值为 0
- tss.ldt 设置到
_LDT(nr): GDT 中的 task nr 对应的 LDT 选择符
- 状态设置为
- 设置子进程的虚拟地址空间并复制页表:
copy_mem- 获得父进程的段限长和段基址:
get_limit和get_base - 计算子进程在虚拟地址空间中的基地址等于 64MB * 其任务号
- linux 0.11 将 4GB 的虚拟地址空间等分给每个进程(最多 64 个进程),每个进程 64 MB 的空间
- 设置子进程的 LDT 的代码段和数据段(在 linux 0.11 中二者相同)
- 设置子进程的页表:
copy_page_tables将父进程的代码段复制到子进程的代码段
- 获得父进程的段限长和段基址:
- 维护子进程的文件描述符表
- 将子进程的 tss 和 ldt 写入 gdt 相应的 entry
- 进程状态设置为
TASK_RUNNING - 返回子进程的 pid
copy_page_tables
- 检查源地址和目标地址空间是否均为 4 MB 对齐
- 最后一级页表管理的地址空间为 4MB
- 计算源地址和目标地址的对应的一级页表项的地址
- 第一页表基地址均为 0 (
__pg_dir__)
- 第一页表基地址均为 0 (
- 计算复制内存需要的页表数(一级页表项数)
- 对占用的所有页表开始进行循环
- 如果目标地址对应的 L1 PTE 已存在,则 panic
- 如果源地址对应的 L1 PTE 不存在(未使用),则 continue
- 获取源地址对应的二级页表的基地址
- 申请一个空闲页作为目的地址对应的二级页表
- 设置目的地址的 L1PTE 的标志为 0x7 (U, RW, P)
- 计算需要复制的页数
- 如果源地址来自内核空间(
from == 0),只需要复制 160 个页 - 否则复制所有的 1024 个页
- 如果源地址来自内核空间(
- 针对当前循环处理的二级页表的每个 PTE
- 如果源地址的 L2PTE not present,不用复制
- 设置目的地址 L2PTE R/W 标志位为 0 (只读)(为实现 Copy On Write)
- 如果源地址 L2PTE 对应的页面地址小于 LOW_MEM (1MB),则循环进入下一次迭代
- 只有进程 0 的页面地址才会小于 1MB
- 否则,将源地址的 L2PTE 也设置为只读(为实现 Copy On Write),
并将目的地址对应页面的 mem_map 数组中对应的元素 +1 (表示页面已使用)
invalidate(): 刷新 TLB
init(): 进程 1 执行的初始化操作
setup()执行sys_setup系统调用 inkernel/blk_dev/hd.ccallable强行使该函数只能被调用一次
- 读取硬盘参数:config.h 未设置则从 BIOS 中读取
- 设置每个硬盘的起始扇区号和扇区总数
- 检测硬盘是否对 AT 控制器兼容,若不兼容,则将硬盘数据结构清零
- 获取每一个兼容的硬盘的分区表信息(第一个扇区)
- 通过
bread()读取数据块 infs/buffer.cgetblk()获取块号- 通过
get_hash_table()在 hash_table 中尝试获取- 通过
find_buffer()在缓冲区中尝试查找块, 找不到返回 NULL- 遍历 hash_table ,对比每一项的 dev 和 blocknr, 匹配则返回对应的
buffer_head* - 如果找不到,则返回 NULL
- 遍历 hash_table ,对比每一项的 dev 和 blocknr, 匹配则返回对应的
- 如果找到,将相应的块引用计数 +1
wait_on_buffer()- 再次检查块号是否匹配,匹配则返回,否则引用计数 -1
- 通过
- 如果找到则直接返回找到的
buffer_head* - 找不到:……
- 通过
- 通过
All articles on this blog are licensed under CC BY-NC-SA 4.0 unless otherwise stated.