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需要判断那个进程应该先执行了。