大战前的初始化工作[linux源码趣读]

引入

在上一回中,跳转到操作系统的骨架代码main.c中的main方法了, 数一数看,总共也就 20 几行代码。但这的确是操作系统启动流程的全部秘密了

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
31
32
33
34
35
36
37
38
void main(void) {
// 一些参数的取值和计算。包括根设备 ROOT_DEV,之前在汇编语言中获取的各个设备的参数信息 drive_info,以及通过计算得到的内存边界 在上一回中setup.s调用BIOS中断获取并放到约定好的0x90000
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO;
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end &= 0xfffff000;
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;

// 内存初始化 mem_init,中断初始化 trap_init、进程调度初始化 sched_init 等等。我们知道学操作系统知识的时候,其实就分成这么几块来学的,看来在操作系统源码上看,也确实是这么划分的,那我们之后照着源码慢慢品,就好了。
mem_init(main_memory_start,memory_end);
trap_init();
blk_dev_init();
chr_dev_init();
tty_init();
time_init();
sched_init();
buffer_init(buffer_memory_end);
hd_init();
floppy_init();

// 切换到用户态模式 这个 init 函数里会创建出一个进程,设置终端的标准 IO,并且再创建出一个执行 shell 程序的进程用来接受用户的命令,到这里其实就出现了我们熟悉的shell画面
sti();
move_to_user_mode();
if (!fork()) {
init();
}

// 死循环,如果没有任何任务可以运行,操作系统会一直陷入这个死循环无法自拔。
for(;;) pause();
}

主内存初始化mem_init

首先设置了内存的边界,包含memory和buffer,边界的设置是根据不同的内存大小设置的

image-20240415111315159

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define LOW_MEM 0x100000
#define PAGING_MEMORY (15*1024*1024)
#define PAGING_PAGES (PAGING_MEMORY>>12)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
#define USED 100

static long HIGH_MEMORY = 0;
static unsigned char mem_map[PAGING_PAGES] = { 0, };

// start_mem = 2 * 1024 * 1024
// end_mem = 8 * 1024 * 1024
void mem_init(long start_mem, long end_mem)
{
int i;
HIGH_MEMORY = end_mem;
for (i=0 ; i<PAGING_PAGES ; i++)
mem_map[i] = USED;
i = MAP_NR(start_mem);
end_mem -= start_mem;
end_mem >>= 12;
while (end_mem-->0)
mem_map[i++]=0;
}

如何管理内存?

  • 分页管理,一个页4k,页号就是 MAP_NR(addr) (((addr)-LOW_MEM)>>12)

  • mem_map 记录下是否使用

    • 1M一下无管理权限
    • 1M到2M为Buffer,标记为已经使用
    • 2M以上标记未使用
  • 之后的内存申请与释放等骚操作,统统是跟着张大表 mem_map 打交道而已

    image-20240415111529580

举个例子

比如我们在 fork 子进程的时候,会调用 copy_process 函数来复制进程的结构信息,其中有一个步骤就是要申请一页内存,用于存放进程结构信息 task_struct。该内存的申请就是选择 mem_map 中首个空闲页面,并标记为已使用。

1
2
3
4
5
6
int copy_process(...) {
struct task_struct *p;
...
p = (struct task_struct *) get_free_page();
...
}

中断初始化trap_init

当你的计算机刚刚启动时,你按下键盘是不生效的,但是过了一段时间后,再按下键盘就有效果了。如何首先的呢,多久会生效呢?

image-20240415112231509

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
void trap_init(void) {
int i;
// set 了一堆 trap_gate

set_trap_gate(0,&divide_error);
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
// 又 set 了一堆 system_gate
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
// 又又批量 set 了一堆 trap_gate 暂时的,后面各个硬件初始化时要重新设置好这些中断,把暂时的这个给覆盖掉 例如键盘中断0x21会在tty_init设置
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);
set_trap_gate(39,&parallel_interrupt);
}

TIPS:set_trap_gateset_system_gate什么关系?

这个 trap 与 system 的区别仅仅在于,设置的中断描述符的特权级不同,前者是 0(内核态),后者是 3(用户态),这块展开将会是非常严谨的、绕口的、复杂的特权级相关的知识,不明白的话先不用管,就理解为都是设置一个中断号和中断处理程序的对应关系就好了。

1
2
3
4
5
#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)

_set_gate就是设置一些列的硬中断处理函数,执行过后,IDT指向的结构变成了:

image-20240415112744376

什么时候开始中断呢?sti(); 也就是这时候开始,按下键盘可以有反应了。

块设备请求项初始化 blk_dev_init

用于让我们能够从硬盘读取数据到磁盘,读取块设备与内存缓冲区之间的桥梁

1
2
3
4
5
6
7
void blk_dev_init(void) {
int i;
for (i=0; i<32; i++) {
request[i].dev = -1;
request[i].next = NULL;
}
}

其中request请求,代表着一次读盘请求

1
2
3
4
5
6
7
8
9
10
11
struct request {
int dev; // 表示设备号,-1 就表示空闲。
int cmd; /* READ or WRITE */
int errors; // 表示操作时产生的错误次数。
unsigned long sector; // 表示起始扇区。
unsigned long nr_sectors; // 表示扇区数。
char * buffer; // 表示数据缓冲区,也就是读盘之后的数据放在内存中的什么位置。
struct task_struct * waiting; // 是个 task_struct 结构,这可以表示一个进程,也就表示是哪个进程发起了这个请求。
struct buffer_head * bh; // 是缓冲区头指针,这个后面讲完缓冲区就懂了,因为这个 request 是需要与缓冲区挂钩的
struct request * next; // 指向了下一个请求项
};

这个 request 结构可以完整描述一个读盘操作。然后那个 request 数组就是把它们都放在一起,并且它们又通过 next 指针串成链表。

image-20240415113321223

如何添加到request链

sys_read核心代码

1
2
3
4
5
6
7
8
9
// 读哪个fd(可以找到inode,进而找到磁盘位置)下多少数据count,buf:读到哪里
int sys_read(unsigned int fd,char * buf,int count) {
struct file * file = current->filp[fd];
struct m_inode * inode = file->f_inode;
// 校验 buf 区域的内存限制
verify_area(buf,count);
// 仅关注目录文件或普通文件
return file_read(inode,file,buf,count);
}

image-20240415113402051

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
int file_read(struct m_inode * inode, struct file * filp, char * buf, int count) {
int left,chars,nr;
struct buffer_head * bh;
left = count;
while (left) {
if (nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE)) {
if (!(bh=bread(inode->i_dev,nr)))
break;
} else
bh = NULL;
nr = filp->f_pos % BLOCK_SIZE;
chars = MIN( BLOCK_SIZE-nr , left );
filp->f_pos += chars;
left -= chars;
if (bh) {
char * p = nr + bh->b_data;
while (chars-->0)
put_fs_byte(*(p++),buf++);
brelse(bh);
} else {
while (chars-->0)
put_fs_byte(0,buf++);
}
}
inode->i_atime = CURRENT_TIME;
return (count-left)?(count-left):-ERROR;
}

接着就是不断的读取数据到我们的buf中,从代码中可以看到是从buffer_head * bh中读的,这是什么?

1
2
3
4
5
6
7
8
9
10
11
struct buffer_head * bread(int dev,int block) { // block read
struct buffer_head * bh = getblk(dev,block);// 申请了一个内存中的缓冲块
if (bh->b_uptodate)
return bh;
ll_rw_block(READ,bh); // 读取数据到内存: 创建一个request,添加到queue
wait_on_buffer(bh); // 阻塞到一直读取完成
if (bh->b_uptodate)
return bh;
brelse(bh);
return NULL;
}

其中 getblk 先申请了一个内存中的缓冲块,然后 ll_rw_block 负责把数据读入这个缓冲块,进去继续看。

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
void ll_rw_block(int rw, struct buffer_head * bh) {
...
make_request(major,rw,bh);
}

static void make_request(int major,int rw, struct buffer_head * bh) {
...
if (rw == READ)
req = request+NR_REQUEST;
else
req = request+((NR_REQUEST*2)/3);
/* find an empty request */
while (--req >= request)
if (req->dev<0)
break;
...
// 创建一个读请求,我要读哪个磁盘哪个扇区 读到哪
/* fill up the request-info, and add it to the queue */
req->dev = bh->b_dev;
req->cmd = rw;
req->errors=0;
// 块是逻辑概念 扇区是最小物理存储单位 假设每个块占用2个扇区
req->sector = bh->b_blocknr<<1;
req->nr_sectors = 2;
req->buffer = bh->b_data;
req->waiting = NULL;
req->bh = bh;
req->next = NULL;
add_request(major+blk_dev,req);
}
  • ll_rw_block会往刚刚的设备的请求项链表 request[32] 中添加一个请求项,作为访问块设备和内存缓冲区之间的桥梁
  • 请求队列中的请求由I/O调度器进行管理。I/O调度器负责确定请求的执行顺序,优化整体的磁盘性能和响应时间。Linux提供了多种I/O调度器,如CFQ(完全公平队列)、Deadline、NOOP等,不同的调度器适用于不同类型的工作负载和硬件配置。

控制台初始化 tty_init

1
2
3
4
5
void tty_init(void)
{
rs_init(); // 串口中断的开启,以及设置对应的中断处理程序,串口在我们现在的 PC机上很少用到
con_init();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
void con_init(void) {
...
if (ORIG_VIDEO_MODE == 7) {
...
if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {...}
else {...}
} else {
...
if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10) {...}
else {...}
}
...
}

非常多的 if else。为了应对不同的显示模式,来分配不同的变量值,那如果我们仅仅找出一个显示模式,这些分支就可以只看一个了。

如何显示一个字符

啥是显示模式呢?那我们得简单说说显示,一个字符是如何显示在屏幕上的呢?换句话说,如果你可以随意操作内存和 CPU 等设备,你如何操作才能使得你的显示器上,显示一个字符‘a’呢?

Image

内存中有这样一部分区域,是和显存映射的。啥意思,就是你往上图的这些内存区域中写数据,相当于写在了显存中。而往显存中写数据,就相当于在屏幕上输出文本了。mov [0xB8000],'h'

image-20240415115222575

代码

假设显示模式是我们现在的这种文本模式:

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
#define ORIG_X          (*(unsigned char *)0x90000)
#define ORIG_Y (*(unsigned char *)0x90001)
void con_init(void) {
register unsigned char a;
// 第一部分 获取显示模式相关信息 之前BIOS中断获取了放到0x90000
video_num_columns = (((*(unsigned short *)0x90006) & 0xff00) >> 8);
video_size_row = video_num_columns * 2;
video_num_lines = 25;
video_page = (*(unsigned short *)0x90004);
video_erase_char = 0x0720;
// 第二部分 显存映射的内存区域
video_mem_start = 0xb8000;
video_port_reg = 0x3d4;
video_port_val = 0x3d5;
video_mem_end = 0xba000;
// 第三部分 滚动屏幕操作时的信息
origin = video_mem_start;
scr_end = video_mem_start + video_num_lines * video_size_row;
top = 0;
bottom = video_num_lines;
// 第四部分 定位光标(同样是0x90000)并开启键盘中断
gotoxy(ORIG_X, ORIG_Y);
set_trap_gate(0x21,&keyboard_interrupt); // 键盘中断函数
outb_p(inb_p(0x21)&0xfd,0x21);
a=inb_p(0x61);
outb_p(a|0x80,0x61);
outb(a,0x61);
}
1
2
3
4
5
6
static inline void gotoxy(unsigned int new_x,unsigned int new_y) {
...
x = new_x;
y = new_y;
pos = origin + y*video_size_row + (x<<1);
}

x 表示光标在哪一列,y 表示光标在哪一行,pos 表示根据列号和行号计算出来的内存指针,也就是往这个 pos 指向的地址处写数据,就相当于往控制台的 x 列 y 行处写入字符了

键盘中断

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
_keyboard_interrupt:
...
call _do_tty_interrupt
...

void do_tty_interrupt(int tty) {
copy_to_cooked(tty_table+tty);
}

void copy_to_cooked(struct tty_struct * tty) {
...
tty->write(tty);
...
}

// 控制台时 tty 的 write 为 con_write 函数 就是往pos写值
void con_write(struct tty_struct * tty) {
...
__asm__("movb _attr,%%ah\n\t"
"movw %%ax,%1\n\t"
::"a" (c),"m" (*(short *)pos)
:"ax");
pos += 2;
x++; // 当然这里后面还需要判断x是否到达video_num_columns进行其他处理
...
}

至此我们可以实现显示功能了,本质就是往内存中pos位置写值,那回车换行删除滚屏清屏等操作,其实底层都操作x y pos,然后修改内存就行,并对外暴露小功能函数

在此之后,内核代码就可以用它来方便地在控制台输出字符啦!这在之后内核想要在启动过程中告诉用户一些信息,以及后面内核完全建立起来之后,由用户用 shell 进行操作时手动输入命令,都是可以用到这里的代码的!

printf 这样的库函数,在屏幕上输出信息,同时支持换行和滚屏等友好设计,这些都是 tty_init 初始化,以及其对外封装的小功能函数,来实现的。

时间初始化 time_init

  • 直接联网
  • 如果没有网呢?
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
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \
inb_p(0x71); \
})

#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)

static void time_init(void) {
struct tm time;
do {
time.tm_sec = CMOS_READ(0);
time.tm_min = CMOS_READ(2);
time.tm_hour = CMOS_READ(4);
time.tm_mday = CMOS_READ(7);
time.tm_mon = CMOS_READ(8);
time.tm_year = CMOS_READ(9);
} while (time.tm_sec != CMOS_READ(0));
BCD_TO_BIN(time.tm_sec); // BCD编码转二进制编码
BCD_TO_BIN(time.tm_min);
BCD_TO_BIN(time.tm_hour);
BCD_TO_BIN(time.tm_mday);
BCD_TO_BIN(time.tm_mon);
BCD_TO_BIN(time.tm_year);
time.tm_mon--;
startup_time = kernel_mktime(&time); // 计算从1970年1月1日到现在的秒数 作为开机时间
}
1
2
3
4
5
// 对一个端口先 out 写一下,再 in 读一下。
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \
inb_p(0x71); \
})

这是 CPU 与外设交互的一个基本玩法,CPU 与外设打交道基本是通过端口,往某些端口写值来表示要这个外设干嘛,然后从另一些端口读值来接受外设的反馈。

以磁盘为例:读硬盘就是,往除了第一个以外的后面几个端口写数据,告诉要读硬盘的哪个扇区,读多少。然后再从 0x1F0 端口一个字节一个字节的读数据。这就完成了一次硬盘读操作。

image-20240415134102265

  1. 在 0x1F2 写入要读取的扇区数
  2. 在 0x1F3 ~ 0x1F6 这四个端口写入计算好的起始 LBA 地址
  3. 在 0x1F7 处写入读命令的指令号
  4. 不断检测 0x1F7 (此时已成为状态寄存器的含义)的忙位
  5. 如果第四步骤为不忙,则开始不断从 0x1F0 处读取数据到内存指定位置,直到读完

当然,读取硬盘的这个无脑循环,可以 CPU 直接读取并做写入内存的操作,这样就会占用 CPU 的计算资源。

也可以交给 DMA 设备去读,解放 CPU,但和硬盘的交互,通通都是按照硬件手册上的端口说明,来操作的,实际上也是做了一层封装。

而在时间读取中,就是和CMOS(主板上的一个可读写的 RAM)这个外设打交道,让他告诉我们时间

CMOS:计算机主板上的一小块芯片,这块芯片使用CMOS技术来存储BIOS(基本输入输出系统)设置等基础系统信息。这些信息包括系统时间、硬件配置设置等,这部分内存被称为CMOS RAM或非易失性BIOS内存。由于CMOS技术的低功耗特点,即使在计算机断电后,CMOS内存也能通过一个小电池供电维持数据存储。这使得计算机在下一次开机时可以记住之前的配置设置。

进程调度初始化 sched_init

这方法可了不起,因为它就是多进程的基石!

终于来到了兴奋的时刻,是不是很激动?不过先别激动,这里只是进程调度的初始化,也就是为进程调度所需要用到的数据结构做个准备,真正的进程调度还需要调度算法、时钟中断等机制的配合。

当然,对于理解操作系统,流程和数据结构最为重要了,而这一段作为整个流程的起点,以及建立数据结构的地方,就显得格外重要了。

1
2
3
4
5
6
void sched_init(void) {
set_tss_desc(gdt+4, &(init_task.task.tss));
set_ldt_desc(gdt+5, &(init_task.task.ldt));
...
}
往后加了两项:TSS和LDT 这是代表一个线程0,如果由其他线程还要继续加

image-20240415134738723

TSS

TSS 在计算机中代表任务状态段 (Task State Segment)。它是一种数据结构,用于存储处理器在任务切换时必须保存的特定任务的状态信息(寄存器的值)。每个任务都有一个对应的 TSS。当操作系统执行任务切换时,它会使用 TSS 来保存当前任务的状态,并加载新任务的状态。

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
struct tss_struct{
long back_link;
long esp0;
long ss0;
long esp1;
long ss1;
long esp2;
long ss2;
long cr3;
long eip;
long eflags;
long eax, ecx, edx, ebx;
long esp;
long ebp;
long esi;
long edi;
long es;
long cs;
long ss;
long ds;
long fs;
long gs;
long ldt;
long trace_bitmap;
struct i387_struct i387;
};

LDT

LDT 叫局部描述符表(Local Descriptor Table),是与 GDT 全局描述符表相对应的,内核态的代码用 GDT 里的数据段和代码段,而用户进程的代码用每个用户进程自己的 LDT 里得数据段和代码段。

image-20240415135055576

task[]

image-20240415141329918

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct desc_struct {
unsigned long a,b; // 段的基址、段的界限(大小)、类型(如代码段、数据段)、特权级(DPL)、存在位(P)、以及其他一些硬件和操作系统使用的标志。
}

struct task_struct * task[64] = {&(init_task.task), };

void sched_init(void) {
...
int i;
struct desc_struct * p;
p = gdt+6;
for(i=1;i<64;i++) {
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
...
}

这个 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
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];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
struct desc_struct ldt[3]; // 上面的LDT指向这里
/* tss for this task */
struct tss_struct tss; // 上面的LDT指向这里
};

接下来告诉通过寄存器告诉系统当前任务的LDT,TSS的位置(内存里每个线程都有LDT,TSS,当前线程的是哪一个),初始指向了第0个

1
2
3
4
5
6
void sched_init(void) {
...
ltr(0);
lldt(0);
...
}

image-20240415135915887

最后

1
2
3
4
5
6
7
8
9
10
11
12
void sched_init(void) {
...
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt); // 时钟中断


outb(inb_p(0x21)&~0x01,0x21);
set_system_gate(0x80,&system_call); // 系统调用
...
}

四行端口读写代码,两行设置中断代码。

端口读写我们已经很熟悉了,就是 CPU 与外设交互的一种方式,之前讲硬盘读写以及 CMOS 读写时,已经接触过了。

而这次交互的外设是一个可编程定时器的芯片,这四行代码就开启了这个定时器,之后这个定时器变会持续的、以一定频率的向 CPU 发出中断信号

  1. 第一个就是时钟中断,中断号为 0x20,中断处理程序为 timer_interrupt。那么每次定时器向 CPU 发出中断后,便会执行这个函数。

    • 更新系统时间或运行时间计数器。
    • 检查和执行定时任务或超时事件(网络通信和用户交互)。
    • 对当前运行的进程或线程的运行时间进行计量,以便进行任务调度。

    是操作系统主导进程调度的一个关键!

  2. 第二个设置的中断叫系统调用 system_call,中断号是 0x80,这个中断又是个非常非常非常非常非常非常非常重要的中断,所有用户态程序想要调用内核提供的方法,都需要基于这个系统调用来进行。

    Java 程序员写一个 read,底层会执行汇编指令 int 0x80,这就会触发系统调用这个中断,最终调用到 Linux 里的 sys_read 方法。

中断号 中断处理函数
0 ~ 0x10 (trap_init) trap_init 里设置的一堆
0x20 timer_interrupt
0x21 (tty_init) keyboard_interrupt
0x80 system_call

找到些感觉没,有没有越来越发现,操作系统有点靠中断驱动的意思,各个模块不断初始化各种中断处理函数,并且开启指定的外设开关,让操作系统自己慢慢“活”了起来,逐渐通过中断忙碌于各种事情中,无法自拔。

小结

  1. 我们往GDT中写入了TSS和LDT,作为每一个线程的任务状态和局部描述符表 初始: init_task.task.tss init_task.task.ldt
  2. 初始化了task_struct [] ,并且第一个位置init_task.task
  3. 设置了时钟中断0x20和系统调用0x80

缓冲区初始化 buffer_init

在讲通过文件系统来读取硬盘文件时,都需要使用和弃用这个缓冲区里的内容,缓冲区即是用户进程的内存和硬盘之间的桥梁。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
extern int end; // 内核的结束 缓冲区的开始
struct buffer_head * start_buffer = (struct buffer_head *) &end;
struct buffer_head {
// 缓冲区的状态
unsigned long b_state;
// 缓冲区的数据指针
char *b_data;
// 块设备指针 + 块号是hash的key
// 块设备指针
struct block_device *b_dev;
// 块号
unsigned long b_blocknr;
// 用于连接缓冲区链表的指针
struct buffer_head *b_this_page;
// 用于连接空闲缓冲区链表的指针
struct buffer_head *b_this_free;
// 用于连接 hash 链表的指针
struct buffer_head *b_next;
// 缓冲区的引用计数
atomic_t b_count;
};

void buffer_init(long buffer_end) {
struct buffer_head * h = start_buffer;
void * b = (void *) buffer_end;
while ( (b -= 1024) >= ((void *) (h+1)) ) {
h->b_dev = 0;
h->b_dirt = 0;
h->b_count = 0;
h->b_lock = 0;
h->b_uptodate = 0;
h->b_wait = NULL;
h->b_next = NULL;
h->b_prev = NULL;
h->b_data = (char *) b;
h->b_prev_free = h-1;
h->b_next_free = h+1;
h++;
}
h--;
free_list = start_buffer;
free_list->b_prev_free = h;
h->b_next_free = free_list;
for (int i=0;i<307;i++)
hash_table[i]=NULL;
}
  • 缓冲头记录下头信息,并通过pre next串联起来
  • b_data指向真正的数据区

image-20240415142819120

  • 读取块设备的数据(硬盘中的数据),需要先读到缓冲区中,如果缓冲区已有了,就不用从块设备读取了,直接取走
  • 怎么知道缓冲区已经有了要读取的块设备中的数据呢? 遍历效率太低,直接hash
  • key是什么? (设备号^逻辑块号) Mod 307 冲突用链表解决
  • 如何实现淘汰呢?哈希表 + 双向链表实现LRU

image-20240415143025425

硬盘初始化 hd_init

  • hd_init硬盘初始化,我们不得不看
  • floppy_init软盘初始化,现在软盘几乎都被淘汰了,计算机中也没有软盘驱动器了,所以这个我们完全可以不看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 */
};

void hd_init(void) {
//每个块设备执行读写请求都有自己的函数实现,在上层看来都是一个统一函数 request_fn 即可,具体实现各有不同,对于硬盘来说,这个实现就是 do_hd_request 函数
blk_dev[3].request_fn = do_hd_request;
set_intr_gate(0x2E,&hd_interrupt); // 硬盘中断的处理函数

// 允许硬盘控制器发送中断请求信号
// 我们向来是不深入硬件细节,知道往这个端口里写上这些数据,导致硬盘开启了中断,即可
outb_p(inb_p(0x21)&0xfb,0x21);
outb(inb_p(0xA1)&0xbf,0xA1);
}
  1. 往某些 IO 端口上读写一些数据,表示开启它;
  2. 然后再向中断向量表中添加一个中断,使得 CPU 能够响应这个硬件设备的动作;
  3. 最后再初始化一些数据结构来管理。不过像是内存管理可能结构复杂些,外设的管理,相对就简单很多了。

操作系统就是一个靠中断驱动的死循环而已,如果不发生任何中断,操作系统会一直在一个死循环里等待。换句话说,让操作系统工作的唯一方式,就是触发中断。

硬盘端口

端口对应硬盘控制器上的寄存器

端口
0x1F0 数据寄存器 数据寄存器
0x1F1 错误寄存器 特征寄存器
0x1F2 扇区计数寄存器 扇区计数寄存器
0x1F3 扇区号寄存器或 LBA 块地址 0~7 扇区号或 LBA 块地址 0~7
0x1F4 磁道数低 8 位或 LBA 块地址 8~15 磁道数低 8 位或 LBA 块地址 8~15
0x1F5 磁道数高 8 位或 LBA 块地址 16~23 磁道数高 8 位或 LBA 块地址 16~23
0x1F6 驱动器/磁头或 LBA 块地址 24~27 驱动器/磁头或 LBA 块地址 24~27
0x1F7 命令寄存器或状态寄存器 命令寄存器

那读硬盘就是,往除了第一个以外的后面几个端口写数据,告诉要读硬盘的哪个扇区,读多少。然后再从 0x1F0 端口一个字节一个字节的读数据。这就完成了一次硬盘读操作。

如果觉得不够具体,那来个具体的版本。

  • 1. 在 0x1F2 写入要读取的扇区数

  • 2. 在 0x1F3 ~ 0x1F6 这四个端口写入计算好的起始 LBA 地址

  • 3. 在 0x1F7 处写入读命令的指令号

  • 4. 不断检测 0x1F7 (此时已成为状态寄存器的含义)的忙位

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect,
    unsigned int head,unsigned int cyl,unsigned int cmd,
    void (*intr_addr)(void)) {
    ...
    do_hd = intr_addr;
    outb_p(hd_info[drive].ctl,HD_CMD);
    port = 0x1f0;
    outb_p(hd_info[drive].wpcom>>2,++port);
    outb_p(nsect,++port);
    outb_p(sect,++port);
    outb_p(cyl,++port);
    outb_p(cyl>>8,++port);
    outb_p(0xA0|(drive<<4)|head,++port);
    outb(cmd,++port);
    }