Linuxのschedのpriorityとリアルタイムシステムについて

  • 40
    Like
  • 4
    Comment

はじめに

Linuxでいわゆるリアルタイムシステムを指向するときはsched(7)priority値を意識することになるが、個人的にいつもpriorityの値を相対的にしか見られず、絶対的にかつどっちが上・下かについてわからなくなってしまうので、そんな自分のためにメモ書きしておくことにした・・・つもりが、なにか途中から趣旨を間違えた記事になってしまった。本当は「priority一覧表」がほしかっただけだったのに...

なお、Linux-4.10くらい、procps-ng-3.3.12くらい、util-linux-2.29.2くらいを見ています。

schedのpolicyとpriority値

Linuxでは、ユーザプログラム・kernelスレッド問わず、タスクにsched policyを設定できる。下記の上3つがいわゆるリアルタイムスケジューリング、下3つがいわゆるタイムシェアスケジューリングになる。

  • SCHED_DEADLINE, SCHED_FIFO, SCHED_RR
  • SCHED_OTHER, SCHED_BATCH, SCHED_IDLE

SCHED_DEADLINEは自動的に最高優先度になるので置いておき、残りのリアルタイムスケジューリングのSCHED_FIFO, SCHED_RRはpriority値を設定できる。これがtask_structのrt_priorityとなり、必ずpriority値の順にスケジューリングされるようになる。

これに対し、タイムシェアスケジューリングはpriorityを設定できない。代わりにnice値を設定できる(SCHED_IDLEを除く)。

なおkernel内でSCHED_OTHERSCHED_NORMALを混同したコードが見られるため注意が必要。どちらも同じ意味。(というかkernel内はSCHED_NORMALを積極的に使いたい?) ※コメント補足より、SCHED_OTHERがユーザ空間(POSIX)、SCHED_NORMALがkernel、それぞれでの統一された表記で、実装としては両者は同じものを指す。

nice値

SCHED_OTHER, SCHED_BATCHでは、nice値によりタイムシェアの割り当て時間がスケールする。寝ていた期間などによりボーナスやpreemptは働くものの、時間はおおむねnice値が1違うと1.25倍変わるようになる。参考サイト、
- CFSのnice値について : 革命の日々 その2
- 帰ってきたCon Kolivas、大論争を呼ぶの巻(3/3) - @IT

指数的に効くため、大きくnice値が異なると相対的にリアルタイムスケジューリングっぽくなるものの、preempt保証があるわけではないので、リアルタイムスケジューリングと異なりlatency問題は残る。sched_latency_ns使いチューニングできるものの、短くしすぎると今度はcontext swtichのオーバヘッドが問題になる。

nice値のコード上の効き方

1.25倍の根拠は、kernel/kernel/sched/core.cより、

core.c
8825 /*
8826  * Nice levels are multiplicative, with a gentle 10% change for every
8827  * nice level changed. I.e. when a CPU-bound task goes from nice 0 to
8828  * nice 1, it will get ~10% less CPU time than another CPU-bound task
8829  * that remained on nice 0.
8830  *
8831  * The "10% effect" is relative and cumulative: from _any_ nice level,
8832  * if you go up 1 level, it's -10% CPU usage, if you go down 1 level
8833  * it's +10% CPU usage. (to achieve that we use a multiplier of 1.25.
8834  * If a task goes up by ~10% and another task goes down by ~10% then
8835  * the relative distance between them is ~25%.)
8836  */
8837 const int sched_prio_to_weight[40] = {
8838  /* -20 */     88761,     71755,     56483,     46273,     36291,
8839  /* -15 */     29154,     23254,     18705,     14949,     11916,
8840  /* -10 */      9548,      7620,      6100,      4904,      3906,
8841  /*  -5 */      3121,      2501,      1991,      1586,      1277,
8842  /*   0 */      1024,       820,       655,       526,       423,
8843  /*   5 */       335,       272,       215,       172,       137,
8844  /*  10 */       110,        87,        70,        56,        45,
8845  /*  15 */        36,        29,        23,        18,        15,
8846 };
8847 
8848 /*
8849  * Inverse (2^32/x) values of the sched_prio_to_weight[] array, precalculated.
8850  *
8851  * In cases where the weight does not change often, we can use the
8852  * precalculated inverse to speed up arithmetics by turning divisions
8853  * into multiplications:
8854  */
8855 const u32 sched_prio_to_wmult[40] = {
8856  /* -20 */     48388,     59856,     76040,     92818,    118348,
8857  /* -15 */    147320,    184698,    229616,    287308,    360437,
8858  /* -10 */    449829,    563644,    704093,    875809,   1099582,
8859  /*  -5 */   1376151,   1717300,   2157191,   2708050,   3363326,
8860  /*   0 */   4194304,   5237765,   6557202,   8165337,  10153587,
8861  /*   5 */  12820798,  15790321,  19976592,  24970740,  31350126,
8862  /*  10 */  39045157,  49367440,  61356676,  76695844,  95443717,
8863  /*  15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
8864 };

計算してもらうとわかるとおり1.25倍になって...いないぞ...結構ぶれていた。下記は小数点3桁までで四捨五入している。

nice値 sched_prio
to_weight
1.25倍? sched_prio
to_wmult
1.25倍?
-19 88761 1.237 48388 1.237
-18 71755 1.270 59856 1.270
-17 56483 1.221 76040 1.221
-16 46273 1.275 92818 1.275
-15 36291 1.245 118348 1.245
-14 29154 1.254 147320 1.254
-13 23254 1.243 184698 1.243
-12 18705 1.251 229616 1.251
-11 14949 1.255 287308 1.255
-10 11916 1.248 360437 1.248
-9 9548 1.253 449829 1.253
-8 7620 1.249 563644 1.249
-7 6100 1.244 704093 1.244
-6 4904 1.256 875809 1.256
-5 3906 1.252 1099582 1.252
-4 3121 1.248 1376151 1.248
-3 2501 1.256 1717300 1.256
-2 1991 1.255 2157191 1.255
-1 1586 1.242 2708050 1.242
0 1277 1.247 3363326 1.247
1 1024 1.249 4194304 1.249
2 820 1.252 5237765 1.252
3 655 1.245 6557202 1.245
4 526 1.243 8165337 1.243
5 423 1.263 10153587 1.263
6 335 1.232 12820798 1.232
7 272 1.265 15790321 1.265
8 215 1.250 19976592 1.250
9 172 1.255 24970740 1.255
10 137 1.245 31350126 1.245
11 110 1.264 39045157 1.264
12 87 1.243 49367440 1.243
13 70 1.250 61356676 1.250
14 56 1.244 76695844 1.244
15 45 1.250 95443717 1.250
16 36 1.241 119304647 1.241
17 29 1.261 148102320 1.261
18 23 1.278 186737708 1.278
19 18 1.200 238609294 1.200
20 15 hoge 286331153 hoge

nice値のその他細かい話

SCHED_IDLEは実装上はタイムシェアであるものの、割り当て時間を極端に短くして、SCHED_BATCH, SCHED_OTHERに負け続けるという実装のように読める。

autogroup(/proc/sys/kernel/sched_autogroup_enabled)やcgroupの機能により時間のスライスがタスクごとではなくてグループごとに行われるため、たくさんのタスクがわさわさ動くと実際の動きは予想しづらくなる。

nice値のことをドキュメントやsyscallの関数名で「priority」と表現していることがあるが、紛らわしいので、私はnice値に統一して呼ぶようにしている。

古い記事によっては「nice値がpriority値のベースとなりそこから動的に-5から+5される」という趣旨の記述が見られるが、コードを追う限り裏付けできなかった。Linux-2.6.23(9th October, 2007)からCFS(Completely Fair Scheduler)に置き換わったが、変わる前のO(1)スケジューラのころの記述なのではないかと考えている。(※コメント補足より、まさにこの通りとのこと。) nice値はあくまでPOSIX仕様でどう動くかは実装次第であり、またLinuxでも古くからあるため適当に書かれた記事が多い。英語記事ではあるがLinux Scheduler – CFS and Nice | oakbytesが比較的まとまっているように見える。

ioprio値との関係

schedのpriority値とioprio値は直接の関係はない。IOPRIO_CLASS_NONEだとsched設定がioprioに派生する。このパターンについては先のCFQとクラスの記事にまとめている。

schedのpolicyとclass

タスクに設定されたschedのpolicyは、コード上はsched classに対応づけられる。kernel/kernel/sched/sched.hより、

sched.h
1306 extern const struct sched_class stop_sched_class;
1307 extern const struct sched_class dl_sched_class;
1308 extern const struct sched_class rt_sched_class;
1309 extern const struct sched_class fair_sched_class;
1310 extern const struct sched_class idle_sched_class;

stop_sched_class

stop_sched_classは、migration/%uのkernelスレッドにのみ使われる(sched_set_stop_task()あたり)。policyで何を指定しても使うことはできない。ユーザランドから観測するとSCHED_FIFOのprilrity値99のように見えるが、classが違い、これが最優先に動く。このスレッドはcpu migrationをしていて、タスクをこれまでとは異なるCPUで動かす時の処理を行う。

dl_sched_class

dl_sched_classは、SCHED_DEADLINEを指定したときに使われる。stop_sched_classの次の優先度で動く。

rt_sched_class

rt_sched_classは、SCHED_FIFO, SCHED_RRを指定したときに使われる。dl_sched_classの次の優先度で動く。rt_sched_class同士の場合は設定したpriority値の順で動く。

fair_sched_class

fair_sched_classは、SCHED_OTHER, SCHED_BATCH, SCHED_IDLEを指定したときに使われる。SCHED_IDLEだけは特殊で、先の通り常に不遇を受けて、fair_sched_classの中で最低の優先度で動く。それ以外はnice値の箇所で説明したとおり。

idle_sched_class

idle_sched_classは、swapper/%d(idle_task)の時にのみ選ばれる。これは、何もすることがないときに割り当てられるタスク。このあたりに詳しい 〆(.. )カリカリッ!! Linuxのidleスレッドめも - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ

なお、linux/kernel/sched/idle_task.cの先頭に注意書きがあるとおり、SCHED_IDLEのidleとidle-taskのidleは別物である。

  3 /*
  4  * idle-task scheduling class.
  5  *
  6  * (NOTE: these are not related to SCHED_IDLE tasks which are
  7  *  handled in sched/fair.c)
  8  */

priority一覧表

priority値はツールや使用する場面によって表現が大きく変わってしまう。基本はkernel内のstruct task_structのメンバprio, static_prio, normal_prio(それぞれの意味要確認)に入る値を正と考えるべきだが、確認時は各種ツールを解することになるので、より注意が必要なところ。パクリもとこの記事を書こうと思うきっかけになったプロセスの優先度@CetnOS 5.5 | Mazn.netに感謝したい。

kernel
prio
chrt top
PRI
ps
rtprio
ps
pri
ps
priority
補足
-1 0 rt 0 140 -101 SCHED_DEADLINEと一部のタスク
0 99 rt 99 139 -100 SCHED_FIFO,SCHED_RRの最高
1 98 -99 98 138 -99
2 97 -98 97 137 -98
... ... ... ... ... ... リアルタイムスケジューリング
97 2 -3 2 42 -3
98 1 -2 1 41 -2 SCHED_FIFO,SCHED_RRの最低
100 0 0 - 39 0 SCHED_OTHER,SCHED_BATCHの最高
101 0 1 - 38 1
102 0 2 - 37 2
... ... ... ... ... ... タイムシェアスケジューリング
119 0 19 - 20 19
120 0 20 - 19 20 SCHED_OTHERデフォルト値
121 0 21 - 18 21
... ... ... ... ... ... タイムシェアスケジューリング
138 0 38 - 1 38
139 0 39 - 0 39 SCHED_OTHER,SCHED_BATCHの最低

なお、priorityとsched policyによりどのようにスケジューリングされるか(CFS)を追った、ムチャしやがった意欲的な記事がこちら。Linux スケジューラーのコア実装とシステムコール - Qiita

sched programming interface

schedに絡むシステムコールはたくさんあるが、歴史的経緯からか、interfaceの一貫性が怪しい。下記に早見表をまとめる。実際に使うときはman等参照されたい。

function ABI nice policy prio 補足事項
int nice(int inc); Y 現在からの相対値
int getpriority(int which, int who); Y PRIO_PROCESS, PRIO_PGRP, PRIO_USER
int setpriority(int which, int who, int prio); Y
int sched_getparam(pid_t pid, struct sched_param *param); Y param.sched_priority
int sched_setparam(pid_t pid, const struct sched_param *param); Y
int sched_getscheduler(pid_t pid); Y priority値取れない
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param); Y Y param.sched_priority
int sched_getattr(pid_t pid, const struct sched_attr *attr, unsigned int size, unsigned int flags); Y Y Y attr.sched_policy, attr.sched_nice, attr.sched_priority
int sched_setattr(pid_t pid, const struct sched_attr *attr, unsigned int flags); Y Y Y sizeof(attr), flagsは0固定,これでしかSCHED_DEADLINE設定できない
int pthread_setschedprio(pthread_t thread, int prio); Y pthread_getschedprioはない
int pthread_getschedparam(pthread_t thread, int *policy, struct sched_param *param); Y Y param.sched_priority
int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param); Y Y
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy); Y attr.schedpolicyへ代入される
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy); Y
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param); Y param.sched_priorityへ代入される
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param); Y

sched_getattr(2), sched_setattr(2)はglibcヘルパー関数がないので自分でsyscall(2)しないといけない。

...テーブルの縦の結合ってどう書くの?...

priority値の定義

値の定義

kernel/include/linux/sched/prio.hにだいたいが定義されている。・・・というかもう全部引用する。

prio.h
  4 #define MAX_NICE        19
  5 #define MIN_NICE        -20
  6 #define NICE_WIDTH      (MAX_NICE - MIN_NICE + 1)
  7 
  8 /*
  9  * Priority of a process goes from 0..MAX_PRIO-1, valid RT
 10  * priority is 0..MAX_RT_PRIO-1, and SCHED_NORMAL/SCHED_BATCH
 11  * tasks are in the range MAX_RT_PRIO..MAX_PRIO-1. Priority
 12  * values are inverted: lower p->prio value means higher priority.
 13  *
 14  * The MAX_USER_RT_PRIO value allows the actual maximum
 15  * RT priority to be separate from the value exported to
 16  * user-space.  This allows kernel threads to set their
 17  * priority to a value higher than any user task. Note:
 18  * MAX_RT_PRIO must not be smaller than MAX_USER_RT_PRIO.
 19  */
 20 
 21 #define MAX_USER_RT_PRIO        100
 22 #define MAX_RT_PRIO             MAX_USER_RT_PRIO
 23 
 24 #define MAX_PRIO                (MAX_RT_PRIO + NICE_WIDTH)
 25 #define DEFAULT_PRIO            (MAX_RT_PRIO + NICE_WIDTH / 2)
 26 
 27 /*
 28  * Convert user-nice values [ -20 ... 0 ... 19 ]
 29  * to static priority [ MAX_RT_PRIO..MAX_PRIO-1 ],
 30  * and back.
 31  */
 32 #define NICE_TO_PRIO(nice)      ((nice) + DEFAULT_PRIO)
 33 #define PRIO_TO_NICE(prio)      ((prio) - DEFAULT_PRIO)
 34 
 35 /*
 36  * 'User priority' is the nice value converted to something we
 37  * can work with better when scaling various scheduler parameters,
 38  * it's a [ 0 ... 39 ] range.
 39  */
 40 #define USER_PRIO(p)            ((p)-MAX_RT_PRIO)
 41 #define TASK_USER_PRIO(p)       USER_PRIO((p)->static_prio)
 42 #define MAX_USER_PRIO           (USER_PRIO(MAX_PRIO))
 43 
 44 /*
 45  * Convert nice value [19,-20] to rlimit style value [1,40].
 46  */
 47 static inline long nice_to_rlimit(long nice)
 48 {
 49         return (MAX_NICE - nice + 1);
 50 }
 51 
 52 /*
 53  * Convert rlimit style value [1,40] to nice value [-20, 19].
 54  */
 55 static inline long rlimit_to_nice(long prio)
 56 {
 57         return (MAX_NICE - prio + 1);
 58 }

よって、NICE_WIDTHは40、MAX_PRIOは140、DEFAULT_PRIOは120 になる。

最大値の定義

linux/kernel/sched/core.c:sched_get_priority_max()が最大値を与える。

core.c
5085 /**
5086  * sys_sched_get_priority_max - return maximum RT priority.
5087  * @policy: scheduling class.
5088  *
5089  * Return: On success, this syscall returns the maximum
5090  * rt_priority that can be used by a given scheduling class.
5091  * On failure, a negative error code is returned.
5092  */
5093 SYSCALL_DEFINE1(sched_get_priority_max, int, policy)
5094 {
5095         int ret = -EINVAL;
5096 
5097         switch (policy) {
5098         case SCHED_FIFO:
5099         case SCHED_RR:
5100                 ret = MAX_USER_RT_PRIO-1;
5101                 break;
5102         case SCHED_DEADLINE:
5103         case SCHED_NORMAL:
5104         case SCHED_BATCH:
5105         case SCHED_IDLE:
5106                 ret = 0;
5107                 break;
5108         }
5109         return ret;
5110 }

MAX_USER_RT_PRIOは先に見たとおり100なので、最大値はrt_priorityでいうところの99となる。

最小値の定義

linux/kernel/sched/core.c:sched_get_priority_min()が最小値を与える。

core
5112 /**
5113  * sys_sched_get_priority_min - return minimum RT priority.
5114  * @policy: scheduling class.
5115  *
5116  * Return: On success, this syscall returns the minimum
5117  * rt_priority that can be used by a given scheduling class.
5118  * On failure, a negative error code is returned.
5119  */
5120 SYSCALL_DEFINE1(sched_get_priority_min, int, policy)
5121 {
5122         int ret = -EINVAL;
5123 
5124         switch (policy) {
5125         case SCHED_FIFO:
5126         case SCHED_RR:
5127                 ret = 1;
5128                 break;
5129         case SCHED_DEADLINE:
5130         case SCHED_NORMAL:
5131         case SCHED_BATCH:
5132         case SCHED_IDLE:
5133                 ret = 0;
5134         }
5135         return ret;
5136 }

より、最小値はrt_priorityでいうところの1となる。

chrt

util-linux/schedutils/chrt.c:245 show_sched_pid_info()より、

chrt.c
        if (sched_getparam(pid, &sp) != 0)
            err(EXIT_FAILURE, _("failed to get pid %d's attributes"), pid);
        else
            prio = sp.sched_priority;

なのでsched_getparam(2)を使っている。これはlinux/kernel/core.c:4584のsys_sched_getparam()より、

core.c
4584         if (task_has_rt_policy(p))
4585                 lp.sched_priority = p->rt_priority;

で、task_has_rt_policy(p)(==SCHED_FIFO,SCHED_RR)の時はrt_priorityが、そうでない場合は0が返る。

top(PRI)

procps-ng/top/top.c:5430のtask_show()のEU_PRIより、

top.c
         case EU_PRI:
            if (-99 > p->priority || 999 < p->priority) {
               cp = make_str("rt", W, Jn, AUTOX_NO);
            } else
               cp = make_num(p->priority, W, Jn, AUTOX_NO, 0);
            break;

p->prioritylibprocpsで取得される値で、/proc/[PID]/statの「(18) priority %ld」のこと。-99より小さいまたは999より大きい場合は"rt"を表示し、それ以外はそのまま表示している。

ps(rtprio)

procps-ng/ps/output.c:702のpr_rtprio()より、

output.c
// Based on "type", FreeBSD would do:
//    REALTIME  "real:%u", prio
//    NORMAL    "normal"
//    IDLE      "idle:%u", prio
//    default   "%u:%u", type, prio
// We just print the priority, and have other keywords for type.
static int pr_rtprio(char *restrict const outbuf, const proc_t *restrict const pp){
  if(pp->sched==0 || pp->sched==(unsigned long)-1) return snprintf(outbuf, COLWID, "-");
  return snprintf(outbuf, COLWID, "%ld", pp->rtprio);
}

pp->rtpriolibprocpsで取得される値で、/proc/[PID]/statの「(40) rt_priority %u」のこと。同様に、pp->schedは「(41) policy %u」のこと。

pp->schedが0とはSCHED_NORMAL(==SCHED_OTHER)のことなので、リアルタイム系policyじゃない時は"-"を、そうでないときはrt_priorityをそのまま表示している

ps(pri)

procps-ng/ps/output.c:655のpr_pri()より、

output.c
// not legal as UNIX "PRI"
// "pri"               (was 20..60, now    0..139)
static int pr_pri(char *restrict const outbuf, const proc_t *restrict const pp){         /* 20..60 */
    return snprintf(outbuf, COLWID, "%ld", 39 - pp->priority);
}

p->prioritylibprocpsで取得される値で、/proc/[PID]/statの「(18) priority %ld」のこと。39オフセットから反転させた値にしている。

ps(priority)

procps-ng/ps/output.c:624のpr_pri()より、

output.c
// legal as UNIX "PRI"
// "priority"         (was -20..20, now -100..39)
static int pr_priority(char *restrict const outbuf, const proc_t *restrict const pp){    /* -20..20 */
    return snprintf(outbuf, COLWID, "%ld", pp->priority);
}

p->prioritylibprocpsで取得される値で、/proc/[PID]/statの「(18) priority %ld」のこと。ここはそのまま表示している。

/proc/[PID]/stat

linux/fs/proc/array.c:389 do_task_stat()より、

array.c
481         /* scale priority and nice values from timeslices to -20..20 */
482         /* to make it look like a "normal" Unix priority/nice value  */
483         priority = task_prio(task);
(----------snip----------)
504         seq_put_decimal_ll(m, " ", priority);
(----------snip----------)
542         seq_put_decimal_ull(m, " ", task->rt_priority);

task_prio()はlinux/kernel/sched/core.c:3838で、

core.c
3838 /**
3839  * task_prio - return the priority value of a given task.
3840  * @p: the task in question.
3841  *
3842  * Return: The priority value as seen by users in /proc.
3843  * RT tasks are offset by -200. Normal tasks are centered
3844  * around 0, value goes from -16 to +15.
3845  */
3846 int task_prio(const struct task_struct *p)
3847 {
3848         return p->prio - MAX_RT_PRIO;
3849 }

となっている。なのでまとめると、

  • (18) priority %ld は、p->prioからMAX_RT_PRIO(==100)を引いたもの
  • (40) rt_priority %u は、task->rt_priorityそのまま

p->prioはおおむね、linux/kernel/sched/core.c:871のnormal_prio()ということでよい。

core.c
871 /*
872  * __normal_prio - return the priority that is based on the static prio
873  */
874 static inline int __normal_prio(struct task_struct *p)
875 {
876         return p->static_prio;
877 }
878 
879 /*
880  * Calculate the expected normal priority: i.e. priority
881  * without taking RT-inheritance into account. Might be
882  * boosted by interactivity modifiers. Changes upon fork,
883  * setprio syscalls, and whenever the interactivity
884  * estimator recalculates.
885  */
886 static inline int normal_prio(struct task_struct *p)
887 {
888         int prio;
889 
890         if (task_has_dl_policy(p))
891                 prio = MAX_DL_PRIO-1;
892         else if (task_has_rt_policy(p))
893                 prio = MAX_RT_PRIO-1 - p->rt_priority;
894         else
895                 prio = __normal_prio(p);
896         return prio;
897 }
  • task_has_dl_policy()(==SCHED_DEADLINE)の時は-1(MAX_DL_PRIOは0(include/linux/sched/deadline.h))
  • task_has_rt_policy()(==SCHED_FIFO,SCHED_RR)の時は0から98(MAX_RT_PRIOは100)
  • それ以外はp->static_prio

Real-Time Linux patch

LinuxをよりリアルタイムOSっぽく扱えるようにするためのパッチ群がReal-Time Linux Wikiにて公開されている。きちんと中身を確認はできていないが、おおむね下記の点に変更が入っている。
- 割り込みコンテキストを割り込みスレッド(irq/xxx)に変える。SCHED_FIFOのpriority==-50のスレッド
- softirqをすべてksoftirqdで処理する
- spin_lock_irqsave()などで割り込み禁止にしなくなる
- spinlockをmutexにして優先度の高いタスクが割り込める機会を増やす (コメント補足より追記)

などのことをして、レイテンシが小さくなるようにしている。

ポエム: Linuxにおけるリアルタイムシステムとは

Linuxで完全なリアルタイム性を保証しようと思うとものすごく難しい。保証しようと思ったとたん抜けがあるといけないわけで、行儀の悪いユーザプログラムがSCHED_FIFOになっているのはもってのほか、行儀の悪いドライバ1つ混ざるだけで保証なんてできなくなってしまう。SMPやVCPUで以前に比べればネックになりにくくはなったものの、L1/L2キャッシュ状態・分岐予測・CPUの動的周波数変更・HyperVisor介入、などのOSに見えにくいところの予測不能点も増えた。まとまりないもののずらずらと書き下してみる。

sched priority

SCHED_FIFOのpriorityは使いこなしの基本。CFS(Completely Fair Scheduler)で多くが改善されたとはいえ、SCHED_OTHER(タイムシェア)のままではリアルタイムは厳しい。

なおSCHED_FIFOにするだけではCPUを100%使うことができない問題があるので、sched_rt_runtime_usの確認も忘れないこと。

mlock

せっかくSCHED_FIFOにしても、初回アクセスでページフォルト起こしたりスワップアウトしたりしていては話にならない。仮想アドレス割り当てだけでなく物理メモリもきちんと割り当てるために、mlock(2), mlockall(2)を使いリアルタイム処理中にアクセスするアドレスを固定しておく必要がある。なおL1,L2キャッシュミスヒットはそれでも起こるが、ページフォルト起こしてディスクから読むのに比べればオーダが異なるので、まだ許容範囲(?)となる。

CPU binding

context switchや割り込みやL1キャッシュ乱れの影響を減らすため、リアルタイム性がほしいタスクとそうでないタスクとで割り当てるCPUを分ける方法がある。HyperVisorでさらにVCPUを分けておく方法もあるが、いずれにしても複数の(論理)CPUを使える前提でないと使えない方法である。幸いにもCPU bindingする方法はそこそこQiitaに書かれているので詳細はそちらを。例えばプロセス・スレッドを特定のCPUコアでのみ動作させる方法。システムコールはsched_setaffinity(2)、コマンドラインからはtaskset(1)で。

Interrupt

Interrupt(割り込み)はその名のごとく何かの処理中に別の処理が割り込むので、割り込みハンドラとそこに登録する関数は注意深く精査する必要がある。割り込み頻度や割り込み優先度・ハンドラで処理する絶対量は常に意識しなければならない。

最近は、割り込みとはいえ直接ハンドラを呼ぶのではなく、割り込みから特定タスクを起こすことだけ行い、実際の処理を特定のタスクに任せるという割り込みスレッドを使った実装が流行っている。割り込みスレッドへcontext switchするコストがかかるものの、割り込みスレッドよりもさらに優先度したいタスクがある場合に、priorityベースでタスクの順を考えるようにすることができる。

割り込み/プリエンプト禁止

割り込み禁止(spin_lock_irqsave()系)やプリエンプト禁止(preempt_disable()系)を長い期間行うとレイテンシに響く。最低限のリソースへのアクセスの期間にとどめるのがよい。

L1/L2キャッシュ状態・分岐予測・CPUの動的周波数変更・HyperVisor

最近はソフト制御の外のハードのレベルで行われるので、ソフト(OS)レベルでは影響を測ることが難しくなっている。結果、昔ながらの命令数を数えて処理時間を見積もるのが現実的でなくなってしまっている。L2キャッシュのway数をvcpuで割り振ったり、vcpuでなくて物理CPUレベルでHyperVisorがVMに割り当てたり、といったパーティショニングをするくらいしか今のところはないのかなぁ。

あとがき

priorityの確認方法を書くだけのつもりだったのに、なにかリアルタイムシステム入門っぽくなってしまった。HPCも含め世の中は一般的にレイテンシよりもスループットを重視するため、意外とリアルタイム処理の視点で書かれている記事は多くない。nice値とpriority値を混同したようなレベルのものも未だに多い。

逆に、Linuxなんかではリアルタイム保証はムリという主張も多い(まぁ間違ってはいないけど)。「保証」のレベルにもよるんだろうけど、0,1で決めるんではなくて「おおむね保証する」レベルで許されるのなら、Linuxを使ったリアルタイム指向の設計という選択肢もある。

ARM CPUのマルチコアも当たり前になってきたので、これからこういう話がもっと必要になってくる・・・のかな?

参考サイト

Documentation

sched関係

priority関係

リアルタイムシステム