Linuxのシステムコールとライブラリ関数・APIに関する世界観1B(stdio)です。
ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考にさせていただきました。
X11やGTK+/QtなどについてはLinux(X11周辺)を参照のこと。またLinuxカーネルの世界観も参照のこと。
システムコールであるopen(), close(), read(), write()を使うのに比べて、stdioの関数を使うことで、より高速にファイルの操作ができます。
なぜ高速なのか、それはstdioがバッファリングを行うからです。
バッファリングは一種のキャッシュのようなもので、システムコールが呼ばれた時点で常にストレージのデータにアクセスするのではなく、頻繁に使われるデータをバッファの中に保持し、バッファの中から読み書きすることで、データを読み書きするのが高速になります。
ストレージのアクセスは遅いため、常にストレージにアクセスするシステムコールは遅くなります。これに対して、常にストレージにアクセスせず、常にバッファにアクセスするstdioのライブラリ関数は高速です。
なので、本当に必要がない時以外、常にstdioを使うべきです。
また、システムコールを使う場合、固定長のアクセスしかできません。stdioでは固定長だけではなく、バイト単位や行単位のアクセスができます。なので、一文字ずつ文字を操作したり、一行ごとに文字列を操作したりできます。また、stdioではフォーマット入出力という機能が実装されており、変数の値を表示する際にも効果を発揮します。
2023.08.19
stdioでは、バッファリングを行うため通常はシステムコールよりも動作速度が速い。また、固定長バイトだけではなく、文字単位(バイト単位)や行単位での入出力ができる。
これらのCライブラリ関数では、FILE型のポインタを使ってファイルの情報を格納します。
FILE型はファイルディスクリプタのラッパーで、stdioバッファの情報も含まれています。
API | 説明 |
---|---|
fopen(3) fclose(3) fread(3) fwrite(3) | ライブラリ関数の版。 バッファリングを行うため、通常はシステムコールより動作が速い。 |
printf(3) | printf()はフォーマットされた文字列を出力出来ます。 任意の文字列や変数の中身を展開して表示する場合など、良く使います。 |
scanf(3) | scanf()はフォーマットを指定して入力できる。 バッファオーバーフローを起こす潜在的な危険がある(文字幅指定で回避できる)。 |
getchar(3) | 標準入力(端末、パイプもしくはリダイレクトされたファイル)から一文字の文字を入力できる。 |
putchar(3) | 標準出力(端末、パイプもしくはリダイレクトされたファイル)へと一文字の文字を出力できる。 |
以下はさらに詳しい解説。
API | 説明 |
---|---|
fopen(3) | open(2)に対応するライブラリ関数。 |
fclose(3) | close(2)に対応するライブラリ関数。 |
fread(3) | 固定長バイト列の入力。 |
fwrite(3) | 固定長バイト列の出力。 |
以下はバイト単位(一文字)での入出力。
API | 説明 |
---|---|
fgetc(3) | バイト単位(一文字)で入力するAPI。 |
fputc(3) | バイト単位(一文字)で出力するAPI。 |
getc(3) | マクロ。基本的にfgetc(3)と同じ。 |
putc(3) | マクロ。基本的にfputc(3)と同じ。 |
getchar(3) | 入力元をstdinに固定してバイト単位(一文字)で入力する。 |
putchar(3) | 出力先をstdoutに固定してバイト単位(一文字)で出力する。 |
以下は行単位での入出力。
API | 説明 |
---|---|
fgets(3) | 行単位での入力API。一行の文字列を入力できて便利。 |
fputs(3) puts(3) | 行単位での出力API。一行の文字列を出力できて便利。 putsは出力先がstdoutに固定され、最後に改行を付け足す。 |
バッファサイズ指定の存在しない、セキュリティの問題があるgets()は廃止されました。
後日注記:これらの関数の引数は、関数によって異なります。ですが、大まかに言えば、出力系の関数であれば出力する文字、あるいは文字列リテラル、あるいはそれが格納されている変数(文字あるいは文字列へのポインタ)を指定します。入力系の関数であれば、入力した文字列を格納するバッファ変数へのポインタと、バッファサイズを指定します。また、入力先・出力先ストリームの指定ができる関数であれば、FILE型のポインタを用いてストリームを指定します。このストリームにオープンしたファイルあるいは標準入力・標準出力・標準エラー出力を指定できます。
以下はフォーマット入出力。
API | 説明 |
---|---|
printf(3) fprintf(3) | フォーマット出力。 |
scanf(3) | フォーマット入力。 |
後日注記:printf()はコンソールへの出力、fprintf()はファイルへの出力によく使います。
後日注記:printf()は、変数の値を文字列の中に埋め込むことや、端末に表示される変数の値の書式フォーマットを指定することができるため、変数の値の表示によく使われます。また、fprintf()は出力先ストリームを指定することができるため、ファイルや標準エラー出力への出力などに使用されます。特に、標準エラー出力を行う際には、ストリーム先にstderrを指定します。
また、コマンドラインオプションの解析には専用のAPIが用意されている。
API | 説明 |
---|---|
getopt(3) | ショートオプション(-オプション)を解析する。 |
getopt_long(3) | ロングオプション(--オプション)を解析する。 |
コマンドラインオプションには慣習があり、以上のAPIを使うことで、ショートオプションとロングオプションを解析できる。
(ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考に執筆しました。)
gets()などの使うべきでない関数については以下が参考になります。バッファサイズ指定がなく、バッファのポインタだけを指定するため、バッファのサイズを超過して文字列が入力されることがあります。
C言語のファイル処理は、ファイルをFILE型のポインタ(ファイルポインタ)とAPI関数を使って操作します。
以下はFILE型のポインタを使った、ファイル操作のコード例:
ファイルをオープンするためのモードは、"r"は読み込み、"w"は書き込み、"a"は追記を表す。
ちなみに、ファイルオフセットを移動させたいだけなら、lseek(2)やfseek(3), fseeko(3)で出来る。
たとえば、ファイルの内容をすべてコンソールに出力するプログラムは以下のようになります。
FILE *fp; int c; if ((fp = fopen("hoge.txt", "r")) == NULL) { fprintf(stderr, "ファイルのオープンができません\n"); exit(1); } while ((c = getc(fp)) != EOF) { putchar(c); } fclose(fp);
以下のページを参考にしました。
詳しくは以下の書籍が参考になります。
libcでは、
API | 説明 |
---|---|
regcomp(3) | 正規表現のコンパイル |
regfree(3) | 解放(open()とclose()と同じ) |
regexec(3) | パターンのマッチング。マッチすると0が返る。 |
regerror(3) | regcomp()が失敗した時に返るエラーコードをエラーメッセージに変換する。 |
などの正規表現インターフェースがある。
(ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考に執筆しました。)
C++11では正規表現をサポートしているが、それ以前のC++などではlibcのC言語の正規表現も使用できる。
正規表現も参照のこと。
ふつうのLinuxプログラミング 第2版 サポートサイトより、サンプルコードがGitHubでMITライセンスに基づいて公開されているので、ここに掲載させていただきます。著作権者は青木峰郎氏です。
コード全文は以下のリンク先をご覧ください。
このコードで重要なのは以下の部分です。
static void do_grep(regex_t *pat, FILE *src) { char buf[4096]; while (fgets(buf, sizeof buf, src)) { if (regexec(pat, buf, 0, NULL, 0) == 0) { fputs(buf, stdout); } } }
ここだけを見ると、bufという変数がバッファであることが分かります。バッファは4096とサイズが決め打ちですが、これはfgets()とfputs()を繰り返すために決め打ちでも構わないのです。
このコードでは、grepコマンドの実装であるため、fgets()やfputs()を使って一行ずつ文字列を読み出してbufに格納し、書き出しを行っていますが、catコマンドの実装などにおいては、システムコールのread()/write()やstdioのfread()/fwrite()など、固定長バイトでの読み書きを行うこともあります。この際にも、こうした一時バッファの固定文字列に一時的にデータを格納する、という手法は良く使われます。
このようにすることで、巨大なメモリ領域を確保しなくても全文字列を書き出せるため、使用メモリの削減にもなります。仕組みは違いますが同じ目的に使えるものとして、Pythonのyield returnがあります。Pythonではyield returnを使うことで一行ごとにデータを読み、一行分のメモリだけでデータを書き出すことができます。
またregcomp()とregexec()はC言語での正規表現のために使います。
2024.08.28編集
上記サンプルコードは以下のライセンス条項に基づきます。
The MIT License Copyright (c) 2017 Minero Aoki Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2024.08.28編集
FreeBSD man stdio(3)は参考になります。
また、Linuxのstdioに関するmanpagesの一覧は以下にあります。
C言語(入出力)も参照のこと。
Linuxカーネル(ソケット)も参照のこと。