ファイルシステム, SSH, C言語
ファイルシステム
Unixのファイルシステムは 階層構造 になっており, その階層構造を作るのが ディレクトリ, その中にデータが ファイル として格納される. ディレクトリはWindowsのGUIで「フォルダ」と呼ばれているものに相当する.
ディレクトリやファイルを特定する文字列を パス と呼ぶ. パスには 絶対パス と 相対パス がある.
絶対パスは, 完全な識別子であり, どのような状態でもファイルシステムさえ同じであれば常に同じものを指す.
Unixでは, 全ての絶対パスは/
(ルート) で始まる.
相対パスは, 現在の 作業ディレクトリ を基準とするパスである. 作業ディレクトリにa.out
という名前のファイルがあれば,
それはa.out
と表せる. また, .
は現在のディレクトリを表す. そのため,
これは./a.out
とも表せる. ..
は上のディレクトリを表す. 例えば, 作業ディレクトリが/usr
のとき,
..
は/
を表す.
一般的な構成
Linuxでは一般的なファイルシステムの構成がある. 次に, ルート以下の基本的なディレクトリ構成を示す.
/bin
/sbin
/lib
/usr
/etc
/var
/tmp
/root
/home
/dev
/proc
/sys
/bin
には, 実行ファイル がある. 試しに/bin
の内容をls
コマンドで見てみる.
ls /bin
/bin/ls
があることに気づくだろう. ls
コマンドもそこにある.
/sbin
にも実行ファイルがある. これは歴史的遺産で, /bin
との差は特にない.
/lib
には ライブラリ がある. ライブラリはアプリケーションを構成する部品であり,
それ単体では通常動作しないが, アプリケーションを作るときに役立つ.
/usr
にはルートに似た構成がある. ルートを汚さないように, ソフトウェアなどは基本的にこちらにインストールされる.
/etc
には, 設定ファイルなどが置かれている.
/var
には変化するファイルが置かれている. 例えば, ログである.
/tmp
には, 一時的なファイルが置かれている.
/root
と/home
には ホームディレクトリ がある. ホームディレクトリはユーザーが作業する場所で,
端末を開くとここが最初に出る. 試しにpwd
コマンドで作業ディレクトリを確認してみるとよい.
全ての権限をもつroot
のホームディレクトリは/root
, それ以外のユーザーは/home
以下にユーザー名のディレクトリを持ち,
それをホームディレクトリとする.
/dev
, /proc
, /sys
は 擬似ファイルシステム である. ディスク上にはないが,
ファイルシステムとして見えるもので, デバイス, プロセス, 設定などへ参照できるようにする.
Unixではそれらへのインターフェイスをファイルシステムに統一することで, 扱いの異なるインターフェイスの氾濫を防いでいる.
/dev
には デバイスファイル がある. デバイスへの参照である.
/proc
では プロセス の情報を参照できる. プロセスは, Unixでの実行単位である.
/sys
ではカーネルの情報, 設定を参照できる. ここで設定を変更してもディスク上にないので,
永続しない (再起動などで破棄される) ことに注意が必要である.
SSH
前回の公開鍵暗号の復習として, サーバーにユーザーを追加して SSH を用いて遠隔でログインしてみる. SSHの場合, クライアントとサーバーの両方が特定される必要がある. そのため, その各々が証明書をもつ必要がある.
SSHの証明書はssh-keygen
コマンドで作れる. 通常, 鍵は~/.ssh
に保存される.
~/
はホームディレクトリの略記である. 標準で__RSA暗号__による証明書が作られ,
id_rsa
が秘密鍵, id_rsa.pub
が公開鍵になる. id_rsa.pub
を管理者に渡し,
アカウントの作成を依頼する.
準備が整ったら, いよいよSSHで接続する.
ssh -p PORT HOST
-p
はポートを指定するオプションである. 続くPORT
をポート番号に置き換える.
HOST
は, ホストのアドレスである.
SSHでログインすると, ローカルの端末と同じように動作する. SSHサーバーがあればどこでも自由にLinux環境にアクセスできる.
C言語
C言語 はUnixを起源に持つ. Unix開発当初, ハードウェアを直接扱うカーネルはほぼ機械語に1対1対応になっている アセンブリ言語 を用いることが多かった. しかし, アセンブリ言語はCPUの ISA (Instruction Set Architecture) などに依存し, 移植性がない. C言語はこの問題を解決すべく開発された言語で, 移植性があり, 論理を記述するのに十分な抽象化がありながらもハードウェアを意識したコーディングを可能にした.
一方で, カーネルを記述する言語としては抽象度が高い, 高級 な言語として誕生したが, アプリケーションを記述する言語より抽象度が低い, 低級 な言語としてみなされている. 低級な表現が, バグ (不具合) に繋がってしまっているが, 抽象化する処理系がなくても動作する, 言語自体が比較的コンパクトである, という利点がある.
C言語は, 実用されている最古のプログラミング言語_ではない_が, 比較的古く, これを参考にした言語も多い. そのため, C言語の知識が他の言語を学ぶ際にも多少役立つ. 一方で, 後発の言語はC言語の欠点を解消するよう設計されており, それらと比べた際にC言語は欠点が目立つ言語でもある.
コンパイラをインストールする
コンパイラ はソースコードをオブジェクトコードに変換する. UbuntuにはC言語コンパイラは標準ではインストールされていない. 次のコマンドで GCC というコンパイラをインストールする.
sudo apt install gcc
hello, world
hello, world
とは, 最初に作るプログラムであり, それにより出力される文字列である.
もとをたどれば, 言語の聖典とも呼ばれる_The Programming Language C_で紹介されたプログラムである.
この本は著者のC言語開発者のBrian W. KernighanとDennis M. Ritchieによって書かれ,
俗に_K&R_と呼ばれている. 紹介するコードはこの本で使われていた書き方 (K&Rスタイル,
カーネルスタイルなどと呼ばれる) でおおよそ統一する. 自分で書く際にも書き方の一貫性に注意するように.
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("hello, world\n");
return EXIT_SUCCESS;
}
これを~/a.c
に保存し, 次のコマンドを実行する.
cc ~/a.c
cc
はC Compilerである. 実行ファイルとしてa.out
が出力されているはずである.
実行してみよう.
$ ./a.out
hello, world
動作したようだ. これが最初のC言語のプログラムである. 次に, このプログラムのソースコードについて簡単に説明しよう.
#include
は プリプロセッサディレクティブ である. これにより読み込まれたファイルがソースコード中にそのまま展開される.
(試しにcc -E a.c
を実行してみよ.) 読み込んでいるファイルは ヘッダファイル である.
/usr/include
を見よ.
stdio.h
は入出力関連の定義を持つ (io
はinput/output
である). printf
を使うために使用した.
stdlib.h
は雑多な標準ライブラリ関連の定義を持つ. EXIT_SUCCESS
を使うために使用した.
main
は, プログラムの開始時に実行される 関数 である. 関数は参照可能な 文 のまとまりである.
printf
は, 文字列を出力する.
\n
は, 改行文字 を表す. コンパイラによって変換され, ソースコード上では\
とn
の2文字でも,
オブジェクトコードでは改行文字1文字になる.
EXIT_SUCCESS
は, 成功を意味する.
printf
これから様々なプログラムを作るわけだが, 入出力ができなければプログラムは本質的に無意味である.
まず出力の方法として, printf
を学ぶ. まずは, 数値を10進数で出力してみる.
printf("The number is %d.\n", 173210);
The number is 173210.
と表示されるはずである. printf
は, 書式 (format.
printf
の末尾のf
はそれである) に合わせて文字列を出力する関数である.
%d
は, 数値を10進数で出力することを示す書式である. d
は, decimalである.
printf("hello, %s\n", "world");
hello, world
と表示されるはずである. "
(ダブルクオート) で囲まれた形のものを
文字列リテラル と呼ぶ. それに対して, ダブルクオートで囲まれていない数字は
数値リテラル とか呼ばれたりする.
%s
は, 文字列に対応する. 今回では"world"
である. printf
の書式として文字列を指定し,
出力させることは可能であるが, %
を含む文字列で意図せぬ結果になるのですべきでない.
変数と型
C言語は変化する状態を記録するために 変数 を持つ. 変数は記録の方式を示す 型 を持ち,
C言語ではコンパイル時に決定される (静的型付け, HSPなどは 動的型付け) .
大抵, ユーザーが明示的に指定する. 1つはint
である. int
は符号付き整数である.
int i;
i = 173210;
printf("The number is %d.\n", i);
i
は型がint
の変数である. %d
は, int
に対応する. %s
は, (const
)
char *
という型に対応する.
引数
例えば, 先のコマンド
cc a.c
これにおいて, cc
は第_0_の 引数, a.c
は第1の引数である. main
ではこれを受け取れる.
次のようにmain
を変更する.
int main(int argc, char *argv[])
{
printf("%s\n", argv[0]);
}
実行すると, 次のようになる.
$ ./a.out
./a.out
ここで, argc
は引数の数 (argument counter), argv
は引数の_配列_である (argument
vector). 第0の引数はargv[0]
として参照できる. なお, 第0の引数もないことがある.
無条件にargv[0]
に参照するのはあまり良い考えではない.
制御構文
制御構文 は, コードの実行される順序 (制御構造, コードパス) を変化させる. おおよそ, Javaと同じである.
if
if
は条件分岐を行う.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
if (argc < 3)
printf("The number of arguments is less than 3.\n");
else
printf("The number of arguments is more than or equal to 3.\n");
return EXIT_SUCCESS;
}
$ ./a.out hello
The number of arguments is less than 3.
$ ./a.out hello world
The number of arguments is more than or equal to 3.
条件によって複数の文を実行したい時には{}
で囲んで__ブロック__にする.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
if (argc < 3) {
printf("The number of arguments is less than 3.\n");
} else {
printf("I'll ignore argument 3 and later ones...\n");
argc = 2;
}
printf("The number of arguments is %d\n", argc);
return EXIT_SUCCESS;
}
switch
switch
はある1つの__式__の値によって条件分岐を行う.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
switch (argc) {
case 1:
printf("The number of arguments is 1\n");
break;
case 2:
case 3:
printf("The number of arguments is 2 or 3\n");
break;
default:
printf("Something happened.\n");
}
return EXIT_SUCCESS;
}
case
に指定する値は定数でなければならない. どのcase
にも当てはまらない場合にはdefault
が実行される.
break
が来るまでラベルを無視して実行される. 例えば, case 2
に当てはまった場合,
次のcase 3
の内容まで実行されるが, default
の手前にbreak
があるのでdefault
以下は実行されない.
これを フォールスルー という. break
を忘れる間違いが多いので注意が必要である.
while
while
は条件式が真である間, 処理を繰り返す.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int i;
i = 1;
while (i < argc) {
printf("%s\n", argv[i]);
i++;
}
return EXIT_SUCCESS;
}
do
while
do
while
は条件式の評価を後で行う.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int i;
i = 1;
do {
printf("%d\n", i);
i++;
} while (i < argc);
return EXIT_SUCCESS;
}
$ ./a.out
1
$ ./a.out hello
1
$ ./a.out hello world
1
2
for
for
は, 3つの式をもち, それぞれ状態の初期化, 条件検証, 更新に用いられる.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int i;
for (i = 1; i < argc; i++)
printf("%s\n", argv[i]);
return EXIT_SUCCESS;
}
ここで, i = 1
の直後にi < argc
が評価されていることに注意が必要である. 例えば,
argc
が1
の場合, printf
は実行されない.
for
の3つの式において, あまり異なる変数を用いるべきではない. 見づらくなるだけである.
echo
の簡易実装を行う
echo
は引数を出力するだけの簡単なプログラムである. 実際に実行して挙動を確認してみて欲しい.
今回の実装の仕様は以下の通り.
- 第1引数以降を出力する. その間に1文字空白を挿入する. (末尾に空白は ない.)
- 最後に改行を出力する. (出力する引数がない場合も必ず出力する.)
以下に例を挙げる.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int i;
if (argc > 1) {
printf("%s", argv[1]);
for (i = 1; i < argc; i++)
printf(" %s");
}
printf("\n");
return EXIT_SUCCESS;
}
じゃんけんプログラムを作る
じゃんけんに必要な次の関数を導入する.
rand
srand
strcmp
これらを使ってテキトーに作ってみて欲しい.
乱数
乱数 は, 十分に予測不能な数である. C言語が提供する乱数は 疑似乱数 であり, コンピュータにさいころを振らせるのではなく, シード と呼ばれる値から計算によって求める. 以下に例を挙げる.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
srand(time(NULL));
printf("%d\n", rand());
return EXIT_SUCCESS;
}
まずは, srand
関数で シード を与える. これがないと乱数生成器は毎回同じパターンで数値を出力してしまう.
丁度, HSPのrandomize
がすることと同じである. time
関数は, time.h
によって定義され,
現在の時刻を エポック (1970-01-01 00:00:00 +0000 (UTC)) からの経過秒数で返す.
これをシードとして用いた.
rand
関数は, 0
からRAND_MAX
までで変化する乱数を返す. 試してみると,
毎度異なる値が出ることがわかるはずだ.
C言語の乱数は注意を要する. 今回のようなシードに時間を用いるような実装は, 時間を操作することにより乱数を操作される危険がある. 某ゲームのようにユーザーの操作から乱数を得るのもあまり良い考えではない. あれはわざとだろうが, 俗に言う 乱数調整 が可能になってしまう.
また, C言語の乱数は品質について規定されておらず, 割とタコな実装なことがあるので信頼してはいけない.
端的に言うと, この乱数はじゃんけん程度にしか使えない.
じゃんけんでは, 3通りの手の出し方がある. これをrand
を使って実現するには以下の方法が簡単である.
rand() % 3
これは, 0
, 1
, 2
の3通りの値を持つ. なお, この3通りは厳密には同様に確からしく_ない_.
じゃんけん程度にしか使えない.
文字列比較
Javaの場合だと比較演算子==
で比較できるが, C言語の場合はそれができない. 代わりにstrcmp
関数を用いる.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
if (argc == 3)
printf("%d\n", strcmp(argv[0], argv[1]));
return EXIT_SUCCESS;
}
$ ./a.out hello world
-58
$ ./a.out hello hello
0
strcmp
はstring.h
によって定義され, 文字列が一致する場合に0
, そうでない場合に0
以外の値を返す.