X86_Linux_0.11--设备环境初始化及激活进程0
这章从main函数开始介绍,main函数执行之前的流程已经全部讲完了。
本章讲的主要是设备初始化和激活进程0两个部分,实际上设备初始化的内容对于理解学习操作系统并不重要,一带而过即可。
设置根设备、硬盘
内核首先初始化根设备和硬盘,使用bootsect中写入机器系统数据0x901FC的根设备(bootsect做的最后一件事)为软盘的信息,设置软盘为根设备,并用起自0x90080的32字节的机器系统数据的硬盘参数表设置内核中的硬盘信息drive_info。代码就不介绍了,没啥意义,总之就是利用之前bootsec和setup程序留下的信息,初始化根设备和硬盘,学操作系统不需要在这个地方浪费时间。
规划物理内存格局,设置缓冲区、虚拟盘、主内存
接下来内核要设置缓冲区、虚拟盘、主内存。
Linus是这么规划的:除内核代码和数据所占的内存空间之外,其余的物理内存主要分为三个部分:主内存区、缓冲区和虚拟盘。主内存区是进程代码运行的空间,也包括内核管理进程的数据结构;缓冲区主要是作为主机与外设进行数据交换的中转站;虚拟盘区是一个可选的区域,如果使用虚拟盘,就可以讲外设上的数据先复制进虚拟盘区,然后加以使用(内存中操作数据的速度远高于外设,这样可以提高系统执行效率)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//代码路径:init/main.c:
…
#define EXT_MEM_K (*(unsigned short *)0x90002) //从1 MB开始的扩展内存(KB)数
…
void main(void)
{
…
memory_end= (1<<20) + (EXT_MEM_K<<10); //1 MB + 扩展内存(MB)数,即内存总数
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; //缓冲区之后就是主内存
…
}
内存格局的划分还要根据内存大小来设置,如果内存大小小于6MB,那么缓冲区的结束位置就是1MB,如果内存大小大于6MB小于12MB,那么缓冲区的结束位置在2MB,如果内存大小大于12MB,那么缓冲区结束位置在4MB。主内存的起始位置就是缓冲区的结束位置。
设置虚拟盘空间并初始化
《Linux内核设计的艺术》一书中提供的Linux源码是通过makefill文件中的一个标志位来配置是否使用虚拟盘的,本书默认使用2MB的虚拟盘。
虚拟盘的位置设置在缓冲区的末尾,也就是紧贴着缓冲区的2MB空间将被设置为虚拟盘,主内存的起始位置后移2MB的空间。代码就不看了,感兴趣可以自己阅读。
内存管理结构mem_map初始化
目前为止,主内存区和缓冲区的位置和大小已经确定了,于是系统开始调用mem_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
//代码路径:init/main.c:
void main(void) {
…
mem_init(main_memory_start,memory_end);
…
}
//代码路径:mm/memory.c:
…
#define LOW_MEM 0x100000 //1 MB
#define PAGING_MEMORY (15*1024*1024)
#define PAGING_PAGES (PAGING_MEMORY>>12) //15 MB的页数
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
#define USED 100
…
static long HIGH_MEMORY= 0;
…
static unsigned char mem_map [PAGING_PAGES]= {0,};
…
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); //start_mem为6 MB(虚拟盘之后)
end_mem -= start_mem;
end_mem >>= 12; //16 MB的页数
while (end_mem-->0)
mem_map[i++]=0;
}
这段代码将1MB以上的分页进行管理,记录页面的使用次数,将缓冲区和虚拟盘(1MB~6MB)对应的mem_map
标记为已使用,将主内存区标记为未使用。
为什么1MB以内的内存区域不进行这种管理? 这是因为设计者需要让内核和用户程序使用了两种不同的内存管理方式。内核使用分页管理方法,线性地址就等于物理地址,也就是内核可以直接获得物理地址。而用户程序不允许这样做,对于用户进程,线性地址和物理地址的差别非常大,没有可以递推的关系,因此用户进程也就推算不到真正的物理地址,也就无法访问内核了,当然也访问不了其它用户进程,但是内核由于一一映射的线性地址因此可以直接访问所有物理内存。1MB以上的内存空间都是留给用户使用的,因此采用上述这种专门的分页管理办法,而对内核当然不能这么用。
异常处理类中断服务程序挂接
之前的setup程序破坏了BIOS建立的中断服务体系,head程序虽然建立了IDT,但所有的中断向量都指向了同一个中断服务程序。因此,内核为了能够响应异常和中断,现在需要重新建立异常处理以及中断服务体系(不需要区分异常和中断)。
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
47
48
49
50
51
52
//代码路径:init/main.c:
void main(void) {
…
trap_init();
…
}
//代码路径:kernel/traps.c:
void trap_init(void) {
int i;
set_trap_gate(0,÷_error);//除零错误
set_trap_gate(1,&debug); //单步调试
set_trap_gate(2,&nmi); //不可屏蔽中断
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); //无效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); //协处理器错误
for (i=17;i<48;i++) //都先挂接好,中断服务程序函数名初
set_trap_gate(i,&reserved); //始化为保留
set_trap_gate(45,&irq13); //协处理器
outb_p(inb_p(0x21)&0xfb,0x21); //允许IRQ2中断请求
outb(inb_p(0xA1)&0xdf,0xA1); // 允许IRQ3中断请求
set_trap_gate(39,¶llel_interrupt); //并口
}
//代码路径:include\asm\system.h:
…
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__("movw %%dx,%%ax\n\t" \ //将edx的低字赋值给eax的低字
"movw %0,%%dx\n\t" \ //%0对应第二个冒号后的第1行的"i"
"movl %%eax,%1\n\t" \ //%1对应第二个冒号后的第2行的"o"
"movl %%edx,%2" \ //%2对应第二个冒号后的第3行的"o"
: \ //这个冒号后面是输出,下面冒号后面
//是输入
: "i" ((short) (0x8000 + (dpl<<13) + (type<<8))), \ //立即数
"o" (*((char *) (gate_addr))), \ //中断描述符前4个字节的地址
"o" (*(4 + (char *) (gate_addr))), \ //中断描述符后4个字节的地址
"d" ((char *) (addr)),"a" (0x00080000)) //"d"对应edx,"a"对应eax
…
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)
看着真麻烦,主要就是设置中断向量对应的中断服务程序(设置IDT),设置了中断号0~16、17~47,并设置允许IRQ2和IRQ3中断,细节就自己扣吧。
初始化块设备请求项结构
Linux 0.11将外设分为块设备、字符设备两类。具体请你RTFW。硬盘、软盘等属于块设备,tty属于字符设备。
进程想要和块设备进行通信就必须经过主机内存中的缓冲区。之前提到的request[32]
就是操作系统管理缓冲区中缓冲块与块设备上逻辑块之间读写关系的数据结构。
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
//代码路径:init/main.c:
void main(void)
{
…
blk_dev_init();
…
}
//代码路径:kernel/blk_dev/blk.h:
…
#define NR_REQUEST 32
struct request {
int dev; /* -1 if no request */
int cmd; /* READ or WRITE */
int errors;
unsigned long sector;
unsigned long nr_sectors;
char * buffer;
struct task_struct * waiting;
struct buffer_head * bh;
struct request * next; //说明request可以构成链表
};
…
//代码路径:kernel/blk_dev/ll_rw_block.c:
…
struct request request[NR_REQUEST]; //数组链表
…
void blk_dev_init(void)
{
int i;
for (i=0;i<NR_REQUEST;i++) {
request[i].dev= -1; //设置为空闲
request[i].next= NULL; //互不挂接
}
}
request[32]是一个由数组构成的链表;request[i].dev = −1说明了这个请求项还没有具体对应哪个设备,这个标志将来会被用来判断对应该请求项的当前设备是否空闲;request[i]. next =NULL说明这时还没有形成请求项队列。
与建立人机交互界面相关的外设的中断服务程序挂接
这里主要就是对tty设备进行初始化。tty设备,简单理解就是串口、键盘、显示器等人机交互设备。
对串口进行初始化
1
2
3
4
5
6
7
8
9
//代码路径:kernel/chr_dev/serial.c:
void rs_init(void)
{
set_intr_gate(0x24,rs1_interrupt); //设置串行口1中断,参看2.5
set_intr_gate(0x23,rs2_interrupt); //设置串行口2中断
init(tty_table[1].read_q.data); //初始化串行口1
init(tty_table[2].read_q.data); //初始化串行口2
outb(inb_p(0x21)&0xE7,0x21); //允许IRQ3,IRQ4
}
两个串行口中断处理程序与IDT的挂接函数set_intr_gate()与之前介绍过的set_trap_gate()函数类似,可参看前面对set_trap_gate()函数的讲解。它们的差别是set_trap_gate()函数的type是15(二进制的1111,代表异常),而set_intr_gate()的type是14(二进制的1110,代表中断)。
这没啥好说的,就把两个串口对应的IDT的中断服务程序字段设置一下,然后初始化串口,不必深究,这都不是重点。
设置显示器
这里要根据机器系统数据提供的显卡是“单色”还是“彩色”来设置配套信息。Linux0.11那么古老,就当是单色的了。不介绍了没意思。
设置键盘
对键盘的设置是首先将键盘对应的IDT的中断服务程序字段填充对应的中断服务程序地址,然后取消8259A对键盘中断的屏蔽,允许IRQ1发送中断信号。不解释了,没意思。
设置开机启动时间
开机启动时间是大部分与时间相关的计算的基础。操作系统中的一些程序的运算需要时间参数,很多事务的处理也需要用到时间,比如文件修改的时间、文件最近访问的时间、i节点自身的修改时间等,这些都是基于开机启动时间推断出来的。
具体步骤是:主板上有一个RTC(或者类似的玩意),系统通过调用time_init()
这个函数去获取数据,包括年月日时分秒等等,然后将他们整合一下就是了。不解释了。
初始化进程0
前面的都没啥意义,对学习操作系统没什么帮助,现在要介绍的就比较重要了。
进程0是Linux操作系统中运行的第一个进程,也是Linux操作系统父子进程创建机制的第一个父进程。
初始化进程0主要有三个方面的工作:
- 系统初始化进程0。这包括将进程0和task_struct中的LDT、TSS和GDT相挂接,对GDT、task数组已经进程调度相关的寄存器进行初始化设置。
- 由于需要进行多任务轮转,因此还需要对时钟中断进行设置,以便进程0运行之后,进程0创造出来的其它进程能进行轮转调度。
- 进程0还需要使用系统调用,其它进程也多多少少需要与内核进行交互,因此这里还需要建立系统调用机制。
这三点都需要使用sched_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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//代码路径:init/main.c:
void main(void)
{
…
sched_init();
…
}
//代码路径:kernel/sched.c:
…
#define LATCH (1193180/HZ) //每个时间片的振荡次数
…
union task_union { // task_struct与内核栈的共用体
struct task_struct task;
char stack[PAGE_SIZE]; // PAGE_SIZE是4 KB
};
static union task_union init_task= {INIT_TASK,};//进程0的task_struct
…
//初始化进程槽task[NR_TASKS]的第一项为进程0,即task[0]为进程0占用
struct task_struct * task[NR_TASKS]= {&(init_task.task), };
…
void sched_init(void)
{
int i;
struct desc_struct * p;
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
set_tss_desc(gdt + FIRST_TSS_ENTRY,&(init_task.task.tss));//设置TSS0
set_ldt_desc(gdt + FIRST_LDT_ENTRY,&(init_task.task.ldt));//设置LDT0
p= gdt + 2+FIRST_TSS_ENTRY; //从GDT的6项,即TSS1开始向上全部清零,并且将进程槽从
for(i=1;i<NR_TASKS;i++) { //1往后的项清空。0项为进程0所用
task[i]= NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
/* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl;andl $0xffffbfff,(%esp);popfl");
ltr(0); //重要!将TSS挂接到TR寄存器
lldt(0); //重要!将LDT挂接到LDTR寄存器
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 *///设置定时器
outb_p(LATCH & 0xff , 0x40); /* LSB */ //每10毫秒一次时钟中断
outb(LATCH >> 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt); //重要!设置时钟中断,进程调度的基础
outb(inb_p(0x21)&~0x01,0x21); //允许时钟中断
set_system_gate(0x80,&system_call); //重要!设置系统调用总入口
}
//代码路径:include\linux\sched.h:// //嵌入汇编参看trap_init的注释
…
#define FIRST_TSS_ENTRY 4 //参看图2-15中GDT的4项,即TSS0入口
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY + 1)//同上,5项即LDT0入口
#define _TSS(n) ((((unsigned long) n)<<4) + (FIRST_TSS_ENTRY<<3))
#define _LDT(n) ((((unsigned long) n)<<4) + (FIRST_LDT_ENTRY<<3))
#define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n)))
#define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))
…
//代码路径:include\asm\system.h:
…
#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)
…
#define _set_tssldt_desc(n,addr,type) \ //嵌入汇编参看trap_init的注释
__asm__ ("movw $104,%1\n\t" \ //将104,即1101000存入描述符的第1、2字节
"movw %%ax,%2\n\t" \ //将tss或ldt基地址的低16位存入描述符的第
//3、4字节
"rorl $16,%%eax\n\t" \ //循环右移16位,即高、低字互换
"movb %%al,%3\n\t" \ //将互换完的第1字节,即地址的第3字节存入第5字节
"movb $" type ",%4\n\t" \ //将0x89或0x82存入第6字节
"movb $0x00,%5\n\t" \ //将0x00存入第7字节
"movb %%ah,%6\n\t" \ //将互换完的第2字节,即地址的第4字节存入第8字节
"rorl $16,%%eax" \ //复原eax
::"a" (addr), "m" (*(n)), "m" (*(n + 2)), "m" (*(n + 4)), \
"m" (*(n + 5)), "m" (*(n + 6)), "m" (*(n + 7)) \
//"m" (*(n))是gdt第n项描述符的地址开始的内存单元
//"m" (*(n + 2)) 是gdt第n项描述符的地址向上第3字节开始的内存单元
//其余依此类推
)
//n:gdt的项值,addr:tss或ldt的地址,0x89对应tss,0x82对应ldt
#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82")
//代码路径:include/linux/sched.h:
…
struct tss_struct {
long back_link; /* 16 high bits zero */
long esp0;
long ss0; /* 16 high bits zero */
long esp1;
long ss1; /* 16 high bits zero */
long esp2;
long ss2; /* 16 high bits zero */
long cr3;
long eip;
long eflags;
long eax,ecx,edx,ebx;
long esp;
long ebp;
long esi;
long edi;
long es; /* 16 high bits zero */
long cs; /* 16 high bits zero */
long ss; /* 16 high bits zero */
long ds; /* 16 high bits zero */
long fs; /* 16 high bits zero */
long gs; /* 16 high bits zero */
long ldt; /* 16 high bits zero */
long trace_bitmap; /* bits: trace 0, bitmap 16-31 */
struct i387_struct i387;
};
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];
/* tss for this task */
struct tss_struct tss;
};
/*进程0的task_struct
* 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, \ //就绪态,15个时间片
/* signals */ 0,{ {},},0, \
/* ec,brk... */ 0,0,0,0,0,0, \
/* pid etc.. */ 0,-1,0,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, \ //eflags的值,决定了cli这类指令只能在0特权级使用
0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
_LDT(0),0x80000000, \
{} \
}, \
}
初始化进程0
1
2
set_tss_desc(gdt + FIRST_TSS_ENTRY,&(init_task.task.tss));//设置TSS0
set_ldt_desc(gdt + FIRST_LDT_ENTRY,&(init_task.task.ldt));//设置LDT0
这两行代码的主要目的就是在GDT中初始化进程0所占的4、5两项,也就是初始化TSS0和LDT0。当前,GDT的0、1、2项被用作内核代码段和数据段,第3项留空,4、5两项分别是进程0的TSS0和LDT0.
movw $104, %1
是将104赋值给了对应GDT表项的低16位(注意到粒度G等于0,因此限长就是104字节,而TSS除去struct i387_struct i387后长度正好是104字节,LDT是3×8=24字节,所以104字节限长够用),movw %%ax,%2
是将ax的值存进对应GDT表项的31~16位,也就是段限长的低16位部分,"rorl $16,%%eax
是将EAX循环右移16位,也就是将高16位移动到低16位,movb %%al,%3
是将al(ax低8位)存入对应GDT表项的39~32位,movb $" type ",%4
是将type字段存入对应GDT表项的第五个字节部分,也就是47~40位,movb $0x00,%5
是将0存进对应GDT表项的第六个字节,也就是55~48位,movb %%ah,%6
是将ah存入对应GDT表项的第七个字节,也就是63~58位。其实就是填充一个GDT表。
当然,这里宏定义的0x89(存入GDT的第五个字节)代表tss,0x82代表ldt。
如果琢磨不透,那你需要RTFW。
接下来sched_init函数用for循环将task[64]除进程0占用的第0项之外的其余63项清空,同时将GDT的TSS1、LDT1往上的所有表项清零。
最后也是最重要的一步:将TR寄存器指向TSS0、LDTR寄存器指向LDT0,这样CPU就能通过TR和LDTR寄存器找到进程0的TSS0和LDT0了,也就能找到一切和进程0相关的信息。
设置时钟中断
时钟中断是时间片轮转调度策略的基础,稍有操作系统基础的同学都知道,时钟中断就是操作系统的心跳,每次时钟中断都会将当前进程的时间片进行减一操作,如果进程的时间片用完了,那么就需要进行调度了,让下一个就绪的进程来运行。
对时钟中断进行配置有三个步骤:
- 对8253定时器进行配置,设置其产生中断请求的周期
- 设置时钟中断,实际上与之前的流程一样,就是将时钟中断对应的IDT的中断服务程序字段填充对应的服务程序入口地址就行了
- 将8259A芯片中与时钟中断相关的屏蔽码打开,这样时钟中断就可以产生了。
Linux0.11的这个设置是让每10ms产生一次时钟中断。当然此时中断还未打开,所以CPU并不响应中断。
设置系统调用总入口
系统调用实际上就是一个中断,它是应用程序调用内核提供的功能的接口。
首先,将系统调用的处理函数(实际上就是中断服务程序,0x80号中断)system_call
与int 0x80
中断描述符表挂接。这个函数就是整个操作系统中系统调用的总入口,所有的系统调用都是通过触发0x80中断,进入这个中断处理程序实现的。
系统调用函数是操作系统对用户程序的基本支持。在操作系统中,如果放任应用程序随意访问IO等资源,那么许多对对IO等资源的操作都可能会引发错误,泄漏隐私,因此这些操作需要由内容确定的内核来完成,因此应用程序想要实现这些功能就必须访问内核,调用内核提供的代码来实现,但是内核也不能允许应用程序随意访问,否则会造成很大的安全问题,因此需要提供一个能让应用程序请求内核代码完成某一功能的接口,让内核来做这件事而不是应用程序,应用程序只有请求内核做某件事的权利。
操作系统的设计者提供了系统调用的解决方案,提供一套系统服务接口。用户进程只要想和内核打交道,就调用这套接口程序,之后,就会立即引发int 0x80软中断,后面的事情就不需要用户程序管了,而是通过另一条执行路线——由CPU对这个中断信号响应,翻转特权级(从用户进程的3特权级翻转到内核的0特权级),通过IDT找到系统调用端口,调用具体的系统调用函数来处理事务,之后,再iret翻转回到进程的3特权级,进程继续执行原来的逻辑,这样矛盾就解决了。
初始化缓冲区管理结构
缓冲区是内存与外设(例如硬盘)进行数据交互的媒介。为什么要有缓冲区?主要是外设速度太慢了,CPU速度太快了,速度严重不匹配,因此需要一个缓冲区保持尚未与外设进行交互的数据(类似于排队)。对外设而言,它仅需要考虑与缓冲区进行数据交互是否符合要求,而不需要考虑内存如何使用这些交互的数据;对内存而言,它也仅需要考虑与缓冲区交互的条件是否成熟,而不需要关心此时外设对缓冲区的交互情况。两者的组织、管理和协调将由操作系统统一操作。
操作系统通过hash_table[NR_HASH]
、buffer_head
双向环形链表组成的复杂哈希表管理缓冲区。
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
47
48
49
50
51
//代码路径:init/main.c:
void main(void)
{
…
buffer_init(buffer_memory_end);
…
}
//代码路径:fs/buffer.c:
…
struct buffer_head * start_buffer= (struct buffer_head *) &end;
struct buffer_head * hash_table[NR_HASH];
static struct buffer_head * free_list;
…
void buffer_init(long buffer_end)
{
struct buffer_head * h= start_buffer;
void * b;
int i;
if (buffer_end== 1<<20)
b= (void *) (640*1024);
else
b= (void *) buffer_end;
//h、b分别从缓冲区的低地址端和高地址端开始,每次对进buffer_head、缓冲块各一个
//忽略剩余不足一对buffer_head、缓冲块的空间
while ( (b -= BLOCK_SIZE) >= ((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; //这两项初始化为空,后续的使用将与hash_table挂接
h->b_prev= NULL;
h->b_data= (char *) b; //每个buffer_head关联一个缓冲块
h->b_prev_free= h-1; //这两项使buffer_head分别与前、
h->b_next_free= h + 1; // 后buffer_head挂接,形成双向链表
h++;
NR_BUFFERS++;
if (b== (void *) 0x100000) //避开ROMBIOS&VGA
b= (void *) 0xA0000;
}
h--;
free_list= start_buffer; // free_list指向第一个buffer_head
free_list->b_prev_free= h; //使buffer_head双向链表
h->b_next_free= free_list; //形成双向环链表
for (i=0;i<NR_HASH;i++) //清空hash_table[307]
hash_table[i]=NULL;
}
这里就是从内核代码的结束位置开始,使用buffer_head
结构初始化每一个buffer_head
大小的内存块,让它关联到从buffer_end
向前的每一个BLOCK_SIZE
大小的缓冲块,直到这两个相向增长的内存块距离不足一个buffer_head
大小,这里描述的有点难懂,还得看代码。
最后,对hash_table[307]进行设置,将hash_table[307]的所有项全部设置为NULL。
初始化硬盘
硬盘是必要的,因为用户程序一般存储在这里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//代码路径:init/main.c:
void main(void)
{
…
hd_init();
…
}
//代码路径:kernel/blk_dev/hd.c: //与rd_init类似,参看rd_init的注释
void hd_init(void)
{
blk_dev[MAJOR_NR].request_fn= DEVICE_REQUEST;//挂接do_hd_request()
set_intr_gate(0x2E,&hd_interrupt); //设置硬盘中断
outb_p(inb_p(0x21)&0xfb,0x21); // 允许8259A发出中断请求
outb(inb_p(0xA1)&0xbf,0xA1); // 允许硬盘发送中断请求
}
在hd_init()函数中,将硬盘请求项服务程序do_hd_request()与blk_dev控制结构相挂接,硬盘与请求项的交互工作将由do_hd_request()函数来处理,然后将硬盘中断服务程序hd_interrupt()与IDT相挂接,最后,复位主8259A int2的屏蔽位,允许从片发出中断请求信号,复位硬盘的中断请求屏蔽位(在从片上),允许硬盘控制器发送中断请求信号。
初始化软盘
跟初始化硬盘一样,没少好介绍的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//代码路径:init/main.c:
void main(void)
{
…
floppy_init ();//与hd_init()类似
…
}
//代码路径:kernel/floppy.c:
…
void floppy_init(void)
{
blk_dev[MAJOR_NR].request_fn= DEVICE_REQUEST; //挂接do_fd_request()
set_trap_gate(0x26,&floppy_interrupt); //设置软盘中断
outb(inb_p(0x21)&~0x40,0x21); //允许软盘发送中断
}
开启中断
目前为止,系统的中所有中断服务程序已经和IDT挂接,这意味着中断服务体系已经建设完毕,系统可以响应32位保护模式下的中断请求了。
开启中断:
1
2
3
4
5
6
7
8
9
//代码路径:include/asm/system.h:
#define sti() __asm__ ("sti"::)
//代码路径:init/main.c:
void main(void)
{
…
sti();
…
}
将进程0由0特权级翻转到3特权级,成为真正的进程
Linus在设计之初规定:Linux中除进程0之外,所有的进程都需要由一个已有进程在3特权级下创建。此前,进程0的代码和数据都是由操作系统设计者写在内核代码区和数据区的,而且处于0特权级。因此为了遵循规则,进程0在创建进程1之前,要将进程0由0特权级转变为3特权级。方法是调用move_to_user_mode
,它模仿有特权级反转的中断返回动作,实现进程0由特权级0转变为特权级3。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//代码路径:init/main.c:
void main(void)
{
…
move_to_user_mode();
…
}
//代码路径:include/system.h: //参看1.3.4节
#define move_to_user_mode() \ //模仿中断硬件压栈,顺序是ss、esp、eflags、cs、eip
__asm__("movl %%esp,%%eax\n\t" \
"pushl $0x17\n\t" \ //SS进栈,0x17即二进制的10111(3特权级、LDT、数据段)
"pushl %%eax\n\t" \ //ESP进栈
"pushfl\n\t" \ //EFLAGS进栈
"pushl $0x0f\n\t" \ //CS进栈,0x0f即1111(3特权级、LDT、代码段)
"pushl $1f\n\t" \ //EIP进栈
"iret\n" \ //出栈恢复现场、翻转特权级从0到3
"1:\tmovl $0x17,%%eax\n\t" \ //下面的代码使ds、es、fs、gs与ss一致
"movw %%ax,%%ds\n\t" \
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::»ax»)
看注释其实就可以看懂了,当然你需要熟悉GDT、LDT这些东西。注意到最后的栈顶的内容是EIP
,存放进入的是1
这个label,也就是iret
的下一条指令,所以其实iret之后,就是进程0在执行1
这个lable的内容了。
所谓进程0在执行,其实就是使用了进程0的代码段、数据段、特权级等等,本质上是寄存器内容的变化,这里我表述的并不清楚但希望你能理解它的本质:状态机。
补充一下段选择子的格式:
位数范围 | 名称 | 值 | 描述 |
---|---|---|---|
15–3 | Index | 0x02 | 指向 LDT 中第 2 个条目。 |
2 | TI | 1 | 表示使用 LDT(局部描述符表)。 |
1–0 | RPL | 3 | 请求特权级为 3(用户态)。 |
其实这段代码就是将一些必要的数据压栈,因为iret指令会将栈指针指向的一些数据依次弹出到指定的寄存器,实现上下文的恢复。这里有个疑问:恢复之后其实就在运行进程0了,那么进程0的数据段代码段是什么样的?
进程0的LDT内容为:
1
2
3
4
5
6
7
8
9
10
11
数据段:
0xc0f200
0x9f
代码段:
0xc0fa00
0x9f
null:
0
0
字段名称 | 数据段值(0xC0F200) | 代码段值(0xC0FA00) | 说明 |
---|---|---|---|
Segment Limit (15:0) | 0x009F | 0x009F | 段限长低 16 位 |
Base Address (15:0) | 0x0000 | 0x0000 | 基地址低 16 位 |
Base Address (23:16) | 0x00 | 0x00 | 基地址中间 8 位 |
Type | 0b0010 (Data) | 0b1010 (Code) | 数据段/代码段的特性字段 |
S | 1 | 1 | 描述符类型(1 表示代码/数据段) |
DPL | 3 | 3 | 描述符特权级 |
P | 1 | 1 | 存在位(1 表示有效) |
Segment Limit (19:16) | 0x0 | 0x0 | 段限长高 4 位 |
AVL | 0 | 0 | 系统软件可用位 |
L | 0 | 0 | 64 位代码段标志(与 32 位无关) |
D/B | 1 | 1 | 数据段/代码段操作模式位 |
G | 1 | 1 | 粒度位(1 表示段限长单位为 4 KB) |
Base Address (31:24) | 0x00 | 0x00 | 基地址高 8 位 |
观察进程0的LDT,寻找进程0的信息 代码段解析为:Segment Limit[15:0]为0x9f, Base Address[15:0]为0x0,base[23:16]为0x0,Type为0b1010,P标志位为1,DPL为3,S标志位为1,Seg Limit[19:16]为0,G标志位为1,D/B为1,AVL为0,Base[31:24]为0,也就是说基地址为0,段限长为(9f + 1) * 4kB = 640KB大小。数据段仅Type域与代码段不同。
思考题:fork进程1之前,为什么先调用move_to_user_mode()?用的是什么方法?解释其中的道理。
答案:Linus在设计之初规定:Linux中除进程0之外,所有的进程都需要由一个已有进程在3特权级下创建。因此为了遵循规则,进程0在创建进程1之前,要将进程0由0特权级转变为3特权级。方法是调用
move_to_user_mode
,它模仿有特权级反转的中断返回动作,恢复进程0的上下文,也就是激活进程0,其中使用了iret指令,iret指令会根据DPL(代码段寄存器)的内容来切换特权级,并执行出栈操作实现上下文的恢复,由于进程0的DPL为3,因此iret实现了进程0由特权级0变为特权级3的转换。
思考题:根据什么判定move_to_user_mode()中iret之后的代码为进程0的代码?
答案:iret指令完成了对进程0上下文的恢复,且恢复到的CS:EIP地址就是iret的下一条指令,此时可以查看CPU的CS、SS等段寄存器,可以看到其指向了进程0的LDT代码段和数据段,表明当前执行的是进程0的代码。
思考题:进程0的task_struct在哪?具体内容是什么?给出代码证据
答案:在task[64]这个数组的第0项所指的位置,是设计者实现设计好的,存在于内核中,在内核数据区。包括了进程0的状态、信号、pid、剩余时间片、ldt、tss等管理该任务所必须的数据。
请看下面这段代码, 这里中断门、陷阱门、系统调用都是通过_set_gate设置的,用的是同一个嵌入汇编代码,比较明显的差别是dpl一个是3,另外两个是0,这是为什么?说明理由。
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)
答案:DPL代表了特权级,因为中断和异常是由内核直接处理完成的,而且不允许用户进程访问他们,因此DPL为0,表示只能由内核访问;但是系统调用是用户进程与内核交互的接口,需要由特权级为3的用户进程触发,因此DPL为3。