linux 0.x 初始化及进程调度

Linux早期版本变化主要体现在启动及初始化的过程中,这一部分在后面则基本不会改动,前面提到linux-0.01版本的启动过程中包含多次内存移动过程,既有引导扇区将自己移动到0x90000,也有引导扇区将系统加载到0x10000后又移动到0x0000。这样做的目的主要是为了避免在加载的过程中覆盖bios的中断处理程序。这样的特点在后面的linux版本中也得以保留,以linux-0.11为例,主要变化就是将启动过程中的一部分代码抽取出来作为setup.s文件,这是因为引导扇区的大小固定为512B。

到linux-0.99版本的变化主要是在如上setup.s代码获取到显卡数据之后立即进行显示模式的切换,当然这一切仍然在实模式下,因为显示模式的切换是需要调用bios中断的。

1 linux-0.99 初始化

前面setup.s跳转的32位入口是32位汇编程序head.s,head.s需要再次重建gdt、idt等分段和中断机制,之后执行初始化代码也就是start_kernel,相对于0.01版本而言,这里初始化的内容多了硬件中断初始化、软盘初始化、socket初始化、以及进程间通信ipc初始化,并且这里使用idle函数替代了0.01中的pause函数。

extern "C" void start_kernel(void) {
	trap_init();
	init_IRQ(); //硬件中断初始化
	sched_init();
	parse_options(command_line);
	//内存初始化
	mem_init(low_memory_start,memory_start,memory_end);
	buffer_init();
	time_init();
	floppy_init(); //软盘初始化
	sock_init(); //初始化socket
	ipc_init(); //sysv ipc
	sti();
	calibrate_delay(); //延迟校准
	move_to_user_mode();
	if (!fork())		
		init();
	for(;;)
		idle(); //idle函数替换之前的pause
}

2 linux-0.99 进程

linux-0.99进程控制块中主要变化就是使用链表来进行控制块的管理,并且增加了一个等待子进程退出队列。用链表来管理进程的好处就是方便进行删除和插入。当然,之前的大小为64的数组结构仍然保留,其中保存真正的task_struct的内容,链表只是将其地址链接起来了。进程链表实则为双向循环链表,其头部为进程0,所以在调度遍历链表时经常是从进程0沿双向循环链表遍历到进程0。进程链表的组织在fork时进行,也就是一个新的进程被fork时就会设置其prev和next指针。

struct task_struct {
	long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	long counter, priority, signal;
	//信号处理函数指针、alarm及运行时间等
	//exit_code及pid、父进程id等
	int tty;	/* -1 if no tty, so it must be signed */
	struct m_inode * pwd, * root;
	struct file * filp[NR_OPEN];
	struct desc_struct ldt[3];	/* 1 - cs 2 - ds& ss */
	struct tss_struct tss;
	/* 0.99 增加的内容 */
	unsigned long signal,blocked,flags;	int errno;
	struct task_struct *next_task, *prev_task;	
	//内核栈和页等,子进程及兄弟进程指针等
	struct wait_queue *wait_chldexit;	//等待子进程退出队列
}

前面提到linux中所有的进程都是用户态进程,即使是进程0也是在切换到用户态之后调用idle()函数不停的执行,idle实则为系统调用,会切换到内核执行sys_idle,也就是空闲函数。

extern "C" int sys_idle(void){
	need_resched = 1;	
	return 0;
}

3 调度

Linux-0.99的进程0不停的执行于sys_idle函数,这里的sys_idle函数与之前的sys_pause不同,sys_pause的特点是直接调用schedule函数进行调度过程,而这里的sys_idle并没有调用schedule,那进程0如何释放cpu呢。这里需要注意的地方有两个,其一是sys_idle将need_resched设置为1,也就是说schedule是会调用的,其二是sys_idle是idle通过系统调用执行过来的,所以可以看看系统调用的执行或退出过程是否有些操作。

	.align 4,0x90
ret_from_sys_call:
	cmpl $0,_intr_count
	jne 2f
	movl _bh_mask,%eax
	andl _bh_active,%eax
	jne handle_bottom_half
9:	movl EFLAGS(%esp),%eax		# check VM86 flag: CS/SS are
	testl $(VM_MASK),%eax		# different then
	jne 1f
	cmpw $(USER_CS),CS(%esp)	# was old code segment supervisor ?
	jne 2f
	cmpw $(USER_DS),OLDSS(%esp)	# was stack segment user segment ?
	jne 2f
1:	sti
	orl $(IF_MASK),%eax		# these just try to make sure
	andl $~NT_MASK,%eax		# the program doesn't do anything
	movl %eax,EFLAGS(%esp)		# stupid
	cmpl $0,_need_resched
	jne reschedule
	movl _current,%eax

从系统调用的退出过程可以看到,在系统调用退出也就是ret_from_sys_call时,会判断need_resched是否为1,是的话就会进行跳到reschedule然后调用schedule进行进程调度。

就调度策略来说,linux-0.99相对于0.01版本则基本没有变化,只是将数组的遍历换成了双向循环链表的遍历而已

void schedule(void) {
	struct task_struct * p,* next;	
	sti();	need_resched = 0; p = & init_task;

	//从0号进程开始遍历进程控制块双向链表
	for(;;) { 将没有被阻塞的或者还没超时的进程设置为TASK_RUNNING }

	//从0号进程开始遍历进程控制块双向链表
	for(;;) { 找到状态为TASK_RUNNING并且counter最大的进程,
		保存为next,c为其counter }

	if (!c) 	
		while ( //从0号进程开始遍历进程控制块双向链表 )			
			 //更新counter=counter/2+priority 	
	switch_to(next); 
}

现在schedule何时调用可以分为两种情况,分别是函数直接调用schedule以及在系统调用时判断need_resched进行调度

函数直接调用schedule:

  • sys_pause, sleep_on(p), interruptible_sleep_on(p), do_timer, tty_write, release(p) ,do_exit等
  • 文件系统操作如namei、truncate、select等
  • 进程间通信如msg、sem

need_resched设置为1,在系统调用结束时会调用schedule

  • 典型的如0号进程执行idle()->sys_idle()之后会被调度

4 进程的阻塞与唤醒

linux中进程执行过程中可能因为资源的访问而唤醒,以读写缓冲区struct buffer_head *bh为例,若进程A正在使用缓冲区来读取块设备,而其它进程B和C在使用缓冲区时会被阻塞,并挂在等待队列b_wait上面。

struct buffer_head {	
	char * b_data;	
	... 
	//0 - ok, 1 -locked 
	unsigned char b_lock;	
	struct wait_queue * b_wait;

这一过程首先表现为由进程A使用buffer来读块设备时会lock_buffer,直到用完才会unlock_buffer。

extern inline void lock_buffer(struct buffer_head * bh)
{
	if (bh->b_lock)
		__wait_on_buffer(bh);
	bh->b_lock = 1;
}

extern inline void unlock_buffer(struct buffer_head * bh)
{
	bh->b_lock = 0;
	wake_up(& bh->b_wait);
}

这时进程B和C想要使用buffer就会进入其等待队列b_wait中,并且状态为TASK_UNINTERRUPTIBLE,并调用schedule进行调度,只有缓冲区资源可用时才会被唤醒

void __wait_on_buffer(struct buffer_head * bh)
{
	struct wait_queue wait = { current, NULL };

	bh->b_count++;
	add_wait_queue(& bh->b_wait, & wait);
repeat:
	current->state = TASK_UNINTERRUPTIBLE;
	if (bh->b_lock) {
		schedule();
		goto repeat;
	}
	remove_wait_queue(& bh->b_wait, & wait);
	bh->b_count--;
	current->state = TASK_RUNNING;
}

当进程A用完缓冲区之后会使用wakeup来唤醒因缓冲区资源而阻塞的B和C进程,但是它们在使用时仍然会进行资源的lock和unlock,仍然会有一个进程因没有拿到资源而阻塞,这就是schedule需要判断那个进程应该先执行了。