这篇笔记研究进程 0 通过 fork 创建进程 1 之后,调度切到进程 1 之后发生的事、bread 和块设备 buffer 工作的详细过程、以及为什么说把进程 0 说成是 Idle Process。
after fork: 进程 1
task 0 执行 pause -> 调度 -> 切换到 task 1,执行:
// init/main.c > main(void)
init()
做三件事:
- setup hd、rd
- mount root fs
- fork task 2: shell
setup 设硬盘、虚拟内存盘
// init/main.c > init(void)
setup((void *) &drive_info);
setup 是有一个参数的系统调用:
setup: _syscall1 -> int 0x80 -> _system_call -> sys_setup
sys_setup
// kernel/blk_dev/hd.c
sys_setup(void *BIOS);
- 设置
hd_info:从参数(BIOS,传进来的是之前汇编拿的机器系统数据)获取硬盘基本信息 - 设
hd:硬盘分区表- 一个盘分 5 个(hd 表中五项):idx%5=0 是物理盘,1~4 是逻辑盘
- 现在只设置 hd 表中已有的物理项:0、5、...
bread从硬盘拿引导块数据
- 设虚拟盘:
rd_load()(kernel/blk_dev/ramdisk.c)- 之前
main() > rd_init()只是建了裸盘 - 现在用
rd_load格式化之- 从(物理的)软盘读引导块、超级块
- 判断是否可用,不可用立即返回
- 把整个软盘读到虚拟盘(一块专门分的内存)
- 虚拟盘设置为根设备
- 之前
- 加载根文件系统:
mount_root()(fs/super.c)
数据结构:hd_info、hd
kernel/blk_dev/hd.c
hd_i_struct hd_info[]:- 磁头数、每道扇区数、柱面数、...
hd_struct hd[]:- 起始扇区、总扇区数
bread
bread 从 blk_dev 读块到 buffer。
buffer
读取块设备:要过 buffer,不能直接读写设备。
Buffer 设计核心思想:要快!
Why 加了一层 buffer,多了一次复制还快:
- 复用(多人共享才快)
- 大于 1 人就不亏
核心思想(块)=> Requirement:
- 尽可能多复用
- 但是不可预知何时何人要再用
- 让块在 buffer 中的时间尽可能长
- 就一直赖着,能拖则拖,拖的时间充分长就会有人服用
- 维护一致性
- 任何时候,用 buffer 应该与不用 buffer 结果相同。
bread
block read: 从 blk_dev 读块到 buffer
(fs/buffer.c)
getblk:在缓冲区里找- got
bhfromgetblk- 快:uptodate:直接返回
- 慢:不 uptodate:
ll_rw_block从设备读(请求)- 等:读到后返回
bh
blk = (dev, block)
def bread(blk) -> buffer_head:
if blk in buffer: # fast path
return buffer.get(blk)
# slow path
b = read_from_dev(blk)
buffer.insert(blk, b)
return buffer.get(blk)
getblk
这个过程写文字不如看代码。。
blk = (dev, block)
def getblk(blk) -> buffer_head:
repeat:
# 快: 在哈希表找
if blk in hash_table:
return hash_table.get(blk)
# 慢: 在空闲链表找
bh = find_in_free_list()
# 可用?不可用就重找
if not check_bh_availiable(hb):
goto repeat;
# 占用
bh.{count, dirt, uptodate = 1, 0, 0}
从 hash_table、free_list 移除 bh
设置 bh 的 dev、block 值
再把 bh 放回 hash_table: 新dev、block 值对应的桶
free_list: 尾
# 返回
return bh
def hash(dev, block):
"""哈希函数"""
return ( dev ^ block ) % 307
def find_in_free_list() -> buffer_head:
"""在空闲链表找没被人占用的、空闲的块
实在没有空闲的,则返回没被人占用的、尽可能快可用的
都被人占用了 return NULL
"""
bh = NULL
for tmp: buffer_head in free_list:
if tmp.count == 0: # 被人占用了
continue
# BADNESS: dirt > lock
hb = min(bh, tmp, by: BADNESS)
if BADNESS(hb) == 0:
break
return bh
def BADNESS(hb):
"""块坏(不可用:要等)的程度(加权):
dirt *= 2: 还没同步(将要同步)
lock *= 1: 正在同步:应该快好了:先等这个:这个 BADNESS 低
"""
return bh.dirt << 1 + bh.lock
def check_bh_availiable(hb):
"""检查是否得到没被人占用的空闲块
且块已同步、得到锁
且整个过程中未被别人先用
"""
if bh == NULL: # 现在所有缓冲块都被占用了
sleep_on(&buffer_wait) # 等通知有了再找
return False
wait_on_buffer(bh) # 加锁 wait
if bh.count > 0: # 有人先占用了
return False
while bh->dirt: # 脏: 要先同步
sync_dev(bh.dev)
wait_on_buffer(bh);
if bh.count > 0: # 又被人先占用了
return False
if bh in hash_table: # 被人加到 hash 表里了
return False
return True # 终于可用了!
一个 buffer_head 同时存在两个集合中:
free_list:所有buffer_head穿在一条链上hash_table:按对应hash(dev, block)放入哈希桶
一个 buffer_head 是否空闲(可被占用)的指标:
count > 0:别人正用着,别想count==0, dirt == 1:别人刚用完(e.g. 刚:wq),但这个块还没同步,将要同步count==0, lock == 1:正在同步了- 要等一个的话,这个状态的应该是最快能给你用的
「竞争」:在等块同步的过程中,当前 task 是睡着的,切到其他进程运行。所以可能被别的进程抢先获取了这个块,所以 check_bh_availiable,如果被别人先用了,也没地说理,就自认倒霉重找一个了。
多次检查,最终得到的一定是:
- unused:
b_count == 0 - unlocked:
b_lock == 0 - clear:
b_dirt == 0
相当于他是有主次、有目的地等,而不是在竞态里碰运气:
while True:
for bh in free_list:
if bh.b_count == 0 and
bh.b_lock == 0 and
bh.b_dirt == 0:
return bh # found
# 刚好碰到一个全满足的,这种就完全撞大运,
# 这样就你就慢了。
ll_rw_block
ll_rw_block(int rw, struct buffer_head * bh): 底层块设备读写 (kernel/blk_drv/ll_rw_blk.c)
- 检查请求的设备是否存在、已挂载
make_request:做一个请求项——对应一个块add_request:把做好的请求项加入队列
make_request
-
给
buffer_head加锁cli(); while(bh->b_lock) sleep_on(&bh->b_wait); bh->b_block = 1; sti();- 有了进程之后:关中断只关了当前 task 自己的
- 切到其他 task 时,从 目标进程的
TSS.EFLAGShv恢复其自己的开/关中断状态 sleep_on:加到等待队列,do schedule- 可能前面也有人等同一块,那个进程先被唤醒了,先加锁成功,所以要用 while 自旋判断反复确认。
-
找空闲请求项
- 读从尾,写从后 2/3
- 读的可用空间大:读比写更优先(着急:人类的需求)
- 从后往前找
dev < 0则空闲- 没找到:睡等空来
- 读从尾,写从后 2/3
-
填写请求项:啥设备、读or写、有几扇区(一块 1KB,一扇512B => 要读两扇)
-
call
add_request
add_request
- 置
dirt = 0:开始处理即不脏(联想前面 BADNESS) - 队为空:立即处理:
(dev->request_fn)() - 队不空:电梯算法放入队
对于此处硬盘,request_fn 是 do_hd_request (kernel/blk_dev/hd.c)
do_hd_request
- 读写共享的操作:
- 解析传送给硬盘硬件的数据
- 检查硬件状态
- 挂接中断响应程序:
- 读挂
read_intr - 写挂
write_intr - 在写完后,发中断,运行 handler,唤醒等该操作的 task & 继续 do_hd_request 处理队列上的下一项。
- 读挂
hd_out将请求下达给硬件。- 硬盘老牛破车慢慢倒腾数据,读到硬盘内的 cache 之后,发中断,让操作系统来拿
- CPU (OS) 这边就先逐层返回了。
发完请求后就就逐层返回了:hd_out -> do_hd_request -> add_request -> make_request -> ll_rw_block -> bread
然后 bread 卡在 wait_on_buffer(bh) 等读完后被唤醒。
wait_on_buffer
这个函数很简单:
cli();
while (bh->b_lock)
sleep_on(&bh->b_wait);
sti();
所以重点是 sleep_on。
sleep_on
- 检查
- 等 NULL:立即返回
- task 0 等东西:不可能的,panic
- 做等待队列
- 在不同 task 的 kernel stack 中串起队列
- 唤醒的时候沿着队(反向)唤醒
- 当前进程状态改为
TASK_UNINTERRUPTIBLE - call
schedule() - 又 switch 回来当前进程了(说明:当前进程被唤醒了!!)
- 唤醒同一个等待队列上的下一个进程
- 下一个进程又继续唤醒上上个,如此直到所有等待的人全唤醒了!
这个太优雅了,必须直接上源码:
// 参数 p 是指针的指针:指向等待的进程(task_struct *)
void sleep_on(struct task_struct** p) {
struct task_struct* tmp;
if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep");
// *p 就是这个等待"队列"上上一个等待的人
tmp = *p;
// 现在把头换成自己
*p = current;
current->state = TASK_UNINTERRUPTIBLE;
schedule();
// 从 schedule 返回,说明当前进程被唤醒了
// 继续唤醒上一个等待的人
if (tmp)
tmp->state = 0; // TASK_RUNNING
}
注意并没有一个显式的队列结构,他就是用一个全局指针(**p),和一个临时变量(tmp),在不同进程的内核栈中串了起来:
**p:e.g.&bh->b_wait所有人都可见,所有等该块的人都是传这个指针进来。tmp:在当前调用 sleep_on 的进程 kernel stack 里存下"队列"的上一个人。- 把
*p指向当前进程,即当前调用 sleep_on 的这个人放到了"队列"头。 - 等待条件已满足,唤醒之后,才会从 schedule 返回,继续执行
- 如果 tmp 不为空(说明队列里还有其他人):唤醒之。
- 它又会去继续唤醒下一个人
schedule
这里去 schedule 之后,现在只有两个进程:
- task 0 是 interruptable
- task 1 是 uninterruptable
schedule 扫了一遍 task 数组,所有人都不能执行啊。。
- 所以
while (--i)循环里没有改变初始的c = -1; next = 0;这两个值。 - 然后
if (c) break;是满足的(-1为真)跳出了while(1) - 然后(仿佛找到了一样)走到
switch_to(next),这里next = 0,即切到 task 0 运行!即便 task 0 从来不是 TASK_RUNNING 状态。
然后切到 task 0 跑:
- 他是 pause 主动调用 schedule 被搁置的,所以现在又一路返回到
for(;;) pause() - 再一次 pause,主动调度
如果再一次没有任何进程可以跑(TASK_RUNNING):
- task 0 再一次被选中,再一次
switch_totask 0 - 但这一次
switch_to中:current == next,不切换! - 直接跳过 switch_to 的 ljmp 走 task gate 啥的那一套
- 直接就沿路返回 switch_to -> schedule -> sys_pause -> ... ->
for(;;) pause()如此重复。
所以就是说:没人能跑时,task 0 老哥出来撑场子,他也没事干,就出来晃一圈,晃一圈,晃一圈,直到有人能运行了他就歇了。这就是 Idle Process 的故事。
之后的事
- mount root fs
- fork task 2: shell
To be continued. (但是结课了[恼],只有我自己预习的笔记了。。怎么才 40(2)课(学)时(分)啊,机都还没开起来捏[大哭],就这样戛然而止了。。)
(蒟蒻的祈愿活动:希望我别挂科)