/ «2008-03-11 (Tue) ^ 2008-03-23 (Sun)» ?
   西田 亙の本:GNU 開発ツール -- hello.c から a.out が誕生するまで --

Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール


2008-03-16 (Sun)

[Thoughts] プログラマの教養は manual pages に宿る (その7)

前回紹介した stdio.h は printf などの標準入出力を担当するヘッダーファイルですが、Cライブラリ中には open, read, write, lseek, close などの低水準入出力も存在しており、これらの解説は manual pages 中の第二セクションに用意されています。

man 2 open: Linux 篇

最初に、Debian GNU/Linux 上で man 2 open を実行した時の先頭部分を見てみます。

 OPEN(2)                    Linux Programmer’s Manual                    OPEN(2)
 
 NAME
        open, creat - open and possibly create a file or device
 
 SYNOPSIS
        #include <sys/types.h>
        #include <sys/stat.h>
        #include <fcntl.h>
 
        int open(const char *pathname, int flags);
        int open(const char *pathname, int flags, mode_t mode);

必要なヘッダーファイルとして、3種類が指定されており、プロトタイプ宣言では2種類の open 関数が記載されています。3つのパラメータを要求する形式は、皆さんご存じの通り、O_CREAT モードを用い新規ファイルを作成する場合に使われるもので、3番目のパラメータにはファイルパーミッションを指定します。

3種類のヘッダーファイルについて説明しますと、sys/types.h ヘッダーファイルは mode_t タイプの宣言を格納、sys/stat.h ヘッダーファイルは mode パラメータで使われる S_IRUSR, S_IWUSR, S_IXUSR などパーミッションビットのマクロ定義を格納、最後の fcntl.h は open 関数のプロトタイプ宣言と O_RDONLY, O_WRONLY, O_RDWR, O_CREAT などのファイルアクセスモードフラグのマクロ定義を格納しています。

以上より、3種類すべてのヘッダーファイルを必要とするのは、ファイルを新規に作成する open(const char *pathname, int flags, mode_t mode) 形式の時だけであり、mode パラメータを持たない open(const char *pathname, int flags) 形式の場合は、fcntl.h ヘッダーファイル(File CoNTroL options)ただひとつで良いことが分かります。

初期の fcntl.h

ちなみに、20年以上前に作成された BSD システム上の fcntl.h は、現在のものよりも遙かに簡素でした(全行掲載)。

 /*
  * Copyright (c) 1983 Regents of the University of California.
  * All rights reserved.  The Berkeley software License Agreement
  * specifies the terms and conditions for redistribution.
  *
  *      @(#)fcntl.h     5.2 (Berkeley) 1/8/86
  */
 
 /*
  * Flag values accessible to open(2) and fcntl(2)-- copied from
  * <sys/file.h>.  (The first three can only be set by open.)
  */
 #define O_RDONLY        000             /* open for reading */
 #define O_WRONLY        001             /* open for writing */
 #define O_RDWR          002             /* open for read & write */
 #define O_NDELAY        FNDELAY         /* non-blocking open */
                                         /* really non-blocking I/O for fcntl */
 #define O_APPEND        FAPPEND         /* append on each write */
 #define O_CREAT         FCREAT          /* open with file create */
 #define O_TRUNC         FTRUNC          /* open with truncation */
 #define O_EXCL          FEXCL           /* error on create if file exists */
 
 #ifndef F_DUPFD
 /* fcntl(2) requests */
 #define F_DUPFD 0       /* Duplicate fildes */
 #define F_GETFD 1       /* Get fildes flags */
 #define F_SETFD 2       /* Set fildes flags */
 #define F_GETFL 3       /* Get file flags */
 #define F_SETFL 4       /* Set file flags */
 #define F_GETOWN 5      /* Get owner */
 #define F_SETOWN 6      /* Set owner */
 
 /* flags for F_GETFL, F_SETFL-- copied from <sys/file.h> */
 #define FNDELAY         00004           /* non-blocking reads */
 #define FAPPEND         00010           /* append on each write */
 #define FASYNC          00100           /* signal pgrp when data ready */
 #define FCREAT          01000           /* create if nonexistant */
 #define FTRUNC          02000           /* truncate to zero length */
 #define FEXCL           04000           /* error if already created */
 #endif

open (2) に関する部分は O_ で始まるマクロ定義8行のみです。当時は関数プロトタイプ宣言という概念がなかったため、現在の unistd.h (read, write, close などのプロトタイプを格納)に相当するヘッダーファイルは存在しませんでした。open 関数のプロトタイプが unistd.h でなく fcntl.h で定義されるようになった背景には、このような歴史があるようです。

man 2 open: NetBSD 篇

次に、NetBSD の man 2 open を見てみましょう。

 OPEN(2)                   NetBSD System Calls Manual                   OPEN(2)
 
 NAME
      open -- open or create a file for reading or writing
 
 LIBRARY
      Standard C Library (libc, -lc)
 
 SYNOPSIS
      #include <fcntl.h>
 
      int
      open(const char *path, int flags, mode_t mode);
 
 DESCRIPTION
      The file name specified by path is opened for reading and/or writing as
      specified by the argument flags and the file descriptor returned to the
      calling process.  The flags are specified by or'ing the values listed
      below.  Applications must specify exactly one of the first three values
      (file access methods):
 
            O_RDONLY    Open for reading only.
 
            O_WRONLY    Open for writing only.
 
            O_RDWR      Open for reading and writing.
 
      Any combination of the following may be used:
 
            O_NONBLOCK  Do not block on open or for data to become available.
 
            O_APPEND    Append to the file on each write.
 
            O_CREAT     Create the file if it does not exist, in which case the
                        file is created with mode mode as described in chmod(2)
                        and modified by the process' umask value (see
                        umask(2)).

まず最初に目につくのは、NAME と SYNOPSIS の間に挿入された "LIBRARY" という見出しですが、この意味については後述します。

SYNOPSIS では、必要ヘッダーファイルは fcntl.h ただひとつが記載されています。続く DESCRIPTION では、O_RDONLY, O_WRONLY, O_RDWR, いずれかひとつのファイルアクセスモードが必須であり、O_CREAT が指定された場合は、chmod (2) の解説に準じたパーミッションフラグを第三パラメータに添えること、と説明されています。

fcntl.h が最も重要なヘッダーファイルであることは分かるのですが、新規ファイルを作成する際に必要となる sys/stat.h への言及がない点は、少々はしょり過ぎのような気がします(BSD では、sys/types.h は fcntl.h より自動的にインクルードされる)。

あちらを立てればこちらが立たず・・。バランスを取るというのは、大変に難しい作業です。

システムコール≠ライブラリ関数

個人的に NetBSD の解説で最も惚れ込んだ点は、先ほど紹介した LIBRARY です。この解説は、open がCライブラリ libc 中のライブラリ関数であり、リンク時に -lc オプションを添付すれば利用できることを明示しています(本記述は FreeBSD には存在するが、OpenBSD には存在しない)。一見、何気ない解説に見えるのですが、この言葉は実に深い配慮に基づいています。

機械語の知識がある人間が、"システムコール" という言葉を目にすれば「その実装は恐らくソフトウェア割り込みによるものであろう」と類推します。printf などのライブラリ関数は manual pages 中で第三セクションに集められていることから、「特別に第二セクションに含まれているシステムコールは、ライブラリとは別個にコード展開されるのだろう」と考えるかもしれません。

実際、若かりし頃の私は「Cコンパイラがソースファイル上のシステムコール呼び出しを認識し、その場でソフトウェア割り込みのインライン展開を行うのだろうか?」と考えました、真剣に。ところが、Linux 上の GNU C コンパイラが出力したアセンブリソースを見ると、システムコールが "call" 命令で呼び出されているではありませんか。しかも、パラメータはレジスタではなく、スタックを介して渡されています。「これはどう見ても外部ライブラリ関数の呼び出しだが、マニュアルページ中では、こいつはシステムコールだと記述してある。一体どっちが本当なんじゃぁ〜〜!」

しばらくの間、本当に悩みました。今から考えれば他愛もない話で、"Cソース上でのシステムコール呼び出し" はCライブラリ中に用意されている "同名のラッパー関数(Wrapper functions)呼び出し" と同義なのです。よって、NetBSD のように「open (2) はCライブラリ関数である」と明記するのが、誤解を生まない正しい表現です。

一方で「open "システムコール" を使いファイルを開くプログラムを作成してみましょう」と読者に語りかけるプログラミング解説書は少なくありません。最終的に open システムコールが発行されることに間違いはありませんが、Cプログラムの内部構造から考えれば、本来は「open "ライブラリ関数" を使いファイルを開くプログラムを作成してみましょう」と記述し、システムコールとラッパー関数の違いについても言及すべきでしょう。

NetBSD, FreeBSD の開発者達は、かっての私のような悩めるユーザのことを知っており、そしてまた「言葉の重み」も知っているのです。

それにしても、各ライブラリ関数の manual page 中に LIBRARY 項目を設けるという着想は素晴らしいと思います。ちょっとした配慮ではあるのですが、開発者側に利用者の立場を思いやる教養がなければ、本表記は誕生しなかったことでしょう。

man 2 open: Single UNIX Specification 篇

ここまで Linux および NetBSD の man 2 open を見てきましたが、SYNOPSIS の表記に関してはいずれも改善の余地があることが分かります。

現時点で、最も厳密かつ信頼できる manual pages は Open Group が提供する Single UNIX Specification でしょう。同仕様は POSIX を拡張した唯一の "公式 UNIX 規格" ですが、認定にはかなりの費用がかかることから、更新頻度の高い *BSD や Linux は登録されていません。

Single UNIX Specification に関しては様々な議論があるかと思いますが、仕様書の完成度は素晴らしく、ひとつひとつの言葉や表現は実に深く吟味されています。少し長くなりますが、open (2) の解説を引用します(是非、オンライン HTML 版も参照してください)。

 NAME
     open - open a file
 
 SYNOPSIS
     [OH]  #include <sys/stat.h> 
     #include <fcntl.h>
 
     int open(const char *path, int oflag, ... );
 
 DESCRIPTION
 
     The open() function shall establish the connection between a file and a file 
   descriptor. It shall create an open file description that refers to a file and a file
   descriptor that refers to that open file description. The file descriptor is used by
   other I/O functions to refer to that file. The path argument points to a pathname
   naming the file.
 
     The open() function shall return a file descriptor for the named file that is the
   lowest file descriptor not currently open for that process. The open file
   description is new, and therefore the file descriptor shall not share it with any
   other process in the system. The FD_CLOEXEC file descriptor flag associated
   with the new file descriptor shall be cleared.
 
     The file offset used to mark the current position within the file shall be set to
   the beginning of the file.
 
     The file status flags and file access modes of the open file description shall be
   set according to the value of oflag.
 
     Values for oflag are constructed by a bitwise-inclusive OR of flags from the
   following list, defined in <fcntl.h>. Applications shall specify exactly one of the 
   first three values (file access modes) below in the value of oflag:
 
 O_RDONLY
     Open for reading only.
 O_WRONLY
     Open for writing only.
 O_RDWR
     Open for reading and writing. The result is undefined if this flag is applied to a FIFO.
 
 Any combination of the following may be used:
 
 O_APPEND
     If set, the file offset shall be set to the end of the file prior to each write.
 O_CREAT
     If the file exists, this flag has no effect except as noted under O_EXCL below. 
   Otherwise, the file shall be created; the user ID of the file shall be set to the
   effective user ID of the process; the group ID of the file shall be set to the group
   ID of the file's parent directory or to the effective group ID of the process; and
   the access permission bits (see <sys/stat.h>) of the file mode shall be set to the
   value of the third argument taken as type mode_t modified as follows: a bitwise
   AND is performed on the file-mode bits and the corresponding bits in the
   complement of the process' file mode creation mask. Thus, all bits in the file
   mode whose corresponding bit in the file mode creation mask is set are cleared.
   When bits other than the file permission bits are set, the effect is unspecified.
   The third argument does not affect whether the file is open for reading, writing,
   or for both. Implementations shall provide a way to initialize the file's group ID to
   the group ID of the parent directory. Implementations may, but need not,
   provide an implementation-defined way to initialize the file's group ID to the
   effective group ID of the calling process.

まず、SYNOPSIS の先頭に現れる [OH] という表記は Optional Header の略であり、sys/stat.h ヘッダーファイルはオプション扱いとなることを明示しています。関数プロトタイプ宣言では最後のパラメータに "..." が指定されていますが、こちらも3番目以降にオプション扱いのパラメータが存在することを示しています。

続く DESCRIPTION では、"flags from the following list, defined in <fcntl.h>" とあるように、O_RDONLY などのフラグ群が fcntl.h 中でマクロ定義されていることを明確に説明し、O_CREAT フラグの箇所では、"the access permission bits (see <sys/stat.h>) of the file mode" と、パーミッションビットのマクロ定義は sys/stat.h 中に存在することを明記しています。

The Single UNIX Specification さらに、HTML 版 Single UNIX Specification では、各ヘッダーファイルへのハイパーリンクが用意されています。試しに、fcntl.h を閲覧してみてください。O_CREAT などの解説はもちろん、関数プロトタイプをはじめとして、fcntl.h 中で登場すべきマクロ定義や宣言がすべて解説されています。もちろん、mode_t についても "The mode_t, off_t, and pid_t types shall be defined as described in <sys/types.h>" と言及。これだけの情報は、他では決して得られません。

ありがたいことに、Single UNIX Specification の HTML 版は無料で公開されていますし、69.95ドルで The Single UNIX Specification を購入すれば、HTML 版 tarball (インストール可能) および4000ページを超える PDF 版を収めた CD-ROM が付いてきます。通常、この種の仕様書は1000ドル単位の価格が設定されていることを考えると、破格の振る舞いと言えるでしょう。日頃、manual pages の曖昧さや、ヘッダーファイルの仕様について悩んでおられる方には、本書をお勧めします(500ページの書籍自体は、仕様の変更点を中心に解説)。


UNIX ライセンスプレート

UNIX License Plate ついでと言っては何ですが、Open Group では「UNIX ライセンスプレート」も販売されています。ライトグリーンで彩られた UNIX が眺めていて気持ちの良いこと、フォントも最高!

"LIVE FREE OR DIE" というちょっと過激なメッセージも添えられて、一枚約10ドル(30x15cm)。皆さんも、おひとついかがですか?ミーハーな私は、もちろん「5枚入りのお得パック」をゲット致しました、ハイ。

最終回へ続く