この章では、仮想記憶サービスを提供していないシステムに提供されるファイル入出力操作を紹介します。仮想記憶機能によって提供される向上した入出力方式についても言及します。また、ファイルとレコードをロッキングする旧式な重量方式も説明します。
一連のデータとして構成されたファイルを「通常」ファイルと言います。このようなファイルには、ASCII テキスト、他の符号化バイナリデータによるテキスト、実行可能コード、またはテキスト、データ、コードの組み合わせが入っています。ファイルは、次の 2 つのコンポーネントに分かれています。
「i ノード」と呼ばれる制御データ。これらのデータには、ファイルタイプ、アクセス権、所有者、ファイルサイズ、データブロックの位置が含まれる。
ファイルの内容。区切れのないバイトのシーケンス
Solaris は、次の 3 つの基本的なファイル入出力インタフェースを用意しています。
第 1 の形式は、伝統的な様式のファイル入出力。詳細は、「基本ファイル入出力」を参照してください。
第 2 の形式は、「標準のファイル入出力」。標準の入出力バッファリングによって、インタフェースが容易になり、仮想メモリのないシステム上で実行されるアプリケーションの効率を改善できます。Solaris 2.x システムなど、仮想メモリ環境で動作するアプリケーションの場合、標準のファイル入出力はきわめて効率の悪い形式です。
第 3 のファイル入出力形式は、「メモリ管理インタフェース」で説明するメモリマッピングインタフェースによって提供されます。マッピングファイルは、Solaris 2.x 環境で実行されるほとんどのアプリケーションに最も効率的で高性能のファイル入出力形式です。
表 6-1 に示している関数は、ファイルで基本操作を実行します。
表 6-1 基本的なファイル入出力関数
関数名 |
目的 |
---|---|
open(2) |
読み取りまたは書き込み用にファイルを開く。 |
close(2) |
ファイル記述子を閉じる。 |
read(2) |
ファイルから読み取る。 |
write(2) |
ファイルに書き込む。 |
creat(2) |
新しいファイルを作成するか、既存のファイルに上書きする。 |
unlink(2) |
ディレクトリエントリを削除する。 |
lseek(2) |
読み取り / 書き込み用のファイルポインタを移動する。 |
次のコード例は、基本的なファイル入出力インタフェースの使用方法を示します。read(2) と write(2) はどちらも、現在のファイルのオフセットから指定された数を超えないバイト数を転送します。実際に転送されたバイト数が戻されます。ファイルの終わりでは、read() の戻り値が 0 になります。
#include <fcntl.h> #define MAXSIZE 256 main() { int fd, n; char array[MAXSIZR] fd = open ("/etc/motd", O_RDONLY); if (fd == -1) { perror ("open"); exit (1); } while ((n = read (fd, array, MAXSIZE)) > 0) if (write (1, array, n) != n) perror ("write"); if (n == -1) perror ("read); close (fd); }
読み取りまたは書き込みを完了したら、必ずファイルを閉じてください。
開いているファイル内のオフセットは、read()、write()、または lseek(2) を呼び出すことによって変更されます。次に lseek() の使用例を示します。
off_t start, n; struct record rec; /* start に現在のオフセットを記録する */ start = lseek (fd, 0L, SEEK_CUR); /* start に戻る */ n = lseek (fd, start, SEEK_SET); read (fd, (char *)&rec, sizeof (rec)); /* 前の記録を書き直す */ n = lseek (fd, -sizeof (rec), SEEK_CUR); write (fd, (char *)&rec, sizeof (rec));
高度なファイル入出力関数は、ディレクトリとファイルの作成と削除、既存のファイルへのリンクの作成、ファイル状態情報の取得または変更を行います。
表 6-2 高度なファイル入出力関数
関数名 |
目的 |
---|---|
link(2) |
ファイルにリンクする。 |
access(2) |
ファイルのアクセス可能性を判定する。 |
mknod(2) |
特殊ファイルまたは通常のファイルを作成する。 |
chmod(2) |
ファイルのモードを変更する。 |
chown(2) lchown fchown |
ファイルの所有者とグループを変更する。 ファイルのアクセス時刻や変更時刻を設定する。 |
stat(2) lstat fstat |
ファイルのステータスを取得する。 ファイル制御機能を実行する。 |
ioctl(2) |
デバイスを制御する。 |
fpathconf(2) pathconf |
設定可能なパス名変数を取得する。 |
opendir readdir closedir |
ディレクトリを操作する。 |
rename(2) |
ファイル名を変更する。 |
rmdir(2) |
ディレクトリを削除する。 |
symlink(2) |
ファイルへのシンボリックリンクを作成する。 |
ファイルシステム制御関数を使用して、ファイルシステムを制御できます。
表 6-3 ファイルシステム制御関数
関数名 |
目的 |
---|---|
ustat(2) |
ファイルシステムの統計情報を取得する。 |
sync(2) |
スーパーブロッキングを更新する。 |
mount(2) |
ファイルシステムをマウントする。 |
unmount |
ファイルシステムのマウントを解除する。 |
statvfs(2) fstatvfs |
ファイルシステム情報を取得する。 |
従来のファイル入出力を使用して、ファイル要素をロッキングする必要はありません。『マルチスレッドのプログラミング』で説明している軽量の同期メカニズムは、マッピングされたファイルと一緒に効果的に使用できるため、この節で説明する旧式のファイルロッキングよりも効率的です。
ファイルまたはその一部をロッキングすると、2 人以上のユーザがファイルの情報を同時に更新しようとした場合に生じるエラーを防止できます。
ファイルロッキングとレコードロッキングは、実際的には同じものです。ただし、ファイルロッキングではファイル全体へのアクセスをブロッキングするのに対し、レコードロッキングではファイルの指定されたセグメントへのアクセスだけをブロッキングします。(Solaris 2.x システムでは、すべてのファイルはデータのバイトシーケンスであり、レコードはファイルを使用するプログラムの概念です。)
次のタイプのファイルシステム上では、アドバイザリロッキングと強制ロッキングがサポートされます。
ufs - ディスクベースのデフォルトのファイルシステム
fifofs - プロセスが共通の方法でデータにアクセスできるようにする名前付きパイプファイルからなる疑似ファイルシステム
namefs - ファイル記述子をファイル上に動的にマウントするために、主に STREAMS によって使用される疑似ファイルシステム
specfs - 特殊なキャラクタ型デバイスやブロッキング型デバイスにアクセスするための疑似ファイルシステム
NFS 上では、アドバイザリファイルロッキングのみがサポートされます。
proc ファイルシステムと fd ファイルシステム上では、ファイルロッキングはサポートされません。
強制ロッキングでは、要求されたファイルセグメントが解放されるまで、プロセスが待機させられます。アドバイザリロッキングでは、ロッキングが取得されたかどうかを示す結果だけが返されます。プロセスは、その結果を無視して入出力を続けることができます。同一のファイルに強制ロッキングとアドバイザリロッキングを同時には適用できません。開いたときのファイルのモードによって、そのファイル上の既存のロッキングが強制ロッキングとして処理されるか、アドバイザリロッキングとして処理されるかが決まります。
2 つの基本的なロッキング呼び出しのうち、fcntl(2) の方が lockf(3C) よりも移植性が高く高性能ですが、より複雑です。fcntl() は POSIX 1003.1 で規格されています。lockf() は、他のアプリケーションとの互換性を保つために用意されています。
この節の残りの部分を読むために役立つ定義の一部を示します。
ロッキングは、有効な記述子を持つファイルに対してだけ要求できます。読み取りロッキングの場合は、少なくとも読み取りアクセスを設定してファイルを開かなければなりません。書き込みロッキングの場合は、書き込みアクセスも設定してファイルを開かなければなりません。次の例では、ファイルは読み取りと書き込みアクセス用に開かれます。
... filename = argv[1]; fd = open (filename, O_RDWR); if(fd < 0) { perror(filename); exit(2); } ...
ファイル全体をロッキングするには、オフセットを 0 に設定し、通常はサイズを 0 に設定します。
ファイルをロッキングするには、いくつかの方法があります。どの方法を選択するかは、ロッキングとプログラムの他の部分との関係、または性能や移植性によって決まります。次の例では、POSIX 標準互換の fcntl(2) 関数を使用します。このプログラムは、次のいずれかの状況が発生するまでファイルをロッキングしようとします。
ファイルが正常にロッキングされた
エラーが発生した
MAX_TRY 回数を超えたため、プログラムがファイルのロッキングをやめた
#include <fcntl.h> ... struct flock lck; ... lck.l_type = F_WRLCK; /* 書き込みロッキングを設定する */ lck.l_whence = 0; /* ファイルの先頭からのオフセットは l_start */ lck.l_start = (off_t)0; lck.l_len = (off_t)0; /* ファイルの最後まで */ if (fcntl(fd, F_SETLK, &lck) <0) { if (errno == EAGAIN || errno == EACCES) { (void) fprintf(stderr, "File busy try again later!¥n"); return; } perror("fcntl"); exit (2); } ...
fcntl(2) を使用して、いくつかの構造体変数を設定し、ロッキング要求の型と開始を設定できます。
マッピングされたファイルを flock() でロッキングできません。詳細は、mmap(2) を参照してください。しかし、(POSIX スタイルまたは Solaris スタイルでの) マルチスレッド指向同期メカニズムをマッピングされたファイルで使用できます。mutex(3T)、condition(3T)、semaphore(3T)、および rwlock(3T) のマニュアルページを参照してください。
レコードのロッキングは、ロッキングセグメントの開始位置と長さが 0 に設定されないこと以外は、ファイルのロッキングと同様に行います。
必要なすべてのロッキングを設定できない場合に備えて、対処方法を用意しておいてください。レコードロッキングを使用するのは、レコードへのアクセスが競合するためです。実行される動作は、プログラムによって次のように異なります。
一定時間待ってから再試行する
手順を中止してユーザに警告する
ロッキングが解除されたことを示すシグナルを受信するまでプロセスを休眠させておく
上記のいくつかを組み合わせて実行する
次の例は、fcntl() を使用してレコードをロッキングする方法を示します。
{ struct flock lck; ... lck.l_type = F_WRLCK; /* 書き込みロッキングを設定する */ lck.l_whence = 0; /* ファイルの先頭からのオフセットは l_start */ lck.l_start = here; lck.l_len = sizeof(struct record); /* this に書き込みロッキングを設定する */ lck.l_start = this; if (fcntl(fd, F_SETLKW, &lck) < 0) { /* this の書き込みロッキングが失敗 */ return (-1); ... }
次の例は、lockf 関数を示します。
#include <unistd.h> ... /* this をロッキングする */ (void) lseek(fd, this, SEEK_SET); if (lockf(fd, F_LOCK, sizeof(struct record)) < 0) { /* this のロッキングが失敗。here のロッキングを解除する */ (void) lseek(fd, here, 0); (void) lockf(fd, F_ULOCK, sizeof(struct record)); return (-1);
各ロッキングはタイプが異なるだけで、設定されたときと同じ方法で解除されます (F_ULOCK)。ロッキングの解除は別のプロセスによってブロッキングされず、そのプロセスが設定したロッキングに対してだけ有効です。ロッキング解除は、前のロッキング呼び出しで指定されたファイルのセグメントに対してだけ有効です。
ロッキングを設定できないようにブロッキングしているプロセスがあれば、それがどのプロセスかを判定できます。これは簡単なテストとして、またはファイル上のロッキングを見つけるために使用できます。ロッキングは上記の例のように設定され、fcntl() 呼び出しで F_GETLK が使用されます。次の例では、ファイル内でロッキングされているすべてのセグメントについてのデータを検索して出力します。
struct flock lck; lck.l_whence = 0; lck.l_start = 0L; lck.l_len = 0L; do { lck.l_type = F_WRLCK; (void) fcntl(fd, F_GETLK, &lck); if (lck.l_type != F_UNLCK) { (void) printf("%d %d %c %8d %8d¥n", lck.l_sysid, lck.l_pid, (lck.l_type == F_WRLCK) ? 'W' : 'R', lck.l_start, lck.l_len); /* このロッキングがアドレス空間の終わりまで続いている場合は、 * それ以上探す必要がないのでループは終了する */ if (lck.l_len == 0) { /* それ以外の場合は、見つかったロッキングの後方のロッキングを探す */ lck.l_start += lck.l_len; } } } while (lck.l_type != F_UNLCK);
F_GETLK() コマンドを伴う fcntl() 関数は、サーバの応答を待つ間に休眠することがあり、クライアント側またはサーバ側の資源が不足すると失敗する場合もあります (ENOLCK を戻します)。
F_TEST コマンドを伴う lockf() 関数は、プロセスがロッキングを保持しているかどうかの検査にも使用できます。ただしこの関数は、ロッキングの位置とそのロッキングを所有しているプロセスについての情報は戻しません。
(void) lseek(fd, 0, 0L); /* ファイルアドレス空間の終わりまで検索するため、 テスト領域の大きさを 0 に設定する */ if (lockf(fd, (off_t)0, SEEK_SET) < 0) { switch (errno) { case EACCES: case EAGAIN: (void) printf("file is locked by another process¥n"); break; case EBADF: /* lockf に渡された引数が不正 */ perror("lockf"); break; default: (void) printf("lockf: unexpected error <%d>¥n", errno); break; }
プロセスが fork を行うと、子プロセスは親プロセスが開いたファイル記述子のコピーを受け取ります。ただし、ロッキングは特定のプロセスによって所有されるので、子プロセスに継承されません。親と子は、ファイルごとに共通のファイルポインタを共有します。両方のプロセスが、同じファイル内の同じ位置にロッキングを設定しようとすることがあります。この問題は、lockf(3C) と fcntl(2) でも発生します。レコードをロッキングするプログラムが fork を行う場合、子プロセスはファイルを閉じてから開き直して、新しく別のファイルポインタを設定する必要があります。
UNIX のロッキング機能を使用すると、デッドロッキングの検出と防止ができます。デッドロッキングが発生する可能性があるのは、システムがレコードロッキング機能を休眠させようとするときだけです。プロセス A がプロセス B に保持されたロッキングを待つと同時に、プロセス B がプロセス A に保持されたロッキングを待つような状況が発生していないかが検査されます。潜在的なデッドロッキングが検出されると、ロッキング関数は失敗し、デッドロッキングを示す値が errno に設定されます。F_SETLK を使用してロッキングを設定するプロセスは、ロッキングが即座に取得できなくても取得できるまで待たないので、デッドロッキングは発生しません。
強制ロッキングの場合、対象のファイルは set-group 識別 (setgid) グループ ID 設定ビットがオンになっており、グループの実行権がオフになっている通常ファイルでなければなりません。どちらかの条件が満たされなければ、すべてのレコードロッキングはアドバイザリロッキングになります。強制ロッキングを使用するには、次の例のようにします。
#include <sys/types.h> #include <sys/stat.h> int mode; struct stat buf; ... if (stat(filename, &buf) < 0) { perror("program"); exit (2); } /* 現在設定されているモードを取得する */ mode = buf.st_mode; /* グループの実効権をモードから削除する */ mode &= ‾(S_IEXEC>>3); /* set-group 識別 (setgid) ビットをモードに設定する */ mode |= S_ISGID; if (chmod(filename, mode) < 0) { perror("program"); exit(2); } ...
レコードロッキングが適用されるファイルの場合は、実行権を設定しないでください。これは、オペレーティングシステムはファイルの実行時にレコードロッキングを無視するからです。
ファイルに強制ロッキングを設定するには、chmod(1) コマンドも使用できます。このコマンドを使用すると次のようになります。
$ chmod +l file
上記の l は、数字の "1" ではなく英字の "l" (エル) なので注意してください。このコマンドは、ファイルモード内の 2 個のアクセス権ビットを設定します。アクセス権ビットは、ファイル上の強制ロッキングを示します。モード内の 2 つのビットは、.1./.../..0/... となります。各ファイルの強制ロッキングを有効にすると同時に、実行時 set-group 識別 (setgid) ビットをオンにできません。また、各ファイルの強制ロッキングとグループの実行権を有効にすることもできません。
ls(1) コマンドで -l オプションを使用してロングリスト形式を指定すると、この設定が表示されます。
$ ls -l file
この場合、次のような情報が表示されます。
-rw---l--- 1 user group size mod_time file
アクセス権の英字 "l" は、set-group 識別 (setgid) ビットがオンになっていることを示します。したがって、通常の set-group 識別 (setgid) ビットがオンになるだけでなく強制ロッキングが有効になっています。
強制ロッキングは、ローカルファイルだけで利用できます。NFS を通じてファイルにアクセスするときは利用できません。
強制ロッキングは、ファイル内のロッキングされているセグメントだけを保護します。ファイルの残りの部分には、通常のファイルアクセス権に従ってアクセスできます。
原子操作的トランザクションのために多重の読み取りや書き込みが必要な場合は、入出力が始まる前に、対象となるすべてのセグメントについてプロセスが明示的にロッキングする必要があります。このように動作するプログラムの場合は、いずれもアドバイザリロッキングで十分です。
レコードロッキングが使用されるファイルについては、どんなプログラムでも無制限のアクセス権を与えないでください。
端末入出力関数は、表 6-4 に示すように、非同期通信ポートを制御する一般的な端末インタフェースを処理します。
表 6-4 端末入出力関数
関数名 |
目的 |
---|---|
tcgetattr tcsetattr |
端末属性を取得または設定する。 |
tcsendbreak |
回線制御関数を実行する。 |
tcdrain |
ボーレートを取得または設定する。 |
tcflush tcflow |
端末のフォアグラウンドプロセスのグループ ID を取得または設定する。 |
tcgetsid |
端末のセッション ID を取得する。 |