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()
      • 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

进程调度 schedule()

in kernel/sched.c

  1. 检查 alarm ,唤醒所有收到信号的被挂起的进程: TASK_INTERRUPTIBLE -> TASK_RUNNING
  2. 将就绪态的所有进程中 counter 最大的进程选出来(永远不会遍历到进程 0)
    • 若所有就绪进程 counter 均为 0, 重新刷新所有进程的 counter
  3. 切换到调度选出的进程:switch_to(next)

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

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

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 系统调用 in kernel/blk_dev/hd.c
    • callable 强行使该函数只能被调用一次
    1. 读取硬盘参数:config.h 未设置则从 BIOS 中读取
    2. 设置每个硬盘的起始扇区号和扇区总数
    3. 检测硬盘是否对 AT 控制器兼容,若不兼容,则将硬盘数据结构清零
    4. 获取每一个兼容的硬盘的分区表信息(第一个扇区)
      • 通过 bread() 读取数据块 in fs/buffer.c
        1. getblk() 获取块号
          1. 通过 get_hash_table() 在 hash_table 中尝试获取
            1. 通过 find_buffer() 在缓冲区中尝试查找块, 找不到返回 NULL
              • 遍历 hash_table ,对比每一项的 dev 和 blocknr, 匹配则返回对应的 buffer_head*
              • 如果找不到,则返回 NULL
            2. 如果找到,将相应的块引用计数 +1
            3. wait_on_buffer()
            4. 再次检查块号是否匹配,匹配则返回,否则引用计数 -1
          2. 如果找到则直接返回找到的 buffer_head*
          3. 找不到:……