结合源码的操作系统学习记录(2)--进程调度

  • 调度程序

    在 kernel/sched.c 中的 schedule 函数中实现。

    首先扫描任务数组,在所有就绪态(TASK_RUNNING)的任务中找出时间滴答计数 counter 中找出值最大的那个任务(优先执行)。

    如果此时所有就绪态进程的时间片都用完了,系统会根据没个进程的优先级(priority),对系统中所有的进程重新计算每个任务的时间片。

    image-20220402093911605

    这样计算的话,当睡眠的进程被唤醒的时候,就会具有较高的时间片值,然后重复执行上述过程,最后调用 switch_to 执行进程切换。

    void schedule(void)
    {
    	int i,next,c;
    	struct task_struct ** p;	// 任务结构体指针
    
    /* 检查 alarm,唤醒任何已经得到信号的可中断任务 */
    
    	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
    		if (*p) {
                // 如果任务的 alarm 已经过期(alarm<jiffies),则在信号位图中指 SIGALRM 信号,然后清零 alarm
    			if ((*p)->alarm && (*p)->alarm < jiffies) {
    					(*p)->signal |= (1<<(SIGALRM-1));
    					(*p)->alarm = 0;
    				}
                // 如果信号位图中除被阻塞的信号外还有其他信号,并且任务处于可中断状态,则置任务为就绪状态
    			if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
    			(*p)->state==TASK_INTERRUPTIBLE)
    				(*p)->state=TASK_RUNNING;	// 设置就绪态
    		}
    
    /* this is the scheduler proper: */
    
    	while (1) {
    		c = -1;
    		next = 0;
    		i = NR_TASKS;
    		p = &task[NR_TASKS];
            // 从最后一个任务开始循环处理
    		while (--i) {
                // 跳过不含任务的数组槽
    			if (!*--p)
    				continue;
                // 比较每个任务的 counter
    			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
    				c = (*p)->counter, next = i;
    		}
            // 如果有 c 大于 0,也就是存在就绪态的任务,则跳出循环,执行任务切换
    		if (c) break;
            // 否则更新每一个任务的 counter,然后重新比较
    		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
    			if (*p)
    				(*p)->counter = ((*p)->counter >> 1) +
    						(*p)->priority;
    	}
        // 任务切换
    	switch_to(next);
    }
    
  • 进程切换

    在 include/asm/system.h 中的 switch_to() 宏中执行实际进程切换操作。

    首先检查要切换到的进程是否是当前进行,不是的话执行后续操作。将全局变量 current 置为新任务的指针,然后跳转到新任务的 TSS 地址处,此时 CPU 会把其所有寄存器的状态保存到当前任务寄存器 TR 中 TSS 段选择符所指向的当前进程的 TSS 中,然后把新任务的 TSS 段选择符指向的新任务的 TSS 中的寄存器信息恢复到 CPU 中,开始执行新进程。

    image-20220402113729084

    #define switch_to(n) {\
    struct {long a,b;} __tmp; \
    // ecx 是当前第 n 个进程对应的 pcb 首地址,通过比较 ecx 和 _current 判断下一个进程是不是当前进程
    __asm__("cmpl %%ecx,_current\n\t" \
    	"je 1f\n\t" \
        	// 把第 n 个进程的 tss 复制到 __tmp.b
    	"movw %%dx,%1\n\t" \
            // current 指向 ecx,ecx 指向 task[n]
    	"xchgl %%ecx,_current\n\t" \
            // 进程切换
    	"ljmp %0\n\t" \
    	"cmpl %%ecx,_last_task_used_math\n\t" \
    	"jne 1f\n\t" \
    	"clts\n" \
    	"1:" \
    	::"m" (*&__tmp.a),"m" (*&__tmp.b), \
    	"d" (_TSS(n)),"c" ((long) task[n])); \
    }
    
  • 终止进程

    当一个进程结束后,内核会释放该进程所占用的系统资源,包括进程运行时打开的文件、申请的内存等。

    exit 调用在 kernel/_exit.c/do_exit 中实现。函数首先释放进程占用内存,关闭进程打开的文件,对进程使用的文件系统的 i 节点进行同步。

    int do_exit(long code)	// code 是错误码
    {
    	int i;
    	// 释放当前进程代码段和数据段所占的内存页
    	free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
    	free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
        // 如果该进程有子进程,则把子进程的父进程置为 1 号进程
    	for (i=0 ; i<NR_TASKS ; i++)
    		if (task[i] && task[i]->father == current->pid) {
    			task[i]->father = 1;
                // 如果子进程已经僵死(ZOMBIE)状态,则向进程 1 发送子进程终止信号 SIGCHLD
    			if (task[i]->state == TASK_ZOMBIE)
    				/* assumption task[1] is always init */
    				(void) send_sig(SIGCHLD, task[1], 1);
    		}
        // 关闭当前进程打开的文件
    	for (i=0 ; i<NR_OPEN ; i++)
    		if (current->filp[i])
    			sys_close(i);
        // 对文件系统进行同步操作
    	iput(current->pwd);
    	current->pwd=NULL;
    	iput(current->root);
    	current->root=NULL;
    	iput(current->executable);
    	current->executable=NULL;
        // 如果该进程是一个 leader 并有控制终端,则释放该终端
    	if (current->leader && current->tty >= 0)
    		tty_table[current->tty].pgrp = 0;
        // 如果进程上次用过协处理器,则将 last_task_used_math 置空
    	if (last_task_used_math == current)
    		last_task_used_math = NULL;
        // 如果是 leader,中止所有相关进程
    	if (current->leader)
    		kill_session();
        // 设置当前进程为僵死状态,并向父进程发送 SIGCHLD
    	current->state = TASK_ZOMBIE;
    	current->exit_code = code;
    	tell_father(current->father);
        // 重新调度
    	schedule();
    	return (-1);	/* just to suppress warnings */
    }
    

    跟进 kill_session 中看一下具体的中止操作

    static void kill_session(void)
    {
        // p 指针指向任务数组的最末端
    	struct task_struct **p = NR_TASKS + task;
    	
        // 除 0 号进程外,如果进程的 session 等于当前进程的 session,就发送挂断进程信号
    	while (--p > &FIRST_TASK) {
    		if (*p && (*p)->session == current->session)
    			(*p)->signal |= 1<<(SIGHUP-1);
    	}
    }
    

    这里的会话(session)是进程组的集合,在 linux 中通过 sys_setsid 建立一个 session

    int sys_setsid(void)
    {
        // 已经是 leader 进程并且不是超级用户则直接退出
    	if (current->leader && !suser())
    		return -EPERM;
        // 标记 leader
    	current->leader = 1;
        // 设置 sessionid 和 组id 为该进程的 pid
    	current->session = current->pgrp = current->pid;
        // 表示当前进程没有控制终端
    	current->tty = -1;
    	return current->pgrp;
    }
    

    跟进 tell_father,看看 tell 了些什么

    static void tell_father (int pid)
    {
      int i;
    
        // 父进程向子进程发送 SIGCHLD
      if (pid)
        for (i = 0; i < NR_TASKS; i++)
        {
    		if (!task[i])
    		  continue;
    		if (task[i]->pid != pid)
    		  continue;
    		task[i]->signal |= (1 << (SIGCHLD - 1));
    		return;
        }
    /* if we don't find any fathers, we just release ourselves */
    /* This is not really OK. Must change it to make father 1 */
      printk ("BAD BAD - no father found\n\r");
      release (current);		// 如果没有找到父进程,则自己释放。
    }
    

    跟进释放操作 release

    void release(struct task_struct * p)
    {
    	int i;
    	if (!p)
    		return;
        // 找到要释放的任务
    	for (i=1 ; i<NR_TASKS ; i++)
    		if (task[i]==p) {
                // 指控该任务向并释放相关内存页
    			task[i]=NULL;
    			free_page((long)p);
                // 重新调度
    			schedule();
    			return;
    		}
    	panic("trying to release non-existent task");
    }
    
posted @ 2022-04-02 13:42  moon_flower  阅读(48)  评论(0编辑  收藏  举报