一.说明

本文以linux-2.4.10 为例主要分析Linux 进程调度模块中的schedule 函数及其相关的函数。另外相关的前提知识也会说明。默认系统平台是自己的i386 架构的pc。

二.前提知识

在进行schedule 分析之前有必要简单说明一下系统启动过程,内存分配使用等。这样才能自然过渡到schedule 模块。

首先是Linux各个功能模块之间的依赖关系:

可见进程调度是整个内核的核心。但这部分,我想说明的是,我的pc是怎样把操作系统从硬盘装载到内存中,并启动进程调度模块的。然后才是后面对schedule的具体分析。

首先,启动操作系统部分,涉及到到三个文件:/arch/i386/boot/bootsect.s、/arch/i386/boot/setup.s、/arch/i386/boot/compressed/head.s。编译安装好一个Linux系统后,bootsect.s模块被放置在可启动设备的第一个扇区(磁盘引导扇区,512字节)。那么下面开始启动过程,三个文件在内存中的分布与位置的移动如下图。

在经过上图这一系列过程后,程序跳转到system模块中的初始化程序init中执行,即/init/main.c文件。该程序执行一系列的初始化工作,如寄存器初始化、内存初始化、中断设置等。之后内存的分配如下图:

此后,CPU有序地从内存中读取程序并执行。前面的main从内核态移动到用户态后,操作系统即建立了任务0,即进程调度程序。之后再由schedule模块进行整个Linux操作系统中进程的创建(fork),调度(schedule),销毁(exit)及各种资源的分配与管理等操作了。值得一说的是schedule将创建的第一个进程是init(pid=1),请注意它不是前面的/init/main.c程序段。如果是在GNU/Debian系统下,init 进程将依次读取rcS.d,rcN.d(rc0.d~rc6.d),rc.local三个run command脚本等,之后系统的初始化就完成了,一系列系统服务被启动了,系统进入单用户或者多用户状态。然后init 读取/etc/inittab,启动终端设备((exec)getty)供用户登陆,如debian中会启动6个tty,你可以用组合键ctrl+alt+Fn(F1~F6)来切换。

到这里就知道了Linux怎样启动进程调度模块了,也知道了进程调度模块启动的第一个进程init及之后的系统初始化和登陆流程。下面就回过头来分析schedule代码及其相关函数调用。

三.进程调度涉及的数据结构

文件:/linux/include/linux/sched.h

下面只简单介绍数据结构task_struct中的两个字段。

在Linux中,进程(Linux中用轻量级的进程来模拟线程)使用的核心数据结构。一个进程在核心中使用一个task_struct结构来表示,包含了大量描述该进程的信息,其中与调度器相关的信息主要包括以下几个:

1. state

volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */

Linux的进程状态主要分为三类:可运行的(TASK_RUNNING,相当于运行态和就绪态);被挂起的(TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE和TASK_STOPPED);不可运行的(TASK_ZOMBIE),调度器主要处理的是可运行和被挂起两种状态下的进程,其中TASK_STOPPED又专门用于SIGSTP等IPC信号的响应,而TASK_ZOMBIE指的是已退出而暂时没有被父进程收回资源的"僵死"进程。

2. counter

long counter;

该属性记录的是当前时间片内该进程还允许运行的时间。

四. 就绪进程选择算法(即进程调度算法)

文件:/kernel/sched.c

1.上下文切换

从一个进程的上下文切换到另一个进程的上下文,因为其发生频率很高,所以通常都是调度器效率高低的关键。schedule()函数中调用了switch_to宏,这个宏实现了进程之间的真正切换,其代码存放于include/i386/system.h。switch_to宏是用嵌入式汇编写成的,较难理解。

由switch_to()实现,而它的代码段在schedule()过程中调用,以一个宏实现。

switch_to()函数正常返回,栈上的返回地址是新进程的task_struct::thread::eip,即新进程上一次被挂起时设置的继续运行的位置(上一次执行switch_to()时的标号"1:"位置)。至此转入新进程的上下文中运行。

这其中涉及到wakeup,sleepon等函数来对进程进行睡眠与唤醒操作。

2.选择算法

Linux schedule()函数将遍历就绪队列中的所有进程,调用goodness()函数计算每一个进程的权值weight,从中选择权值最大的进程投入运行。

Linux的调度器主要实现在schedule()函数中。

调度步骤:

Schedule函数工作流程如下:

(1)清理当前运行中的进程

(2)选择下一个要运行的进程(pick_next_task)

(3)设置新进程的运行环境

(4) 进程上下文切换

五. Linux 调度器将进程分为三类

进程调度是操作系统的核心功能。调度器只是调度过程中的一部分,进程调度是非常复杂的过程,需要多个系统协同工作完成。本文所关注的仅为调度器,它的主要工作是在所有 RUNNING 进程中选择最合适的一个。作为一个通用操作系统,Linux 调度器将进程分为三类:

1. 交互式进程

此类进程有大量的人机交互,因此进程不断地处于睡眠状态,等待用户输入。典型的应用比如编辑器 vi。此类进程对系统响应时间要求比较高,否则用户会感觉系统反应迟缓。

2. 批处理进程

此类进程不需要人机交互,在后台运行,需要占用大量的系统资源。但是能够忍受响应延迟。比如编译器。

3. 实时进程

实时对调度延迟的要求最高,这些进程往往执行非常重要的操作,要求立即响应并执行。比如视频播放软件或飞机飞行控制系统,很明显这类程序不能容忍长时间的调度延迟,轻则影响电影放映效果,重则机毁人亡。

根据进程的不同分类 Linux 采用不同的调度策略。对于实时进程,采用 FIFO 或者 Round Robin 的调度策略。对于普通进程,则需要区分交互式和批处理式的不同。传统 Linux 调度器提高交互式应用的优先级,使得它们能更快地被调度。而 CFS 和 RSDL 等新的调度器的核心思想是“完全公平”。这个设计理念不仅大大简化了调度器的代码复杂度,还对各种调度需求的提供了更完美的支持。

六. 调度时机:调度什么时候发生?即:schedule()函数什么时候被调用?

调度的发生主要有两种方式:

1:主动式调度(自愿调度)

在内核中主动直接调用进程调度函数schedule(),当进程需要等待资源而暂时停止运行时,会把状态置于挂起(睡眠),并主动请求调度,让出cpu。

2:被动式调度(抢占式调度、强制调度)

用户抢占(2.4  2.6)

内核抢占(2.6)

(1)用户抢占发生在:从系统调用返回用户空间;

从中断处理程序返回用户空间。

内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。

主动式调度是用户程序自己调度schedule,也许有人会觉得自己的代码中能引用schedule吗?也许不行吧,但大家知道wait4我们是可以调用的,前面我们没有给出wait4的代码,但我们知道在执行了wait4效果是父进程被挂起,所谓的挂起就是不运行了,放弃了CPU,这里发生了进程调度是显而易见的,其实在代码中有如下几行:

current->state = TASK_INTERRUPIBLE;schedule();

还有exit也有

current->state = TASK_ZOMBIE; schedule();

这2种发生了进程调度,从代码上也可以看出(状态被改成了睡眠和僵死,然后去调度可运行进程,当前进程自然不会再占有CPU运行了),从效果中也能看出。这说明用户程序自己可以执行进程调度。

(2)内核抢占:在不支持内核抢占的系统中,进程/线程一旦运行于内核空间,就可以一直执行,直到它主动放弃或时间片耗尽为止。这样一些非常紧急的进程或线程将长时间得不到运行。

在支持内核抢占的系统中,更高优先级的进程/线程可以抢占正在内核空间运行的低优先级的进程/线程。

关于抢占式调度(强制调度),需要知道的是,CPU在执行了当前指令之后,在执行下一条指令之前,CPU要判断在当前指令执行之后是否发生了中断或异常,如果发生了,CPU将比较到来的中断优先级和当前进程的优先级(有硬件参与实现,如中断控制器8259A芯片;通过比较寄存器的值来判断优先级;中断服务程序的入口地址形成有硬件参与实现,等等,具体实现请见相关资料和书籍),如果新来任务的优先级更高,则执行中断服务程序,在返回中断时,将执行进程调度函数schedule。

关于抢占式调度,系统代码中,除了前面我们说到的wait4和exit等外(这两个系统函数是自愿或主动调度),还有一个地方会出现了schedule,就是中断返回代码里面出现了,这里出现了还加了限制条件,我们可以看看这个代码(所谓的中断返回代码,就是恢复中断现场的代码,每一个发生中断都会执行到的代码,无论是什么中断),这段代码是:

277 testl $(VM_MASK | 3),%eax  # return to VM86 mode or non-supervisor?

278 jne ret_with_reschedule

279 jmp restore_all

我们看到jne ret_with_reschedule在此之前还有一次条件判断,代码就不过多解释了,意思是:当中断发生在用户控件时候才会执行ret_with_reschedule,那么我们就看到,在中断返回到用户空间的前夕也是可能会发生进程调度的。

简单的说进程调度发生的两种情况:中断返回用户空间前夕,和用户程序自愿放弃CPU,这2种情况会发生进程调度。

在支持内核抢占的系统中,某些特例下是不允许内核被抢占的:

(a)内核正在运行中断处理程序,进程调度函数schedule()会对此作出判断,如果是在中断中调用,会打印出错误信息。

(b) 内核正在进行中断上下文的bottom half(中断的底半部)处理,硬件中断返回前会执行软中断,此时仍然处于中断上下文。

(c) 进程正持有spinlock自旋锁,writelock/readlock读写锁等,当持有这些锁时,不应该被抢占,否则由于抢占将导致其他cpu长时间不能获得锁而死锁。

(d) 内核正在执行调度程序scheduler

为了保证linux内核在以上情况下不会被抢占,抢占式内核使用了一个变量preempt_count,称为内核抢占计数。这一变量被设置在进程的thread_info结构体中,每当内核要进入以上几种状态时,变量preempt_count就加1,指示内核不允许抢占,反之减1。

内核抢占可能发生在:

1:中断处理程序完成,返回内核空间之前

2:当内核代码再一次具有可抢占性的时候,如解锁及使能软中断等。

调度标志——Tif_NEED_RESCHED

作用:内核提供了一个need_resched标志来表明是否需要重新执行一次调度。

设置:当某个进程耗尽它的时间片,会设置这个标志

当一个优先级更高的进程进入可执行状态的时候,也会设置这个标志位

进程并发不能靠进程自觉调度,只有靠中断(时钟中断)。

七. 内核调度和内核的理解

1. 内核调度也算是一个任务吗??

答:不,内核调度只能说是一种任务调度的算法,它不一直在运行,只是在任务结束/时间片结束的时候才执行,选择下一个要运行的任务。

2. 任务和内核的关系?

答:任务是运行在内核的管理之下的,也可以说任务是运行在内核的这个环境里的。

内核调度只是内核功能的一部份。内核本身不存在调度,它可以说一直在运行,主要是运行在任务之内和之间,它负责任务所需的资源处理。

3. 它和正在运行的那个最高优先级的任务是一种什么样的关联呢??

答:不管优先级多高,它都是运行在内核环境下的,内核是一直在运行的,只不过它是把CPU和其它资源分配给任务,让它运行而已。

4. 什么是内核?

答:其实内核不是一个进程,也不是一个现程。

内核通过他提供的api,融合进了应用程序。也就是说内核只是一种抽象的说法,他本身并不存在,而是在一些特定的时间和特定的条件才运行,才给我们的应用程序提供各种服务。

深入分析linux调度机制的更多相关文章

  1. Linux select 机制深入分析

    Linux select 机制深入分析            作为IO复用的实现方式.select是提高了抽象和batch处理的级别,不是传统方式那样堵塞在真正IO读写的系统调用上.而是堵塞在sele ...

  2. Linux进程组调度机制分析【转】

    转自:http://oenhan.com/task-group-sched 又碰到一个神奇的进程调度问题,在系统重启过程中,发现系统挂住了,过了30s后才重新复位,真正系统复位的原因是硬件看门狗重启的 ...

  3. (笔记)Linux内核学习(十一)之I/O层和I/O调度机制

    一 块I/O基本概念 字符设备:按照字符流的方式被有序访问的设备.如串口.键盘等. 块设备:系统中不能随机(不需要按顺序)访问固定大小的数据片(chunk 块)的设备. 如:硬盘.软盘.CD-ROM驱 ...

  4. 《深入分析Linux内核源代码》读书、私藏笔记大放送

    秉承着"不懂操作系统原理的程序员不是合格的程序员"的至理名言,鄙人又是买陈莉君老师的“Linux教学视频”,又是研读其力作<深入分析Linux内核源代码>,先将总结笔记 ...

  5. [k8s]k8s的控制层kubelet+docker配合调度机制(k8架构)

    意外停掉一台node的kubelet,发现调度有问题,研究了下调度的细节 k8s架构 控制层- kubelet(配合节点docker工作) 数据层- kube-proxy 逻辑图: object 参考 ...

  6. [转]golang的goroutine调度机制

    golang的goroutine调度机制 版权声明:本文为博主原创文章,未经博主允许不得转载.   目录(?)[-] 一直对goroutine的调度机制很好奇最近在看雨痕的golang源码分析基于go ...

  7. Linux信号机制

    Linux信号(signal) 机制分析 [摘要]本文分析了Linux内核对于信号的实现机制和应用层的相关处理.首先介绍了软中断信号的本质及信号的两种不同分类方法尤其是不可靠信号的原理.接着分析了内核 ...

  8. linux 调度总结(转载)

    调度: 操作系统的调度程序的两项任务: 1: 调度: 实现调度策略,决定就绪的进程.线程竞争cpu的次序的裁决原则.说白了就是进程和线程何时应该放弃cpu和选择那个就绪进程.线程来执行. 2: 分派: ...

  9. Skynet服务器框架(八) 任务和消息调度机制

    引言: 在我看来,消息和任务调度应该是skynet的核心,整个skynet框架的核心其实就是一个消息管理系统.在skynet中可以把每个功能都当做一个服务,整个skynet工程在执行过程中会创建很多个 ...

随机推荐

  1. 关于 HTTP meta 的 IE=edge 说明

    http://www.oschina.net/question/54100_17414 陌生标记标记一: < meta http-equiv = "X-UA-Compatible&qu ...

  2. com.alibaba.fastjson.JSONException: For input string: "8200-12-31"

    https://www.cnblogs.com/mengjinluohua/p/5544987.html https://samebug.io/exceptions/458113/com.alibab ...

  3. sql模糊查询,解除绑定的单号

    --610007570320-610007571319 1000张 delete from (select t.* from (select t1.bill_code, t1.bind_code, t ...

  4. 一、SQL基础知识点补充

    SQL DML 和 DDL 可以把 SQL 分为两个部分:数据操作语言 (DML) 和 数据定义语言 (DDL). SQL (结构化查询语言)是用于执行查询的语法.但是 SQL 语言也包含用于更新.插 ...

  5. mongodb 的创建和使用

    1. sudo apt-get install mongodb 2. 登陆数据库: mongo, 3. 创建数据库:use dbname 4. 插入数据: db.dbname.insert({&quo ...

  6. python+RobotFramework

    今天有人问我,她想在在robot里面用到数据库的一个值的随机数,但是不知道怎么实现,我用python写了一段代码链接数据库给表中所需的字段的值取随机数,代码如下: import random,pymy ...

  7. noip 2010 数字统计

    数位dp解水题 luogu1179 dp[i][j]表示 有i位,且首位是j(包括0) 的 ‘2’的个数 dp[i][j]={ Σ(dp[i-1][k]),j!=2; Σ(dp[i-1][k])+va ...

  8. 【二分图匹配】E. 过山车

    https://www.bnuoj.com/v3/contest_show.php?cid=9154#problem/E [题意] 裸的最大匹配 [教训] 一开始边数开了k,建的是无向图,结果T了,改 ...

  9. C#.net获取存储过程的Return返回值和Output输出参数值

    原文发布时间为:2008-10-25 -- 来源于本人的百度文章 [由搬家工具导入] 1.获取Return返回值 程序代码//存储过程//Create PROCEDURE MYSQL//     @a ...

  10. Intersection--poj1410(判断线段与矩形的关系)

    http://poj.org/problem?id=1410 题目大意:给你一个线段和矩形的对角两点  如果相交就输出'T'  不想交就是'F' 注意: 1,给的矩形有可能不是左上 右下  所以要先判 ...