第7回 システムソフトウェア、計算機アーキテクチャ
本日の講義内容
- システムソフトウェア
- 計算機アーキテクチャ
シェル
本日は一旦、これまで学んできたC言語やRustから少し離れて、プログラムの実行やファイル操作など、日常的に利用してきた環境そのものについて考えてみましょう。これまで、ターミナルを利用して、
- プログラムを実行する
- 各種ツールやコマンドを呼び出す
- ファイルやディレクトリを操作する
といった作業をおこなってきたと思います。より詳細には、シェルと呼ばれるプログラムが起動しており、各コマンドはこれを介して実行されていました。
コマンドラインシェル
コマンドラインシェルは、ターミナルに入力されたユーザの文字列を読み取り、処理を実行したり、対応する処理を実行するようOSに指示する役割を担います。ターミナルからさまざまな作業を行う際、コマンドラインシェルを利用することには、次のような利点があります。
- ファイル操作やプログラムの実行を、簡潔な記述で行うことができる
- 既存の複数のプログラムを組み合わせて利用できる
あるディレクトリにあるファイルを別のディレクトリに移動する操作を考えてみましょう。このような単純な作業のために、C言語で一からプログラムを書くのは現実的ではないと思います。
| move.c | |
|---|---|
というコードをコンパイルして実行すれば確かに要件を満たしますが、コマンドmvを知っていれば、
と、より簡単に実現できると思います。
こうしたコマンドは先人の需要に基づいて実装され、取り込まれてきたものなので、これらを知ってさっと使えることは非常に価値があります。これらに加えて、コマンドラインシェルにもC言語やRustと同様に制御構造が存在し、たとえば、似たようなことを変数を変えてN回繰り返す、といった処理もコマンドラインシェルで実現することができます。
今日はコマンドラインシェルの中でも、特に現在の多くの環境で標準となっているbashを使ってみましょう。たとえば、Google Cloud ShellやWSL2のUbuntu環境ではbashがデフォルトで利用できます。デフォルトのシェルは
というコマンドで確認できます。/bin/bashというようなパスが表示されると思います。このように、シェル自体もプログラムです。
$ /bin/bash --version
GNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
とすると、bashのバージョン情報が表示されると思います。
コラム: 色々なシェル
macOSで上のコマンドを実行すると、/bin/zshと表示されると思います。このように、macOSではzshという別のシェルがデフォルトになっています。また、ほかにもkshやfish、tcshなど、さまざまなシェルが存在します。WindowsのPowerShellは残念ながら互換性が低いですが、ほかのものはそれなりに似ています。
また、コマンドラインシェルだけではなく、グラフィカルシェルも存在します。たとえば、macOSではFinderなどはグラフィカルシェルです。WindowsではPowerShellがコマンドラインシェルですが、File Explorerなどはグラフィカルシェルです。この辺りはさまざまな定義があるのですが、たとえばこうしたものをシェルと理解することができます。
頻出コマンド1
これまでのソフトウェア1/2を通して基本的なコマンドにはいくつか触れてきたと思います。少し復習してみましょう。
-
組み込みコマンド
cd: カレントディレクトリの移動echo: 引数の文字列を表示
-
外部コマンド
ls: ファイル一覧表示pwd: 現在のディレクトリを表示cat: 引数で与えられたファイルを連結し表示(1ファイルの場合はその中身を表示)mkdir: ディレクトリ作成mv: ファイルの移動/リネームcp: ファイルのコピーrm: ファイルの削除
上記に示したように、実はcdは他のコマンドと違います。他の頻出コマンドは、通常のプログラムであり、実行可能ファイルが$PATHに含まれるディレクトリに配置されています。一方cdはシェル自身の内部で実装された組み込みコマンドであり、実行可能ファイルはないため、which cdとしても見つかりません。一方type cdとすると、cdはシェルの組み込みコマンドであることが表示されると思います。cdやexitなどシェル自身の状態を変更する必要があるコマンドは組み込みコマンドになっています。
$ which ls
/usr/bin/ls
$ ls --version
ls (GNU coreutils) 8.32
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Written by Richard M. Stallman and David MacKenzie.
演習
いくつかのコマンドについて、実行可能ファイルの場所やバージョンを確認してみましょう。
頻出コマンド2
他にも存在する基本的なコマンドをいくつかざっくり紹介します。
- 外部コマンド
head: ファイルの先頭から指定行数表示tail: ファイルの末尾から指定行数表示sed: ストリームエディタ、よく使うのは文字列置換。nl: 行頭に行番号を付与find: 指定条件を満たすファイルを探してくるxargs: 受け取った入力をコマンドライン引数にするsort: 指定列の条件に基づいてファイルの行を並べ替えuniq: ファイル中ユニークな行を取り出してくる
上記は主にファイルを操作したりするコマンドですが、もちろん他にも基本として知っておくべきコマンドは数多く存在します。触っていくうちに慣れていきましょう。
ターミナルのショートカット
ターミナルを操作する際に、矢印だけを使っていないでしょうか。bashで使えるショートカットを使ってみましょう。以下の表ではCtrlキーとの同時押しをC-x、Metaキー(WindowsだとAlt / Macだと esc が標準)との同時押しをM-x と記載しています。
| ショートカット | 機能 |
|---|---|
| C-a | 行頭へ移動 |
| C-e | 行末へ移動 |
| C-f | 1文字進む |
| C-b | 1文字戻る |
| M-f | 1単語進む |
| M-b | 1単語戻る |
| C-l | 現在行を残して画面クリア |
| C-k | 行末まで切り取り削除 |
| C-u | 行頭まで切り取り削除 |
| C-y | 上記で切り取ったものを貼り付ける |
| C-r | コマンド履歴のインクリメンタルサーチ |
こうしたショートカットとファイル名などのTAB補完を効果的に用いることで、かなり効率的に作業が進められるようになると思います。
複合コマンド
シェルでは、より複雑なコマンドを実行することもできました。それに先立ってまず、C言語での簡単なプログラムについて見てみましょう。
例えば、整数引数を受け取って、それが3の倍数のときに「3の倍数です」と出力する以下のC言語のプログラムがあったとします。
| example.c | |
|---|---|
上記のプログラムをコンパイルして、プログラムを実行可能にします。
さてこのプログラムが正しく動きそうか、1から1000ぐらいまで試したいとします。これまではこのような繰り返しはCのプログラムの方に書いていたかと思います。もしくはターミナルに変数を変えながら1000回実行するという人もいたかもしれません。このようなシチュエーションはよくあります、つまり、実験用のプログラムとして汎用的なものを作ったのち、それをテストしたり、複数のパラメータで実行したりする場合です。そのとき、変化させるパラメータなどはシェルで制御する方が簡単なケースが多いです。
bashで連続する数字を出力する場合、いくつかの方法がありますが、ここではforを使う方法を示します。
上記の構文はbashで繰り返しを書く際によく書く構文です。iは変数であり、in以降の要素を順次代入します。変数名はiに限りません。実際に実行されるのはdoからdoneに囲まれた部分で、forのところでセットされたiを$iという形で参照しています。なお、{1..100}という書き方は、空白区切りで数字の列を出力するものです。
などとするとどのような出力が得られるでしょうか?この{..} の記述は便利で、例えばaからzまでの系列を得たい場合には
とすると空白区切りで出力できます。一つ注意点として、開始・終了の文字や数字と.. の間に空白を入れてはいけません。
演習
各コマンドを実行してみましょう。
シェルスクリプト
シェルのコマンドをファイルに書いておき、それを実行することができます。シェルスクリプトと呼びます。シェルスクリプトは文字通りコマンドラインシェルをベースに動作するスクリプト言語と見ることができます。スクリプトといっても一番最初は、普段ターミナルに入力しているコマンドを並べるだけで立派なシェルスクリプトになります。以下の内容をhello.shという名前で保存してみましょう。
一行目にある#!/bin/bashはシェバンとよばれ、OSにこのスクリプトはこの言語で実行してねということを指示するおまじないです。Pythonを書いたことのある人は#!/usr/bin/pythonといった書き方をしたことがあるかと思います。
最も基本的な実行法はbashで呼び出すことです。
ただ、実際にはスクリプト単独で実行するケースが多いので、一工夫します。まずは現在のファイルのパーミッションを確認してみましょう。ファイルのパーミッションとは、今のユーザがそのファイルに対して何ができるのかの情報です。hello.shがあるディレクトリで以下を実行します。
以下のような結果が得られるかと思います。
-rw-r--r-- 1 kadomoto kadomoto 118 Jan 20 20:56 hello.sh
│ │ │ │ │ │ │
│ │ │ │ │ │ └─ ファイル名
│ │ │ │ │ └─ 更新日時
│ │ │ │ └─ ファイルサイズ(bytes)
│ │ │ └─ 所有グループ
│ │ └─ 所有者
│ └─ ハードリンク数
└─ パーミッション
先頭にある-rw-r--r--がファイルに対する権限(パーミッション)を表しています。このファイルは、ユーザkadomotoが所有し、rw-r--r--といった権限があるということを表しています。
- 所有者(最初のブロック)は読み取り
rと書き込みwができる - 同じグループのユーザ(2番目のブロック)は読み取り
rのみできる - グループも異なる他のユーザ(3番目のブロック)は読み取り
rのみできる
といった状態です。この状態ではこのシェルスクリプト単体で実行することができません。
以下のコマンドでスクリプトに実行権限を付与します。
このコマンドは所有者(ユーザ: u)に実行権限(x)を付与する(+)という意味になります。
この状態で以下のように実行ができます。
演習
上記の手順でhello.shの写経と実行をしてみましょう。
変数
シェルスクリプトにおける変数は基本的に全て文字列を扱います。型はありません。文字列ですがダブルコーテーションで囲む必要はありません。シェルスクリプトにおける変数の最大の注意点は以下です。
- 変数を代入する際は空白を開けずに
=で代入する - 変数を使うとき(参照するとき)は
$をつける
例をみてみましょう。
| copy.sh | |
|---|---|
変数参照は$をつける他にも、${}で囲むことも可能です。また、シェル変数の参照時は基本的にダブルクォートで囲むと安全です。これは、展開時に空白が含まれている場合、空白によって区切られた複数の文字列として解釈されてしまうのを防ぐためです。
算術演算
シェルスクリプトにおける変数は基本的に文字列なので、数字も文字列として解釈されます。たとえば以下の例を実行してみましょう。
ただし、数字を扱いたい場合もあるので、その場合は$(())を使います。算術式を評価し、その結果の展開となります。
$(( ))は内部で演算を行ってくれる便利な変数展開です。これを使う場合は中のxは$が不要です。なお$(())内部は空白に対して寛容です。注意点としてbashの場合は整数演算のみです。zshと呼ばれる高機能なシェル(bashの構文はほぼ使える)では、実数の演算も受け付けます。
その他の変数展開
bashの変数は様々な形で展開することができます。以下に例を示します。一つずつみていきましょう。こちらの変数展開では${}で変数を囲みつつ必要な演算をかきます。
上の変数展開を用いることで、文字列の抽出や置換が容易に実現できます。例えばファイルの拡張子部分を除いて別のファイルを作るなどの場合に用いることができます。
演習
filename.shを写経して実行してみましょう。
代入
コマンドの結果を代入する方法は二つあります。一つは$()で囲む方法です。
もう一つはバッククォートで囲む方法です。
for
forループを使うケースはいくつかあります。以下に例を示します。
ちょっと便利なコマンドにseq があります。
これを利用してforループを書くと以下のようになります
ディレクトリ以下にあるファイルそれぞれに操作をしたい場合をやってみましょう。下準備としてディレクトリ の下に100個ファイルを作ります。各ファイルにはランダムな数字が入るとします。シェルでは環境変数$RANDOMが、毎回異なる乱数を出してくれるのでこれを利用します。
| rand.sh | |
|---|---|
続いてこれらのファイルに、データ0を追記したいとします。
| append.sh | |
|---|---|
最後に拡張子.txtを除いてみましょう。
なお、一応別の書き方として、C言語っぽいものもあるので紹介します。二重括弧でくくります。
演習
ここまでに示したスクリプトを写経し実行してみましょう。
if
例えば、決められたファイル名を並べて、該当するファイルが存在するときだけlsするようなシチュエーションを考えましょう。シェルスクリプトにもif文による制御が存在します。
| file_check.sh | |
|---|---|
シェルスクリプトのif文は空白にきびしく、条件文を空白を開けて[と]で囲みます。-eはその名前のファイルが存在するかどうかをチェックする演算子です。他にも以下の表のような演算子があります。
| ファイル演算子 | 役割 |
|---|---|
| -d file | ディレクトリかどうか |
| -f file | ファイルかどうか |
| -e file | ファイルが存在するかどうか |
| -L file | シンボリックリンクかどうか |
| -r file | 読み取り可能か |
| -w file | 書き込み可能か |
| -x file | 実行可能か |
演習
b.txtを作成し、スクリプトを写経し実行してみましょう。
引数
シェルスクリプトでは引数は$1、$2といった形で取り出します。$0は自身の実行コマンド名です。$#で引数の個数を取り出せます。
| args.sh | |
|---|---|
演習
スクリプトを写経し実行してみましょう。
配列
bashでも配列定義が可能です。
なお、zshはbash互換が基本なのですが、配列のインデックスがなぜか1始まりなので、少し(かなり)注意が必要です。
プロセス置換
コマンドの実行結果を一時ファイルを作成せずに、ファイルのように扱うテクニックです。読み込み用と書き込み用があります。 パイプに近いですが、かなり柔軟な処理ができるのと、個々のプロセスが並列で走るので並列化が可能です。
#!/bin/bash
# プロセス置換を使わない場合
# file1 file2 は最終的にはいらない一時ファイル
echo "1 2 3" > file1
echo "daaaa" > file2
cat file1 file2 > file3
rm file1 file2
# プロセス置換を用いると
cat <(echo "1 2 3") <(echo "daaaa") > file3
シェルスクリプトの機能をざっくりと説明しました。シェルスクリプトの基本機能の範囲で、C言語やRustで書くには複雑になることを場合によっては極少量の記述で実現可能になります。さまざまなコマンド群が存在するので、それらを組み合わせることで作業の効率化を実現できます。
実行速度はあくまでコマンドを一つずつ実行するのと同様ですので、すごく速いとはいえません。ファイル操作等であっても本当に速さが要求される場合は、C言語で専用の処理を書く必要性もありますが、普段使いでシェルスクリプトを使いこなすことで、大幅な効率化を実現できます。
OS
オペレーティングシステム(OS)は、ハードウェアを安全かつ効率的に管理し、アプリケーションプログラムに対して統一的な実行環境を提供する基本的なソフトウェアです。最後にこれについても簡単に触れてみます。
OSは、たとえば、CPUを複数のプログラムで分け合うためのプロセス管理、限られたメモリを仮想的に拡張し、安全に分配するメモリ管理、データを扱うためのファイルシステム、そして多様なハードウェアを抽象化するデバイスドライバといった機能を提供しています。

OSの内部では、中核的な機能はカーネルモードと呼ばれる特権的な実行モードで動作しています。カーネルモードでは、CPUやメモリ、ストレージ、I/Oデバイスといったハードウェアに直接アクセスすることが許可されており、一般のアプリケーションが動作するユーザモードとは明確に区別されています。シェルやアプリケーションはプロセス単位で動作しており、
特にLinuxの場合、ファイルシステムやデバイスドライバなどの中核機能はカーネルモードで実装されており、各アプリケーションプログラムやシェルは、システムコールを通じてカーネルに処理を依頼することで、ハードウェアにアクセスしています。
たとえば、以前書いたテキストファイル読み込みのプログラムを再度実行して確かめてみましょう。
演習
上記のプログラムをコンパイルし、以下のように実行してみましょう。
ここでは、プログラムが実行されてから終了するまでにどのようなシステムコールを実行したかを追跡しています。たとえば、execve()ではカーネルにプログラムを実行するよう要請し、openat()ではカーネルにファイルを開くよう要請し、read()ではファイルからデータを読み取るよう要請しています。
ここで、プログラムの実行可能ファイルは、ELFという形式で保存されているのでした。execve()が呼ばれるとカーネルはこのELFファイルを読み込み、それにしたがって、動的リンカを呼び出し依存ライブラリを探したり、適切にメモリへ配置したりします。
演習
先ほどのプログラムの実行可能ファイルを観察してみましょう。
命令セットアーキテクチャ
ソフトウェアから見たCPUの機械語命令、何をどのように実行できるか、といった内容をまとめたものを、命令セットアーキテクチャ (ISA: Instruction Set Architecture) と呼びます。ISAは、ソフトウェアとハードウェアの間の取り決め(インタフェース)に相当する概念です。
コンパイラやアセンブラはあるISAを前提として設計されており、同じISAを持つCPUであれば、理想的には同一の実行可能ファイルを実行できるという互換性が実現されています。逆に、ISAが異なる場合には、別のツールチェーンでプログラムを再コンパイルする必要があります。
ISAの具体例としては、x86-64、ARMv9-A、RISC-Vなどがあります。現在はそれぞれ、PC/サーバ向け、モバイル/PC向け、車載などの組み込み向けなどで用いられています。
アセンブラ
アセンブラ、は、アセンブリ言語を機械語に変換するもものです。以前示したコンパイラの図では、アセンブラはコンパイラの一部として扱われていました。しかし、アセンブラは独立したツールとしても利用可能であり、たとえば、以下のように使うことができます。
# C言語ソースファイルをアセンブリ言語ソースファイルに変換
$ gcc readtextfile.c -S -o readtextfile.s
# アセンブラ: アセンブリ言語ソースファイルを実行可能ファイルに変換
$ as readtextfile.s -o readtextfile
ここで、アセンブリ言語とは、機械語とほぼ1対1対応する人間が読みやすい表現です。たとえば、x86-64命令セットアーキテクチャにおけるmov命令は、mov eax, 0x0といった形で表現されます。こうした低レベルの実行形式を意識しておくと、低レイヤにおける性能問題や不具合の原因について考えることができると思います。
演習
アセンブリ言語ソースファイルや、先ほどの実行可能ファイルの、アセンブリ言語/機械語としての中身を見てみましょう。
計算機ハードウェア

さて、ここまではソフトウェアの話をしてきました。最後にソフトウェアとつながる計算機ハードウェアの話をします。現代的な計算機ハードウェア、たとえばデスクトップPCは、マザーボード上にCPUやメモリ、ストレージ、GPUなどを混載しており、CPUは多数のコアを搭載しています。CPUやGPUの中枢は半導体集積回路でできたマイクロプロセッサとして実装され、数千億といったオーダのCMOSトランジスタから構成されています。こうした構成はサーバやラップトップ、スマートフォンでもそれほど変わらないと思います。
これまで開発してきたソフトウェアは、コンパイルされ、OSによるサポートを受けつつ、こうしたCPU上で実行されていました。今後もCPUのコア数増加や、GPUのような専用計算コアとの混載はますます進んでいくかもしれません。ソフトウェアは抽象的に記述されていても、最終的には具体的なハードウェアの上で実行されます。コアの数、構成、メモリ階層(キャッシュや主記憶)、デバイスとの通信方法といった要素は、プログラムの性能やスケーラビリティに直接影響を与えます。良い情報処理の実現にあたっては、このような複雑なハードウェアの特性も考慮しながらソフトウェアを設計・実装していくことが重要です。
まとめ
本日はここまでです。ソフトウェア2では、C言語とRust言語によるソフトウェア開発について学びました。特にメモリ領域やその管理にフォーカスしてきました。また関連する情報学分野としての、データ構造とアルゴリズムや、計算機アーキテクチャについて学びました。これにて終了です、お疲れさまでした。