0%

Linux 0.11: Init

main() 函数

  1. 获取机器系统数据 (0x90000 - 0x90200)
    • 根设备号
    • 驱动信息
    • 扩展内存 KB 容量
  2. 布局内存
    1. 规定物理内存
      • 1MB 基本内存 + 扩展内存
      • 最多管理 16 MB 内存,超过 16MB 的忽略不记
    2. 根据物理内存大小设置硬盘缓冲区区间 buffer_memory_end
      • memory_end > 12MB: 硬盘缓冲区设置到 4MB
      • 6MB < memory_end < 12MB: 硬盘缓冲区设置到 2MB
      • memory_end < 6MB: 硬盘缓冲区设置到 1MB
    3. 主存起始地址 main_memory_start 设置为硬盘缓冲区末端
  3. 初始化
  4. 开中断: sti()
  5. 切换特权级到用户模式: 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 的数据段
  6. 通过 fork() 创建子进程
    • 对于父进程(进程0) 进入死循环执行 pause()
      • 死循环是必要的,因为当所有进程均不处于就绪态时,需要切换到进程 0 死循环等待
      • pause() 将通过 _syscall0 执行 sys_pause 系统调用 in kernel/sched.c
      • sys_pause 将当前进程(进程 0)挂起(TASK_INTERRUPTIBLE)后开始调度:schedule()
    • 对于子进程(进程1):执行 init()

设备初始化

字符设备初始化:

  • 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.hblk.h 将根据 MAJOR_NR 定义相应的 DEVICE_REQUEST

虚拟盘初始化 rd_init()


in kernel/blk_dev/ramdisk.c

  1. 设置 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

  1. 如果 buffer_end 是 1MB ,则实际可用的 buffer 的末端在 640KB
    • 因为 640KB - 1MB 区域被内存和 BIOS 占用

硬盘控制器初始化 hd_init()

in kernel/blk_dev/hd.c

  1. 设置 hd 的请求处理函数 do_hd_request : blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST
  2. 设置 hd 的中断处理函数:set_intr_gate(0x2E,&hd_interrupt)
  3. 复位 hd 的中断屏蔽位,允许从硬盘控制器发出中断:outb

软盘控制器初始化 floppy_init()

in kernel/blk_dev/floppy.c

  1. 设置 fd 的请求处理函数 do_fd_request : blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST
  2. 设置 fd 的中断处理函数:set_trap_gate(0x26,&floppy_interrupt)
  3. 复位 fd 的中断屏蔽位,允许从软盘控制器发出中断:outb

物理内存初始化 mem_init()


in mm/memory.c

  1. 设置主存终点 HIGH_MEMORY
  2. 遍历 1-16MB 的页,设置每个页为 USED
    • 0-1MB 用于内核系统,不进行初始化
    • mem_map 每个元素管理一个页的进程引用计数,用于分配内存
    • USED: 100 表示页面不允许被分配(linux 0.11 最多支持 64 个进程)
  3. 从起始地址开始,计算可以使用的页面数,从起始页面开始,将相应的 mem_map 元素置 0
    • 此时从 start_memend_mem 之间的页面的引用计数为 0,其他页面为 USED (100).

异常处理初始化:trap_init()

in kernel/traps.c

  1. 设置 IDT 中的 trap_gate 和 system_gate: set_trap_gate, set_system_gate
    • trap_gate 用于异常处理, dpl 为 0
    • system_gate 用于系统调用, dpl 为 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))

#define set_intr_gate(n,addr) \
_set_gate(&idt[n],14,0,addr)

#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)

#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)

进程调度初始化: sched_init()

  1. 补充 GDT :为初始进程 init_task 补充 TSS 和 LDT
    • 从 GDT 的第 4 项开始(FIRST_TSS_ENTRY),每个进程占用一个 TSS 项和一个 LDT 项
    • init_task 属于 task_union
      • task_union 包含 task_structstack(内核栈) 的联合体, 每个进程有一个
      • task_structldt 只包括 3 项: 第 0 项空,第 1 项进程代码段,第 2 项进程数据段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct task_struct {
/* these are hardcoded - don't touch */
long state; /* -1 unrunnable, 0 runnable, >0 stopped */
long counter; // 进程调度时间片
long priority;
long signal;
struct sigaction sigaction[32];
long blocked; /* bitmap of masked signals */
/* various fields */
int exit_code;
unsigned long start_code,end_code,end_data,brk,start_stack;
long pid,father,pgrp,session,leader;
unsigned short uid,euid,suid;
unsigned short gid,egid,sgid;
long alarm;
long utime,stime,cutime,cstime,start_time;
unsigned short used_math;
/* file system info */
int tty; /* -1 if no tty, so it must be signed */
unsigned short umask;
struct m_inode * pwd;
struct m_inode * root;
struct m_inode * executable;
unsigned long close_on_exec;
struct file * filp[NR_OPEN]; // 每个进程最多打开 NR_OPEN 个文件(20 in linux 0.11)
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
struct desc_struct ldt[3];
/* tss for this task */
struct tss_struct tss;
};

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
* INIT_TASK is used to set up the first task table, touch at
* your own risk!. Base=0, limit=0x9ffff (=640kB)
*/
#define INIT_TASK \
/* state etc */ { 0,15,15, \
/* signals */ 0,{{},},0, \
/* ec,brk... */ 0,0,0,0,0,0, \
/* pid etc.. */ 0,-1,0,0,0, \
/* uid etc */ 0,0,0,0,0,0, \
/* alarm */ 0,0,0,0,0,0, \
/* math */ 0, \
/* fs info */ -1,0022,NULL,NULL,NULL,0, \
/* filp */ {NULL,}, \
{ \
{0,0}, \
/* ldt */ {0x9f,0xc0fa00}, \
{0x9f,0xc0f200}, \
}, \
/*tss*/ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
0,0,0,0,0,0,0,0, \
0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
_LDT(0),0x80000000, \
{} \
}, \
}

内核初始化完成后将直接使用 INIT_TASK 作为 0 号进程:

  • INIT_TASK 的 TSS 中 CR3 设置为 pg_dir ,即 INIT_TASK 将使用内核的页表作为其页表。
  • INIT_TASK 和内核初始化时使用相同的段基址和段限长,二者的线性地址空间也相同。
  1. 为剩余进程补充 TSS 和 LDT (linux 0.11 最多支持 NR_TASK 个进程)
  2. 加载 0 进程的 TSSR 和 LDTR
    • ltr(0);
    • lldt(0);
  3. 设置时钟中断: set_intr_gate(0x20,&timer_interrupt);
  4. 设置系统调用: set_system_gate(0x80,&system_call);
    • system_call 定义于 kernel/system_call.s
    • 之后系统调用将通过 int 80 陷入 IDT 的 system_gate 跳转到 _system_call

fork()

通过 _syscall0(int, fork) 定义

  • _syscall0 macro 定义于 include/unistd.h,本质是一条 int 80, __NR_fork 指令(trap in syscall gate)
  • 通过 _system_call 调用 _sys_call_table 中的 sys_fork
  1. 判断传入参数是否是系统调用(< nr_system_calls),否则跳转到 bad_sys_call
  2. 将内核数据段(0x10)赋值 edx, ds, es
  3. 将用户数据段(0x17)赋值 eax, fs
  4. 调用 _sys_call_table 的第 eax 个 entry
    • sys_call_table 定义于 include/linux/sys.h,存放 nr_system_calls 个系统调用函数入口: sys_fork()
  5. 将 eax 压栈(系统调用返回值),将 _current(当前进程的 task struct 指针) 赋值给 eax
  6. 如果当前进程不在就绪状态(state != 0),跳转到 schedule 调度进程
  7. 如果当前进程时间片用完(counter == 0),跳转到 schedule 调度进程
  8. 准备用系统调用中返回
    • 出栈相应参数后 iret 返回

sys_fork

in kernel/system_call.S

  • 调用 _find_empty_process 找到 0 号进程 (in kernel/fork.c)
    • 从 task 数组中找到空闲的 entry 的 idx
  • 压栈 6 个寄存器 : 为 copy_process 准备参数
  • 调用 _copy_process
  • 将栈恢复为调用 _sys_fork 之前的状态
  • ret 返回

copy_process


in kernel/fork.c
参数来源: (右序压栈)

  • int 80 硬件压栈(5 个寄存器): eip, cs, eflags, esp, ss
  • _system_call 软件压栈(6 个寄存器) :ebx, ecx, edx, fs, es, ds
  • call _sys_call_table 会压入一次栈且未返回 :none
  • _sys_fork 软件压栈(5 个寄存器): eax(nr), ebp, edi, esi, gs
  1. 为 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 选择符
  2. 设置子进程的虚拟地址空间并复制页表:copy_mem
    • 获得父进程的段限长和段基址: get_limitget_base
    • 计算子进程在虚拟地址空间中的基地址等于 64MB * 其任务号
      • linux 0.11 将 4GB 的虚拟地址空间等分给每个进程(最多 64 个进程),每个进程 64 MB 的空间
    • 设置子进程的 LDT 的代码段和数据段(在 linux 0.11 中二者相同)
    • 设置子进程的页表: copy_page_tables 将父进程的代码段复制到子进程的代码段
  3. 维护子进程的文件描述符表
  4. 将子进程的 tss 和 ldt 写入 gdt 相应的 entry
  5. 进程状态设置为 TASK_RUNNING
  6. 返回子进程的 pid

copy_page_tables


in mm/memory.c

  1. 检查源地址和目标地址空间是否均为 4 MB 对齐
    • 最后一级页表管理的地址空间为 4MB
  2. 计算源地址和目标地址的对应的一级页表项的地址
    • 第一页表基地址均为 0 (__pg_dir__)
  3. 计算复制内存需要的页表数(一级页表项数)
  4. 对占用的所有页表开始进行循环
    • 如果目标地址对应的 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 (表示页面已使用)
  5. invalidate(): 刷新 TLB

init(): 进程 1 执行的初始化操作

  1. setup() 执行 sys_setup 系统调用
  2. open() 系统调用打开 /dev/tty0 设备

setup 系统调用: sys_setup


in kernel/blk_dev/hd.c

callable 强行使该系统调用只能被调用一次

  1. 读取硬盘参数:config.h 未设置则从 BIOS 中读取
  2. 设置每个硬盘的起始扇区号和扇区总数
  3. 检测硬盘是否对 AT 控制器兼容,若不兼容,则将硬盘数据结构清零
  4. 获取每一个兼容的硬盘的分区表信息(第一个扇区)
    • 硬盘的逻辑设备号为 : 0x300, 0x305, ...
    • 通过 bread() 读取数据块
  5. rd_load()
  6. 安装根文件系统: mount_root()

buffer 硬盘缓冲区读写

bread() 读取 buffer_head


in fs/buffer.c

  1. getblk() 获取块号
    1. 通过 get_hash_table() 在 hash_table 中尝试获取
      (不存在主动的释放操作,hash_table 中的 buffer_head 可能引用计数为 0)
      1. 通过 find_buffer() 在缓冲区中尝试查找块, 找不到返回 NULL
        • 遍历 hash_table ,对比每一项的 dev 和 blocknr, 匹配则返回对应的 buffer_head*
        • 如果找不到,则返回 NULL
      2. 如果找到,将相应的块引用计数 +1
      3. wait_on_buffer(): 等待其他进程释放该 buffer_head 的锁
      4. 再次检查块号是否匹配,匹配则返回,否则引用计数 -1
    2. 如果找到则直接返回找到的 buffer_head*
    3. 找不到:
      • 扫描 free_list,找到空闲的 buffer_head
        • 引用计数需等于 0 (buffer 正在被使用的标志是 buffer_head 引用计数为 0)
        • 查找 BADNESS 权重最低的 buffer_head (BADNESS 权重代表的是 buffer_head 的 b_dirt 和 b_lock 标志)
      • 如果没有空闲的 buffer_head
      • 获取空闲块后继续等待其他线程释放 buffer 锁: wait_on_buffer()
      • 检查获取的空闲块,检查不通过则重新尝试获取新的空闲块:repeat
        • 是否空闲
          • 如果获取 buffer 的锁之后发现引用计数不为 0 (再次被其他线程使用),继续重新尝试获取新的空闲块
        • 是否 dirty
          • 如果获取的空闲 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
      • 至此,新的 buffer_head 设置完毕,返回
  2. 如果找不到块号,则 panic()
  3. 如果获取的块未被同步到磁盘 b_uptodate, 则直接返回
  4. 否则(块未被同步到磁盘)
    • 向硬盘发送命令以读取数据到 buffer: ll_rw_block(READ,bh)
    • 等待硬盘将数据写入 buffer :wait_on_buffer(bh);
    • 再次检查获取的块是否同步到磁盘 b_uptodate, 则直接返回
    • 如果依旧未同步到磁盘, 则释放 buffer,并返回 NULL : brelse()

等待硬盘处理 buffer :wait_on_buffer

1
2
3
4
cli();                           // 关中断(只会关闭当前进程的中断,仅 reset 当前进程的 IF flag)
while (bh->b_lock) // 等待 b_lock (此时进程持有锁,由 make_request 加锁)
sleep_on(&bh->b_wait); // 已被线程持有锁则睡眠当前进程(挂起,并将进程加入当前 buffer_head 的进程等待队列)
sti(); // 开中断
  • bh->b_wait 为所有等待当前 buffer_head 的所有进程组成的队列。
  • sleep_on 若没有其他就绪进程,则会调度到进程 0 死循环等待。
  • 这里关中断开中断是为了实现对 b_lock(共享数据) 的原子操作,因为调度到其他进程之后硬盘中断可能会解锁

ll_rw_blk 最后一级读写块设备管理

ll_rw_block(): in kernel/blk_dev/ll_rw_blk.c

  1. 根据 buffer_head 的磁盘设备号 (b_dev) 获取主设备号
    • #define MAJOR(a) (((unsigned)(a))>>8)
    • 检查是否大于 NR_BLK_DEV: 大于则表示非合法硬盘设备
  2. 对设备发出请求:make_request(major,rw,bh);

创建针对硬盘的请求项,并加入请求队列:make_request()

  • 对于请求命令为 READAWRITEA 的情况(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
    • 通过 outb_p 向硬盘发送命令

由 ll_rw_blk 向 hd 发送一次命令,hd 将发送 2 次中断,2 次数据传输(一个块 2 个扇区)

中断返回处理: _hd_interrupt

  • edx 清零,并与 _do_hd 交换, 并 call %edx 跳转到 do_hd 设置的地址

读取请求中断处理程序:read_intr

  • 将硬盘数据读取到硬盘当前请求项的 buffer 中
    • HD_DATA 端口读取 256 个 word (512 字节): port_read(HD_DATA,CURRENT->buffer,256)
  • 调整当前请求项的 buffer 指针地址和扇区号,步进 512 字节(1 个扇区)
  • 如果当前请求项的所有扇区还未读完,则继续设置 do_hdread_intr,等待下一次硬盘中断
  • 否则(当前请求项的所有扇区均已读完),则结束请求:
    • end_request(1): 1 表示 uptodate, 即 buffer_head 中数据与硬盘同步
    • do_hd_request() 继续处理下一个硬盘的请求项:发送命令等待中断

结束当前请求项 end_request

in kernle/blk_dev/blk.h

  1. 关闭设备,对于硬盘而言将磁头挂起:DEVICE_OFF(CURRENT->dev)
  2. 对于当前请求项的 buffer_head
    • 更新 bh->b_uptodate
    • 释放当前请求项 bh 的锁: unlock_buffer(CURRENT->bh) in kernel/blk_dev/ll_rw_blk.c
      • bh->b_lock = 0
      • 唤醒当前请求项 buffer_head 的等待队列中的所有进程:wake_up(&bh->b_wait)
  3. wake_up(&CURRENT->waiting):在 linux 0.11 中 &CURRENT->waiting 实现为空,未使用
  4. 唤醒当前请求项等待队列(等待空闲请求项)中的所有进程:wake_up(&wait_for_request)
  5. 将当前请求项设置为空闲(-1): CURRENT->dev = -1
  6. 选择请求项队列中的下一个请求项作为当前请求项: CURRENT = CURRENT->next

进程调度 sched

in kernel/sched.c

sleep_on(): 将当前进程加到参数指定的等待队列中,并挂起当前进程

  1. 检查当前进程是否为进程 0
    • 是进程 0 则 panic (进程 0 不允许被睡眠)
  2. 将进程加入参数指定的等待队列
    • tmp 指向当前等待队列头中的 task_struct
    • 将睡眠队列头的等待指针指向当前任务 current
  3. 将当前进程 current 状态设置为 TASK_UNINTERRUPTIBLE
  4. schedule() 切换到其他进程
  5. 切换回当前进程之后,状态设置为 0,即就绪态

进程调度 schedule()

  1. 检查 alarm ,唤醒所有收到信号的被挂起的进程: TASK_INTERRUPTIBLE -> TASK_RUNNING
  2. 将就绪态的所有进程中 counter 最大的进程选出来(永远不会遍历到进程 0)
    • 若所有就绪进程 counter 均为 0, 重新刷新所有进程的 counter
  3. 切换到调度选出的进程:switch_to(next)
    • 若当前所有进程均未处于 TASK_RUNNING 状态,则 next = 0, 切换到 0 号进程
    • 而 0 号进程为循环等待(等待其他进程恢复为就绪态)

switch_to 宏定义于 include/linux/sched.h:

  1. 比较 next 和 _current: 如果相同,则不需要切换进程,直接返回
  2. 否则原子性将 _current 切换到 next