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()- 死循环是必要的,因为当所有进程均不处于就绪态时,需要切换到进程 0 死循环等待
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
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系统调用open()系统调用打开/dev/tty0设备
setup 系统调用: sys_setup
callable 强行使该系统调用只能被调用一次
- 读取硬盘参数:config.h 未设置则从 BIOS 中读取
- 设置每个硬盘的起始扇区号和扇区总数
- 检测硬盘是否对 AT 控制器兼容,若不兼容,则将硬盘数据结构清零
- 获取每一个兼容的硬盘的分区表信息(第一个扇区)
- 硬盘的逻辑设备号为 :
0x300, 0x305, ... - 通过
bread()读取数据块
- 硬盘的逻辑设备号为 :
rd_load()- 安装根文件系统:
mount_root()
buffer 硬盘缓冲区读写
bread() 读取 buffer_head
getblk()获取块号- 通过
get_hash_table()在 hash_table 中尝试获取
(不存在主动的释放操作,hash_table 中的 buffer_head 可能引用计数为 0)- 通过
find_buffer()在缓冲区中尝试查找块, 找不到返回 NULL- 遍历 hash_table ,对比每一项的 dev 和 blocknr, 匹配则返回对应的
buffer_head* - 如果找不到,则返回 NULL
- 遍历 hash_table ,对比每一项的 dev 和 blocknr, 匹配则返回对应的
- 如果找到,将相应的块引用计数 +1
wait_on_buffer(): 等待其他进程释放该 buffer_head 的锁- 再次检查块号是否匹配,匹配则返回,否则引用计数 -1
- 通过
- 如果找到则直接返回找到的
buffer_head* - 找不到:
- 扫描
free_list,找到空闲的 buffer_head- 引用计数需等于 0 (buffer 正在被使用的标志是 buffer_head 引用计数为 0)
- 查找 BADNESS 权重最低的 buffer_head (BADNESS 权重代表的是 buffer_head 的 b_dirt 和 b_lock 标志)
- 如果没有空闲的 buffer_head
sleep_on使当前线程睡眠,等待buffer_wait信号唤醒- 被唤醒后重新尝试获取相应的块:
repeat
- 获取空闲块后继续等待其他线程释放 buffer 锁:
wait_on_buffer() - 检查获取的空闲块,检查不通过则重新尝试获取新的空闲块:
repeat- 是否空闲
- 如果获取 buffer 的锁之后发现引用计数不为 0 (再次被其他线程使用),继续重新尝试获取新的空闲块
- 是否 dirty
- 如果获取的空闲 buffer_head b_dirt 被置位
sync_dev(): 将 buffer 写入磁盘wait_on_buffer: 再次等待 buffer 释放锁- 获取锁之后发现引用计数不为 0 (再次被其他线程使用),继续重新尝试获取新的空闲块
- 如果获取的空闲 buffer_head b_dirt 被置位
- 检查此时 hash_table 中是否有进程将该 buffer_head 加入
- 如果已经有进程开始操作该 buffer_head, 继续重新尝试获取新的空闲块
- 是否空闲
- 当前进程占用找到的空闲 buffer_head
- 引用计数 b_count = 1
- dirty 位 b_dirt = 0
- 磁盘同步位 b_uptodate = 0
- 移动 buffer_head 在 hash_table 和 free_list 的位置
- 从 hash 队列和 free_list 中移出该 buffer_head (旧的位置):
remove_from_queues - 更新 buffer_head 的 设备号和块号
- 根据新的设备号和块号重新插入free_list 和 hash_table 的新位置处:
insert_into_queues
- 从 hash 队列和 free_list 中移出该 buffer_head (旧的位置):
- 至此,新的 buffer_head 设置完毕,返回
- 扫描
- 通过
- 如果找不到块号,则
panic() - 如果获取的块未被同步到磁盘
b_uptodate, 则直接返回 - 否则(块未被同步到磁盘)
- 向硬盘发送命令以读取数据到 buffer:
ll_rw_block(READ,bh) - 等待硬盘将数据写入 buffer :
wait_on_buffer(bh); - 再次检查获取的块是否同步到磁盘
b_uptodate, 则直接返回 - 如果依旧未同步到磁盘, 则释放 buffer,并返回 NULL :
brelse()
- 向硬盘发送命令以读取数据到 buffer:
等待硬盘处理 buffer :wait_on_buffer
1 | cli(); // 关中断(只会关闭当前进程的中断,仅 reset 当前进程的 IF flag) |
bh->b_wait为所有等待当前 buffer_head 的所有进程组成的队列。sleep_on若没有其他就绪进程,则会调度到进程 0 死循环等待。- 这里关中断开中断是为了实现对 b_lock(共享数据) 的原子操作,因为调度到其他进程之后硬盘中断可能会解锁
ll_rw_blk 最后一级读写块设备管理
ll_rw_block(): in kernel/blk_dev/ll_rw_blk.c
- 根据 buffer_head 的磁盘设备号 (b_dev) 获取主设备号
#define MAJOR(a) (((unsigned)(a))>>8)- 检查是否大于
NR_BLK_DEV: 大于则表示非合法硬盘设备
- 对设备发出请求:
make_request(major,rw,bh);
创建针对硬盘的请求项,并加入请求队列:make_request()
- 对于请求命令为
READA或WRITEA的情况(read write ahead 提前读写硬盘,类似于预取)- 如果请求的 buffer_head 已经持有锁,则认为提前读写没有必要,放弃建立预读写请求,并返回
- 否则(未持有锁),则创建一般的读写请求项 (
READ/WRITE)
- 给请求的
buffer_head上锁:lock_buffer(bh) - 如果写请求的 buffer_head 并未 dirty 或者读请求已经和硬盘同步
uptodate,则意味着没有必要访问硬盘,直接返回 repeat:- 对于读请求,将请求项队列指针指向请求项队列尾部
- 对于写请求, 将请求项队列指针指向请求项队列 2/3 处
- 寻找空闲请求项:请求项队列指针倒序查找,直到找到
req->dev < 0的一项 - 如果没找到,(
req < request)- 对于 ahead 请求,直接放弃,释放 buffer_head 的锁之后返回
- 对于正常的读写请求,睡眠当前进程等待请求项空闲队列的唤醒:
sleep_on(&wait_for_request);
被唤醒之后重新repeat查找
- 找到后初始化该请求项
- 调用
add_request将该请求项加到硬盘的请求项队列中
将指定的请求项加入指定设备的请求项队列: add_request()
- 关中断
- 如果当前设备的当前请求项为空
- 将该请求项设置为当前请求项
- 开中断
- 执行设备请求函数, 之后返回
- 对于硬盘设备而言,是
do_hd_request
- 对于硬盘设备而言,是
- 如果当前设备已经有当前请求项,利用电梯调度算法选择请求项队列的最佳插入位置(
tmp)- 电梯算法的作用是让磁盘磁头的移动距离最小,从而改善硬盘访问时间
- 插入当前设备的请求项队列 (
tmp->next) - 开中断返回
hd 设备读写
设备请求函数: do_hd_request
- 根据当前请求项的请求命令(READ/WRITE)进行
hd_out- 设置
do_hd即硬盘的中断处理函数- 对于 READ 是
read_intr - 对于 WRITE 是
write_intr
- 对于 READ 是
- 通过
outb_p向硬盘发送命令
- 设置
由 ll_rw_blk 向 hd 发送一次命令,hd 将发送 2 次中断,2 次数据传输(一个块 2 个扇区)
中断返回处理: _hd_interrupt
edx清零,并与_do_hd交换, 并call %edx跳转到do_hd设置的地址- 对于读请求,
do_hd为read_intr
- 对于读请求,
读取请求中断处理程序:read_intr
- 将硬盘数据读取到硬盘当前请求项的 buffer 中
- 从
HD_DATA端口读取 256 个 word (512 字节):port_read(HD_DATA,CURRENT->buffer,256)
- 从
- 调整当前请求项的 buffer 指针地址和扇区号,步进 512 字节(1 个扇区)
- 如果当前请求项的所有扇区还未读完,则继续设置
do_hd为read_intr,等待下一次硬盘中断 - 否则(当前请求项的所有扇区均已读完),则结束请求:
end_request(1): 1 表示 uptodate, 即 buffer_head 中数据与硬盘同步do_hd_request()继续处理下一个硬盘的请求项:发送命令等待中断
结束当前请求项 end_request
in kernle/blk_dev/blk.h
- 关闭设备,对于硬盘而言将磁头挂起:
DEVICE_OFF(CURRENT->dev) - 对于当前请求项的 buffer_head
- 更新 bh->b_uptodate
- 释放当前请求项 bh 的锁:
unlock_buffer(CURRENT->bh)inkernel/blk_dev/ll_rw_blk.cbh->b_lock = 0- 唤醒当前请求项 buffer_head 的等待队列中的所有进程:
wake_up(&bh->b_wait)
wake_up(&CURRENT->waiting):在 linux 0.11 中&CURRENT->waiting实现为空,未使用- 唤醒当前请求项等待队列(等待空闲请求项)中的所有进程:
wake_up(&wait_for_request) - 将当前请求项设置为空闲(-1):
CURRENT->dev = -1 - 选择请求项队列中的下一个请求项作为当前请求项:
CURRENT = CURRENT->next
进程调度 sched
in kernel/sched.c
sleep_on(): 将当前进程加到参数指定的等待队列中,并挂起当前进程
- 检查当前进程是否为进程 0
- 是进程 0 则 panic (进程 0 不允许被睡眠)
- 将进程加入参数指定的等待队列
- 将
tmp指向当前等待队列头中的 task_struct - 将睡眠队列头的等待指针指向当前任务 current
- 将
- 将当前进程 current 状态设置为
TASK_UNINTERRUPTIBLE schedule()切换到其他进程- 切换回当前进程之后,状态设置为 0,即就绪态
进程调度 schedule()
- 检查 alarm ,唤醒所有收到信号的被挂起的进程:
TASK_INTERRUPTIBLE->TASK_RUNNING - 将就绪态的所有进程中 counter 最大的进程选出来(永远不会遍历到进程 0)
- 若所有就绪进程 counter 均为 0, 重新刷新所有进程的 counter
- 切换到调度选出的进程:
switch_to(next)- 若当前所有进程均未处于
TASK_RUNNING状态,则next = 0, 切换到 0 号进程 - 而 0 号进程为循环等待(等待其他进程恢复为就绪态)
- 若当前所有进程均未处于
switch_to 宏定义于 include/linux/sched.h:
- 比较 next 和 _current: 如果相同,则不需要切换进程,直接返回
- 否则原子性将 _current 切换到 next