14)进程调度初始化

shed_init

shed_init主要负责进程调度的初始化,是多进程的基石

TSS 和 LDT

1
2
3
4
5
6
// kernel/sched.c
void sched_init(void) {
set_tss_desc(gdt+4, &(init_task.task.tss));
set_ldt_desc(gdt+5, &(init_task.task.ldt));
...
}

又看到了熟悉的gdt,前面讲过gdt是全局描述符表,里面存着段基址,TSS和LDT就是在gdt中又加了两个段描述符:

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
27
// include/linux/sched.h
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(Local Descriptor Table) 叫局部描述符表,是与 GDT 全局描述符表相对应的,内核态的代码用 GDT 里的数据段和代码段,而用户进程的代码用每个用户进程自己的 LDT 里的数据段和代码段。这样不同的进程不会互相乱用或破坏对方的内存。

初始化 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
// kernel/sched.c
struct desc_struct {
unsigned long a,b;
}


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数组初始化,然后把gdt后面的项也全部初始化。

每个进程都有自己的LDT和TSS,每一个desc_struct指向GDT里的一项,所以每初始化一个task结构体,需要初始化GDT中的两项(LDT和TSS)。

因为正在执行的代码未来会变成第一个进程的指令流,所以第一个task以及对应的TSS和LDT已经赋好初值了,所以要从第二个开始初始化

desc_struct

在 x86 保护模式下,一个完整的段描述符可以在内存中占用 8 字节(64 位),而这两个 unsigned long 变量(每个 4 字节)可以用来简化对描述符的访问和操作。

  • **a**:通常用于存储描述符的低 32 位部分,包括基地址和一些权限信息。
  • **b**:用于存储描述符的高 32 位部分,通常包含剩余的基地址、段界限和其他控制信息。

通过这种方式,使用两个变量能够简化描述符的处理,并且在汇编语言中访问这些字段时更为方便。

task_struct

task里保存的是进程信息。数组大小是64,说明这里一共可以有64个进程。

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
// include/linux/sched.h
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;
};

tr寄存器和ldt寄存器

1
2
3
4
5
6
7
8
9
10
11
// kernel/sched.c
#define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n)))
#define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))


void sched_init(void) {
...
ltr(0);
lldt(0);
...
}

ltr 是给 tr 寄存器赋值,以告诉 CPU 任务状态段 TSS 在内存的位置;lldt 一个是给 ldt 寄存器赋值,以告诉 CPU 局部描述符 LDT 在内存的位置。

tr和ldt

这样,CPU 之后就能通过 tr 寄存器找到当前进程的任务状态段信息,也就是上下文信息,以及通过 ldt 寄存器找到当前进程在用的局部描述符表信息。

中断设置

1
2
3
4
5
6
7
8
9
10
11
// kernel/sched.c
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);
...
}

这段代码的作用是:

  • 初始化定时器(设置周期性中断)。
  • 设置定时器中断向量和定时器中断的处理程序。
  • 禁用定时器的中断。
  • 设置系统调用中断向量和处理程序。

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

system_call,中断号是 0x80,所有用户态程序想要调用内核提供的方法,都需要基于这个系统调用来进行。


14)进程调度初始化
https://carl-5535.github.io/2024/10/18/Linux0.11/14)进程调度初始化/
作者
Carl Chen
发布于
2024年10月18日
许可协议