OS とソフトウェア

INPUT · スライド

1つの CPU を分け合う

01 / 12

同時に動いて見えるのは、なぜ

端末で node main.js と打つ。その裏では、ブラウザがページを描き、音楽が鳴り、エディタが動いている。

でも CPU は1つだ。1つの CPU は、ある瞬間には1つの命令しか実行できない。同時に動いているように見えるのは、じつは同時ではないんだよ。

やっているのは高速な交代だ。あなたのプログラムを少し進めて、いったん止めて、ブラウザを少し進めて、また止めて、音楽を少し進める。この切り替えが1秒に何百回も起きているので、人間の目には同時に見える。

この「1つの CPU に複数のプログラムを詰めこんで交代させる」やり方を マルチプログラミング(マルチタスク)と呼ぶよ。このレッスンは、その交代のさせ方をぜんぶ見ていく。

CPU は 1つ[A][B][A][B][A][B] ↑ 短く切って交代人には同時に見える

02 / 12

OS が配っているもの

その交代をとりしきっているのが OS(オペレーティングシステム)だよ。OS の仕事を一言でいうと、限られた資源をプログラムたちに配り分けることだ。

配るものは3種類ある。

  • CPU の時間 … 誰に、どれだけ実行させるか(このレッスンの主題)
  • メモリ … どこの領域を貸すか
  • 入出力装置とファイル … ディスクや画面、キーボードの取り合いを整理する

OS の中でも、この配り分けそのものを担当する中心部分を カーネル と呼ぶ。

周りには役割の違う部品も並んでいる。デバイスドライバ は、プリンタやディスクといった機器ごとの言葉づかいを知っていて、アプリの要求どおりにハードウェアを直接動かす通訳だよ。シェル は、あなたが打ったコマンドの文字列を読んで対応するプログラムを起動する係。名前が並んで出てきたら、どこを相手にしているかで見分けよう。

OS が配るもの  CPU の時間  メモリ  ファイル・周辺機器デバイスドライバ  → 機器を直接動かすシェル  → 打った文字を読む

03 / 12

プロセスとスレッド

交代させられる「1つぶんのプログラム」を プロセス と呼ぶ。試験では タスク という言い方もよく出てくるけれど、CPU の配り方の話ではほぼ同じものと思っていい。

プロセスは入れ物だよ。自分のメモリ領域、開いているファイル、使っている装置。これらをひとまとめに抱えている。だからプロセスどうしは互いのメモリを見られない。あなたのプログラムがブラウザの変数を壊せないのは、これが分けてあるからだ。

その入れ物の中で、実際に命令を1本ずつ実行していく流れスレッド だよ。1つのプロセスは、スレッドを複数持てる。

違いはここに出る。プロセスを切り替えるのは重い。抱えているものが多いので、入れ替える情報が多いからだ。同じプロセスの中のスレッドを切り替えるのは軽い。メモリや開いたファイルは共有したままなので、入れ替えるものが少ない。

ただし軽さには代償がある。同じプロセスのスレッドは同じメモリを共有しているので、片方が壊した値をもう片方が読んでしまう。あとで出てくる排他制御が必要になるのは、まさにこの場面だよ。

プロセス(入れ物) ├ メモリ ├ 開いたファイル └ スレッド     スレッド切替えの重さ  プロセス 重い  スレッド 軽い

04 / 12

タスクがとる3つの状態

タスクは、生まれてから終わるまでずっと走っているわけじゃない。3つの状態を行き来しているよ。

  • 実行可能状態 … 走れる準備はできているが、CPU の順番待ち
  • 実行状態 … いま CPU をもらって実際に走っている
  • 待ち状態 … CPU をもらっても進めない。何かの完了を待っている

待ち状態がいちばん分かりにくいので、例を出そう。ファイルを読む命令を出した瞬間、そのタスクはディスクの返事を待つしかない。CPU をあげても何もできないので、CPU は他のタスクに回される。これが待ち状態だよ。

移り方には決まった向きがある。実行可能 → 実行 は CPU をもらったとき。実行 → 待ち は自分が入出力を要求したとき。待ち → 実行可能 はその入出力が終わったとき。注意したいのは、待ちが明けてもそのまま実行に戻るのではないこと。いったん順番待ちの列に並び直すんだ。

そして 実行 → 実行可能 もある。走っている途中で CPU を取り上げられたときだよ。次のスライドから、この取り上げの話に入っていく。

実行可能  │ 割当て実行  │ 入出力要求待ち  │ 完了実行可能実行 → 実行可能  取り上げられた

05 / 12

スケジューラとディスパッチャ

交代のときには、名前の違う2つの仕事が並んでいる。ここは試験でそのまま問われるので分けて覚えよう。

スケジューラ は「次はどのタスクにするか」を決める係だよ。実行可能状態のタスクを見比べて、順番を作る。

ディスパッチャ は、そこで選ばれたタスクに CPU の使用権を実際に割り当てる係。決めるのではなく、渡すほうだ。

渡すときにやることは3つある。まず、いま走っていたタスクの状態(レジスタの中身など)を退避する。次に次のタスクを選ぶ。最後にそのタスクが前回どこまで進んでいたかを回復する。

この「状態のひとまとめ」を コンテキスト、切り替えの作業を コンテキストスイッチ と呼ぶ。退避と回復があるから、切り替えそのものにも時間がかかる。交代を細かくしすぎると、この手間ばかりが増えてしまうんだ。

スケジューラ  次はどれかを決めるディスパッチャ  CPU の使用権を渡す切替えの手順  1 いまの状態を退避  2 次のタスクを選ぶ  3 その状態を回復

06 / 12

順番の決め方は4つ

ではスケジューラは、どんな決め方をするんだろう。代表的なものが4つあるよ。

  • 到着順(FCFS) … 来た順に並べ、先頭のタスクに CPU を渡す。いちばん素直
  • 優先度順 … あらかじめ決めた重要度・緊急度の高い順に実行する
  • ラウンドロビン … 順番に少しずつ。全員に均等な CPU 時間を配る
  • 処理時間順(SJF) … 処理予定時間が短いものから片づける

ラウンドロビンは、決めた持ち時間(タイムクウォンタム)を使い切ったら列の最後尾に回す。全員が少しずつ進むので、待たされ続ける人が出ない。画面を触っている人を待たせたくない用途に向いているよ。

処理時間順は、全体の平均待ち時間を短くするのが得意だ。でも弱点がある。短い仕事が次々に来ると、長い仕事はいつまでも順番が来ない。これを 飢餓(スタベーション)というよ。「特定のタスクが待ち続ける可能性が最も高い方式は?」と聞かれたら、これが答えになる。

優先度順にも同じ危険がある。だから、待たされた時間に応じて優先度を少しずつ上げていく工夫を足すことがある。こうすると、低い優先度のタスクもいつかは順番が来るよ。

到着順  来た順に1つずつ優先度順  大事な順にラウンドロビン  順番に少しずつ処理時間順  短いものから

07 / 12

取り上げるか、手放すのを待つか

もう1つ、方式とは別の軸がある。走っているタスクから CPU を取り上げられるかだよ。

プリエンプティブ(プリエンプションあり)は、OS が強制的に取り上げる。持ち時間を使い切ったとき、あるいはより優先度の高いタスクが実行可能になったときに、走っているタスクを止めて交代させる。止められたタスクは終わったわけではないので、実行可能状態に戻って順番待ちに並ぶ。

ノンプリエンプティブは取り上げない。そのタスクが自分から待ち状態に入るか、終了するまで、他のタスクは実行状態になれない。

ここでよく引っかかるのが戻る先だよ。優先度の高いタスクに割りこまれたタスクは、実行可能状態になる。待ち状態にはならない。待ち状態は「入出力の完了などを待っていて、CPU をもらっても進めない」状態のことだからね。割りこまれたタスクは、CPU さえもらえればすぐ進めるんだ。

この区別を問う問題は本当によく出る。取り上げられたら実行可能、自分で入出力を頼んだら待ちと唱えておこう。

プリエンプティブ  OS が取り上げる  → 実行可能に戻るノンプリエンプティブ  自分で手放すまで  → 待ちに入るか終了

08 / 12

CPU が空いている時間を数える

ここで計算問題の型を1つ覚えよう。CPU が誰にも使われていない時間(遊休時間)は何ミリ秒か、という問いだ。

鍵になるのは、入出力の間 CPU は空くという性質だよ。タスクが I/O(5) に入っている5ミリ秒のあいだ、そのタスクは待ち状態なので CPU を使わない。だから他のタスクがそこに入れる。誰も入れなかった時間だけが遊休になる。

数え方は、時間の軸を1本引いて、上から順に埋めていくだけだよ。

右の例は優先度方式で、高・中・低の3つが同時に実行可能になった場合。高が CPU を3使い、入出力に抜けたら中が入り、中が抜けたら低が入る。低も抜けた時点で3つ全部が入出力中になるので、そこが空きだ。

埋めるときのコツは2つ。CPU は必ず優先度の高いタスクに渡すこと、そして 入出力は競合しない(何本でも同時に進む)という前提を忘れないことだよ。

高 CPU3 I/O5 CPU2中 CPU2 I/O6 CPU2低 CPU1 I/O5 CPU10- 3 高が CPU3- 5 中が CPU5- 6 低が CPU6- 8 空き(2)8-10 高が CPU10-11 空き(1)11-13 中が CPU13-14 低が CPU空き 合計 3

09 / 12

割込み ― 内側と外側

交代のきっかけになっているのが 割込み だよ。走っている処理をいったん中断して、緊急度の高い別の処理に移るしくみだ。

割込みが起きると、CPU はまずいまの続きの場所を保存する。具体的には、次に実行する命令の番地を持っている プログラムカウンタ を退避する。これがあるから、割込みの処理が終わったあと中断された場所から再開できるんだ。

割込みは原因がどこにあるかで2つに分かれる。ここが試験の狙いどころだよ。

  • 内部割込み … 実行していたプログラム自身が原因。0 で割った、演算があふれた、無い命令を実行した、ソフトウェア割込み命令を実行した
  • 外部割込み … プログラムのが原因。タイマが指定時間を知らせた、入出力装置が完了を知らせた、電源が異常を知らせた

見分け方はシンプルで、そのプログラムを止めても起きるかどうか。タイマは止めても時を刻むから外部、0 で割るのはそのプログラムが実行しなければ起きないから内部だよ。

もう1つ、割込みと対になる考え方が ポーリング だ。こちらは CPU が自分から繰り返し様子を見に行くやり方で、装置の状態レジスタやビジー信号を読んで監視する。割込みは「終わったら向こうが呼んでくれる」、ポーリングは「こちらが何度も見に行く」。向きが逆だよ。

内部割込み(自分のせい)  0 で割った  無い命令を実行した外部割込み(外のせい)  タイマの時間切れ  入出力の完了割込み → 呼ばれるポーリング → 見に行く

10 / 12

同じものを同時に触ると壊れる

交代させると、新しい困りごとが生まれる。2つのタスクが同じものを触ったときだよ。

プリンタが1台しかないのに、2つのタスクが同時に印刷を始めたら、文字が混ざって出てくる。共有している変数を2つのスレッドが同時に書きかえたら、片方の書きこみが消える。途中まで書きかえた中途半端な状態を、他のタスクに見せてはいけないんだ。

そこで、共有している資源を、一度に1つのタスクだけが使えるようにする。これが 排他制御 だよ。

触ってはいけない、この「一度に1つだけ」を守るべきコードの範囲を クリティカルセクション(危険域)と呼ぶ。入るときに鍵をかけ、出るときに鍵を開ける。

その鍵の代表が セマフォ だ。仕組みは空き枠の数を数えているカウンタだと思えばいい。使い始めるときに数を1つ減らし、使い終わったら1つ戻す。0 になったら、次に来たタスクは待たされる。枠が1つだけのものはとくに ミューテックス(バイナリセマフォ)と呼ぶよ。

大事なのは、待たされたタスクは死んでいないこと。前のタスクが鍵を開ければ順番が来る。次のスライドの困りごととは、そこが違う。

資源 R は 1つだけA: R を確保 → 使用中B: R を要求 → 待つA: R を解放B: R を確保 → 使用中セマフォ = 空き枠の数  確保で −1  解放で +1  0 なら待つ

11 / 12

1つのプログラムを何人でも

では、共有されるのがデータではなくプログラムそのものだったら、どうなるだろう。

同じプログラムを2つのタスクが同時に走らせることは、よくある。このとき、先に呼ばれた処理が終わっていないのに、別のところから呼び出されても、それぞれ正しい結果を返す性質を 再入可能(リエントラント)というよ。

これが成り立つ条件は、書きかえる場所をタスクごとに分けておくこと。命令の並びは共有していい。でも作業用の値を全員が同じ場所に書いていたら、あとから来たタスクが前のタスクの途中の値を上書きしてしまう。

名前のよく似た性質が3つ並んで出るので、区別しておこう。

  • 再入可能 … 終わる前に別から呼ばれても、並行して正しく動く
  • 再使用可能 … 一度動かしたあと、読み込み直さずにもう一度使える。ただし同時ではない
  • 再配置可能 … どの番地に置いても動く
  • 再帰的 … 自分自身を呼び出せる

「同時に呼ばれて大丈夫」が再入可能、「もう一度使える」が再使用可能。同時かどうかが分かれ目だよ。

再入可能  同時に呼ばれても平気再使用可能  続けて使えるが順番に再配置可能  置く場所を選ばない再帰的  自分を呼び出せる

12 / 12

OS でも起きるデッドロック

排他制御を入れると、こんどは別の詰まり方が起きる。

タスク A がプリンタを確保し、タスク B がファイルを確保した。次に A はファイルを、B はプリンタを欲しがる。どちらも相手が離すのを待つので、永久に動かない。これが デッドロック だよ。

ただの待ちとは区別しよう。1つの資源をめぐって片方が待つだけなら、先のタスクが解放すれば順番が来る。デッドロックは互いに相手の持ちものを要求して、待ちが輪になっている状態のことだ。

起きるには条件が要る。ざっくり言えば、一度に1つしか使えない資源が複数あり、持ったまま次を待ち、取り上げられず、要求が輪になっているとき。逆に言えば、どれか1つを崩せば起きない。

いちばん簡単な崩し方は、全員が同じ順番で資源を確保すると決めておくことだよ。「プリンタ → ファイル」の順に統一すれば、B もプリンタから取ろうとするので、A が持っている間はそこで待つだけになる。輪ができないんだ。

輪ができてしまった場合は、OS が気づいて片方を強制的に打ち切る。データベースの章でトランザクションが片方ロールバックされていたのと、まったく同じ考え方だよ。同じ資源を取り合う場所には、必ず同じ問題が出る

A: 資源1 を確保B: 資源2 を確保A: 資源2 を待つB: 資源1 を待つ  → 動けない同じ順で取れば起きないA: 資源1 → 資源2B: 資源1 → 資源2