ファイルシステム, 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は入出力関連の定義を持つ (ioinput/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が評価されていることに注意が必要である. 例えば, argc1の場合, printfは実行されない.

forの3つの式において, あまり異なる変数を用いるべきではない. 見づらくなるだけである.

echoの簡易実装を行う

echoは引数を出力するだけの簡単なプログラムである. 実際に実行して挙動を確認してみて欲しい. 今回の実装の仕様は以下の通り.

以下に例を挙げる.

#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;
}

じゃんけんプログラムを作る

じゃんけんに必要な次の関数を導入する.

これらを使ってテキトーに作ってみて欲しい.

乱数

乱数 は, 十分に予測不能な数である. 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

strcmpstring.hによって定義され, 文字列が一致する場合に0, そうでない場合に0以外の値を返す.

[top]