Linux0.11内核--进程调度分析之2.调度

时间:2022-09-15 09:35:47

【版权所有,转载请注明出处。出处:http://www.cnblogs.com/joey-hua/p/5596830.html

上一篇说到进程调度归根结底是调用timer_interrupt函数,在system_call.s中:

#### int32 -- (int 0x20) 时钟中断处理程序。中断频率被设置为100Hz(include/linux/sched.h,5),
# 定时芯片8253/8254 是在(kernel/sched.c,406)处初始化的。因此这里jiffies 每10 毫秒加1。
# 这段代码将jiffies 增1,发送结束中断指令给8259 控制器,然后用当前特权级作为参数调用
# C 函数do_timer(long CPL)。当调用返回时转去检测并处理信号。
.align 2
_timer_interrupt:
push %ds # save ds,es and put kernel data space
push %es # into them. %fs is used by _system_call
push %fs
pushl %edx # we save %eax,%ecx,%edx as gcc doesn't
pushl %ecx # save those across function calls. %ebx
pushl %ebx # is saved as we use that in ret_sys_call
pushl %eax
movl $0x10,%eax # ds,es 置为指向内核数据段。
mov %ax,%ds
mov %ax,%es
movl $0x17,%eax # fs 置为指向局部数据段(出错程序的数据段)。
mov %ax,%fs
incl _jiffies
# 由于初始化中断控制芯片时没有采用自动EOI,所以这里需要发指令结束该硬件中断。
movb $0x20,%al # EOI to interrupt controller #1
outb %al,$0x20 # 操作命令字OCW2 送0x20 端口。
# 下面3 句从选择符中取出当前特权级别(0 或3)并压入堆栈,作为do_timer 的参数。
movl CS(%esp),%eax
andl $3,%eax # %eax is CPL (0 or 3, 0=supervisor)
pushl %eax
# do_timer(CPL)执行任务切换、计时等工作,在kernel/shched.c,305 行实现。
call _do_timer # 'do_timer(long CPL)' does everything from
addl $4,%esp # task switching to accounting ...
jmp ret_from_sys_call

前面一堆push指令保存当前的寄存器,然后在ret_from_sys_call中弹出。

movl $0x10,%eax把段选择子0x10也就是内核数据段选择子赋值给eax,然后再赋给ds、es;

然后_jiffies加1,jiffies在sched.h中定义:

extern long volatile jiffies;	// 从开机开始算起的滴答数(10ms/滴答)。

接下来三句指令比较关键:

movl CS(%esp),%eax
andl $3,%eax # %eax is CPL (0 or 3, 0=supervisor)
pushl %eax

从上面push的寄存器当中取出cs寄存器的值,也就是代码段选择子,根据选择的结构,0-1位是特权级,andl $3,%eax就是取eax中0-1位的值,然后把eax压栈当成do_timer的参数传递,4个字节。

好了,现在进入do_timer函数,在sched.c中:

//// 时钟中断C 函数处理程序,在kernel/system_call.s 中的_timer_interrupt(176 行)被调用。
// 参数cpl 是当前特权级0 或3,0 表示内核代码在执行。
// 对于一个进程由于执行时间片用完时,则进行任务切换。并执行一个计时更新工作。
void do_timer (long cpl)
{
extern int beepcount; // 扬声器发声时间滴答数(kernel/chr_drv/console.c,697)
extern void sysbeepstop (void); // 关闭扬声器(kernel/chr_drv/console.c,691) // 如果发声计数次数到,则关闭发声。(向0x61 口发送命令,复位位0 和1。位0 控制8253
// 计数器2 的工作,位1 控制扬声器)。
if (beepcount)
if (!--beepcount)
sysbeepstop (); // 如果当前特权级(cpl)为0(最高,表示是内核程序在工作),则将内核程序运行时间stime 递增;
// [ Linus 把内核程序统称为超级用户(supervisor)的程序,见system_call.s,193 行上的英文注释]
// 如果cpl > 0,则表示是一般用户程序在工作,增加utime。
if (cpl)
current->utime++;
else
current->stime++; // 如果有用户的定时器存在,则将链表第1 个定时器的值减1。如果已等于0,则调用相应的处理
// 程序,并将该处理程序指针置为空。然后去掉该项定时器。
if (next_timer)
{ // next_timer 是定时器链表的头指针(见270 行)。
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 的数字输出寄存器中马达启动位有置位的,则执行软盘定时程序(245 行)。
if (current_DOR & 0xf0)
do_floppy_timer ();
if ((--current->counter) > 0)
return; // 如果进程运行时间还没完,则退出。
current->counter = 0;
if (!cpl)
return; // 对于超级用户程序(内核态程序),不依赖counter 值进行调度。
schedule ();
}

传递来的参数cpl的作用就是如果为0,表示是内核程序,则stime加1,否则都是普通用户程序,则utime加1。

用户定时器等用到再说。

接下来判断时间片counter,在sched.h的进程描述符中:

long counter;		// long counter 任务运行时间计数(递减)(滴答数),运行时间片。

如果还有时间片则不调用调度函数schedule(),然后时间片减1并退出此函数。

如果时间片已用完(<=0),则置时间片为0,紧接着判断特权级,如果是内核级程序则直接退出函数。否则进入最核心的调度函数schedule:

/*
* 'schedule()'是调度函数。这是个很好的代码!没有任何理由对它进行修改,因为它可以在所有的
* 环境下工作(比如能够对IO-边界处理很好的响应等)。只有一件事值得留意,那就是这里的信号
* 处理代码。
* 注意!!任务0 是个闲置('idle')任务,只有当没有其它任务可以运行时才调用它。它不能被杀
* 死,也不能睡眠。任务0 中的状态信息'state'是从来不用的。
*/
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)
{
// 如果设置过任务的定时值alarm,并且已经过期(alarm<jiffies),则在信号位图中置SIGALRM 信号,
// 即向任务发送SIGALARM 信号。然后清alarm。该信号的默认操作是终止进程。
// jiffies 是系统从开机开始算起的滴答数(10ms/滴答)。定义在sched.h 第139 行。
if ((*p)->alarm && (*p)->alarm < jiffies)
{
(*p)->signal |= (1 << (SIGALRM - 1));
(*p)->alarm = 0;
}
// 如果信号位图中除被阻塞的信号外还有其它信号,并且任务处于可中断状态,则置任务为就绪状态。
// 其中'~(_BLOCKABLE & (*p)->blocked)'用于忽略被阻塞的信号,但SIGKILL 和SIGSTOP 不能被阻塞。
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];
// 这段代码也是从任务数组的最后一个任务开始循环处理,并跳过不含任务的数组槽。比较每个就绪
// 状态任务的counter(任务运行时间的递减滴答计数)值,哪一个值大,运行时间还不长,next 就
// 指向哪个的任务号。
while (--i)
{
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
// 如果比较得出有counter 值大于0 的结果,则退出124 行开始的循环,执行任务切换(141 行)。
if (c)
break;
// 否则就根据每个任务的优先权值,更新每一个任务的counter 值,然后回到125 行重新比较。
// counter 值的计算方式为counter = counter /2 + priority。[右边counter=0??]这里计算过程不考虑进程的状态。
for (p = &LAST_TASK; p > &FIRST_TASK; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
}
// 切换到任务号为next 的任务运行。在126 行next 被初始化为0。因此若系统中没有任何其它任务
// 可运行时,则next 始终为0。因此调度函数会在系统空闲时去执行任务0。此时任务0 仅执行
// pause()系统调用,并又会调用本函数。
switch_to (next); // 切换到任务号为next 的任务,并运行之。
}

前面的比较好理解,直接分析主要部分,此部分的主要工作就是从所有的任务中找出时间片最大的任务,也就意味着运行的时间较少,next就指向这个任务并跳出循环去切换任务。

如果所有任务的时间片都为0,就根据每个任务的优先权值来更新每个任务的时间片counter值。然后重新找到next,最后切换任务,调用switch_to(next):

// 宏定义,计算在全局表中第n 个任务的TSS 描述符的索引号(选择符)。
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3)) /*
* switch_to(n)将切换当前任务到任务nr,即n。首先检测任务n 不是当前任务,
* 如果是则什么也不做退出。如果我们切换到的任务最近(上次运行)使用过数学
* 协处理器的话,则还需复位控制寄存器cr0 中的TS 标志。
*/
// 输入:%0 - 新TSS 的偏移地址(*&__tmp.a); %1 - 存放新TSS 的选择符值(*&__tmp.b);
// dx - 新任务n 的选择符;ecx - 新任务指针task[n]。
// 其中临时数据结构__tmp 中,a 的值是32 位偏移值,b 为新TSS 的选择符。在任务切换时,a 值
// 没有用(忽略)。在判断新任务上次执行是否使用过协处理器时,是通过将新任务状态段的地址与
// 保存在last_task_used_math 变量中的使用过协处理器的任务状态段的地址进行比较而作出的。
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__( "cmpl %%ecx,_current\n\t" \ // 任务n 是当前任务吗?(current ==task[n]?)
"je 1f\n\t" \ // 是,则什么都不做,退出。
"movw %%dx,%1\n\t" \ // 将新任务的选择符??*&__tmp.b。
"xchgl %%ecx,_current\n\t" \ // current = task[n];ecx = 被切换出的任务。
"ljmp %0\n\t" \ // 执行长跳转至*&__tmp,造成任务切换。
// 在任务切换回来后才会继续执行下面的语句。
"cmpl %%ecx,_last_task_used_math\n\t" \ // 新任务上次使用过协处理器吗?
"jne 1f\n\t" \ // 没有则跳转,退出。
"clts\n" \ // 新任务上次使用过协处理器,则清cr0 的TS 标志。
"1:"::"m" (*&__tmp.a), "m" (*&__tmp.b),
"d" (_TSS (n)), "c" ((long) task[n]));
}

分析这段代码前先要知道,在32位保护模式下,有2种直接发起任务切换的方法:

1.call 0x0010:0x00000000

2.jmp 0x0010:0x00000000

在这两种情况下,call和jmp指令的操作数是任务的TSS描述符选择子或任务门。当处理器执行这两条指令时,首先用指令中给出的描述符选择子访问GDT,分析它的描述符类型。如果是一般的代码段描述符,就按普通的段间转移规则执行;如果是调用门,按调用门的规则执行;如果是TSS描述符,或者任务门,则执行任务切换。此时,指令中给出的32位偏移量被忽略,原因是执行任务切换时,所有处理器的状态都可以从TSS中获得

当任务切换发生的时候,TR寄存器的内容也会跟着指向新任务的TSS。在任务切换时,任务寄存器tr 由CPU 自动加载。这个过程是这样的:首先,处理器将当前任务的现场信息保存到由TR寄存器指向的TSS;然后,再使TR寄存器指向新任务的TSS,并从新任务的TSS中恢复现场。

注意:任务门描述符可以安装在中断描述符表中,也可以安装在GDT或者LDT中。

知道了理论知识,上面的代码就不难分析了,关键的一句是把新任务的TSS选择子赋值给%1也就是*&_tmp.b处,现在b的值就是TSS选择子,注意这里ljmp %0相当于ljmp *%0,表示是间接跳转,相当于“ljmp *__tmp.a”,也就是跳转到地址&__tmp.a中包含的48bit逻辑地址处。而按struct _tmp的定义,这也就意味着__tmp.a即为该逻辑地址的offset部分,__tmp.b的低16bit为seg_selector(高16bit无用)部分。

直到这行指令执行完,才算真正的任务切换!至此进程调度分析结束。

Linux0.11内核--进程调度分析之2.调度的更多相关文章

  1. Linux0&period;11内核--进程调度分析之1&period;初始化

    [版权所有,转载请注明出处.出处:http://www.cnblogs.com/joey-hua/p/5596746.html ] 首先看main.c里的初始化函数main函数里面有个函数是对进程调度 ...

  2. Linux-0&period;11内核源代码分析系列:内存管理get&lowbar;free&lowbar;page&lpar;&rpar;函数分析

    Linux-0.11内存管理模块是源码中比較难以理解的部分,如今把笔者个人的理解发表 先发Linux-0.11内核内存管理get_free_page()函数分析 有时间再写其它函数或者文件的:) /* ...

  3. Linux0&period;11内核--引导程序分析

    1.简介 本文主要介绍三个文件bootsect.s.setup.s.head.s,主要是做了些从软盘加载内核和设置32位保护模式的操作. 2.程序分析 当PC电源打开后,BIOS自检后将bootsec ...

  4. linux0&period;11内核源码剖析&colon;第一篇 内存管理、memory&period;c【转】

    转自:http://www.cnblogs.com/v-July-v/archive/2011/01/06/1983695.html linux0.11内核源码剖析第一篇:memory.c July  ...

  5. Linux0&period;11内核源码——内核态线程&lpar;进程&rpar;切换的实现

    以fork()函数为例,分析内核态进程切换的实现 首先在用户态的某个进程中执行了fork()函数 fork引发中断,切入内核,内核栈绑定用户栈 首先分析五段论中的第一段: 中断入口:先把相关寄存器压栈 ...

  6. linux0&period;11内核源码——进程各状态切换的跟踪

    准备工作 1.进程的状态有五种:新建(N),就绪或等待(J),睡眠或阻塞(W),运行(R),退出(E),其实还有个僵尸进程,这里先忽略 2.编写一个样本程序process.c,里面实现了一个函数 /* ...

  7. Linux0&period;11内核剖析--内核体系结构

    一个完整可用的操作系统主要由 4 部分组成:硬件.操作系统内核.操作系统服务和用户应用程序,如下图所示: 用户应用程序是指那些字处理程序. Internet 浏览器程序或用户自行编制的各种应用程序: ...

  8. Linux0&period;11内核--内存管理之1&period;初始化

    [版权所有,转载请注明出处.出处:http://www.cnblogs.com/joey-hua/p/5597705.html ] Linux内核因为使用了内存分页机制,所以相对来说好理解些.因为内存 ...

  9. linux0&period;11内核源码——boot和setup部分

    https://blog.csdn.net/KLKFL/article/details/80730131 https://www.cnblogs.com/joey-hua/p/5528228.html ...

随机推荐

  1. 一言不合敲代码(1)——DIV&plus;CSS3制作哆啦A梦头像

    先展示一下我的头像吧. 作为一个前端ER,我的头像当然不能是绘画工具画出来的.没错,这个玩意是由HTML+CSS代码实现的,过年的某一天晚上无聊花了一个小时敲出来的.来看看它原本的样子: 为什么会变成 ...

  2. sqlserver日志的备份与还原

    ----------完整备份与还原----------                --完整备份数据库--backup database studb to disk='e:\stu.bak'back ...

  3. php include

    get_include_path  获取当前 include_path 配置选项的值,在当前代码目录未找到include文件时,则到include_path去include. set_include_ ...

  4. SOAP&plus;WSDL

    一 SOAPSOAP最开始是用作RPC机制的,后来XML的出现使其应用非常广泛.它与HTTP一样是一种应用级协议,使用他可以在不同的应用程序之间进行数据交换.SOAP可以基于HTTP,也可以基于HTT ...

  5. &lbrack;liu yanling&rsqb;测试用例的设计方法

    一.功能测试      1.对话框测试输入进行测试.包括中文字符.英文字符.数字字符.特殊字符.及几种字符的组合.      2.对界面可操作按钮进行测试.包括[新增(N)][保存(S)][修改(M) ...

  6. thinkphp 常见问题

    0.写在最前面的不断更新 (1)trace不起作用 A:必须要输出到模板,才会有trace信息 (2)提示“您浏览的页面暂时发生了错误!请稍后再试-” A:检查控制器(看看能进到控制器没有,设断点输出 ...

  7. poj1700--贪心算法

    题意:一群人坐船过河,船只有一辆,且一次最多坐两人,时间按慢的算.求最短过河时间? 总共有两种做法可使用: 1.先让最快和次快的过去,让最快的把船开回,再让最慢和次慢的过去,让次快的把船开回.需两个来 ...

  8. Python教程&lpar;2&period;7&rpar;——条件分支

    像其它语言一样,Python也有条件分支. 例如,输入用户年龄,可能需要判断是否成年,并做出不同反应.这就需要用到条件分支. if条件分支 if条件分支的一般格式如下: if condition: s ...

  9. java io系列19之 CharArrayWriter&lpar;字符数组输出流&rpar;

    本章,我们学习CharArrayWriter.学习时,我们先对CharArrayWriter有个大致了解,然后深入了解一下它的源码,最后通过示例来掌握它的用法. 转载请注明出处:http://www. ...

  10. ComputeShader中Counter类型的使用

    接上一篇:https://www.cnblogs.com/hont/p/10122129.html 除了Append类型对应的Consume/AppendStructuredBuffer还有一个Cou ...