内核经过划分物理内存格局、设置缓冲区、虚拟盘、主内存、IDT等初始化后,接下来就要初始化进程0。 内核已经初始化好一个进程(init_task)数据结构,接下来往GDT中添加任务0的TSS与LDT数据,并设置好TR寄存器与LDTR寄存器,设置时钟中断与系统调用中断。下一步等待调度。
void sched_init(void)
{
int i;
struct desc_struct * p; // 描述符表结构指针
if (sizeof(struct sigaction) != 16) // sigaction 是存放有关信号状态的结构
panic("Struct sigaction MUST be 16 bytes");
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); //初始阿化TSS
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt)); // 初始化LDT
// 清空任务表
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与任务嵌套有关,如果置位1,iretd就会使用TSS中的back_link,用于返回上一次的任务
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); // 复位NT标志
ltr(0); //把任务0的TSS段描述符地址存入TR寄存器
lldt(0); //把任务0的LDT段描述符地址存入LDTR寄存器
// 下面代码用于初始化8253定时器。通道0,选择工作方式3,二进制计数方式。通道0的
// 输出引脚接在中断控制主芯片的IRQ0上,它每10毫秒发出一个IRQ0请求。LATCH是初始
// 定时计数值。
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); // 设置系统调用中断
}
sizeof(struct sigaction) != 16
:
Linux系统开发之初,内核不成熟。内核代码会被经常修改。Linus怕自己无意中修改了这些关键性的数据结构,造成与POSIX标准的不兼容。这里加入下面这个判断语句并无必要,纯粹是为了提醒自己以及其他修改内核代码的人。
看看进程的结构是什么样的
struct task_struct {
long state; /* 任务的运行状态(-1 不可运行,0 可运行(就绪),>0 已停止) */
long counter; /* 运行时间片,每经过一次时钟中断, counter就会减去1*/
long priority; /* 运行优先数。任务开始运行时counter = priority,越大运行越长 */
long signal; /* 信号。是位图,每个比特位代表一种信号,信号值=位偏移值+1 */
struct sigaction sigaction[32]; /* 信号执行属性结构,对应信号将要执行的操作和标志信息 */
long blocked; /* 进程信号屏蔽码(对应信号位图)。 */
int exit_code; /* 任务执行停止的退出码,其父进程会取 */
unsigned long start_code,end_code,end_data,brk,start_stack; /* 代码段地址、代码长度(字节数)、代码长度 + 数据长度(字节数)、总长度(字节数)、堆栈段地址 */
long pid,father,pgrp,session,leader;/* 进程pid、父进程pid、父进程组pid、会话号、会话首领 */
unsigned short uid,euid,suid; /* uid、有效用户id、保存的用户id*/
unsigned short gid,egid,sgid; /* gid、有效组id、保存的组id*/
long alarm; /* 滴答数 */
long utime,stime,cutime,cstime,start_time; /* 用户态运行时间(滴答数)、系统态运行时间(滴答数)、子进程用户态运行时间、子进程系统态运行时间、进程开始运行时刻 */
unsigned short used_math; /* 是否使用了协处理器 */
int tty; /* 进程使用tty 的子设备号。-1 表示没有使用。*/
unsigned short umask; /* 文件创建属性屏蔽 */
struct m_inode * pwd; /* 当前工作目录i 节点结构 */
struct m_inode * root; /* 根目录i 节点结构 */
struct m_inode * executable; /* 执行文件i 节点结构 */
unsigned long close_on_exec; /* 执行时关闭文件句柄位图标志 */
struct file * filp[NR_OPEN]; /* 进程使用的文件表结构 */
struct desc_struct ldt[3]; /* 0-空,1-代码段cs,2-数据和堆栈段ds&ss*/
struct tss_struct tss; /* 本进程的任务状态段信息结构 */
};
tss用于任务切换时,保留当前的任务状态以及CPU会从下一个任务的TSS结构中数据设置寄存器的值,以及还有低特权级向高特权级转移时,高特权级就会使用相应的栈,如特权级0就会使用esp0,ss0,因为linux只有2个特权级,特权级0与特权级3,其他没用。
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;
};
include/asm/system.h
#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)),((int)(addr)),"0x89")
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),((int)(addr)),"0x82")
这里的n
就是描述符的起始地址。
movw $104,%1
就是把104,即1101000存入描述符的第1、2字节(Segment Limit 这里)movw %%ax,%2
把addr基地址的低16位存入描述符的第3、4字节(Base Address 15:00这里)rorl $16,%%eax
eax寄存器中的值循环右移16位,即高、低字互换movb %%al,%3
将互换完的第1字节,即地址的第3字节存入第5字节(Base 23:16这里)movb $" type ",%4
将0x89或0x82存入第6字节,0x89即0101 1001,0x82即1000 0010movb $0x00,%5
将0x00存入第7字节movb %%ah,%6
将互换完的第2字节,即地址的第4字节存入第8字节(Base 31:24这里)rorl $16,%%eax
复原eax
include/linux/sched.h
#define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n)))
#define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))
// GDT中第1 个任务状态段(TSS)描述符的选择符索引号。
#define FIRST_TSS_ENTRY 4
// GDT中第1 个局部描述符表(LDT)描述符的选择符索引号。
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
// 计算在 GDT中第n 个任务的TSS 描述符的地址。
//每个任务都有TSS和LDT,占据16字节,左移4就是乘以16,这是从第0个任务偏移的地址
// 内核初始化后已经有4个描述符了,内核使用了前面32字节的内存了,FIRST_TSS_ENTRY<<3就是32
//2个合起来就是第n个任务的TSS 描述符的地址
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
// 计算在GDT中第n 个任务的LDT 描述符的地址。
//与TSS一样,只不过LDT在TSS的后面
#define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
#define INIT_TASK \
/* state etc */ { 0,15,15, \ //状态0表示可以运行,等待调度,第一个15表示15个滴答数,第二个15表示优先级
/* signals */ 0,{{},},0, \ // 不对信号做处理
/* ec,brk... */ 0,0,0,0,0,0, \
/* pid etc.. */ 0,-1,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}, \ // 默认NULL
/* 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, \
0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
_LDT(0),0x80000000, \
{} \
}, \
}
{0x9f,0xc0fa00}
为代码段的描述符,我们把{0x9f,0xc0fa00}
展开完整(8个字节):0x0000 009f,0x00c0 fa00
;其中第一个数在低32位,第二数在高32位。再来看下段描述符的结构
0x0000 009f,0x00c0 fa00
转为二进制就是
00000000 11000000 // G=1,D=1 如果G=0,则段长度Limit范围可从1B~1MB,单位是1B;如果G=1,则段长度Limit范围可从4KB~4GB,单位是4KB
11111010 00000000 //P=1,DPL=3表示用户态,S=1,TYPE = 1010
00000000 00000000
00000000 10011111 // 15-00为段限长:0x009f,其单位根据G=1,为说明单位是4KB,所以段长640KB
再来看看内核的,基址是一样的0,但是内核的DPL=0,就是说进程0的代码段基址与内核的段基址是相同的,都为0。
{0x9f,0xc0f200}
就是G=1,D=1,P=1,DPL=3,TYPE =0x0010,段大小位640KB,段基址0。
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 */
每个任务(进程)在内核态运行时都有自己的内核态堆栈。
union task_union {
struct task_struct task;
char stack[PAGE_SIZE];
};
static union task_union init_task = {INIT_TASK,};
PAGE_SIZE+(long)&init_task
表示的就是0特级权限时使用的栈esp0
0x10
表示的是选择子0001 0000,选择子的数据结构还记得吗?RPL=0,TI=0,索引index = 2,就是内核的数据段。因为linux只支持特权级0和特权级3,所以esp1、ss1、esp2、ss2都是0。
控制寄存器cr3保存的是页目录地址,所以(long)&pg_dir
表示的就是页目录地址
参考: