Linux 0.11 进程 1: init

1,280 阅读7分钟

main-fork.png

这篇笔记研究进程 0 通过 fork 创建进程 1 之后,调度切到进程 1 之后发生的事、bread 和块设备 buffer 工作的详细过程、以及为什么说把进程 0 说成是 Idle Process。

after fork: 进程 1

task 0 执行 pause -> 调度 -> 切换到 task 1,执行:

// init/main.c > main(void)
init()

做三件事:

  1. setup hd、rd
  2. mount root fs
  3. 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);
  1. 设置 hd_info:从参数(BIOS,传进来的是之前汇编拿的机器系统数据)获取硬盘基本信息
  2. hd:硬盘分区表
    • 一个盘分 5 个(hd 表中五项):idx%5=0 是物理盘,1~4 是逻辑盘
    • 现在只设置 hd 表中已有的物理项:0、5、...
      • bread 从硬盘拿引导块数据
  3. 设虚拟盘:rd_load() (kernel/blk_dev/ramdisk.c)
    • 之前 main() > rd_init() 只是建了裸盘
    • 现在用 rd_load 格式化之
      • 从(物理的)软盘读引导块、超级块
      • 判断是否可用,不可用立即返回
      • 把整个软盘读到虚拟盘(一块专门分的内存)
      • 虚拟盘设置为根设备
  4. 加载根文件系统: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.png

Buffer 设计核心思想:要快

Why 加了一层 buffer,多了一次复制还快:

  • 复用(多人共享才快)
  • 大于 1 人就不亏

核心思想(块)=> Requirement:

  • 尽可能多复用
    • 但是不可预知何时何人要再用
    • 让块在 buffer 中的时间尽可能长
    • 就一直赖着,能拖则拖,拖的时间充分长就会有人服用
  • 维护一致性
    • 任何时候,用 buffer 应该与不用 buffer 结果相同。

bread

block read: 从 blk_dev 读块到 buffer

(fs/buffer.c)

bread.png

  1. getblk:在缓冲区里找
  2. got bh from getblk
    • 快: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)

ll_rw_block.png

  1. 检查请求的设备是否存在、已挂载
  2. make_request:做一个请求项——对应一个块
  3. add_request:把做好的请求项加入队列
make_request
  1. buffer_head 加锁

    cli();
    while(bh->b_lock)
        sleep_on(&bh->b_wait);
    bh->b_block = 1;
    sti();
    
    • 有了进程之后:关中断只关了当前 task 自己的
    • 切到其他 task 时,从 目标进程的 TSS.EFLAGS hv恢复其自己的开/关中断状态
    • sleep_on:加到等待队列,do schedule
    • 可能前面也有人等同一块,那个进程先被唤醒了,先加锁成功,所以要用 while 自旋判断反复确认。
  2. 找空闲请求项

    • 读从尾,写从后 2/3
      • 读的可用空间大:读比写更优先(着急:人类的需求)
    • 从后往前找
    • dev < 0 则空闲
    • 没找到:睡等空来
  3. 填写请求项:啥设备、读or写、有几扇区(一块 1KB,一扇512B => 要读两扇)

  4. call add_request

add_request
  • dirt = 0:开始处理即不脏(联想前面 BADNESS)
  • 队为空:立即处理:(dev->request_fn)()
  • 队不空:电梯算法放入队

对于此处硬盘,request_fndo_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_to task 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)课(学)时(分)啊,机都还没开起来捏[大哭],就这样戛然而止了。。)

蒟蒻的祈愿活动:希望我别挂科)