linux0.11内核源码修仙传第十章——进程调度始化
🚀 前言
本文是非常非常非常重要的一节,是后续进程调度的基础,对应于书中的第18回。希望各位给个三连,拜托啦,这对我真的很重要!!!
目录
- 🚀 前言
- 🏆 任务状态段(TSS)与局部描述符(LDT)
- 📃 什么是TSS与LDT
- 📃 初始化TSS0与LDT0
- 🏆 为进程调度做铺垫
- 📃 初始化剩下的TSS与LDT
- 📃 告诉内存TSS与LDT的位置
- 🏆 设置时钟中断与系统调用
- 📃 设置时钟中断
- 📃 设置系统调用
- 🎯总结
- 📖参考资料
🏆 任务状态段(TSS)与局部描述符(LDT)
📃 什么是TSS与LDT
TSS是在操作系统进程管理过程中,任务,也就是进程或线程,在切换时保存的上下文信息。其实里面存放的所谓上下文信息就是各个寄存器的情况,这样进程切换的时候,才能做到保存和恢复上下文,继续执行。具体可以看下面的结构体:
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 叫局部描述符表,对应的就是全局描述符(GDT),为什么做出这样的区分呢?GDT的作用是为整个系统定义了全局的段描述符。它包含了系统中所有进程和线程所使用的公共段描述符;LDT 是进程本地的,为每个进程定义了私有的段描述符。讲人话就是, 通过LDT,每个进程可以定义自己的代码段、数据段、堆栈段等。这样,不同进程之间的内存可以隔离开来,一个进程无法直接访问其他进程的内存 。
📃 初始化TSS0与LDT0
在看代码之前,先来回顾一下内存与GDT,不记得GDT与段描述符的话可以查看这篇博客:linux0.11内核源码修仙传第二章——setup.s。这里贴出目前的内存分布:
来看一下进程调度算法中如何初始化TSS与LDT的,FIRST_TSS_ENTRY
与FIRST_LDT_ENTRY
表明其在GDT中的位置:
#define FIRST_TSS_ENTRY 4
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
void sched_init(void)
{
···
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
···
}
这两个函数的作用是初始化TSS与LDT并添加进GDT后面两个,具体函数的定义如下:
#define _set_tssldt_desc(n,addr,type) \
__asm__ ("movw $104,%1\n\t" \
"movw %%ax,%2\n\t" \
"rorl $16,%%eax\n\t" \
"movb %%al,%3\n\t" \
"movb $" type ",%4\n\t" \
"movb $0x00,%5\n\t" \
"movb %%ah,%6\n\t" \
"rorl $16,%%eax" \
::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \
"m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \
)
#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")
这两个函数共同调用_set_tssldt_desc
,通过参数type
区分TSS和LDT。其实这两个也是段描述符,汇编所做的事情就是设置TSS与LDT对应的短描述符各个位。下面是段描述符,详情依旧可以参看linux0.11内核源码修仙传第二章——setup.s。
有了这个,就可以看一看汇编具体对应的设置了:
// 操作数约束:输入和内存操作数
::"a" (addr), // 输入:addr存入eax寄存器
"m" (*(n)), // 内存操作数:n对应%1(Limit低16位)
"m" (*(n+2)), // 内存操作数:n+2对应%2(Base低16位)
"m" (*(n+4)), // 内存操作数:n+4对应%3(Base中间8位)
"m" (*(n+5)), // 内存操作数:n+5对应%4(Type)
"m" (*(n+6)), // 内存操作数:n+6对应%5(Limit高4位 + Flags)
"m" (*(n+7)) // 内存操作数:n+7对应%6(Base高8位)
指令 | 含义 |
---|---|
movw $104, %1 | 设置段限长(0~16位) |
movw %%ax, %2 | Base低16位 |
rorl $16, %%eax movb %%al, %3 | Base中间8位 |
movb $" type ", %4 | 设置Type至P的8位,type参数指定了描述符类型(0x89对应TSS) |
movb $0x00, %5 | 设置Limit往前共8位 |
movb %%ah, %6 | 设置Base高8位 |
设置完成之后,内存如下所示(主要看右侧的GDT分布变化,多了TSS与LDT):
🏆 为进程调度做铺垫
📃 初始化剩下的TSS与LDT
接着往下看sched_init
函数:
typedef struct desc_struct {
unsigned long a,b;
} desc_table[256];
struct task_struct * task[64] = {&(init_task.task), };
void sched_init(void)
{
···
struct desc_struct * p;
p = gdt+2+FIRST_TSS_ENTRY; // 在TSS基础上再往后挪两个单位,即GDT下一个空白处
for(i=1;i<64;i++) {
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
···
}
这里做的事情还是比较好懂的,其实就是初始化了一个64长度的task_struct
数组并赋上初值,同时给GDT剩下的位置填充上0。先来看看task_struct
数组,初始化后数组如下:
这个数组里面每个都是task_struct
结构体,这个结构体很重要,里面存放了每一个进程的信息:
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;
};
这个结构体可以先看着,后面等进程调度实际应用时可以再细看。
接下来上上强度,下面图是未来整个内存的规划,目前还没有涉及到进程,下面的图可以先不用理解的很细。每个进程都有自己的LDT,每个LDT都有对应的数据段,代码段等,和GDT最开始的一样。
这里还有一个很重要的东西,就是最开始循环之前两行代码设置的TSS0和LDT0。为什么说很重要呢,那是因为虽然目前还没有创建进程,但是我们目前正在运行的代码就是未来的一个进程的指令流 ,也就是在后面进程调度建立起来后,当前代码就会变为进程0。因此需要TSS0和LDT0将这些信息提前存好。等到后面进程调度建立后会更加直观。
📃 告诉内存TSS与LDT的位置
接下来看后面两行:
#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);
···
}
之前我们就有介绍,lidt
指令是给idtr
寄存器赋值,告诉CPU中断描述符IDT在内存的位置。详情可以见linux0.11内核源码修仙传第二章——setup.s。这里的ltr
与lldt
与之前的类似,分别给tr寄存器和ldt寄存器赋值,告诉CPU,LDT与TSS在内存中的位置。如下所示:
这样,CPU 之后就能通过 tr 寄存器找到当前进程的任务状态段信息,也就是上下文信息,以及通过 ldt 寄存器找到当前进程在用的局部描述符表信息。至于这么多LDT与TSS,放哪个,那自然是最开始的LDT0和TSS0,之后的只需要依据指针加减即可获得。
🏆 设置时钟中断与系统调用
📃 设置时钟中断
在接下来的代码中,还需要设置时钟中断。所谓时钟中断就是一个定时器会持续以一定频率向CPU发送中断信号:
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);
···
}
好了,上面解释了时钟中断的来源,既然是外界的信息,那自然又涉及到了CPU交互。这个中断信号是一个可编程定时芯片,CPU要与其进行交互,就需要先写入指定端口对应的指令,具体设置方式需要参考芯片手册。
之后就需要开启一个中断处理函数,给的中断号是0x20,中断处理函数为timer_interrupt
,每次定时器向CPU发送中断后便会执行这个函数。 这个定时器的触发,以及时钟中断函数的设置,是操作系统主导进程调度的一个关键! 没有他们这样的外部信号不断触发中断,操作系统就没有办法作为进程管理的主人,通过强制的手段收回进程的 CPU 执行权限。
📃 设置系统调用
第二个设置的中断叫系统调用 system_call
,中断号是 0x80
,这个中断又是个非常非常非常非常非常非常非常重要的中断,所有用户态程序想要调用内核提供的方法,都需要基于这个系统调用来进行。比如 Java 程序员写一个 read,底层会执行汇编指令 int 0x80,这就会触发系统调用这个中断,最终调用到 Linux 里的 sys_read 方法。
void sched_init(void)
{
···
set_system_gate(0x80,&system_call);
}
截止目前为止,设置的中断如下:
中断号 | 中断处理函数 |
---|---|
0 ~ 0x10 | trap_init 里设置的一堆 |
0x20 | timer_interrupt |
0x21 | keyboard_interrupt |
0x80 | system_call |
0-0x10 这 17 个中断是 trap_init 里初始化设置的,是一些基本的中断,比如除零异常等。设置见linux0.11内核源码修仙传第六章——中断初始化。 0x21是键盘中断,设置见linux0.11内核源码修仙传第八章——控制台初始化。0x20是定时器中断,0x80是系统调用中断。
🎯总结
本文就干了三件事:第一,我们往全局描述符表写了两个结构,TSS 和 LDT,作为未来进程 0 的任务状态段和局部描述符表信息。第二,我们初始化了一个结构为 task_struct 的数组,未来这里会存放所有进程的信息,并且我们给数组的第一个位置附上了 init_task.init 这个具体值,也是作为未来进程 0 的信息。第三,设置了时钟中断 0x20 和系统调用 0x80,一个作为进程调度的起点,一个作为用户程序调用操作系统功能的桥梁。
其实整个操作系统就是靠中断驱动,各个模块不断初始化各种中断处理函数,并且开启指定的外设开关,让操作系统自己慢慢“活”了起来,逐渐通过中断忙碌于各种事情中。
📖参考资料
[1] linux源码趣读
[2] 一个64位操作系统的设计与实现
[3] 任务状态段(Task State Segment)
[4] Linux 内存管理(二)之GDT与LDT