Skip to content

Latest commit

 

History

History
460 lines (327 loc) · 23.4 KB

File metadata and controls

460 lines (327 loc) · 23.4 KB

十一、调度编排

系统编程是关于与底层操作系统的交互。调度器是每个操作系统的核心组件之一,影响进程在处理器上的分配方式。最终,这是最终用户所关心的:流程平稳运行,并且优先于其他流程。本章将教你通过改变进程的策略、其nice值、实时优先级、处理器关联性以及实时进程如何产生处理器来与调度程序交互所需的实用技能。

本章将涵盖以下食谱:

  • 学习设置和获取调度程序策略
  • 学习获取时间片值
  • 学习如何设定一个好的值
  • 学习如何生产处理器
  • 了解处理器关联性

技术要求

为了尝试本章中的程序,我们设置了一个 Docker 映像,其中包含了我们在本书中需要的所有工具和库。它基于 Ubuntu 19.04。

要设置它,请按照下列步骤操作:

  1. www.docker.com下载安装 Docker 引擎。

  2. 从 Docker Hub 中拉出图像:docker pull kasperondocker/system_programming_cookbook:latest

  3. 图像现在应该可以使用了。输入以下命令查看图像:docker images

  4. 你应该有如下图像:kasperondocker/system_programming_cookbook

  5. 借助docker run -it --cpu-rt-runtime=95000 --ulimit rtprio=99 --cap add=sys_nice kasperondocker/system_programming_cookbook:latest /bin/bash命令,用交互式外壳运行 Docker 图像。

  6. 运行容器上的外壳现已可用。使用root@39a5a8934370/# cd /BOOK/获取为本书开发的所有程序。

需要--cpu-rt-runtime=95000--ulimit rtprio=99--cap add=sys_nice参数来允许用 Docker 编写的软件设置调度器参数。如果主机配置正确,软件不会有任何问题。

Disclaimer: The C++ 20 standard has been approved (that is, technically finalized) by WG21 in a meeting in Prague at the end of February. This means that the GCC compiler version that this book uses, 8.3.0, does not include (or has very, very limited support for) the new and cool C++ 20 features. For this reason, the Docker image does not include the C++ 20 recipe code. GCC keeps the development of the newest features in branches (you have to use appropriate flags for that, for example, -std=c++ 2a); therefore, you are encouraged to experiment with them by yourself. So, clone and explore the GCC contracts and module branches and have fun.

学习设置和获取调度程序策略

在系统编程环境中,有些情况下某些进程必须以不同于其他进程的方式进行处理。我们所说的不同是指一个进程获得处理器时间或不同优先级的不同方式。系统程序员必须意识到这一点,并学习如何与调度程序的应用编程接口进行交互。本食谱将向您展示如何更改一个流程的策略,以满足不同的调度要求。

怎么做...

该配方将向您展示如何获取和设置流程的策略以及可以分配给它的限制。让我们开始吧:

  1. 在一个 shell 中,让我们打开一个名为schedParameters.cpp的新源文件。我们需要检查当前(默认)的流程策略是什么。为此,我们将使用sched_getscheduler()系统调用:
#include <sched.h>
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

int main ()
{
    int policy = sched_getscheduler(getpid());
    switch(policy) 
    {
        case SCHED_OTHER: std::cout << "process' policy = 
            SCHED_OTHER" 
                                    << std::endl ; break;
        case SCHED_RR: std::cout << "process' policy = SCHED_RR" 
                                 << std::endl; break;
        case SCHED_FIFO: std::cout << "process' policy = SCHED_FIFO" 
                                   << std::endl; break;
        default: std::cout << "Unknown policy" << std::endl;
    }
  1. 现在,我们要为SCHED_FIFO策略分配一个实时(rt)优先级。为了使代码可移植,我们从sched_get_priority_minsched_get_priority_maxAPI 获取最小值和最大值:
    int fifoMin = sched_get_priority_min(SCHED_FIFO);
    int fifoMax = sched_get_priority_max(SCHED_FIFO);
    std::cout << "MIN Priority for SCHED_FIFO = " << fifoMin
        << std::endl;
    std::cout << "MAX Priority for SCHED_FIFO = " << fifoMax
        << std::endl;

    struct sched_param sched;
    sched.sched_priority = (fifoMax - fifoMin) / 2;
    if (sched_setscheduler(getpid(), SCHED_FIFO, &sched) < 0)
        std::cout << "sched_setscheduler failed = " 
                  << strerror(errno) << std::endl;
    else
        std::cout << "sched_setscheduler has set priority to = "
                  << sched.sched_priority << std::endl;
  1. 我们应该能够检查分配了sched_getscheduler()功能的新SCHED_FIFO策略:
    policy = sched_getscheduler(getpid());
    std::cout << "current process' policy = " << policy << std
        ::endl ;
    return 0;
} 

下一节将详细描述前面的代码。

它是如何工作的...

POSIX 标准定义了以下策略:

  • SCHED_OTHER:正常的调度策略(即不针对实时进程)
  • SCHED_FIFO:先进先出
  • SCHED_RR:循环赛

这里SCHED_OTHER是默认的,SCHED_FIFO``SCHED_RR是实时的。实际上,Linux 将SCHED_NORMALSCHED_BATCHSCHED_IDLE定义为其他实时策略。这些在sched.h头文件中定义。

步骤 1 调用sched_getscheduler()检查流程的当前策略。不出所料,默认为SCHED_OTHER。我们将输入传递给getpid()函数(<unistd.h>,该函数返回当前进程的 PID。sched_getscheduler()也接受0,这种情况下代表当前流程。

第二步的目标是设置实时策略,用sched_setscheduler()功能优先当前进程。我们希望这个进程比机器上运行的正常进程具有更高的优先级。例如,考虑一个(软)实时应用,其中计算不能被中断,或者如果接收到软件中断,其处理不能被推迟。这些 Linux 盒子通常很少运行专用的进程。为此,要设置的策略是SCHED_FIFO,我们设置的优先级是当前系统上可以设置的最小值和最大值之间的中间值。总是建议用sched_get_priority_max()sched_get_priority_min()函数检查这些值,以便编写可移植代码。需要强调的一点是sched_setscheduler()功能在内部设置struct task_structrt_priority字段。

步骤 3 通过调用sched_getscheduler()功能检查SCHED_FIFO是否已正确设置,类似于步骤 1 中发生的情况。

还有更多...

SCHED_FIFOSCHED_RR是 POSIX 定义并在 Linux 上实现的两个策略,在更适合实时软件的处理器上分配任务。让我们看看它们是如何工作的:

  • SCHED_FIFO:当一个任务被这个策略返回时,它继续运行,直到它阻塞(例如,I/O 请求),它让出处理器,或者一个更高优先级的任务抢占它。
  • SCHED_RR:这与SCHED_FIFO的逻辑完全相同,但有一点不同:使用此策略调度的任务分配了一个时间片,以便任务继续运行,直到时间片到期或更高的任务抢占它或让出处理器。

请注意,当SCHED_OTHER(或SCHED_NORMAL)实现抢先形式的多任务处理时,SCHED_FIFOSCHED_RR是合作的(它们没有被抢先)。

Linux 主调度器功能循环所有策略,对于每个策略,它要求下一个任务运行。它通过pick_next_task()功能来实现这一点,该功能由每个策略来实现。主调度器在kernel/sched.c中定义,它定义了sched_class结构。这表示必须定义和实施每个策略,以便所有不同的策略都能正常工作。让我们从图形层面来看一下:

  • kernel/sched.c:定义struct sched_class并循环执行以下策略:
    • kernel/rt.c(针对SCHED_FIFOSCHED_RR)设置const struct sched_class rt_sched_class具有特定的实时策略功能。
    • kernel/fair.c(对于SCHED_NORMALSCHED_OTHER)设置const struct sched_class fair_sched_class特定于公平调度程序的功能。

观察 Linux 调度程序设计的一种方式是这样的:kernel/sched.c定义接口和接口下的特定策略。界面由struct sched_class结构表示。以下是SCHED_OTHER/SCHED_NORMAL(CFS 公平调度策略)的接口实现:

static const struct sched_class fair_sched_class = {
 .next = &idle_sched_class,
 .enqueue_task = enqueue_task_fair,
 .dequeue_task = dequeue_task_fair,
 .yield_task = yield_task_fair,
 .check_preempt_curr = check_preempt_wakeup,
 .pick_next_task = pick_next_task_fair,
 .put_prev_task = put_prev_task_fair,

#ifdef CONFIG_SMP
 .select_task_rq = select_task_rq_fair,
 .load_balance = load_balance_fair,
 .move_one_task = move_one_task_fair,
 .rq_online = rq_online_fair,
 .rq_offline = rq_offline_fair,
 .task_waking = task_waking_fair,
#endif
 .set_curr_task = set_curr_task_fair,
 .task_tick = task_tick_fair,
 .task_fork = task_fork_fair,
 .prio_changed = prio_changed_fair,
 .switched_to = switched_to_fair,
 .get_rr_interval = get_rr_interval_fair,

#ifdef CONFIG_FAIR_GROUP_SCHED
 .task_move_group = task_move_group_fair,
#endif
};

SCHED_FIFOSCHED_RR策略的实时优先级范围为[1, 99],而SCHED_OTHER优先级(称为nice)为[-20, 10]

请参见

  • 学习如何设置一个好的值配方,看看实时优先级与好的优先级是如何相关的
  • 学习如何产生处理器配方学习如何产生运行的实时任务
  • Linux 内核开发第三版,罗伯特·拉芙

学习获取时间片值

Linux 调度程序为任务分配处理器时间提供了不同的策略。学习设置和获取调度程序策略食谱显示了哪些策略可用以及如何更改它们。SCHED_RR策略,即循环策略,是用于实时任务的策略(使用SCHED_FIFO)。SCHED_RR策略为每个进程分配一个时间片。这个食谱将告诉你如何配置时间片。

怎么做...

在本食谱中,我们将编写一个小程序,通过使用sched_rr_get_interval()函数来获取循环时间片:

  1. 在一个新的 shell 中,打开一个名为schedGetInterval.cpp的新文件。我们必须包含调度程序功能的<sched.h><iostream.h>以登录到标准输出,以及<string.h>以使用strerror功能并将errno整数转换为可读字符串:
#include <sched.h>
#include <iostream>
#include <string.h>

int main ()
{
    std::cout << "Starting ..." << std::endl;
  1. 为了获得循环间隔,我们必须为我们的进程设置调度器策略:
    struct sched_param sched;
    sched.sched_priority = 8;
    if (sched_setscheduler(0, SCHED_RR, &sched) == -1)
        std::cout << "sched_setscheduler failed = "
            << strerror(errno) 
                  << std::endl;
    else
        std::cout << "sched_setscheduler, priority set to = " 
                  << sched.sched_priority << std::endl;
  1. 现在,我们可以用sched_rr_get_interval()函数得到区间:
    struct timespec tp;
    int retCode = sched_rr_get_interval(0, &tp);
    if (retCode == -1)
    {
        std::cout << "sched_rr_get_interval failed = " 
                  << strerror(errno) << std::endl;
        return 1;
    }    

    std::cout << "timespec sec = " << tp.tv_sec 
              << " nanosec = " << tp.tv_nsec << std::endl;
    std::cout << "End ..." << std::endl;
    return 0;
}

让我们看看这在引擎盖下是如何工作的。

它是如何工作的...

当一个任务获得具有SCHED_RR策略的处理器时,它拥有优先于SCHED_OTHERSCHED_NORMAL任务的优先权,并被分配一个定义的时间片,该时间片继续运行直到时间片到期。较高优先级的任务会一直运行,直到它们明确地让出处理器或块。对于系统程序员来说,一个重要的因素是知道SCHED_RR策略的时间片。这很重要。如果时间片太大,其他进程可能会等待很长时间才能获得 CPU 时间,而如果时间片太小,系统可能会花费大量时间进行上下文切换。

步骤 1 显示了程序其余部分所需的内容。<iostream>为标准输出,<sched.h>用于访问调度器功能,<string.h>用于strerror()功能。

第 2 步非常重要,因为它为当前流程设定了SCHED_RR政策。大家可能已经注意到了,我们通过0作为第一个参数。这很好,因为sched_setscheduler()功能的手册页上说,如果 pid 等于零,调用线程的策略将被设置

第三步调用sched_rr_get_interval()功能。它接受两个参数:PID 和struct timespec。第一个是输入参数,第二个是输出参数,包含{sec, nanoseconds}形式的时间片。对于第一个参数,我们可以通过getpid()函数,返回当前流程的 PID。然后,我们简单地将标准输出记录到返回的时间片上。

还有更多...

SCHED_RR时间片从何而来?正如我们已经知道的,Linux 调度程序有不同的策略。它们都是在不同的模块中实现的:SCHED_NORMALSCHED_OTHERkernel/sched_fair.cSCHED_RRSCHED_FIFOkernel/rt.c。通过查看kernel/rt.c,我们可以看到sched_rr_get_interval()函数返回sched_rr_timeslice()变量,该变量在模块顶部定义。我们还可以看到,如果sched_rr_timeslice()是为SCHED_FIFO策略调用的,它会返回0

请参见

  • 学习如何产生处理器配方,作为停止运行任务而不是等待时间片的替代方案
  • 学习设置和获取调度程序策略配方
  • Linux 内核开发,第三版,罗伯特·拉芙

学习如何设定一个好的值

SCHED_OTHER / SCHED_NORMAL策略实现了所谓的完全公平调度器(CFS)。这个食谱将向您展示如何为正常流程设置好值,以增加它们的优先级。我们将看到这个好的值被用来衡量一个进程的时间片。优先级不能和实时优先级混淆,实时优先级是针对SCHED_FIFOSCHED_RR政策的。

怎么做...

在这个食谱中,我们将实现一个程序,增加一个过程的美好价值:

  1. 在一个 shell 中,打开一个名为schedNice.cpp的新源文件。我们需要添加一些包含,并通过传递我们想要为当前进程设置的值来调用nice()系统调用:
#include <string.h>
#include <iostream>
#include <unistd.h>

int main ()
{
    std::cout << "Starting ..." << std::endl;

    if (nice(5) == -1)
        std::cout << "nice failed = " << strerror(errno)
            << std::endl;
    else
        std::cout << "nice value successfully set = " << std::endl;

    while (1) ;

    std::cout << "End ..." << std::endl;
    return 0;
}

在下一节中,我们将看到这个程序是如何工作的,以及如何使用nice值来影响任务进入处理器的时间。

它是如何工作的...

第 1 步基本调用nice()系统调用,将任务的静态优先级递增给定量。为了清楚起见,假设一个进程以0的优先级开始(这是SCHED_OTHERSCHED_NORMAL策略的默认值),连续两次调用nice(5)会将其静态优先级设置为10

让我们构建并运行schedNice.cpp程序:

在这里,我们可以看到,在左边,我们有我们的进程正在运行,在右边,我们已经运行了ps -el命令来获得正在运行的进程的良好值。我们可以看到./a.out流程现在的nice值为5。要赋予任务更高的优先级(然后是更低的nice值),流程需要以 root 身份运行。

还有更多...

struct task_struct结构有三个值来表示任务优先级:rt_priostatic_prioprio。我们在学习设置和获取调度器策略配方中讨论了rt_prio,并定义该字段代表实时任务的优先级。static_priostruct task_struct字段,用于存储nice值,而prio包含实际任务优先级。static_prio越低,任务的prio值越高。

可能有些情况下,我们需要在运行时设置进程的nice值。这种情况下我们应该使用的命令是renice value -p pid;例如,renice 10 -p 186

请参见

  • 学习如何产生处理器配方,作为停止运行任务而不是等待时间片的替代方案
  • 学习设置和获取调度程序策略配方

学习如何生产处理器

当使用实时调度策略之一(即SCHED_RRSCHED_FIFO)调度任务时,您可能需要从处理器中让出任务(让出任务意味着让出 CPU,使其可用于其他任务)。正如我们在学习设置和获取调度器策略配方中所描述的,当一个任务用SCHED_FIFO策略调度时,它直到某个事件发生才离开处理器;也就是说,没有时间片的概念。该配方将向您展示如何产生具有sched_yield()功能的流程。

怎么做...

在这个配方中,我们将开发一个程序来产生当前的过程:

  1. 在 shell 中,打开一个名为schedYield.cpp的新源文件,并输入以下代码:
#include <string.h>
#include <iostream>
#include <sched.h>

int main ()
{
    std::cout << "Starting ..." << std::endl;

    // set policy to SCHED_RR.
    struct sched_param sched;
    sched.sched_priority = 8;
    if (sched_setscheduler(0, SCHED_RR, &sched) == -1)
        std::cout << "sched_setscheduler failed = " 
                  << strerror(errno) 
                  << std::endl;

   for( ;; )
   {
      int counter = 0;
      for(int i = 0 ; i < 10000 ; ++ i)
         counter += i;

      if (sched_yield() == -1)
      {
         std::cout << "sched_yield failed = " 
                   << strerror(errno) << std::endl;
         return 1;
      }
   }

   // we should never get here ...
   std::cout << "End ..." << std::endl;
   return 0;
}

在下一节,我们将描述我们的程序和sched_yield()是如何工作的。

它是如何工作的...

当对通过SCHED_FIFOSCHED_RR调度的任务调用sched_yield()时,它被移动到具有相同优先级的队列的末尾,并且运行另一个任务。收益会导致上下文切换,因此应该在严格需要时谨慎使用。

第 1 步定义程序,向我们展示如何使用sched_yield()。我们模拟了一种受中央处理器限制的进程,在这种进程中,我们定期检查以产生处理器。在此之前,我们必须将此流程的策略类型设置为SCHED_RR,优先级设置为8。如您所见,没有关于要产出的流程(PID)的信息,因此它假设当前任务将产出。

还有更多...

sched_yield()是用户空间应用可以使用的系统调用。Linux 通常调用yield()系统调用,这样做的好处是让进程保持RUNNABLE状态。

请参见

  • 学习设置和获取调度器策略配方,以查看如何更改策略的类型
  • Linux 内核开发, 第三版,作者:罗伯特·拉芙

了解处理器关联性

在多处理器环境中,调度器必须处理多个处理器或内核上的任务分配。从 Linux 的角度来看,进程和线程是一回事;两者都由struct task_struct内核结构表示。可能需要强制两个或多个任务(即线程或进程)在同一处理器上运行,以通过避免缓存失效来利用例如缓存。这个食谱将教你如何在任务中设定硬亲和力

怎么做...

在这个食谱中,我们将开发一个小软件,我们将强制它在一个中央处理器上运行:

  1. 在一个 shell 中,打开一个名为schedAffinity.cpp的新源文件。我们想要的是检查新创建的进程的相似性掩码。然后,我们需要准备cpu_set_t掩码,将中央处理器上的亲和力设置为3:
#include <iostream>
#include <sched.h>
#include <unistd.h>

void current_affinity();
int main ()
{
    std::cout << "Before sched_setaffinity => ";
    current_affinity();

    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
int cpu_id = 3;
    CPU_SET(cpu_id, &cpuset);
  1. 现在,我们准备调用sched_setaffinity()方法,并在 CPU 号3上强制当前任务的硬关联。为了检查关联性是否设置正确,我们还将打印掩码:
    int set_result = sched_setaffinity(getpid(), 
                                       sizeof(cpu_set_t), 
                                       &cpuset);
    if (set_result != 0) 
    {
        std::cerr << "Error on sched_setaffinity" << std::endl;
    }

    std::cout << "After sched_setaffinity => ";
    current_affinity();
    return 0;
}
  1. 现在,我们必须开发current_affinity()方法,它将只打印处理器的掩码:
// Helper function
void current_affinity()
{
    cpu_set_t mask;
    if (sched_getaffinity(0, sizeof(cpu_set_t), &mask) == -1) 
    {
        std::cerr << "error on sched_getaffinity";
        return;
    }
    else
    {
        long nproc = sysconf(_SC_NPROCESSORS_ONLN);
        for (int i = 0; i < nproc; i++) 
        {
            std::cout << CPU_ISSET(i, &mask);
        }
        std::cout << std::endl;
    }
}

如果我们在一个不存在的 CPU(例如cpu_id = 12)上设置亲缘关系会发生什么?亲和掩码信息存储在内核的什么地方?我们将在下一节回答这些和其他问题。

它是如何工作的...

第一步做两件事。首先,它打印默认的相似性掩码。我们可以看到,该进程计划在所有处理器上运行。其次,它通过用CPU_ZERO宏初始化来准备代表一组 CPU 的cpu_set_t,并用CPU_SET宏在 CPU 3上设置亲和力。请注意,cpu_set_t对象必须直接操作,但只能通过提供的宏操作。手册页上记录了宏的完整列表:man cpu_set

步骤 2 调用sched_setaffinity()系统调用,用getpid()函数返回的 PID 在进程上设置亲和度(在mask变量中指定,即cpu_set_t)。我们本可以通过0而不是getpid(),也就是目前的进程。setaffinity功能后,我们打印了 CPU 的掩码,验证新值是否正确。

步骤 3 包含了我们用来将标准输出打印到 CPU 掩码上的辅助函数的定义。请注意,我们通过sysconf()系统调用并通过传递_SC_NPROCESSORS_ONLN获得可用处理器的数量。该功能检查/sys/文件夹中的系统信息。然后,我们遍历每个处理器并调用CPU_ISSET宏,同时通过i-thCPU_ISSET宏将为i-th中央处理器设置相应的位。

如果你试图修改int cpu_id = 3并通过一个不同的处理器,也就是一个不存在的处理器(例如15),那么sched_setaffinity()函数显然会失败,返回EINVAL,亲和掩码保持不变。

现在让我们来看看这个程序:

我们可以看到,每个处理器的 CPU 掩码都设置为 1。这意味着在这个阶段,可以在每个中央处理器上调度进程。现在,我们设置掩码,要求调度程序只在 CPU 3上运行进程(硬关联)。当我们叫sched_getaffinity()的时候,面具反映了这一点。

还有更多...

当我们调用sched_setaffinity()系统调用时,我们要求调度程序在特定的处理器上运行一个任务。我们称之为硬亲和力。还有一种柔软的亲和力。这是由调度程序自动管理的。Linux 总是试图优化资源,避免缓存失效,以加快整个系统的性能。

当我们通过宏设置亲和掩码时,我们基本上是在task_struct结构中设置cpus_allowed。这很有意义,因为我们在一个或多个处理器上设置了进程或线程的亲缘关系。

如果要设置一个任务与多个 CPU 的亲缘关系,必须为要设置的 CPU 调用CPU_SET宏。

请参见

  • 学习如何产生处理器配方
  • 学习获取时间片值配方
  • 学习设置和获取调度程序策略配方