Yabu.log

ITなどの雑記

「ふつうのLinuxプログラミング 第2版」を読んだ

本書はシステムコールとlibcのAPIなどを使ったLinuxで動作するコマンドの作成を通して、 Linuxについて深く知ろうという趣旨の本です。

読んだきっかけ

本書を読んだきっかけは自分のlinux力があまりにも低すぎると感じたため。 具体的に言うと、cmakeに-CMAKE-INSTALL_PREFIXをつけずにビルドして、/usr/localに実行バイナリをおいてしまった。 この件に関して「これじゃぁ他のユーザーに影響でちゃうよ。」と怒られたが、

  • Linuxのフォルダ構成について何も理解していない。
  • -CMAKE-INSTALL_PREFIXをつけるのはかなり基本的なことっぽい。

の2点からググってネット上のコマンドをコピペするしか能のない自分のLinux力を猛省したところ、本書を見つけた。

  • TLの強めのエンジニアがリコメンドしている
  • 最近改版された。
  • 自分が分からなかったフォルダ構成について書かれている
  • 会社に1版に大量の付箋がついているものが存在している(おそらくこれで猛勉強した同僚がいる(いた?))

の4点から、本書を学習してみることにした。

雑感

include<stdio.h>を呪文とすることなく順序立てて初心者にも理解できるように解説している。

  • 1.システムコールやそのAPIのmanコマンド読み方
  • 2.実際にシステムコールなどを使ってみる小さなコードを作る
  • 3.システムコールだけで実現できない動作をバッファなどを実装して自前で作る
  • 4.3で作った相当のものがライブラリとして用意されていることをmanコマンドで説明する

という流れでstdioのprintfの解説を試みている。本や大学の授業などでこの部分の解説を受けたことがなかったので、 ここの流れは見事だなと思った。

manコマンドのセクションについて

manコマンドで実行コマンドまたはライブラリの関数等の使い方を調べることができます。

調べる対象のコマンド名のほかにもセクション番号を引数で渡すことができます。 引数としてなにがあり、どの数字に対応しているかはman manで調べることができます。

$man man
・・・(略)
       1   Executable programs or shell commands
       2   System calls (functions provided by the kernel)
       3   Library calls (functions within program libraries)
       4   Special files (usually found in /dev)
       5   File formats and conventions eg /etc/passwd
       6   Games
       7   Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7)
       8   System administration commands (usually only for root)
       9   Kernel routines [Non standard]
・・・(略)

名前がかぶっているもの、例えばprintfなどはstdioで定義されている関数以外にもユーザーコマンドとして存在しているため、manコマンドで調べると、ライブラリの関数よりも先にユーザーコマンドのセクションがヒットして表示されてしまいます。

$man printf

PRINTF(1)  User Commands  PRINTF(1)

NAME
       printf - format and print data

ライブラリの関数を調べたいときはman 3 prinftと引数にセクション番号を渡すことで表示できます。

$man 3 printf

PRINTF(3)  Linux Programmer's Manual  PRINTF(3)

NAME
       printf, fprintf, dprintf, sprintf, snprintf, vprintf, vfprintf, vdprintf, vsprintf, vsnprintf - formatted output conversion

SYNOPSIS
       #include <stdio.h>

       int printf(const char *format, ...);
・・・(略)

本書ではライブラリ関数やユーザーコマンドを見分けるため、printf(1)やprintf(3)といった書き方に統一しています。 これは分かりやすく、ほかの本でも見かける*1ので、知らない人は覚えておくとよいと思います。

本書を読むまではmanはコマンドの意味を調べるもの、という理解でしたが、実際は更に協力なものでした。 ちょっとTIPS的な要素ですが、man asciiと打つとアスキーコード表が出てきます。

Linux世界を構成する3つの概念とは

Linux世界はこの3つの概念によって成立しています。つまりLinuxプログラミングを理解するためにはこの3つの概念をよく理解しなければならないということです。そしてそれことが本書の目的です。*2

ストリームとは

ファイルディスクリプタで表現され、read()またはwrite()で操作できるもののこと(p82)

  • STDIO,STDOUT
  • ネットワーク
  • ファイル

などを抽象化したインターフェースのことです。

ファイルディスクリプタとは

OSがストリームに対して割り当てる番号(int)のこと。 この割り当てられたファイルディスクリプタに対して、read(2),wreite(2)を使って入出力を制御します。 read(2)やwrite(2)は引数として操作対象としてフアィルディスクリプタを引数に取ります。

       #include <unistd.h>

       ssize_t read(int fd, void *buf, size_t count);
       ssize_t write(int fd, const void *buf, size_t count);

open(2)でストリームを作成し、ディスクリプタを得る。close(2)でストリームを破棄する。

stdioとFILE型

C言語にはファイルディスクリプタをラップし、内部でバッファ管理している標準ライブラリがあります。 それがstdioです。

       #include <stdio.h>

       size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

       size_t fwrite(const void *ptr, size_t size, size_t nmemb,
                     FILE *stream);

unistd.hのread(2),write(2)等はUNIXシステムコールであり、なるべく移植性が高いstdioの関数を使うように本書に記されています。

read()やwrite()は基本的にはUNIXシステムコールであり、ほかのOSでも存在するとは限りません。しかし、fread()やfwrite()はC言語の標準関数ですから、より多くの環境で使えることが期待できるのです。(p121)

stdioではファイルディスクリプタではなく、それをラップしたFile型を使います。

File型からファイルディスクリプタを得るにはfileno(3)を使います。逆にファイルディスクリプタからFile型を作るにはfdopen型を使います。

以下のプログラムではSTDOUTにはファイルディスクリプタ=1がデフォルトで割り当てられていますが、こちらをラップしたFILE型を作ってfwriteで書き込みを行っています。

#include<stdio.h>
#include<unistd.h>
#include<string.h>

int main(){
                 FILE *stdout_file = fdopen(STDOUT_FILENO,"r+");
                 char *hello = "hello world\n";
                 fwrite(hello,strlen(hello),1,stdout_file);
}

ヘッダファイルは必要になったときに書けば良い。無理して関数とヘッダファイルの対応は覚えなくてOK

大学時代はstdlibはとりあえず書いとけ、みたいな雑な教え方をされた覚えがある。 プログラムを写経して覚えるスタイルだと、そんなかんじで最初に書くことになるのがヘッダファイルのインクルードになるが、 これから各プログラムに何が必要で、それを予め書いておくというプログラミングスタイルに固執する必要はなさそうだと思いました。

manを見れば必要なヘッダファイルはかいてありますから、コードを書いていて新しいAPIが必要になった時点でmanを参照すればいいのです。関数とヘッダファイルの対応をう躍起になって覚える必要はありません。記憶力はもっと有意義なところに使いましょう(p93)

Linuxやlibcに足りないものが沢山見えてくる。

printf()が濫用される理由

これはちょっとした雑学ですが。

printf()はフォーマット付きの出力用のAPIなのですから、 HelloWorldプログラムのようにフォーマット機能が必要ないときに、printf()を使う必然性はないはずです。それでも何かとprintf()が使われています。このようにprintf()が乱用されるのは「標準出力に文字列をそのまま出力するAPIがstdioにないことが原因でしょう。fputs()だと標準入出力を明示的に指定しないといけませんし、puts()だけだと改行が内化されるのが邪魔です。(p118より)

コマンドライン引数のフォーマットに統一性がない。

古いunixのコマンドなどは前述のgetopt(3)を使わずに自前で引数をパースしているものも多いようです。 そのようなツールでは一般的なコマンドライン引数の扱いとは違ったルールで動作している可能性があります。 本書では今linuxツールを作るならgetoptを利用することを推奨しています。

例外処理の仕組み

これは本書に書かれていることではありませんが、自分の感触です。

erronoを使った例外処理なんかはAPI自体がグローバル変数を強要してくる作りになっていて、あまり綺麗なAPIになっていないと思いました。*3

ディレクトリツリーの標準規格FHS(The FilesystemHierarchy Standard)

読む動機となった/usr/localなどのフォルダ役割についてのまとめ。 ディレクトリツリーの標準規格から、主要なディレクトリとその用途。主に9章の内容をまとめたもの

/                   # ルートディレクトリ。bin,etc,dev,proc,sysあたりがまともな動作に必須
├── bin           # ブートするときに必要なコマンド
├── boot          # カーネルのプログラム
├── dev           # デバイスファイル
├── etc           # マシンごとの設定ファイル
├── lib           # ライブラリ置き場(ディストリビューションが管理)
├── proc          # プロセスファイル
├── sbin          # (ブート時も必要となる)管理者用コマンド
├── sys           # デバイスやデバイスドライバの情報 
├── tmp           # 一時ファイルやリカバリファイル
├── usr           # NFSで共有する前提のものを置く。(単ノード利用前提は/var配下に)
│   ├── bin      # コマンド(ディストリビューションが管理)
│   ├── include  # システムのヘッダファイル    
│   ├── local    # ユーザーが管理しても良い。(/usr直下はディストリビューション管理)
│   │   └── bin # コマンド(ユーザーが管理)
│   ├── sbin     # 管理者用コマンド
│   ├── share    # ドキュメント。(manやinfo)
│   │   └── man # manページ。roff形式
│   └── src      # linuxカーネルのソースなどのシステムが使うコード置き場
└── var           # 頻繁に書き換えるファイル・複数マシンで共有しないファイル等
    ├── log       # サーバープロセスのログ
    ├── spool     # メールやプリンタの入力
    └── run       # 起動中のサーバープロセスのPIDファイル
    └── tmp       # 一時ファイルやリカバリファイル

感想

本書はC言語文法の解説などはほとんど書かれておらず、全くの初心者向け、というわけではありませんし、 ライブラリの中の実装などを詳しく説明するなどもないため、上級者向けというわけでもありません。 自分のようにOSやC言語の本を適当に読み漁り、中途半端に知識はついているが基礎が出来ていない人にちょうどいいのかなと思います。 本書は割と細かく丁寧に書かれており、printfに辿り着くまでに100ページ以上あるので、それが我慢できない人にはおすすめしません。

あたりにおすすめです。

というわけで全く新しく知ること、はあまり書かれていませんでしたが、過去の経験の蓄積からなんとなく、そうなんだ、こういうふうになっているんだ、と思い込んでいた事象について、はっきりと仕組みの解説などが書かれていて、いい加減な知識を補強する土台のようなものが出来、安心することができました。個人的にはmanコマンドを引く癖がついたのが一番でかい収穫だと思います。

Linuxのユーザーとしてはまだまだ知識が足りない気がする。LPICの勉強でもすればいいのかな。

次に読む本

シェルスクリプト書けないとやっていけないよ、とのことなので

入門UNIXシェルプログラミング―シェルの基礎から学ぶUNIXの世界

入門UNIXシェルプログラミング―シェルの基礎から学ぶUNIXの世界

*1:紹介システムパフォーマンスとか

*2:p5の1.1本書の概要より引用

*3:この仕組みはマルチスレッド化した際に利用できなくなるケースがあるらしい