/*
* 2010-1-21
* 该文件时内核中有关任务调度的函数程序,其中包含基本函数sleep_on,
* wakeup,schedule等,以及一些简单的系统调用。同时将软盘的几个操作
* 函数也放置在这里。
*
* schedule函数首先对所有的任务检查,唤醒任何一个已经得到信号的任务,
* 具体的方法是针对任务数组中的每个任务,检查其警报定时值alarm。如果任务
* 的alarm已经超期(alarm < jiffies),则在它的信号位图中设置SIGALARM,然后
* 情书alarm值。jiffies是系统自从开机之后算起的滴答数。在scheed.h中定义,
* 如果进程信号的位图中除去被阻塞的信号之外还有其他信号,并且任务处于可
* 中断睡眠状态,则置任务为就绪状态。
* 随后是调度函数的核心处理,这部分代码根据进程时间片和优先权的调度机制,
* 来选择将要执行的程序。他首先是循环检查任务数组中的所有任务。根据每个就绪
* 任务剩余执行时间值counter中选取一个最大的,利用switch_to函数完成任务
* 转换。如果所有的就绪任务的该值都是0,则表示此刻所有任务的时间片都已运行完。
* 于是就根据任务的优先权值priority,重置每个任务的运行时间counter。在重新
* 循环检查所有的任务重的执行的时间片值。
* 另一个值得一说的是sleep_on函数,该函数虽短,却要比schedule函数难理解,
* 简单的讲,sleep_on函数主要的功能是当一个进程所请求的资源正在忙,或者是
* 不在内存中被切换出去,放在等待队列中等待一段时间。切换回来后在继续执行。
* 放入等待队列的方式是,利用了函数中的tmp指针为各个正在等待任务的联系。
* 还有一个函数interrupt_sleep_on,该函数的主要功能是在进程调度之前,把当前
* 任务设置为可中断等待状态,并在本任务被唤醒之后还需要查看队列上是否还有
* 后来的等待任务,如果有,先调度他们。
*
*/
/*
* linux/kernel/sched.c
*
* (C) 1991 Linus Torvalds
*/
/*
* 'sched.c' is the main kernel file. It contains scheduling primitives
* (sleep_on, wakeup, schedule etc) as well as a number of simple system
* call functions (type getpid(), which just extracts a field from
* current-task
*/
#include
#include
#include
#include
#include
#include
#include
#include
#define _S(nr) (1<<((nr)-1)) // 取nr(1-32)对应的位的二进制数值,取出的
// 并不是一位
// 定义除了SIGKILL和SIGSTOP之外,其他信号位全是阻塞的
#define _BLOCKABLE (~(_S(SIGKILL) | _S(SIGSTOP)))
//----------------------------------------------------------------------
// show_task
// 显示任务号nr,pid,进程状态和内核堆栈空闲字节
void show_task(int nr,struct task_struct * p)
{
int i,j = 4096-sizeof(struct task_struct);
printk("%d: pid=%d, state=%d, ",nr,p->pid,p->state);
i=0;
while (i i++; printk("%d (of %d) chars free in kernel stack/n/r",i,j); } //--------------------------------------------------------------------- // show_stat // 显示所有任务的信息 void show_stat(void) { int i; for (i=0;i if (task[i]) // task是一个指针数组,如果存在”任务“ show_task(i,task[i]); } #define LATCH (1193180/HZ) // 每个时间片滴答数 extern void mem_use(void); extern int timer_interrupt(void); // 时钟中断处理程序 extern int system_call(void); // 系统调用处理程序 union task_union { // 定义任务联合(任务结构成员和stack 字符数组程序成员),联合体是共享内存的 struct task_struct task; // 因为一个任务数据结构与其堆栈放在同一内存页中,所以 char stack[PAGE_SIZE]; // 从堆栈段寄存器ss 可以获得其数据段选择符 }; static union task_union init_task = {INIT_TASK,}; // 定义初始任务数据 long volatile jiffies=0; // 定义开机以来时钟滴答数 long startup_time=0; // 开机时间 struct task_struct *current = &(init_task.task); // 当前任务指针 struct task_struct *last_task_used_math = NULL; // 使用过协处理器的任务指针 struct task_struct * task[NR_TASKS] = {&(init_task.task), }; // 定义任务数组 long user_stack [ PAGE_SIZE>>2 ] ; // 定义系统堆栈指针 struct { // 该结果用于设置堆栈ss:esp long * a; short b; } stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 }; //--------------------------------------------------------------------- // math_state_restore /* * 'math_state_restore()' saves the current math information in the * old math state array, and gets the new ones from the current task */ /* * 将当前协处理器内容保存在原来协处理器数组中,并将当前任务的协处理器 * 内容加载到协处理器 * */ // 当任务被调度交换之后,该函数用以保存员任务的协处理器的状态,并回复 // 新调度进来的当前协处理器的执行状态 void math_state_restore() { if (last_task_used_math == current) // 如果任务没有改变返回 return; __asm__("fwait"); // 在发送协处理器指令之前首先发出wait指令 if (last_task_used_math) // 如果上个使用协处理器的进程存在 { // 保存状态 __asm__("fnsave %0"::"m" (last_task_used_math->tss.i387)); } last_task_used_math=current; // 现在last_task_used_math指向当前任务 if (current->used_math) // 如果当前的任务使用过协处理器 { __asm__("frstor %0"::"m" (current->tss.i387));// 恢复状态 } else // 没有使用过协处理器 { __asm__("fninit"::); // 初始化协处理器 current->used_math=1; // 设置使用协处理器标志 } } //-------------------------------------------------------------------- // schedule /* * 'schedule()' is the scheduler function. This is GOOD CODE! There * probably won't be any reason to change this, as it should work well * in all circumstances (ie gives IO-bound processes good response etc). * The one thing you might take a look at is the signal-handler code here. * * NOTE!! Task 0 is the 'idle' task, which gets called when no other * tasks can run. It can not be killed, and it cannot sleep. The 'state' * information in task[0] is never used. */ void schedule(void) { int i,next,c; struct task_struct ** p; /* check alarm, wake up any interruptible tasks that have got a signal */ /* 检查alarm,唤醒任何已得到信号的可中断任务 */ // 从任务数组最后开始检查alarm for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) if (*p) // 任务存在? { if ((*p)->alarm && (*p)->alarm < jiffies) /* * 下面是对(*p)->alarm < jiffies理解: * 如果不调用alarm()函数,alarm的值就是0,但是调用了alarm函数 * 之后,比如说alarm(5),就是5秒后报警 */ { // 在位图信号中置位AIGALARM (*p)->signal |= (1<<(SIGALRM-1)); // 将alarm值置位0 (*p)->alarm = 0; } // 如果信号位图中除被阻塞的信号外还有其他信号,即是该 // 进程申请的自愿被别的进程释放,或者是其他条件导致该 // 进程能够被执行,并且该任务处于的是可中断编程。linux // 内核进程转换见文档<进程运行状态图.txt>。此时将该进程 // 进程状态置位就绪。 if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && (*p)->state==TASK_INTERRUPTIBLE) (*p)->state=TASK_RUNNING; } /* this is the scheduler proper: */ /* 这是调度的主要部分 */ while (1) // 循环,直到存在counter的值大于0 { c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS]; // 下面的代码是寻找最大counter值的进程。counter // 值的含义见文档 while (--i) { if (!*--p) continue; if ((*p)->state == TASK_RUNNING && (*p)->counter > c) c = (*p)->counter, next = i; } if (c) break; // 如果进程存在。退出,去执行switch_to // 否则更新counter值 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority; } switch_to(next); } //-------------------------------------------------------------------- // sys_pause // 将当前任务设置为可中断,执行调度函数 int sys_pause(void) { current->state = TASK_INTERRUPTIBLE; schedule(); return 0; } //------------------------------------------------------------------- // sleep_on // struct task_struct **p是等待队列头指针 // 该函数就是首先将当前任务设置为TASK_UNINTERRUPTIBLE,并让睡眠队列 // 头指针指向当前任务,执行调度函数,直到有明确的唤醒时,该任务才重新 // 开始执行。 void sleep_on(struct task_struct **p) { struct task_struct *tmp; if (!p) // 无效指针 return; if (current == &(init_task.task)) // 当前任务是0,死机 panic("task[0] trying to sleep"); tmp = *p; *p = current; current->state = TASK_UNINTERRUPTIBLE; // 设置状态 schedule(); // 执行调度,直到明确的唤醒 // 肯能存在多个任务此时被唤醒,那么如果还存在等待任务 // if (tmp),则将状态设置为”就绪“ if (tmp) tmp->state=0; } //-------------------------------------------------------------------------- // interruptible_sleep_on // struct task_struct **p是等待队列对头 void interruptible_sleep_on(struct task_struct **p) { /* * 首先需要明确的是TASK_INTERRUPTIBLE。它是指当进 * 程处于可中断等待状态时,系统不会调度该进行执行。 */ struct task_struct *tmp; if (!p) return; if (current == &(init_task.task)) panic("task[0] trying to sleep"); tmp=*p; *p=current; // 保存该任务指针 repeat: current->state = TASK_INTERRUPTIBLE; schedule(); if (*p && *p != current) { /* * *p != current表示当前任务不是原来保存的任务,即是 * 有新的任务插入到等待队列中了。由于本任务是不可中断的 *,所以首先执行其他的任务 */ (**p).state=0; goto repeat; } /* * 下面的代码错误,应该是*p = tmp,让队列头指针指向队列中其他任务 */ *p=NULL; // 原因同上。sleep_on if (tmp) tmp->state=0; } //---------------------------------------------------------------- // wake_up // 唤醒任务p void wake_up(struct task_struct **p) { if (p && *p) { (**p).state=0; // 置状态为可运行 *p=NULL; } } /* * OK, here are some floppy things that shouldn't be in the kernel * proper. They are here because the floppy needs a timer, and this * was the easiest way of doing it. */ /* * 由于软盘需要时钟,所以就将软盘的程序放到这里了 */ // 利用下面的数组来存储的是马达运转或者是停止的滴答数,下列的 // 数组在函数do_floppy_timer中更新 static struct task_struct * wait_motor[4] = {NULL,NULL,NULL,NULL}; static int mon_timer[4]={0,0,0,0}; // 软驱到正常运转还需要的时间 static int moff_timer[4]={0,0,0,0}; // 马达到停止运转还剩下的时刻 unsigned char current_DOR = 0x0C; // 数字输出寄存器(初值:允许dma和中断请求,启动fdc) // oxoc -- 0000,1100 //---------------------------------------------------------------- // ticks_to_floppy_on // 指定软盘到正常运转所需的滴答数 // nr -- 软盘驱动号,该函数返回的是滴答数 int ticks_to_floppy_on(unsigned int nr) { extern unsigned char selected; // 当前选中软盘号 // ox10 -- 0001, 0000 unsigned char mask = 0x10 << nr; // 所选软盘对应的数字输出 // 寄存器启动马达比特位 /* * 数字输出端口(数字控制端口)是一个8位寄存器,它控制驱动器马达开启 * ,驱动器选择,启动和复位FDC,以及允许和禁止DMA及中断请求。 * FDC的主状态寄存器也是一个8位寄存器,用户反映软盘控制器FDC和软盘 * 驱动器FDD的基本状态。通常,在CPU想FDC发送命令之前或者是从FDC获得 * 操作结果之前,都要读取主状态寄存器的状态位,以判定当前的数据是否 * 准备就绪,以及确定数据的传输方向 * */ if (nr>3) // 最多3个软盘驱动号 panic("floppy_on: nr>3"); moff_timer[nr]=10000; /* 100 s = very big :-) */ cli(); /* use floppy_off to turn it off 关中断 */ mask |= current_DOR; // mask = // 如果不是当前软驱,则首先复位其它软驱的选择位,然后置对应软驱选择位 if (!selected) { mask &= 0xFC; // 0xfc -- 1111,1100 mask |= nr; } if (mask != current_DOR) // 如果数字输出寄存器的当前值和要求不同 // 即是需要的状态还没有到达 { outb(mask,FD_DOR); // 于是向FDC数字输出端口输出新值 if ((mask ^ current_DOR) & 0xf0) // 如果需要启动的马达还没有 // 启动,则置相应的软驱马达定 // 时器值(50个滴答数) mon_timer[nr] = HZ/2; else if (mon_timer[nr] < 2) mon_timer[nr] = 2; current_DOR = mask; // 更新数字输出寄存器的值current_DOR,即是反映 // 当前的状态 } sti(); // 开中断 return mon_timer[nr]; } //-------------------------------------------------------------- // floppy_on // 等待指定软驱马达启动所需时间,启动时间 void floppy_on(unsigned int nr) { cli(); // 关中断 while (ticks_to_floppy_on(nr))// 还没有到时间? sleep_on(nr+wait_motor); // 为不可中断睡眠状态并放在 // 等待马达运行队列中 sti(); // 开中断 } //--------------------------------------------------------------- // floppy_off // 初始化数组moff_timer,即是表示置相应软驱马达停转定时器(3秒) void floppy_off(unsigned int nr) { moff_timer[nr]=3*HZ; } //---------------------------------------------------------------- // do_floppy_timer // 软盘定时处理子程序。更新马达启动定时值和马达关闭停转计时值。该子程序 // 是在时钟定时中断被调用,因此每一个滴答(10ms)被嗲用一次,更新马达开启 // 或者是停止转动定时器值。如果在某一个马达停转时刻到达,那么则将数字输出 // 寄存器马达启动复位标志 void do_floppy_timer(void) { int i; unsigned char mask = 0x10; for (i=0 ; i<4 ; i++,mask <<= 1) { if (!(mask & current_DOR)) // 如果不是指定马达 continue; if (mon_timer[i]) { if (!--mon_timer[i])// 如果马达启动定时器到达 wake_up(i+wait_motor);//则唤醒进程 } else if (!moff_timer[i]) { // 如果马达停止运转时刻到达 current_DOR &= ~mask; // 复位相应马达启动位 outb(current_DOR,FD_DOR); // 更新数字输出寄存器 } else moff_timer[i]--; // 马达停转计时器递减 } } #define TIME_REQUESTS 64 static struct timer_list { long jiffies; // 定时滴答数 void (*fn)(); // 定时处理函数 struct timer_list * next; } timer_list[TIME_REQUESTS], * next_timer = NULL; //---------------------------------------------------------------------- // add_timer // 增加定时器。输入参数为指定的定时器的滴答数和相应的处理函数指针。 // jiffies -- 以10ms计时的滴答数 // *fn() -- 定时时间到达时执行函数 void add_timer(long jiffies, void (*fn)(void)) { struct timer_list * p; if (!fn) // 如果处理函数为空 return; // 退出 cli(); // 关中断 if (jiffies <= 0) // 定时器到达 (fn)(); // 执行函数fn else { // 否则,从定时器数组中,找到一个空闲项,使用fn来标识 for (p = timer_list ; p < timer_list + TIME_REQUESTS ; p++) if (!p->fn) break; // 当上面的循环结束时,可能存在p == timer_list + TIME_REQUESTS if (p >= timer_list + TIME_REQUESTS) // 这样的话,表明用完了数组 panic("No more time requests free"); // 将定时器数据结构填入相应信息 p->fn = fn; p->jiffies = jiffies; p->next = next_timer; next_timer = p; // 链表项按定时器值的大小排序。下面就是链表排序的实现。这样的好处是 // 在查看是否有定时器到期时,只需要查看链表的头结点即可 while (p->next && p->next->jiffies < p->jiffies) { p->jiffies -= p->next->jiffies; fn = p->fn; p->fn = p->next->fn; p->next->fn = fn; jiffies = p->jiffies; p->jiffies = p->next->jiffies; p->next->jiffies = jiffies; p = p->next; } } sti(); // 开中断 } //------------------------------------------------------------------------ // do_timer // 时钟中断处理函数,在/kernel/system_call.s中_timer_interrupt_中被调用 // 参数cpl是当前的特权级0或3,0标识在内核代码段运行 // 对于一个进程由于时间片用完,则进行内核任务切换。并进行及时更新工作 void do_timer(long cpl) { extern int beepcount; // 扬声器发声时间滴答数 extern void sysbeepstop(void); // 关闭扬声器 if (beepcount) if (!--beepcount) // 如果扬声器计数次数到 sysbeepstop(); // 关闭发生器 if (cpl) // 如果实在内核程序,即是超级用户 current->utime++; // 超级用户时间增加 else current->stime++; // 普通用户运行时间增加 // 如果有用户定时器存在的话,则将第一个定时器的值减去1,如果已经等于0 // 那么调用函数fn,并将函数指针置为空值,然后去掉该定时器。注意的是上述 // 链表已经排序,同时在寻找空闲结点时,是通过fn是否为空来判断的,所以 // 将fn置位null if (next_timer) { next_timer->jiffies--; while (next_timer && next_timer->jiffies <= 0) { void (*fn)(void); // // 删除定时器 fn = next_timer->fn; next_timer->fn = NULL; next_timer = next_timer->next; (fn)(); } } // 如果当前软盘控制器FDC的数字输出寄存器马达启动位有置位,则执行相应的 // 软盘定时程序 if (current_DOR & 0xf0) do_floppy_timer(); if ((--current->counter)>0) return; // 如果进程运行时间还没有完,退出 current->counter=0; if (!cpl) return; // 超级用户程序,不依赖counter值来调度 schedule(); } //---------------------------------------------------------------------- // sys_alarm // 如果已经设置了alarm的值,那么返回的是旧值,如果没有设置返回0 int sys_alarm(long seconds) { int old = current->alarm; if (old) old = (old - jiffies) / HZ; current->alarm = (seconds>0)?(jiffies+HZ*seconds):0; return (old); } //----------------------------------------------------------------------- // sys_getpid // 取得当前的进程号 int sys_getpid(void) { return current->pid; } //---------------------------------------------------------------------- // sys_getppid // 取得父进程进程号 int sys_getppid(void) { return current->father; } //----------------------------------------------------------------------- // sys_getuid // 取得用户号uid int sys_getuid(void) { return current->uid; } //------------------------------------------------------------------------ // sys_geteuid // 取得用户号euid int sys_geteuid(void) { return current->euid; } //---------------------------------------------------------------------- // sys_getgid // 取得组号gid int sys_getgid(void) { return current->gid; } //---------------------------------------------------------------------- // sys_getegid // 取得进程的egid,有关egid的解释,参见文档 int sys_getegid(void) { return current->egid; } //-------------------------------------------------------------------- // sys_nice // 改变进程优先级 int sys_nice(long increment) { if (current->priority-increment>0) current->priority -= increment; return 0; } //---------------------------------------------------------------------- // sched_init // 调度程序初始化,即是初始化进程0,init void sched_init(void) { int i; struct desc_struct * p; if (sizeof(struct sigaction) != 16) // sigaction中存放的是信号状态结构 panic("Struct sigaction MUST be 16 bytes"); // 设置初始任务(任务0)的任务状态描述符表和局部数据描述符表 set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt)); // 清楚任务数组和描述符表项 p = gdt+2+FIRST_TSS_ENTRY; for(i=1;i task[i] = NULL; p->a=p->b=0; p++; p->a=p->b=0; p++; } /* Clear NT, so that we won't have troubles with that later on */ /* nt标志置位的话,那么当前的中断任务执行iret命令时引起任务的切换 */ __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); ltr(0); // 将任务0的tss加载到寄存器 lldt(0); // 将局部描述符表加载到局部描述符表寄存器 // 是将gdt和相应的ldt描述符的选择符加载到ldtr。只是明确的加载这一次 // 以后任务的ldt加载,是cpu根据tss中的ldt自动加载的 // 初始化8253定时器 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); } /* * 下面解释linux的init进程的整体认识 * 1.在系统的启动阶段时,bootsect.s文件只是见将系统加载到内存中,内核都 * 没有加载完成,谈不上对于进程管理的影响。setup.s中加载了全局的gdtr, * 但是此时的gdt只是为了让程序运行在保护模式下,没有什么作用。在setup.s * 文件中设置的gdt的格式如下 : * ----------- * | 0 0 0 0 | * ----------- * | code seg| * ------------ * | data seg| * ----------- * | ...... | * 在head.s文件中还需要改写该gdt。经过该文件的修改后新生成的gdt如下: * ----------- * | 0 0 0 0 | * ----------- * | code | * ----------- * | data | * ----------- * | system | -- do not use in linux * ----------- * | | -| * ----------- |--剩下的各项是预留给其他任务,用于放置ldt和tss * | | -| * * 在main.c文件中调用函数sched_init,此函数加载进程init。 * 加载init进程的ldt和tdd段 * | * 由程序来执行加载ldt和tss段寄存器的任务 * | * 即实现init进程的加载,即是手工创建的第一次进程。 * 此时的gdt大概上讲是: * ---------- * | 0 0 0 0 | * ----------- * | code | * ----------- * | data | * ----------- * | system | * ----------- * | ldt0 | * ----------- * | tss0 | * ----------- * | ... | * 此后,进程使用fork系统调用来产生新的进程,同时进程调度开始起作用。 * */ 参考《linux内核完全注释》和网上相关文章