一、前言
Linux调度器神秘而充满诱惑,每个Linux工程师都想深入其内部一探究竟。不过中国有一句古话叫做“相由心生”,一个模块精巧的内部逻辑(也就是所谓的“心”)其外延就是简洁而优雅的接口(我称之为“相”)。通过外部接口的定义,其实我们也可以收获百分之六七十的该模块的内部信息。因此,本文主要描述Linux调度器开放给用户空间的接口,希望可以通过用户空间的调度器接口来理解Linux调度器的行为。

二、nice函数

nice函数用来修改调用进程的nice value,其接口定义如下:

#include <unistd.h>
       int nice(int inc);

为了方便说明该接口的作用,我们还是举实际的例子说明。程序调用nice(3),则将当前进程的nice value增加3,这也就是意味着该进程的优先级降低3个level(提升nice value也就是对别人更加nice,自己的优先级就会低)。如果程序调用nice(-5),则将当前进程的nice value减去5,这也就是意味着该进程的优先级提升5个level。当调用错误的时候返回-1,调用成功会稍微有一些歧义。POSIX标准规定了nice函数返回新的nice value,但是linux的系统调用和c库都是采用了操作成功返回0的方式。这样的处理方式使得在调用nice函数的时候无法得到当前的优先级,如果想要得到当前优先级,需要调用getpriority函数,我们在下一小节描述。

虽然说nice函数是用来调整优先级,实际上调整nice value就是调整调度器分配给该进程的CPU时间,具体是如何影响cpu time的呢?我们在后面描述内核代码的时候再详聊。此外,需要注意的是:根据POSIX标准,nice value是一个per process的设定,但是在linux中,nice value没有遵从这个标准,它是per-thread的一个属性。

三、getpriority/setpriority函数

从上节的描述中,我们了解到了nice的函数的限制,例如只能修改自己的nice value,无法获取当前的nice value值等,为此我们给出加强版本的nice接口,也就是getpriority/setpriority函数了。getpriority/setpriority函数定义如下:

#include <sys/time.h>
#include  <sys/resource.h>

int getpriority(int which, int who);
int setpriority(int which, int who, int prio);

你说接口增加功能是好事,怎么就把名字也改了呢?为何不是getnice/setnice呢?其实从上节的描述也看出稍许端倪,我们并没有区分调度优先级和nice value这两个值,历史上,首先被使用的是nice value,很快大家觉得这个词不是那么好理解,特别是对于初学者,因此改成优先级(priority)这样的名词可以让用户更好的理解这个API的作用,当然,事实证明这个改动并不是非常理想,我们后面会描述。

getpriority/setpriority功能比较强大,能处理多种请求,不同的请求通过which和who这两个参数来制定。当which等于PRIO_PROCESS的时候,who需要传入一个process id的参数,getpriority将返回指定进程的nice value。当which等于PRIO_PGRP的时候,who需要传入一个process group id的参数,此时getpriority将返回指定进程组中优先级最高的那个(BTW,nice value是最小的)。当which等于PRIO_USER的时候,who需要user id的信息,这时候,getpriority将返回属于该user的所有进程中nice value最小的那个。who等于0说明要get或者set的对象是当前进程(或者当前进程组,或者当前的user)。

setpriority类似与nice,当然功能要强那么一点点,因为它可以接收PRIO_PROCESS,PRIO_PGRP或者PRIO_USER参数用来设定一组进程的nice value。setpriority的返回值和其他函数类似,0表示成功,-1表示操作失败,不过getpriority就稍微有一点绕了。作为linux程序员,我们都知道的nice value是[-20, 19],如果getpriority返回这个范围,那么这里的-1优先级就有点尴尬了,因为一般的linux c库接口函数返回-1表示调用错误,我们是如何区分-1调用错误的返回还是优先级-1的返回值呢?getpriority是少数返回-1也是有可能正确的接口函数:在调用getpriority之前,我们需要首先将errno清零,调用getpriority之后,如果返回-1,我们需要看看errno是否还是保持0值,如果是,那么说明返回的是优先级-1,否则说明发生了错误。

四、操作rt priority的接口

传统的类unix内核,调度器是采用round-robin time-sharing的算法:如果有若干个进程是runnable的,那么不着急,大家排排队、吃果果,每个进程分配一个cpu时间片,大家轮流按照分配的时间片来获取cpu资源,所有的时间片用完,那么就重新一轮的分配。在这样的模型下面,间接影响cpu时间片的nice接口函数就够用了。当然,分配了更多的时间片也就是意味着有更高的优先级,因此nice vlaue也被称为进程的优先级。

但是,新的需求层出不穷(人类的欲望是无穷D),特别是实时性方面的需求,因此,POSIX标准(2008版本)增加了实时调度的内容,并且提供了POSIX realtime scheduling API来让用户空间来修改调度策略和调度优先级。这下子有点尴尬了,原来的nice value大家已经习惯称之为进程优先级了,现在真正的进程优先级登场了,怎么区分?为了解决这个问题,我们引入一个新的名词叫做调度策略(scheduling policy)。调度器在运作的时候往往设定一组规则来决定何时,选择哪一个进程进入执行状态,执行多长的时间。那些“规则”就是调度策略。

好的调度策略依赖于对进程的分类,有一类进程是大家都灰常的熟悉了就是普通进程,使用时间片轮转算法的那些进程。当然这类进程还可以细分,例如运算密集型进程(SCHED_BATCH,调度器最好不要太经常的唤醒这种进程),例如idle类进程(SCHED_IDLE),idle类进程优先级非常低,也就是说如果系统有其他事情要处理就去干别的事情(调度其他进程执行),实在没有活干了,再考虑IDLE类型的进程。不论哪一种普通进程,其优先级使用nice value这样一个调度参数来描述就OK了。

除了普通进程,还有一类是严格按照优先级来调度的进程,如果熟悉RTOS的话,对priority-base的调度器应该不会陌生,官大一级压死人,只要优先级高的进程是runnable的,那么优先级低的进程是根本没有机会执行的。这里的优先级才是真正意义的优先级,但是nice value已经被称为进程优先级了,因此这里的优先级被叫做rt priority。rt进程的调度又被细分成两类:SCHED_FIFO和SCHED_RR。这两种调度策略在相同rt priority的时候稍有差别,SCHED_FIFO是谁先到谁先获取cpu资源,并且一直占用,直到主动让出cpu或者退出,相同rt priority的进程才有机会执行。SCHED_RR稍微人性化了一点,相同rt priority的进程有时间片,大家轮流执行。对于实时进程而言,rt priority这个调度参数就描述了全部。

介绍到这里,是时候总结一下了:进程优先级有两个范围,一个是nice value,用前两个小节的API来set或者get。另外一个优先级是rt priority,完全碾压nice value这种优先级,操作rt priority的接口就在这一小节描述。

OK,经过漫长的铺垫过程,我们终于可以介绍realtime process scheduling API了,具体API定义如下:

#include <sched.h>

int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);

int sched_getscheduler(pid_t pid);

int sched_get_priority_max(int policy);--返回指定policy的最大的rt priority
int sched_get_priority_min(int policy);--返回指定policy的最小的rt priority

int sched_setparam(pid_t pid, const struct sched_param *param);
int sched_getparam(pid_t pid, struct sched_param *param);

sched_get_priority_max和sched_get_priority_min分别返回了指定调度策略的最大和最小的rt priority,不同的操作系统实现不同的优先级数量。在linux中,实时进程(SCHED_FIFO和SCHED_RR)的rt priority共计99个level,最小是1,最大是99。对于其他的调度策略,这些函数返回0。

sched_getscheduler函数可以获取指定进程的scheduling policy(如果pid等于0,那么是获取调用进程的调度策略)。sched_setscheduler函数是用来设定指定进程的scheduling policy,对于实时进程,该接口函数还可以设定rt priority。如果设定进程的调度策略是非实时的调度策略的时候(例如SCHED_NORMAL),那么param参数是没有意义的,其sched_priority成员必须设定为0。sched_setparam/sched_getparam非常简单,大家自己看man page好了。

五、一统江湖的接口

看起来前面小节描述的API已经够用了,然而,故事并未结束。经过前面关于调度接口的讨论,基本上我们对调度器的行为也已经有了了解:调度器就是按照优先级(指rt priority)来工作,优先级高的永远是优先调度。范围落在[1,99]的rt priority是实时进程,而rt priority等于0的是普通进程。对于普通进程,调度器还要根据nice value(这个也曾经被称为优先级,不要和rt priority弄混了)来进行调整。用户空间的进程可以通过各种前面描述的接口API来修改调度策略、nice value以及rt priority。一切看上去已经完美,CFS类型的调度器处理普通的运算密集形(例如编译内核)和用户交互形的应用(例如vi编辑文件)。如果有应用有实时需求,可以考虑让rt类型的调度器来运筹帷幄。但是,如何混合了一些realtime的应用以及有一些timing要求的应用的时候,SCHED_FIFO和SCHED_RR并不能解决问题,因为在这种调度策略下,高优先级的任务会永远的delay低优先级的任务,如果低优先级的任务有一些timing的需求,这时候,你根本控制不了调度延迟时间。

为了解决上一节中描述的问题,一类新的进程被定义出来,这类进程的优先级比实时进程和普通进程的优先级都要高,这类进行有自己的特点,参考下图:

这类进程的特点就是每隔固定的周期都会起来干活,需要一定的时间来处理事务。这类进程很牛,一上来就告诉调度器,我可是有点脾气的进程,和其他的那些妖艳的进程不一样的,我每隔一段时间(period)你就得固定分配给我一定的cpu资源(computer time),当然,分配的cpu time必须在该周期内执行完毕,因此就有deadline的概念。为了应对这种需求,3.14内核引入了一类新的进程叫做deadline进程,这类进程的调度策略是SCHED_DEADLINE。调度器对这类进程也会高看一眼,每当一个周期的开始时间到来的时候(也就是该deadline进程被唤醒的时间),调度器要优先处理这个deadline进程对cpu timer的需求,并且在某个指定的deadline时间内调度该进程执行。执行了指定的cpu time后,可以考虑调度走该进行,不过,当下一个周期到来的时候,调度器仍然要奋不顾身的在deadline时间内,再次调度该deadline进程执行。

虽然deadline进程优先级高于其他两类进程,但是用“优先级”来描述这类进程当然是不合理的,应该使用下面的三个参数来描述:

(1)周期时间(上图中的period)

(2)deadline时间(上图中的relative deadline)

(3)一次调度周期内分配多少的cpu时间(上图中的comp. time)

至此,估计您也已经发现,前面描述的接口其实都是不适合设定这些参数的,因此,GNU/linux操作系统中增加了下面的接口API:

#include <sched.h>

int sched_setattr(pid_t pid, const struct sched_attr *attr, unsigned int flags);
int sched_getattr(pid_t pid, const struct sched_attr *attr, unsigned int size, unsigned int flags);

attr这个参数的数据类型是struct sched_attr,这个数据结构囊括了一切你想要的关于调度的控制参数:policy,nice value,rt priority,period,deadline等等。用这个接口可以完成所有前面几个小节描述API能完成的任务,唯一的不好的地方就是这个接口是linux特有的,不是posix标准,是否应用这个接口就是见仁见智了。更细节的知识这里就不描述了,大家还是参考man page好了。

六、其他

上面描述的接口API都是和调度器参数相关,其实Linux调度器还有两类接口。一个是sched_getaffinity和sched_setaffinity,用于操作一个线程的CPU affinity。另外一个接口是sched_yield,该接口可以让出CPU资源,让Linux调度器选择一个合适的线程执行。这些接口很简单,大家仔细学习就OK了。

参考文档:

1、POSIX标准2008

2、linux下的各种man page

3、linux 4.4.6内核源代码

Linux调度器 - 用户空间接口的更多相关文章

  1. Linux时间子系统之(三):用户空间接口函数

    专题文档汇总目录 Notes:用户空间时间相关接口函数: 类型 API 精度 说明 时间 time stime time_t 精度为秒级 逐渐要被淘汰.需要定义__ARCH_WANT_SYS_TIME ...

  2. Linux时间子系统(三) 用户空间接口函数

    一.前言 从应用程序的角度看,内核需要提供的和时间相关的服务有三种: 1.和系统时间相关的服务.例如,在向数据库写入一条记录的时候,需要记录操作时间(何年何月何日何时). 2.让进程睡眠一段时间 3. ...

  3. Linux 调度器模拟

    http://www.ibm.com/developerworks/cn/linux/l-linux-scheduler-simulator/ LinSched LinSched 是驻留在用户空间中的 ...

  4. linux调度器源码分析 - 初始化(二)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言 上期文章linux调度器源码分析 - 概述(一)已经把调度器相关的数据结构介绍了一遍,本篇着重通过代码说明 ...

  5. (转载)Linux系统调用及用户编程接口(API)

    (转载)http://www.farsight.com.cn/news/emb167.htm 1 Linux系统调用 所谓系统调用是指操作系统提供给用户程序调用的一组“特殊”接口,用户程序可以通过这组 ...

  6. linux调度器源码分析 - 运行(四)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言 之前的文章已经将调度器的数据结构.初始化.加入进程都进行了分析,这篇文章将主要说明调度器是如何在程序稳定运 ...

  7. Linux 调度器发展简述

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

  8. Linux环境下用户空间与内核空间数据的交换方式

    在linux环境开发过程中,经常会需要在用户空间和内核空间之间进行数据交换. 介绍了 Linux 系统下用户空间与内核空间数据交换的几种方式 第一节:使用procfs实现内核交互简明教程(1) 第二节 ...

  9. Linux调度器 - 进程优先级

    一.前言 本文主要描述的是进程优先级这个概念.从用户空间来看,进程优先级就是nice value和scheduling priority,对应到内核,有静态优先级.realtime优先级.归一化优先级 ...

随机推荐

  1. 深入理解JSON

    一.JS判断字符串是否为JSON的方法: function isJSON(str) { if (typeof str == 'string') { try { JSON.parse(str); ret ...

  2. Android 如何修改默认输入法

    前言          欢迎大家我分享和推荐好用的代码段~~ 声明          欢迎转载,但请保留文章原始出处:          CSDN:http://www.csdn.net        ...

  3. Medication Reconciliation Overview

    http://www.hcsinc.net/HCS-Medication-Reconciliation/med-rec-overview.html At HCS, we've worked with ...

  4. Sqlserver存储过程生成日期维度

    话不多说,之前已经有一篇日志是利用oracle的存储过程生成日期维度表,接下来我们就用sqlserver来实现这个操作,如下面的步骤所示 1:创建日期维度表(Dim_time) USE [DW] GO ...

  5. window系统下调度数据库类型资源库中的kettle job

    已经存在kettle的一个资源库enfo,在目录/works/wxj下面有一个job (testmailsuccess.kjb)如何实现手工在kettle外部执行此job和让系统每天定时的调用此job ...

  6. Jmeter-Maven-Plugin高级应用:Test Results File Format-Test Results

    Test Results File Format Test Results Disabling The <testResultsTimestamp> Enabling <append ...

  7. 如何使用Ultraiso制作U盘启动盘

    准备好可启动的ISO文件和足够容量的U盘.点击工具-写入硬盘镜像. 各种U盘启动模式简介 1.USB-HDD:硬盘仿真模式,DOS启动后显示C:盘,HP U盘格式化工具制作的U盘即采用此启动模式.此模 ...

  8. ZH奶酪:Ubuntu启动/重启/停止apache服务

    Start Apache 2 Server /启动apache服务 # /etc/init.d/apache2 start or $ sudo /etc/init.d/apache2 start Re ...

  9. T-SQL 之 自定义函数

    和存储过程很相似,用户自定义函数也是一组有序的T-SQL语句,UDF被预先优化和编译并且作为一个单元进行调用.UDF和存储过程的主要区别在于返回结果的方式. 使用UDF时可传入参数,但不可传出参数.输 ...

  10. sql面试-查询选修课程的学生

    首先: 1 .  EXISTS 子查询找到的提交 NOT EXISTS 子查询中 找不到的提交 说明:不要去翻译为存在和不存在,把脑袋搞晕. 2 . 建立程序循环的概念,这是一个动态的查询过程.如 F ...