linux0.12内核源码解读(12)-任务调度初始化sched_init

作者:小牛呼噜噜 ,首发于公众号「小牛呼噜噜

哈喽,大家好呀,我是呼噜噜,好久没有更新old linux了,本文接着上一篇文章tty是什么?聊聊linux0.12中tty与time的初始化,继续我们的内核探索之路

本文我们继续先回到init/main.c文件中,接着往下看,目光聚焦于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
// init/main.c
void main(void) { //内核初始化主程序

ROOT_DEV = ORIG_ROOT_DEV;//复制并保存根文件系统设备号
SWAP_DEV = ORIG_SWAP_DEV;//复制并保存交换文件设备号
sprintf(term, "TERM=con%dx%d", CON_COLS, CON_ROWS);
envp[1] = term; //设置初始 init 进程中执行 etc/rc 文件和 shell 程序使用的环境变量
envp_rc[1] = term;
drive_info = DRIVE_INFO; // 复制并内存 0x90080 处的硬盘参数表。

//下面这段主要用来,计算内存边界值,划分内存
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;

#ifdef RAMDISK //如果配置了RAMDISK,就则初始化虚拟盘,主内存适当减少
main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif

mem_init(main_memory_start,memory_end);// 主内存区初始化
trap_init(); // 陷阱门(硬件中断向量)初始化
blk_dev_init(); // 块设备初始化
chr_dev_init(); //字符设备初始化
tty_init(); // tty 初始化
time_init(); //设置开机启动时间

sched_init();//任务调度初始化!!!

...
}

sched_init()这个函数主要是用来进行任务调度初始化,也叫做进程调度初始化,这个也就是操作系统核心功能之一,分配任务调度资源。可以这么说,在这个函数之前,操作系统仅仅是将程序加载到内存中,并初始化各种数据结构;但从这个函数开始,整个操作系统就开始”活”起来了

我们来看看sched_init()函数的内部实现细节

sched_init

我们先整体过一下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
// /kernel/sched.c
void sched_init(void)
{
int i;
struct desc_struct * p;// 描述符表 结构指针, (idt,gdt)

//sigaction 是存放有关信号状态的结构,主要用来提示内核中关键性的数据结构,不能随意修改
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");

//设置初始任务(任务0)的任务状态段描述符和局部数据表描述符
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));//设置初始任务(任务0)的任务状态段描述符
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));//设置初始任务(任务0)的局部数据表描述符

//清任务数组和描述符表项(注意i=1 开始,所以初始任务的描述符还在)
p = gdt+2+FIRST_TSS_ENTRY;
for(i=1;i<NR_TASKS;i++) {
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}

/* 清除标志寄存器中的位NT,这样以后就不会有麻烦*/
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");// 复位NT 标志
ltr(0); // 将任务0 的TSS 加载到任务寄存器tr,其中参数(0)是任务号
lldt(0);// 将局部描述符表加载到局部描述符表寄存器中
//下面代码用于初始化 8253 定时器
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /*定时值低字节*/
outb(LATCH >> 8 , 0x40); /* 定时值高字节 */
set_intr_gate(0x20,&timer_interrupt);//设置时钟中断处理程序句柄(设置时钟中断门)
outb(inb_p(0x21)&~0x01,0x21);//修改中断控制器屏蔽码,允许时钟中断
set_system_gate(0x80,&system_call);//设置系统调用中断门!
}

desc_struct指针

1
2
3
4
5
6
struct desc_struct * p;// 描述符表 结构指针,(idt,gdt)

// /include/linux/head.h
typedef struct desc_struct {
unsigned long a,b;
} desc_table[256];

定义了一个描述符表 结构体指针,desc_struct定义了描述符的数据结构。该结构仅仅说明每个段描述符是由8个字节构成,每个描述符表共有256 项,即0~255

1
2
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");

这处代码主要是由于Linux系统开发之初,内核不够成熟,内核中的代码会被经常修改,Linus怕自己无意中修改了这些关键性的数据结构,造成与POSIX标准的不兼容,所以此处仅用于提醒自己和他人

另外panic(恐慌)该函数是用来在控制台打印显示,内核中出现的错误信息;然后运行文件系统同步函数,最终进入死循环(死机)。另外如果当前进程是任务(进程)0 的话,还说明是交换任务出错,并且还没有运行文件系统同步函数

上面2部分都是比较易懂的,接下来就是核心难点了

设置0号任务(进程)的tss、ldt

我们都知道在操作系统中,进程都是fork出来的,但是对于操作系统的第一个进程,0号进程(任务),它非常特殊

0号进程是操作系统中第一个进程,是os中其他所有进程的”爷爷”,所有进程都是复制0号进程或者fork其后代进程产生的,它也是唯一一个没有通过fork或者kernel_thread产生的进程

所以0号进程的创建,需要我们去手动地通过代码去设置,所以接下来都围绕0号进程的参数设置展开!

1
2
3
4
5
6
7
8
9
10
11
12
//初始化0号任务(进程)的任务状态段TSS描述符
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
//初始化0号任务(进程)的局部数据表LDT描述符
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));


// /include/linux/sched.h
#define FIRST_TSS_ENTRY 4
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)

// /include/linux/head.h
extern desc_table idt,gdt;

我们具体来看看这2个函数,其中FIRST_TSS_ENTRY和FIRST_LDT_ENTRY 的值分别是4和5,而gdt又是desc_table数组,那么gdt+FIRST_TSS_ENTRY也可以表示成gdt[4],即FIRST_TSS_ENTRY是GDT的地址下标

set_tss_descset_ldt_desc其实都调用了同一个函数_set_tssldt_desc,参数略有不同。_set_tssldt_desc和之前的_set_gate一样,也是内联汇编的写法,大家慢慢看下:

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
// /include/asm/system.h

//// 在全局表中设置任务状态段/局部表描述符。状态段和局部表段的长度均被设置成 104 字节。
// 参数:n - 在全局表中描述符项 n 所对应的地址;addr - 状态段/局部表所在内存的基地址。
// type - 描述符中的标志类型字节。
// %0 - eax(地址 addr);%1 - (描述符项 n 的地址);%2 - (描述符项 n 的地址偏移 2 处);
// %3 - (描述符项 n 的地址偏移 4 处);%4 - (描述符项 n 的地址偏移 5 处);
// %5 - (描述符项 n 的地址偏移 6 处);%6 - (描述符项 n 的地址偏移 7 处);

#define _set_tssldt_desc(n,addr,type) \
__asm__ ("movw $104,%1\n\t" \ // 将TSS(或LDT)长度放入描述符长度域(第0-1 字节)
"movw %%ax,%2\n\t" \ // 将基地址的低字放入描述符第2-3 字节
"rorl $16,%%eax\n\t" \ // 将基地址高字移入ax 中
"movb %%al,%3\n\t" \ // 将基地址高字中低字节移入描述符第4 字节
"movb $" type ",%4\n\t" \ // 将标志类型字节移入描述符的第5 字节
"movb $0x00,%5\n\t" \ // 描述符的第6 字节置0
"movb %%ah,%6\n\t" \ // 将基地址高字中高字节移入描述符第7 字节
"rorl $16,%%eax" \ // eax 清零
::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \
"m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \
)

//// 在全局表中设置任务状态段描述符。
// n - 是该描述符的指针(向量);addr - 是描述符中的基地址值。任务状态段描述符的类型是0x89。
#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr, "0x89")
//// 在全局表中设置局部表描述符。
// n - 是该描述符的指针(向量);addr - 是描述符中的基地址值。局部表描述符的类型是0x82。
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr, "0x82")

需要注意一下,_set_tssldt_desc函数第2个参数,是需要传入一个函数的地址,但实际上是tss或者ldt这2者(数据结构)在内存中的基地址

这段本质其实就是往内存中GDT处,写入相应的描述符项,最终设置好任务0的TSS和LDT描述符

这里可能有朋友会疑惑,这里的TSS、LDT是什么呢?

TSS

TSS,任务状态段,是Intel的32位处理器在硬件上原生支持多任务的一种实现方式,当发生任务切换,需要保存和恢复进程的上下文,即各个寄存器的信息,所以每个任务都需要自己的TSS来保存这些信息。CPU能够识别TSS中信息,在任务切换时自动读取其中的信息


上图为TSS描述符,存放在GDT中,TYPE中B是任务是繁忙标志,1为繁忙,CPU使用繁忙标志来检查并尝试调用执行已被中断的任务,这样可以防止任务切换到自身

为了确保只有一个繁忙标志与一个任务相关联,每个TSS应该只有一个TSS描述符指向它。另外TSS描述符DPL必须为0,也就是说只有CPL为0的特权级才能调用;

我们再看来下TSS的结构图:

  • SS0,SS1,SS2 分别是0,1,2特权级的栈段选择子和对应栈段的栈顶指针;
  • ESP0,ESP1,ESP2,该部分应由任务创建者填写,且属于一般不变的静态部分,用于当通过门进行特权级转移时切换的栈
  • CR3是页目录机制,和分页有关
  • 32~92是常见的寄存器,其中当创建一个进程时,至少要设置EIP、EFLAGS、ESP、CS、SS、DS、ES、FS、GS这些寄存器;当任务切换时,保存状态以便将来恢复现场,其实就是这些寄存器;如果任务初次执行,CPU会从CS:EIP处开始执行
  • LDT段选择子,即当前任务的LDT描述符的GDT选择子
  • T用于软件调试,若为1,每次切换到该任务引发一次调试异常中断

我们再来看下linux0.12中tss定义的数据结构,基本上和intel IA-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
37
38
//数学协处理器使用的结构,主要用于保存进程切换时i387的执行状态信息
struct i387_struct {
long cwd; // 控制字(Control word)
long swd; // 状态字(Status word)
long twd; // 标记字(Tag word)
long fip; // 协处理器代码指针
long fcs; // 协处理器代码段寄存器
long foo;
long fos;
long st_space[20]; /* 8*10 bytes for each FP-reg = 80 bytes */
};

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;
};

LDT

LDT,局部描述符表,是和GDT全局描述符表类似的,只不过在操作系统里面只有一份GDT,而LDT有多个,linux0.12中每一个任务(进程),都有属于自己的LDT

LDT描述符和TSS描述符基本一模一样,可以参考上文的TSS描述符图,这里就不贴了,除了TYPE固定为0010,表明为LDT描述符;

在Linux0.12中,就是依靠TSS的切换真正实现进程的切换,为了更有效地实现任务之间的隔离,每个任务都有自己专属的TSS描述符、LDT描述符,都必须存放在GDT中;另外内核态的任务用GDT里的数据段和代码段,而用户态的任务则用每个用户进程自己专属的LDT里的数据段和代码段

由于每个任务的TSS和LDT的描述符都需要存在GDT中,而GDT的大小是有限的,比如Linux0.12中,GDT只能存放256项描述符,所以这其实会严重限制了系统中进程的数量。这种情况直到Linux2.4后,才得到有效地解决,每一个CPU执行的所有任务,都使用同一个TSS、LDT,当然这2个描述符也都放在GDT中,这块等后面我们讲解到Linux高版本内核时再细聊

我们简单总结一下,GDT、IDT、TSS、LDT的区别

再次设置GDT

我们回到sched_init的源码处,大家还记得我们在上次设置GDT是什么时候?我们来回忆一下在head.s中相关部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
call setup_gdt #设置GDT

setup_gdt:
lgdt gdt_descr # 加载全局描述符表寄存器(内容已设置好)
ret

gdt_descr:
.word 256*8-1 # so does gdt (not that that's any
.long _gdt # magic number, but it works for me :^)

.align 3


_gdt: .quad 0x0000000000000000 /* NULL descriptor */
.quad 0x00c09a0000000fff /* 16Mb */
.quad 0x00c0920000000fff /* 16Mb */
.quad 0x0000000000000000 /* TEMPORARY - don't use */
.fill 252,8,0 /* space for LDT's and TSS's etc */

此时GDT被重新设置,其中第1个为空描述符,不使用但必须存在,第2个描述符为代码段,第3项为数据段,第4个也为空不使用

后面256-4=252个描述符项,暂未被设置,留待LDT和TSS使用,这不就和这里对应上了嘛

我们再来看下,设置完0号进程的LDT、TSS后,此时GDT的详情图:

上图中的1号任务、2号任务此时实际上并未设置对应的tss、ldt,只是为了向大家展示后面的任务如何设置

在GDT中,代码段是用来存放可执行文件的操作指令,通俗点讲就是用来存放我们编写的程序(更准确点是被装载进内存程序的镜像),代码段只允许读取操作,不允许修改或者写入操作,防止在运行时被非法修改

代码段的段基地址放在寄存器CS中,常与指令指针寄存器IP(用来表示下一条指令在段中的偏移地址)进行配合 ,在x86中,CS:IP指向的内容,表示当前读取的指令的地址,其地址中的内容是 CPU当前执行的指令

数据段则是用来存放可执行文件中已初始化全局变量,即存放程序处理的相关数据,段的基地址存放在寄存器DS中

我们在上图可以发现GDT中是有代码段、数据段描述符,而每个LDT中也有各自的代码段、数据段描述符,它们是如何保证任务切换时,各个任务之间映射的虚拟地址空间互不干扰的,我们来看下具体示意图:


0号进程运行时,可以访问LDT_0映射的代码段_0数据段_0(给0号进程自己用),再加上代码段_os数据段_os(给内核使用);当1号进程运行时,可以访问LDT_1映射的代码段_1数据段_1(给1号进程自己用),再加上代码段_os数据段_os(给内核使用)

GDT所映射的一半虚拟地址空间是系统中所有任务共有的,但是LDT所映射的另一半空间则在任务切换时被改变,也就是通过LDT来隔离每个进程。比如0号进程没有办法访问1号进程的内存,所有进程(任务)共用一份GDT,当发生任务切换时,LDTR会指向成新任务专属的LDT,以实现对不同进程的段进行访问。

PCB(task_struct)

我们继续往下看,set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss))&(init_task.task.tss)这部分参数源码,最终发现里面有一个很重要的数据结构进程描述符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
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
// /kernel/sched.c =====================

set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));//设置初始任务(任务0)的任务状态段描述符
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));//设置初始任务(任务0)的局部数据表描述符


//定义任务联合 数据结构
union task_union {
struct task_struct task;
char stack[PAGE_SIZE]; // 因为一个任务数据结构与其堆栈放在同一内存页中,所以从堆栈段寄存器ss 可以获得其数据段选择符
};

//设置初始任务(0号进程)的数据
static union task_union init_task = {INIT_TASK,};


// /include/linux/sched.h =====================

#define NR_TASKS 64 //定义 Linux0.12系统中同时最多任务(进程)数 !!!
#define TASK_SIZE 0x04000000 // 定义 每个任务的长度(64MB)!!!
#define LIBRARY_SIZE 0x00400000 // 定义 动态加载库长度(4MB)!!!

#if (TASK_SIZE & 0x3fffff)
#error "TASK_SIZE must be multiple of 4M" // 任务长度必须是 4MB 的倍数
#endif

#if (LIBRARY_SIZE & 0x3fffff)
#error "LIBRARY_SIZE must be a multiple of 4M" // 库长度也必须是 4MB 的倍数
#endif

#if (LIBRARY_SIZE >= (TASK_SIZE/2))
#error "LIBRARY_SIZE too damn big!" // 加载库的长度不得大于任务长度的一半
#endif

#if (((TASK_SIZE>>16)*NR_TASKS) != 0x10000)
#error "TASK_SIZE*NR_TASKS must be 4GB" // 任务长度*任务总个数必须为 4GB
#endif

...

//任务(进程)数据结构 !!!
struct task_struct {
/* these are hardcoded - don't touch */
long state; /* 任务的运行状态(-1 不可运行,0 可运行(就绪),>0 已停止) */
long counter;/* 运行时间片,每经过一次时钟中断, counter就会减去1*/
long priority;/* 运行优先数,任务开始运行时counter = 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;
// unsigned long start_code 代码段地址。
// unsigned long end_code 代码长度(字节数)。
// unsigned long end_data 代码长度 + 数据长度(字节数)。
// unsigned long brk 总长度(字节数)。
// unsigned long start_stack 堆栈段地址
long pid,pgrp,session,leader;
// long pid 进程标识号(进程号)!!!
// long pgrp 进程组号。
// long session 会话号。
// long leader 会话leader

int groups[NGROUPS];//进程所属组号。一个进程可以属于多个组
/*
* pointers to parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->p_pptr->pid)
*/
struct task_struct *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
// task_struct *p_pptr 指向父进程的指针。
// task_struct *p_cptr 指向最新子进程的指针。
// task_struct *p_ysptr 指向比自己后创建的相邻进程的指针。
// task_struct *p_osptr 指向比自己早创建的相邻进程的指针
unsigned short uid,euid,suid;
// unsigned short uid 用户标识号(用户 id)。
// unsigned short euid 有效用户 id。
// unsigned short suid 保存的用户 id
unsigned short gid,egid,sgid;
// unsigned short gid 组标识号(组 id)。
// unsigned short egid 有效组 id。
// unsigned short sgid 保存的组 id。
unsigned long timeout,alarm;
// long timeout 内核定时超时值。
// long alarm 报警定时值(滴答数)
long utime,stime,cutime,cstime,start_time;
// long utime 用户态运行时间(滴答数)。
// long stime 系统态运行时间(滴答数)。
// long cutime 子进程用户态运行时间。
// long cstime 子进程系统态运行时间。
// long start_time 进程开始运行时刻。
struct rlimit rlim[RLIM_NLIMITS]; //进程资源使用统计数组

unsigned int flags; /* 各进程的标志*/
unsigned short used_math; //标志:是否使用了协处理器
/* file system info */
int tty; /* 进程使用 tty 终端的子设备号。必须设置;-1 表示没有使用*/
unsigned short umask; //文件创建属性屏蔽位
struct m_inode * pwd; //当前工作目录 i 节点结构指针
struct m_inode * root; //根目录 i 节点结构指针
struct m_inode * executable; //执行文件 i 节点结构指针
struct m_inode * library; //被加载库文件 i 节点结构指针
unsigned long close_on_exec; //执行时关闭文件句柄位图标志。(参见 include/fcntl.h)
struct file * filp[NR_OPEN]; //文件结构指针表,最多 32 项。表项号即是文件描述符的值。
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
struct desc_struct ldt[3]; //局部描述符表。0-空,1-代码段 cs,2-数据段和堆栈段 ds&ss !!!
/* tss for this task */
struct tss_struct tss; //进程的任务状态段信息结构
};

...

/*
* INIT_TASK 用于初始化系统中第一个任务,即0号进程,这些参数都是手动设置的
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,0,0,0, \
/* suppl grps*/ {NOGROUP,}, \
/* proc links*/ &init_task.task,0,0,0, \
/* uid etc */ 0,0,0,0,0,0, \
/* timeout */ 0,0,0,0,0,0,0, \
/* rlimits */ { {0x7fffffff, 0x7fffffff}, {0x7fffffff, 0x7fffffff}, \
{0x7fffffff, 0x7fffffff}, {0x7fffffff, 0x7fffffff}, \
{0x7fffffff, 0x7fffffff}, {0x7fffffff, 0x7fffffff}}, \
/* flags */ 0, \
/* math */ 0, \
/* fs info */ -1,0022,NULL,NULL,NULL,NULL,0, \
/* filp */ {NULL,}, \
{ \
{0,0}, \ // ldt第0项是空
/* ldt */ {0x9f,0xc0fa00}, \ //代码段长640K,基地0,G=1,D=1,DPL=3,P=1,TYPE=0x0a
{0x9f,0xc0f200}, \ //数据段长640K,基地0,G=1, D=1, DPL=3,P=1, TYPE=0x02
}, \
/*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, \ // ldt表选择符指向gdt中的LDT0处
{} \
}, \
}

task_struct在操作系统中,是一个非常重要的数据结构,它是专门提供给操作系统用的,来管理任务的调度,也叫做进程控制块(PCB,Process Ctrl Block)

不同于tss是专门给CPU使用task_struct标识了进程的属性,包括运行时间片,任务的运行状态,LDT,TSS等参数,大家可以详细看看上述代码注释,它是专门给操作系统来控制和管理进程的

需要注意一下,struct desc_struct ldt[3]中有LDT[0] = 空;LDT[1] = 用户代码段;LDT[2] = 用户数据/堆栈段描述符数据段ds和堆栈段ss是同一个,都在ldt[2]处,因为一个任务的数据结构与其内核态堆栈放在同一内存页中,所以从堆栈段寄存器ss可以获得其数据段选择符。

还规定了NR_TASKS=64,TASK_SIZE=0x04000000表明linux0.12中,最多有64个进程,每个进程分配了64M的线性空间,总空间为4G,这也是80386处理器的最大寻址空间

这里聪明的读者应该已经发现奇怪的地方了,那个年代内存都很昂贵,linux0.12只能管理内存16M,但一个进程就占64M的线性空间,更别提64个进程了,那么操心系统是如何解决这个问题的呢?

INIT_TASK用于初始化系统中第一个任务,即0号进程,它非常特殊,需要我们手动设置其所需的参数,和task_struct数据结构对应起来

清除任务数组和描述符表项

我们接着回到sched_ini源码处,继续往下看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// /kernel/sched.c

union task_union {
struct task_struct task;
char stack[PAGE_SIZE];
};
...
struct task_struct * task[NR_TASKS] = {&(init_task.task), };// 定义任务指针数组。

...

// 清任务数组和描述符表项
p = gdt + 2 + FIRST_TSS_ENTRY;
for (i = 1; i < NR_TASKS; i++)
{
task[i] = NULL;
p->a = p->b = 0;
p++;
p->a = p->b = 0;
p++;
}

task[i]是一个存放task_struct数据结构的数组,长度NR_TASKS=64,这个我们刚刚看过它的定义。另外还需注意这里循环是从i=1开始,清除task_struct数组和描述符表项,所以我们刚刚手动设置的0号任务描述符还在,这段主要就是打扫干净屋子再请客嘛

设置0号进程的TR和LDTR

我们接着继续往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// /include/linux/sched.h
// 全局表中第1 个任务状态段(TSS)描述符的选择符索引号。
#define FIRST_TSS_ENTRY 4
// 全局表中第1 个局部描述符表(LDT)描述符的选择符索引号。
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
// 宏定义,计算在全局表中第n 个任务的TSS 描述符的索引号(选择符)。
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
// 宏定义,计算在全局表中第n 个任务的LDT 描述符的索引号。
#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)))


/* 清除标志寄存器中的位NT,这样以后就不会有麻烦*/
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");// 复位NT 标志

ltr(0); // 将任务0 的TSS 加载到任务寄存器tr,其中参数(0)是任务号
lldt(0);// 将局部描述符表加载到局部描述符表寄存器中

ltr是在给TR寄存器赋值,TR指向当前任务对应的TSS

lldt是在给LDTR寄存器赋值,LDTR指向的当前任务的LDT,需要注意的是,只明确加载这一次,以后新任务LDT的加载,是CPU根据TSS中的LDT段选择子自动加载

初始化8253定时器

1
2
3
4
5
6
7
8
9
10
11
#define LATCH (1193180/HZ) //HZ定义为100

...

//下面代码用于初始化 8253 定时器
outb_p(0x36,0x43); /* 配置定时器0,设置定时器0的工作模式为模式3,二进制计数*/
outb_p(LATCH & 0xff , 0x40); /*定时值低字节*/
outb(LATCH >> 8 , 0x40); /* 定时值高字节 */
set_intr_gate(0x20,&timer_interrupt);//设置时钟中断处理程序句柄(设置时钟中断门)
outb(inb_p(0x21)&~0x01,0x21);//修改中断控制器屏蔽码,允许时钟中断
set_system_gate(0x80,&system_call);//设置系统调用中断门!

这一段主要是初始化8253定时器,其是一个可编程定时器的芯片,CPU通过端口与外设交互,定时器0的端口地址是0x40,而控制寄存器的端口地址是0x43,设置定时器0的通道 0, 模式3, 二进制计数方式

另外由于8253定时器的数据总线只有8位,所以需要分两次写入(先低8位,后高8位),填入LATCH=11931.8Hz,这个值也是有讲究的,8253定时器的输入时钟频率为1.193180MHZ,这样设置可以让它每10毫秒发出一个IRQ0请求(定时器通道0的输出引脚接在中断控制主芯片的IRQ0上),这个时间节拍是操作系统运行的脉搏,称之为1个系统滴答

系统每经过1个时钟滴答(10毫秒),就会产生一次中断,这涉及到硬件的知识,我们就了解一下即可

再通过set_intr_gate绑定中断处理函数timer_interrupt,其中断号0x20,挂到IDT中,接着修改中断控制器屏蔽码,开启时钟中断(还记得早在boot/setup.s处,为了搬运system模块代码,我们使用cli指令把系统的中断全部关闭了!),这样定时器每个10ms就会产生一次中断请求,去调用一次中断处理函数timer_interrupt

timer_interrupt这块非常重要,是Linux抢占式任务调度的基础!但也比较复杂,我们留待后面和进程调度一起再讲

设置系统调用

最后set_system_gate(0x80,&system_call)设置系统调用中断门!这个大家应该非常熟悉了吧,系统调用system_call是一个非常重要的中断处理函数,其中断号0x80

系统调用是操作系统专门为用户态运行的进程与硬件设备之间进行交互提供了一组接口,是用户态主动要求切换到内核态的一种方式

这里就不再赘述了,感兴趣地可以去看看,笔者之前的一篇图文并茂的文章 什么是系统调用机制?结合Linux0.12源码图解

至此sched_init完成了进程调度的初始化!

文章那个问题的答案是什么?为啥我们费那么大的劲来设置时间中断?那么Linux是如何进行进程调度的?我们下一篇文章再来慢慢道来!点赞在看就是对笔者最好的催更,我们下期再见~


参考资料:

https://elixir.bootlin.com/linux/0.12/source/kernel/sched.c

英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A-英特尔®

《Linux内核完全注释5.0》


点击查看,更多精彩文章