x86 系コンピュータを動かす理論と実装 作って理解する OS の読書メモ。第 1 章「ハードウェアの基礎」をサクッと流し読みしつつ、気になりどころだけ自分向けにメモ。
本はこれ ↓
スイッチング動作の基礎
- 電圧の時間発展を表す図を タイミングチャート という
- システムの動作の基準となる信号を クロック信号 という
- Enabled 状態と Disable 状態の比率を デューティ比 という
- 高電圧時にアクティブ状態とするものを 正論理、低電圧時にアクティブ状態とするものを 負論理 という
ハードウェアに計算させる方法
コンピュータができる論理演算は以下の 4 つ
- NOT
- AND
- OR
- XOR
加算回路
これらを組み合わせると加算回路を作れる
- ある 1 ビット値 X0, Y0 に対してその和は最大 2 ビットになりうる
- 演算結果の上位ビット B1 は桁上がりの有無を示すが、これは単純に X0, Y0 が共に 1 の場合、つまり AND 演算の結果となる
- 演算結果の下位ビット B0 は桁上がり、もしくは双方ともゼロのときに 0 となる、すなわち XOR 演算の結果となる
小学校レベルの回路におきかえると、入力ビットを乾電池(使える/使えなくなった乾電池の差異によってアクティブか否かを表現できる)、出力を電球として表現可能だが、チャタリングが発生するため実際の回路ではスレッショルド電圧を定め、チャタリングの影響を緩和している
全加算器と半加算器
- 前述の加算器を横に並べていけば計算できる数値の範囲を広げられるが、上位の場合、下位からの桁上がりを考慮する必要があるので少し回路に手を加える必要がある(当然入力信号はひとつふえて、計 3 つとなる)
- 下位からの桁上がりを考慮した加算器を 全加算器、そうでないものを 半加算器 という
- 全加算器の各ビットの演算結果は以下のようになる
- 全加算器の演算結果の下位ビット B は入力値すべての XOR 演算の結果となる、すなわち Ci XOR Xi XOR Yi
- 全加算器の演算結果の上位ビット C は入力値 Xi, Yi が共に 1 (= Xi AND Yi) または Xi + Yi の下位ビットと Ci が共に 1 (Xi XOR Yi) AND Ci のときにアクティブ、すなわち Xi AND Yi OR (Xi XOR Yi) AND Ci
- 単純に考えれば下位ビットは Xi, Yi を半加算器を突っ込んだ結果の下位ビットと Ci をさらに半加算器に突っ込んだ下位ビット
- 単純に考えれば上位ビットは「Xi, Yi を半加算器を突っ込んだ結果の上位ビット」と「Ci を半加算器を突っ込んだ結果の上位ビット」の論理和
乗算器
単純に 2 倍する回路を考えてみる
- 2 進数の特性を生かしてビットシフトするだけ
- 例: 1 = 001(2) を 1 ビット左にシフトすると 010(2) = 2、繰り返すと 100(2) = 4
- 右シフトすれば 1/2 となる
- 符号付演算の場合最上位ビットを保持する 算術シフト を行う
- MSB を気にせずに単純にシフトすることを 論理シフト という
次に一般化して N 倍する回路を考えてみる。ある数 X を 7 倍する計算を行う場合、X + 2X + 4X の演算を行えばよい。すなわち以下のような回路を用意しておけばよい
- 図中で、乗算器の演算結果は Y からの入力ビットが立っているときにのみ有効となる
- 7 倍であれば 0111(2) という信号を送る
- このような信号を イーネブル信号 という
演算装置
- 以上にように加算器、減算器、乗算器、除算器を作成したのちに、それらを組み合わせ、イネーブル信号によってどの演算を行うか選択できるようにしてあげれば演算装置ができあがる
- このような演算装置に出した演算命令を プログラム とよぶ
より高度な演算を行うために
- 前述のような演算装置を用いて回路を組んだ場合、プログラミングとその実行は回路の構成 → 演算 → 計算結果の視認という流れになる
- 連続した演算処理を行うためにあらかじめ処理内容を記録しておき、その結果を格納できるような記憶装置があればより高速に演算できる
- メモリから処理内容を読む → 処理を実行する → 次の処理内容を読む → 処理を実行する →...
ハードウェア
CPU とは
- これまでにみてきた基本的な演算処理に加えて、いくつかの演算を可能にしたものを ALU(Arithmetic Logic Unit) という
- こうしたものと、演算を補助する記憶領域(レジスタ)などをいくつかの装置を組み合わせたものを CPU(Central Processing Unit) という
- ゼロフラグ、パリティフラグ、キャリーフラグなど計算を効率化させるためいくつかの特殊な用途のレジスタもある
- レジスタは ALU に直接接続されており、高速にアクセスが可能
- 演算装置、記憶装置およびその制御回路の組みをコアとよび、1 チップに複数のコアを搭載した CPU を マルチコア CPU とよぶ
メモリ とは
- レジスタは非常に限られたデータのみしか保持できない
- レジスタよりもアクセスは低速となるが多くのデータを保持できる記憶装置としてメモリを利用する
- CPU からメモリ上のアドレスを指定して 1 byte 単位で読み書きを行う
- アドレスの指定は アドレスバス、データの転送は データバス を介してやりとりする
- アドレスバスにて指定されたデータはメモリ内のセレクタによってアクセス可能な状態となる
メモリの操作とプログラムの実行
メモリ上の値の操作を理解するために「変数をインクリメントする処理」を考えると、処理の流れは以下のようになる
- メモリから値をレジスタに読み出す
- レジスタの値をインクリメントする
- レジスタの値をメモリに書き込む
すると必要な CPU 命令は以下のように定義できる(数字は対応する機械語、{...} はオペランド)
- メモリからレジスタに読み出す命令: [0] LD_X {ADDRESS}
- レジスタの値をインクリメントする命令: [2] INC_X
- レジスタからメモリに書き込む命令: [4] ST_X {ADDRESS}
定義した CPU 命令を使って「変数をインクリメントする処理」を記述すると以下のようになる(操作対象のメモリのアドレスは 8 と仮定)
- LD_X 8
- INC_X
- ST_X 8
これを機械語に読み替えると 08128 となるが、オペランドをとる命令(LD_X, ST_X)ととらない命令(INC_X)があり、いま命令とオペランドのどちらを読んでいるのかという状態を保持している必要がある
CPU は各命令の長さを プログラムカウンタレジスタ に保持しており、各命令長分のデータを読み取り処理が行えるよう プログラムカウンタ を用いて制御している
- 命令とデータが同じメモリ領域に書かれていると、命令開始アドレスが 1 バイトずれただけで以後正常に動作しなくなるがこれを ノイマン型アーキテクチャ という
- 命令とデータ領域が別れているものを ハーバード型アーキテクチャ という
タスクの切り替え
前述の「変数をインクリメントする処理」を他の変数に対しても行う処理を交互に行いたいと考えると、以下のような処理の流れとなる
- メモリから値をレジスタ X に読み出す
- レジスタ X の値をインクリメントする
- レジスタ X の値をメモリに書き込む
- 次のタスクに切り替える
- メモリから値をレジスタ Y に読み出す
- レジスタ Y の値をインクリメントする
- レジスタ Y の値をメモリに書き込む
- 次のタスクに切り替える
- 以下同様
これに際してタスクを切り替えるため、次に実行したい命令のアドレスにジャンプする命令、およびレジスタ Y の操作に関する命令が必要になる
- メモリからレジスタに読み出す命令: [0] LD_X {ADDRESS}
- メモリからレジスタに読み出す命令: [1] LD_X {ADDRESS}
- レジスタの値をインクリメントする命令: [2] INC_X
- レジスタの値をインクリメントする命令: [3] INC_X
- レジスタからメモリに書き込む命令: [4] ST_X {ADDRESS}
- レジスタからメモリに書き込む命令: [5] ST_X {ADDRESS}
- 指定されたアドレスにジャンプする命令: [6] JMP {ADDRESS}
これらを用いてメモリに次のように命令を書き込めば良い
00 LD_X
01 8
02 INC_X
03 ST_X
04 8
05 JMP
06 10
07 -
08 0 # ここのデータをインクリメントしていく
09 -
10 LD_Y
11 18
12 INC_Y
13 ST_Y
14 18
15 JMP
16 0
17 -
18 0 # ここのデータをインクリメントしていく
19 -
20 ...
このようなタスク切り替えを 協調型マルチタスク とよぶ
- 前述のプログラムは操作対象のメモリのアドレスを 8, 18 などと絶対値で指定しているが、プログラムがメモリ空間のどの位置に展開されるのかは実行時に定まるため、絶対アドレス を指定しているとアドレスの重複が避けられない
- こういった状況でうまく動作するように 相対アドレス を指定できる必要がある
- プログラムカウンタ(PC)レジスタを基準として相対アドレスを指定できる
- また実際にプログラムが実行される際には、プログラムが配置される コード領域 と実行時に必要なデータが配置される データ領域 に分けてメモリの割り当てが行われる
- コード領域とデータ領域は連続しているとは限らず、実行時に OS によって割り当てられる
- 一般にコード領域のサイズはプログラムサイズからわかるが、データ領域のサイズは実行時にならないとわからない
- コード領域は同じタスク間で不変なので共有できる
- データ領域の管理は スタックポインタ(SP) というレジスタを用いて行う
- OS 割り当てられたアドレス範囲+1 の値が SP に割り当てられる
- SP に対してデータをスタックして利用する形となり、この領域に収められているデータを ローカル変数 ともよぶ
スタック領域の操作を行う以下のような命令を追加できます
- SP をデクリメントし、X レジスタの値を保存する命令: [8] PUSH_X
- SP をデクリメントし、Y レジスタの値を保存する命令: [9] PUSH_Y
- X レジスタの値を保存し、SP をインクリメントする命令: [10] POP_Y
- Y レジスタの値を保存し、SP をインクリメントする命令: [11] POP_Y
すると前述のプログラムは以下のように絶対アドレスを排除して書き換えられる
00 PUSH_X
01 LOAD_X
02 SP[0]
03 INC_X
04 SAVE_X
05 SP[0]
06 10
07 JMP
08 PC - 5
- データ操作の対象が相対アドレスで指定されている場合、レジスタの状態が正しく保存・復元されればば、タスクをいつでも中断し、切り替えることができる
- タスクが使用するすべてのレジスタの状態のことを コンテキスト といい、異なるタスクに移行する際にはコンテキストスイッチがオーバーヘッドとなる
- OS がマルチタスクを実現するために 割り込み という仕組みがある
- 割り込みによって PC レジスタに設定される値を 割り込みベクタ(ベクタアドレス) という
- 特に複数タスクの実行を定期的に切り替えるためには タイマー割り込み という一定期間に割り込みが発生する仕組みを利用している
- 処理とは別に一定の信号をキープしたい外部デバイスなどがある際には ポート がその役割を担う
- CPU は任意のタイミングでポートの信号を変化させられる
- また機器によっては逆に CPU に割り込みを行うことも可能
外部記憶装置
- 電源が投入されていない状態でも大量データを保持するために使う
memo
Pinned Articles
About
ウェブ界隈でエンジニアとして労働活動に励んでいる @gomi_ningen 個人のブログです
Tags
JavaScript
PowerShell
kibana
elasticsearch
fluentd
nginx
イベント
五十嵐裕美
村川梨衣
logrotate
IoT
Scala
Java
C言語
iputils
ICMP
WUG
mastodon
Swift
AWS
Clock
Windows
アーキテクチャ
PoEAA
iOS
DeviceFarm
プログラミング言語
OS
StepFunctions
Lambda
Serverless
terraform
ポエム
RHEL
ネットワーク
GraphQL
CloudWatch
Linux
Coreutils
network
nc
telnet
LinuxKernel
fpinscala
ELB
IAM
AppSync
EFS
Gradle
english