linux0.11源码分析-初始化进程0

842 阅读5分钟

内核经过划分物理内存格局、设置缓冲区、虚拟盘、主内存、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 0010
  • movb $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表示的就是页目录地址

参考:

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