linux sched.c

sched.c

sched.c
kernel/里面一个非常重要的角色,就是它。所有进程运行都在它的管控之内,因此学习好这个.c事关重大,有些地方我个人认为是需要细致分析的,不能草率带过。为了尽量不遗漏任何一个可疑的痕迹,我们需要挨着盲扫所有的变量及其函数,即使遇到完全不懂的内容,我们也需标记下来,等后面懂了在做补充。

一、变量(define)
1. _BLOCKABLE:它将SIGKILL和SIGSTOP信号或起来在取反,看起来是要屏蔽这两个信号,看看,它只在schedule函数中使用了,表示同意带有这两个信号的可中断进程被置为就绪态。这个应用其实我还是有些印象,比如应用程序无法手动屏蔽这两个信号,这两个信号是一定会被触发的,导致的结果就是进程退出。
2. LATCH:抓住,占有的意思。在shced_init中被使用了一次,第一个是将(LATCH & 0xff)这个值写入0x40端口,0x40属于8253定时器的端口寻址范围,第一个值是写的是低字节,第二个值(LATCH >> 8)写的是高字节,也就是设定8253定时器的计数值,即定时器要响应LATCH次才发出一次时钟中断,而定时器的芯片的输入频率是1.193180MHz,那么这里我们做个算术题,计算1s发生一次时钟中断的LATCH值是多少?(哈哈),不要迷糊,其实很简单,就是时钟芯片的频率1193180,紧着着我们就可以轻松算得n秒发生一次时钟中断的LATCH值为1193180*n,如果10ms呢?n=0.01s,也即11931.8,如果换算成n Hz,LATCH=1193180/n。
3. TIME_REQUESTS:time list数组的大小。这里设为64。
收藏 分享

二、变量(static)
1. static union task_union init_task:联合体就是一个page的内存,保存着第一个进程结构体内容,在sched.h中已经将它初始化好了,我们在来体会一下这个最初的生命体。
  1)long state:0,表示就绪态。
  2)long counter:15,时间片滴答数。
  3)long priority:15,优先级值。
  4)long signal:0,还没有任何信号存在。
  5)struct sigaction sigaction[32]:{{},},空。
  6)long blocked:0,不屏蔽任何信号。
  7)long exit_code:0。
  8)long start_code,end_code,end_data,brk,start_stack:0,0,0,0,0,对于进程0用不到。
  13)long pid:0,初始进程。
  14)long pgrp,session,leader:0,0,0,初始化为0,其实也只有0号进程。
  17)int groups[32]:{-1,},暂时还没有。
  18)struct task_struct *p_pptr,*p_cptr,*p_ysptr,*p_osptr:&init_task.task,0,0,0:,它将父进程指向了自己,其他的都还没有。
  22)unsigned short uid,euid,suid,gid,egid,sgid:0,0,0,0,0,0,都是0号进程。
  28)unsigned long timeout:0,没有启动。
  29)unsigned long alarm:0,没有启动。
  30)long utime,stime,cutime,cstime,start_time:0,0,0,0,0,都没有需要。
  35)struct rlimit rlim[6]:{{0x7fffffff,0x7fffffff},...},都赋值为最大值。
  36)unsigned int flags:0。
  37)unsigned short used_math:0,没有使用数学协处理器。
  38)int tty:-1,没有对应的tty设备。
  39)unsigned short umask:0022,八进制,用户进程不可读不可写不可执行,同一用户组进程不可读可写不可执行,其他用户不可读可写不可执行。怎么感觉怪怪的!
  40)struct m_inode *pwd,*root,*executable,*library:NULL,NULL,NULL,NULL,是的,都没有。
  44)unsigned long close_on_exec:0,木有。
  45)struct file *filp[20]:{NULL,},木有。
  46)struct desc_struct ldt[3]:{{0,0},{0x9f,0xc0fa00},{0x9f,0xc0f200}},嘿嘿,这个是重点,这些可是进程0的段描述符哦,非常重要。ldt[0]没有,ldt[1]是代码段,ldt[2]是数据段,我们拆开看看:
      1-段限长:0xc0fa00[19:16]+0x9f[15:0]=0x9f,+1,0xa0,x4Kb,640Kb。
      2-基地址:0xc0fa00[31:24][7:0]+0x9f[31:16]=0x0。
      3-TYPE:类型,0xc0fa00[11:8]=0xa,b1100,代码,一致性,仅执行。
                    0xc0f200[11:8]=0x2,b0010,数据,可读写。
      4-DPL:特权级,0xc0fa00[14:13]=0x3,ring 3环,表示用户态。
      5-P:存在位,0xc0fa00[15:15]=0x1,存在。
      6-AVL:软件可用位,0xc0fa00[20:20]=0x0。
      7-B:默认操作大小,0xc0fa00[22:22]=0x1,32位地址。
      8-G:颗粒度标志,0xc0fa00[23:23]=0x1,4Kb。
  47)struct tss_struct tss:
      1> long back_link:0。
      2> long esp0,ss0:PAGE_SIZE+(long)&init_task,0x10。
      4> long esp1,ss1,esp2,ss2:0,0,0,0。
      8> long cr3:(long)&pg_dir。
      9> long eip:0,到时候会通过iret重设的。
      10> long eflags:0,到时候会通过iret重设的。
      11> long eax,ecx,edx,ebx:0,0,0,0,用不到。
      15> long esp:0,到时候会通过iret重设的。
      16> long ebp:0,用不到。
      17> long esi,edi:0,0,用不到。
      18> long es,cs,ss,ds,fs,gs:0x17,0x17,0x17,0x17,0x17,0x17,都要用数据段即可记得这个是用户态,因此得用LDT,bit2=1。
      24> long ldt:_LDT(0),任务0的LDT选择子。
      25> long trace_bitmap:0x80000000,?。
      26> struct i387_struct i387:{},到时候任务切换的时候会将该内容填满。
工作后发现linux内核才是我的最爱.

TOP

 
2. static struct task_struct *wait_motor[4]:
3. static int mon_timer[4]:
4. static int moff_timer[4]:
5. static unsigned char current_DOR:以上2~5都是关于floppy driver的,之所以放到了sched.c里面,是因为需要用到一些关于timer的处理的,这里写是最近的
6. static struct timer_list timer_list[64],*next_timer:这两个变量也是用于floppy的,可是他们已经脱离driver,组成了一个实现定时队列的机制。下面我们详细分析一下这个变异的算法。
  1)struct timer_list:用于实现定时队列的结构体。
      1> long jiffies:最重要的成员,用于实现优先执行序列。
      2> void (*fn)():执行的无参数函数指针。
      3> struct timer_list *next:用于链表。
  2)add_timer:添加定时队列成员。
  3)do_timer:检查执行定时函数。
第一种方法:我们可以利用插入排序来实现优先队列,时间复杂度为o(n),这样jiffies就表示闹钟时间,这样我们只需每次弹出队列头来和当前时间比较即可,小于等于当前时间就需要执行它。
第二种方法:作者采用了一种稍微复杂的做法来实现优先队列,时间复杂度仍是o(n),其实也是插入排序的思想,不过变量jiffies有了另外的意思,剩余的时间滴答数。我们需要分析出进行插入的情况,假设优先队列的jiffies为x0,x1,x2,..xi,..xn,那么我们就可以计算出每个成员执行的时间点,y0,y1,y2,..yi,..yn,这里yi=y(i-1)+xi,i>=1,i=0,y0=x0,即yi=x0+x1+x2+...+xi,假设现在有一个jiffies为yq的成员想插入优先队列中,yq的时间点刚好i-1 xi'=y(i-1)-yq+xi=xi-xq,而xq我们已经计算出来了,可以轻松得到xi'这个新的xi,这样我们就完成了yq的插入。如果队列头为0,说明时间到了可以弹出成员执行其函数。
作者写的这个方法是有问题的,少考虑一种情况,已经修改了原来的方案(自add_timer/sched.c):
        while (p->next) {
            if (p->jiffies <= p->next->jiffies) {
                p->next->jiffies -= p->jiffies;
                break;
            }

            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;
        }
工作后发现linux内核才是我的最爱.

TOP

 
三、变量(global)
1. unsigned long volatile jiffies:0,时钟滴答,系统设定每隔10ms发生一次。
2. unsigned long startup_time,0,系统启动时间,单位是秒。
3. int jiffies_offset,0,主要是用于时间设定相关的。
4. struct task_struct *current:&(init_task.task),当前运行进程。
5. struct task_struct *last_task_used_math:NULL,数学协处理器,这个有专门的模块负责。
6. struct task_struct *task[64]:{&(init_task.task),},任务指针数组。
7. long user_stack[PAGE_SIZE>>2]:任务0的用户堆栈,1Kb,在系统启动跳入保护模式后使用的内核堆栈是同一个。
8. struct {long *a,short b;} stack_start:{&user_stack[PAGE_SIZE>>2],0x10},系统进入任务0前使用的内核堆栈。

四、函数
1. show_task:打印一些任务结构体中的成员值。
2. show_state:打印所有任务的信息。
3. math_state_restore:在device_not_available(sys_call.s)中被call,存储协处理器的状态。
4. schedule:进程调度。
5. __sleep_on:睡眠调度。
6. interruptible_sleep_on:可中断睡眠。
7. sleep_on:不可中断睡眠。
8. wake_up:唤醒。
9. ticks_to_floppy_on:
10. floppy_on:
11. floppy_off:
12. do_floppy_timer:9~12这4个函数是floppy driver的,且不谈。
13. add_timer:也是floppy driver的,已经分析了。
14. do_timer:时钟中断C语言区,在timer_interrupt(sys_call.s)中被call。
15. sched_init:进程调度模块初始化。

五、系统调用
sys_pause,sys_alarm,sys_getpid,sys_getppid,sys_getuid,sys_geteuid,sys_getgid,sys_getegid,sys_nice。
工作后发现linux内核才是我的最爱.

TOP

 
=> sched_init
进程调度模块初始化函数。主要是任务0的设定,包括tss和ldt,初始化task数组,清NT以免造成麻烦,设定时钟响应频率,设定时钟中断门和系统调用门,开启时钟中断,开启后就可以认为任务0在内核态已经开始运行了,不过此时用的栈还是任务0的用户态栈。

=> schedule
进程调度,可以从当前进程切换到其他进程去。该函数可以分为3段说,第一段将超时的、定时的、有信号的可中断进程都切换到就绪态,第二段调度算法,找到下一个合适运行的进程,第三段开始切换。我们重点分析第二段和第三段,调度算法个人总结了几点:1)只调度就绪态进程,2)选择剩余时间片更多的进程,3)如果没有就绪态进程,选择0号进程,4)就绪态进程时间片都用完,重新分配时间片,这里包括了非就绪态进程,5)非就绪态进程变为就绪态的时间越晚,被分配的时间片就越多。切换进程其实只有一句ljmp长跳转,运用一个TSS选择子即可找到TSS的地址位置,找到后就可以开始自动的任务切换了,把当前进程的所有register都保存到当前的TSS段中,记得此时的eip已经指向了下一条指令,然后把新任务的TSS段中的所有保存的register覆盖CPU register,开始运行新任务。

=> __sleep_on
这个函数在0.11上写了两套,还有网友出贴分析了其中的bug,在0.12中,作者改进了睡眠队列的运作情况,保证了同一时刻一个资源只会存在一个睡眠队列。其实0.11的思想也应该是想这样的,不过可能code写的时候没有考虑完全导致出现的bug。保证一个睡眠队列的原因就是“*p = tmp”这点code,这就保证了资源队列头不会游离出来,于是条件“*p && *p != current”的判断也就起到了正确的效果,这个条件可以卡主不是队列头的进程(可中断进程)却先执行起来了,那么我们直接把它设为不可中断进程,然后继续调度即可,这都是为了保证队列的步伐整齐一致。

=> interruptible_sleep_on
可中断的睡眠,这样的话,进程就可以因为超时、定时和信号而变为就绪态得到执行权。

=> sleep_on
直接睡去,除非wake up主动唤醒。

=> wake_up
唤醒进程。

=> do_timer
我们先来看看它的前奏,timer_interrupt(sys_call.s),堆栈情况(貌似我很喜欢这个),进入前的堆栈:old ss,old esp,eflags,old cs,old eip,进入do_timer前的堆栈:old ss,old esp,eflags,old cs,old eip,ds,es,fs,-1,edx,ecx,ebx,eax,CPL。进入内核前都有几件必须做的事情,像ds,es这些数据段都需设为0x10,而fs数据段用作读取用户态数据,因此设为0x17。中断的话,要中断复位。do_timer完成后再返回去执行信号的东西,这个中断比较特殊一点。
下面在来看看do_timer函数做了些什么,第一部分和控制台相关,第二部分和harddisk driver相关,第三部分也和控制台相关,第四部分计算本进程的内核态用户态运行滴答数,第五部分和floppy driver相关,最后将本进程的时间片递减一次,若小于等于0了,那么可以调度其他进程运行了,然后在判断如果本进程是从内核态过来的就不能被调度即抢占,否则开始调度。

ok,sched.c基本分析完了,至于floppy driver和协处理器还有系统调用相关的内容会直接挪到对应模块中去。看起来没多少内容,也是的。我觉得慢慢的就要突出重点了,不必要的分析应该尽量避免,不然就有很罗嗦的赶脚。剩下内容重头戏就是信号处理的部分,信号处理说实话,应用写得蛮少的,何况现在我都不写linux应用了,这可能导致理解不够或者不深入,考虑不完全等,有难度啊,不过在难的坎还得自己来迈。

你可能感兴趣的:(linux sched.c)