C言語による実践プログラミング

目次

6.1. C言語プログラミングのためのツールたち
6.1.1. Cコンパイラ: gcc
6.1.1.1. 動作全体に関わるオプション
6.1.1.2. 警告オプション
6.1.1.3. 最適化オプション
6.1.1.4. 処理対象となるディレクトリやファイルを追加するオプション
6.1.1.5. プロセッサ固有のオプション
6.1.2. クロスツールチェーン
6.1.2.1. プレフィックス
6.1.3. makeとmakefile
6.1.3.1. make
6.1.3.2. makefileへのルールの記述
6.1.3.3. makefileでの変数の使用
6.1.3.4. makefileで使用される暗黙のルールと定義済み変数
6.1.3.5. 変数の外部定義とオーバーライド
6.1.3.6. 条件文
6.1.3.7. make動作の実際
6.2. C言語プログラミングの復習
6.2.1. コマンドライン引数の扱いと終了ステータス
6.2.2. 終了処理
6.2.3. エラー処理
6.2.4. 共通ヘッダファイル
6.3. ファイルの取り扱い
6.3.1. テキストファイルを扱う
6.3.2. 設定ファイルに対応する
6.3.3. バイナリファイルを扱う
6.4. デバイスの操作
6.4.1. デバイスファイルを使う
6.4.2. sysfsファイルシステムを使う
6.5. シリアルポートの入出力
6.5.1. シリアルエコーサーバー
6.5.2. 改行コードの違いを吸収する
6.5.3. より効率的な入出力方法
6.6. ネットワークを使う
6.6.1. TCP/IP
6.6.2. TCP/IPでHello!
6.6.3. ネットワークエコーサーバー
6.7. プログラムをデバッグする
6.7.1. gdbによるデバッグ
6.7.2. straceでシステムコールをトレースする
6.7.3. メモリ破壊やメモリリークのデバッグ
6.7.3.1. Electric Fenceを使ったメモリ破壊検出
6.7.3.2. MemWatchを使ったメモリリーク検出

この章では、C言語を使用した実践的なプログラミングを取り上げます。

一口にプログラミングといっても、ちょっとしたファイルの読み書きやデバイ ス操作を実現するだけの簡単なものから、複雑な演算を行ったりネットワーク を介してサービスを提供し続けるような高度なものまで、多岐に渡ります。こ こではその中から、誰もが様々な場面で使うであろう基本的技術 と、Armadilloが持つインターフェースを通じて行う操作の代表的なもの を中心に、分野ごとに分けて紹介していきます。

Linuxや開発環境に依存した独特な部分に留意しつつ、組み込みならではの使 用方法を想定した応用例やノウハウについても多く記載したつもりです。プロ グラミング経験豊富な方であってもおさらいのつもりで読んでみて、一般的 なプログラミング本では解説されていない情報を見つけていただければ幸いで す。

6.1. C言語プログラミングのためのツールたち

C言語で書かれたプログラムは、実行できる状態にするためにコンパイルが必 要です。このためのツールがCコンパイラでありツールチェーンですが、この 他にも一連のビルド作業を手助けしてくれる色々なツールが存在します。C言 語プログラミングのための基礎知識として、これらビルド用ツールの機能や使 い方について説明します。

6.1.1. Cコンパイラ: gcc

「Armadillo入門編」の「開発の基本的な流れ」の「アプリケーションプログラムの作成」で説 明したように、C言語で記述したソースコードのコンパイルにはgcc(GNU C Compiler[24])を使用します。

gccはいくつかの動作形態を持っており、また多くの機能を備えています。こ れらはgccに与えるコマンドラインオプションにより制御されますが、このオ プションはかなりの多種に及びます。ここでは、gccを使いこなすために必須 といえるオプションをピックアップして紹介します。

6.1.1.1. 動作全体に関わるオプション

gccにソースファイル名のみを与えると、コンパイル、アセンブル、リンクの 一連の処理を自動で行って、実行ファイルを出力します。これが基本動作です 。

-o 出力ファイル名オ プションを付けることで、出力ファイルの名前を指定することができます。こ のオプションを付けなかった場合、実行ファイルは a.outという名前になります。

-cオプションを付けると、コンパイルからアセンブ ルまでを行い、リンク処理を行いません。出力ファイルは、アセンブラが出力 したオブジェクトファイルになります。

6.1.1.2. 警告オプション

-Wで始まるものは、警告オプションです。コンパイ ル時の警告表示を制御することができます。

本書のサンプルプログラムでは、バグを生みやすいコードの書き方をしていれ ば警告表示が出るように、コンパイル時のオプションとして -Wallと-Wextraを必ず指定 するようにしています。これらのオプションを付けても警告が出ないような書 き方を目指すことで、C言語の構文が原因であるバグの大半を防ぐことができ ます。

6.1.1.3. 最適化オプション

-Oで始まるものは、最適化オプションです。コンパ イラの最適化レベルを制御することができます。

-O0は、最適化を行いません。最適化オプション未指 定のときも同じ動作です。

-O1では、コードサイズと実行時間を小さくするいく つかの最適化を行います。-Oのように数字をつけな かったときも、この動作になります。

-O2では、サポートするほとんどの種類の最適化を行 います。ただし、コードサイズと実行速度のどちらかを大きく犠牲にするよう なもの(例えば関数の自動インライン化)は、このレベルには含まれません。

-O3では、さらに高速にするための最適化を行います 。コードサイズは大きくなるが実行速度を稼ぐことのできる関数の自動インラ イン化は、このレベルで有効になります。

-Osでは、コードサイズが小さくなるように最適化を 行います。-O2で有効になる最適化のうちコードサイ ズが大きくならないものすべてに加え、さらにコードサイズが小さくなるよう に設計された特別な最適化も行います。

6.1.1.4. 処理対象となるディレクトリやファイルを追加するオプション

-Iディレクトリ名オプ ションを付けると、ヘッダファイルの検索対象に指定ディレクトリが追加され ます。ここで指定したディレクトリは、標準のシステムインクルードディレク トリよりも先に検索されます。

-iquoteディレクトリ名 オプションを付けると、ローカルヘッダファイル(#include "ヘッダファイル名"という形で、ダブル クォート囲みでヘッダ指定したもの)の検索対象に指定ディレクトリが追加さ れます。システムヘッダファイル(#include <ヘッダファイ ル名>と指定したもの)については、この指定ディレクトリ からは検索されません。

-Lや-lは、リンク時のリン カ動作に影響を与えるオプションです。

-Lディレクトリ名オプ ションを付けると、ライブラリファイルの検索対象に指定ディレクトリが追加 されます。

-lライブラリ名オプシ ョンを付けると、libライブラリ名 .soまたはlibライ ブラリ名.aという名前のライブラリファイルを 検索し、リンクします。

6.1.1.5. プロセッサ固有のオプション

PCには一般的にx86と呼ばれる種類のプロセッサが搭載されていますが、 Armadilloに搭載されているプロセッサはARMコアを採用したものです。各々の プロセッサ固有の機能を制御するときは、-mで始ま るオプションを使用します。

ARMプロセッサ固有のオプションには、アーキテクチャや浮動小数点演算ユニ ット、ABIの種類などを指定するものがあります。

-march=アーキテクチャ名 を付けると、指定アーキテクチャ向けのインスト ラクションセットを用いたコンパイルが行われます。

[注記]ARMインストラクションセットの互換性

ARMのインストラクションセットは、後方互換性が維持されています [25]。このため異なるプロセッサを搭載 したマシン間であっても、より古い方のアーキテクチャを指定してコンパイル することで、バイナリレベルでの互換性を保つことができます。しかしながら 、新しいアークテクチャ上で古いアーキテクチャ向けのインストラクションセ ットを使用することは、使用可能な新しいインストラクション(例えば、ARMv3 まではハーフワード単位の入出力インストラクションが使えません)を使用し ないことになりますから、実行効率面から見ればもったいない状態になります 。

6.1.2. クロスツールチェーン

ツールチェーンには、Cコンパイラ(gcc)を始めとして、Cプリプロセッサ(cpp) 、アセンブラ(as)、リンカ(ld)、アーカイバ(ar)、デバッガ(gdb) などが含ま れます。ARM向けにクロス開発する際は、クロスツールチェーンを使用します 。

6.1.2.1. プレフィックス

「Armadillo入門編」の「開発の基本的な流れ」の「アプリケーションプログラムの作成」で説明 したように、作業用PC上でARM向けにクロスコンパイルする際には arm-linux-gnueabihf-gccを使用します。このコマン ド名の前に付いているarm-linux-gnueabihf-の部分を、プレフィックス(前頭詞) といいます。

クロスツールチェーンは、すべてこのプレフィックスが付いたコマンド名にな っています。例えばARMクロスアセンブラであれば arm-linux-gnueabihf-as、ARMクロスリンカであれば arm-linux-gnueabihf-ldになります。

6.1.3. makeとmakefile

「Armadillo入門編」の「開発の基本的な流れ」の「make」で紹介したように、C言語で開発する 際にはコンパイル、アセンブル、リンクといった一連のビルド作業を自動化す るために、makefileを記述してmakeを使用することが一般的です。

ここではmakeの使い方と、makefileの書き方について紹介します。

6.1.3.1. make

makeは、プログラムのビルドを簡単にするツールです。makefileにプログラム のビルド手順ルールを記述しておくと、makeはそのルールに従って次に行うべ き手順を自動的に見つけ出し、必要なコマンドだけを実行してくれます。

何のオプションも付けずにmakeを実行すると、カレントディレクトリにある GNUmakefile、 makefile、 Makefileといった順にファイルを検索し、最初 に見つかったものをルールとして使用します[26]

-C ディレクトリ名オ プションを使用して指定したディレクトリに移動した状態で実行したり、 -f ファイル名オプシ ョンを使用して指定したファイル名をmakefileとして読み込むことなども可能 です。

[ティップ]makeの詳細情報

gccと同様に、makeのオプションやmakefileの書き方などの詳細情報について は、infoページが充実しています。

makeのinfoページはmake-docパッケージに含まれており、ATDEなどのDebian環 境ではaptコマンドでインストールすることができます。

6.1.3.2. makefileへのルールの記述

makeは、makefileに記載されたルールに従ってビルドを行います。このルール の書き方について説明します。

makefileには、複数のルールを記述することができます。1つのルールは必ず1 つのターゲットを持ち、このターゲットがそのルールで生成されるファイルと なります。ターゲットと組み合わせて、そのターゲットを生成するための依存 ファイル(事前に必要なファイル)の名称と、実行するコマンドラインを記述し ます。

ターゲット1: 依存ファイル1
        コマンドライン1

ターゲット2: 依存ファイル2 依存ファイル3
        コマンドライン2
        コマンドライン3

図6.1 ルールの記述方法


依存ファイルは、ターゲット名:の後にスペース区切りで複数記述することが できます。コマンドラインは、次の行の先頭からタブ(スペースではありませ ん)を入力した後に記述します。コマンドラインを複数行書くことも可能です 。

複数のルールが記述されたmakefileに対しmakeを実行すると、一番上に記述さ れているターゲットに対するルールのみが適用されます。このターゲット (図6.1「ルールの記述方法」でいえばターゲット1) を、デフォルトゴールと呼びます。

デフォルトゴール以外のターゲットを指定してmakeしたい場合、 makeにターゲット名を与えて実行します。 図6.1「ルールの記述方法」でターゲット2をmakeす る場合は、make ターゲット2になります。

ターゲット1: ターゲット2
        コマンドライン1

ターゲット2:
        コマンドライン2

図6.2 ルールの記述方法2


図6.2「ルールの記述方法2」のように、別のルール のターゲットを依存ファイルとして指定することが可能です。この場合、ター ゲット1を生成するためにターゲット2が必要となるため、先にコマンドライン 2が実行されます。

あるルールを適用する際に、必ずコマンドラインが実行されるわけではありません。 makeはルールを評価する際、必ずターゲットの存在と更新日時を確認します。 ターゲットが存在しない場合、またはターゲットの更新日時より依存ファイル のいずれかの更新日時が新しくなっていた場合のみ、コマンドラインを実行し ます。

つまり、2回目以降にmakeした際は、依存ファイルが更新されたルールのコマ ンドラインのみが実行されるわけです。このように、makeはビルド時間を短縮し てくれます。

より実際に近い、makefileの例を見てみます。

target1: target2
        cat target2

target2: depend1 depend2
        cat depend1 > target2
        cat depend2 >> target2

図6.3 makefileの実例


図6.3「makefileの実例」では、デフォルトゴー ルはtarget1です。target1はtarget2に依存するので、target2がない場合は先 にtarget2を作成しにいきます。

target2は、depend1とdepend2に依存します。target2という名前のファイルが ないか、target2が作られた後にdepend1やdepend2が変更されていた場合、下 のコマンドライン2行が実行されます。

target2が存在してtarget1という名前のファイルがない場合、またはtarget1 作成後にtarget2が再作成されていた場合、cat target2が実行されます。この 例ではtarget1が作成されることはありませんので、makeをするたびに毎回cat target2が実行されることになります。

図6.3「makefileの実例」を Makefileという名前でファイル保存し、適当な 内容のファイルdepend1とdepend2を作成してからmakeすると、以下のように動 作します。

[ATDE ~]$ ls 1
Makefile  depend1  depend2
[ATDE ~]$ cat depend1 2
hello
[ATDE ~]$ cat depend2
world
[ATDE ~]$ make 3
cat depend1 > target2 4
cat depend2 >> target2
cat target2 5
hello
world
[ATDE ~]$ ls 6
Makefile  depend1  depend2  target2
[ATDE ~]$ make target2 7
make: `target2' は更新済みです
[ATDE ~]$ echo "byebye" > depend2 8
[ATDE ~]$ make target2 9
cat depend1 > target2
cat depend2 >> target2
[ATDE ~]$ make 10
cat target2
hello
byebye

図6.4 makefileの実例: 実行結果


1

Makefileと依存ファイルを用意します。

2

depend1はhello、depend2はworldと書かれたテキストファイルです。

3

makeすると、target1に対するルールが適用されます。

4

target1はtarget2に依存するので、target2に対するルールが適用されコマンドラインが実行されます。

5

target2が作成されると、target1のためのコマンドラインが実行されます。

6

target2が作成されています。

7

target2をmakeしますが、既に存在するtarget2が依存ファイルより新しいので、何も実行されません。

8

target2の依存ファイルを変更してみます。

9

target2をmakeすると、今度は依存ファイルが更新されているので、コマンドラインが実行されます。

10

target1が作成されることはないので、target1に対するコマンドラインは毎回実行されます。

6.1.3.3. makefileでの変数の使用

makefile内では、変数[27]を使うことが できます。

変数名には、前後がスペースでなく、「:」(コロン)、「#」(ナンバー記号 [28])、「=」(イコール)を含 まない文字列を使用できますが、通常は英数字と「_」(アンダースコア)のみで 構成するのが無難です。なお、大文字と小文字は区別されます。

変数の定義は、変数名 = 値という形式で初期値を 代入することによって成されます。シェルスクリプトの場合とは異なり、 =の前後にはスペースを入れることができます。変 数の値は文字列、または文字列のリストです。リストの場合は、 変数名 = 値1 値2 ...と、スペースで値を区切っ て指定します。

変数を参照するには、$(変数名)または ${変数名}とします。変数を参照すると展開され、 展開された文字列と置き換えられます。

変数名 = 値という形式で定義された変数は、正確 には再帰展開変数(recursivery expanded variable)といいます。再帰展開変数 は記述されたままの形で値を保持し、参照するまで展開されません。そのため、

FOO = foo $(BAR)
BAR = bar baz

と定義することができます。このとき、$(FOO)を 展開すると「foo bar baz」となります。ただし、

FOO = foo
FOO = $(FOO) bar baz

とすると無限ループになるため、定義することはできません。

変数は、変数名 := 値という形式でも定義するこ とができます。この形式で定義された変数を、単純展開変数(simply expanded variable)といいます。単純展開変数は、定義された時点で値を展開して保持し ます。そのため以下のように記述することで、変数に値を追加することができ ます。

FOO := foo
FOO := $(FOO) bar baz

また、変数名 += 値とすることでも変数に文字列 を追加できます。

FOO = foo bar
FOO += baz

とした場合、$(FOO)を展開すると「foo bar baz」 となります。+=で文字列を追加した場合、追加す る文字列の前に半角スペースが一つ追加されます。展開がいつ行われるかは、 文字列を追加する変数がどのように定義されたかに準じます。再帰展開変数に +=で文字列を追加した場合、参照の際に展開され ます。また、単純展開変数に文字列を追加した場合は、代入の際に展開されま す。

変数定義は、以下のように書くこともできます。

FOO ?= bar

こうすると変数FOOが定義されていない場合だけ、 変数FOOに「bar」を代入します。

6.1.3.4. makefileで使用される暗黙のルールと定義済み変数

makefileでは、よく使われるルールは明示的に記述しなくても暗黙のルールとして 適用されます。

C/C++言語のソースファイルをビルドする際に適用される暗黙のルールには、 以下のものがあります。

  1. Cプログラムのコンパイル

    オブジェクトファイル(.o)が、Cソースファイル(.c)から $(CC) -c $(CPPFLAGS) $(CFLAGS) Cソースファイル名 というコマンドラインで生成されます。

  2. C++プログラムのコンパイル

    オブジェクトファイル(.o)が、Cソースファイル(.cc/.cpp/.Cのいずれか) から$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) Cソースファイル名というコマンドラ インで生成されます。

  3. アセンブラソースのプリプロセス

    プリプロセス済みアセンブラソースファイル(.s)が、アセンブラソース(.S)か ら$(CPP) $(CPPCFLAGS) アセンブラソース名 というコマンドラインで生成されます。

  4. プリプロセス済みアセンブラソースのアセンブル

    オブジェクトファイル(.o)が、プリプロセス済みアセンブラソースファイル (.s)から$(AS) $(ASFLAGS) プリプロセス済みアセ ンブラソースファイル名というコマンドライン で生成されます。

  5. リンク

    実行ファイル(拡張子なし)が、オブジェクトファイル(.o)から $(CC) $(LDFLAGS) オブジェクトファイル名 $(LOADLIBES) -o 実行ファイル名 というコマンドラインで生成されます。

    このルールは、複数のオブジェクトファイルに対して適用することもできます 。ルールに、実行ファイル名:オブジェクト1ファイル名 オブジェクト2ファイ ル名とだけ記述しておくと、オブジェクト1ファイルとオブジェクト2ファイル をルールに従って生成した後、2つのオブジェクトファイルから実行ファイル を生成します。

ここに登場したCCやCFLAGSといった変数は、暗黙のうちに定義されている変数です。 主なものを挙げます。

表6.1 暗黙のルールで使用される変数

変数名 デフォルト値 説明

AR

ar

アーカイバ

AS

as

アセンブラ

CC

cc

Cコンパイラ。Linuxシステムでは、ccはgccコマンドへのリンクになっています。

CXX

g++

C++コンパイラ

CPP

$(CC) -E

Cプリプロセッサ

RM

rm -f

ファイル削除コマンド

ARFLAGS

rv

アーカイバに渡されるフラグ

ASFLAGS

アセンブラに渡される拡張フラグ

CFLAGS

Cコンパイラに渡される拡張フラグ

CXXFLAGS

C++コンパイラに渡される拡張フラグ

CPPFLAGS

Cプリプロセッサとそれを使うプログラムに渡される拡張フラグ

LDFLAGS

コンパイラがリンカ(ld)を呼び出すときに渡される拡張フラグ


パターンルールを使用して、新しい暗黙のルールを定義することもできます。 パターンルールは、ターゲットと依存ファイルの一部に%を用いて記述します。 %は、空でない任意の文字列に適合します。

例えば、Cプログラムのコンパイルを行う暗黙のルールとして、あえてデフォルト状態と 同じものをパターンルールで記述すると、このようになります。

%.o : %.c
        $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

ここで、$<や$@は自動変数と呼ばれる特殊な変数です。自動変数はルールが実 行されるたびに、ターゲットと依存ファイルに基づいて設定される変数です。 この例では$@はターゲットとなるオブジェクトファイル名に、$<は依存ソース ファイル名に展開されます。

自動変数には、以下のようなものがあります。

表6.2 自動変数

自動変数 説明

$@

ターゲットファイル名

$<

最初の依存ファイル名

$?

ターゲットより新しいすべての依存ファイル名

$^

すべての依存ファイル名


6.1.3.5. 変数の外部定義とオーバーライド

変数は、makefileの外で定義することもできます。定義方法は2種類あります。

1つ目の方法は、makeコマンドの引数として指定す る方法です。make 変数名=値とすることで、変数が 定義されます。なお、makefile内に同じ名前の変数定義があった場合でも、こ ちらの引数による定義の方が優先(オーバーライド)されます。

2つ目の方法は、環境変数として指定する方法です。すべての環境変数は、makeの変数と 同等に扱われます。しかし、こちらの変数定義はそれほど強いものではありません。 make引数による定義や、makefile内の定義があった場合、そちらが 優先(オーバーライド)されます。

ちなみに、makefile内で同じ変数を重複定義した場合、最後に定義されたものが 優先(オーバーライド)されます。

オーバーライドが発生する例を見てみます。

[ATDE ~]$ cat Makefile 1
VARIABLE = value

all:
        echo $(VARIABLE)
        echo $(SHELL)
[ATDE ~]$ make
echo value
value
echo /bin/sh
/bin/sh
[ATDE ~]$ make VARIABLE=arg 2
echo arg
arg
echo /bin/sh
/bin/sh
[ATDE ~]$ VARIABLE=env make 3
echo value
value
echo /bin/sh
/bin/sh

図6.5 オーバーライドの発生例


1

makefile内で定義した変数VARIABLEと環境変数SHELLを表示するだけのMakefile。

2

makeコマンドへの引数で定義した変数が、makefile内で定義した変数よりも優先されます。

3

makefile内で定義した変数が、環境変数よりも優先されます。

6.1.3.6. 条件文

makefile内には、ある条件が成立したときだけ有効になる行を書くことができ ます。基本的な構文は以下のようになります。

CONDITIONAL-DIRECTIVE
TEXT-IF-TRUE
endif

CONDITIONAL-DIRECTIVEの条件が真の時に、 TEXT-IF-TRUEの行が有効になります。

また、else節を使って以下のように書くこともできます。

CONDITIONAL-DIRECTIVE
TEXT-IF-TRUE
else
TEXT-IF-FALSE
endif

または

CONDITIONAL-DIRECTIVE
TEXT-IF-ONE-IS-TRUE
else CONDITIONAL-DIRECTIVE
TEXT-IF-TRUE
else
TEXT-IF-FALSE
endif

CONDITIONAL-DIRECTIVEは、 ifeq、ifneq、 ifdef、ifndefのいずれ かの構文を使って記述します。

ifeqを使う場合、 CONDITIONAL-DIRECTIVEは以下のようになります。

ifeq (ARG1, ARG2)
ifeq 'ARG1', 'ARG2'
ifeq "ARG1", "ARG2"

どの書き方をしても意味は同じです。ARG1と ARG2を展開し両者が等しい場合、真と判定され TEXT-IF-TRUEが有効になります。

ifneqもifeqと同様に 記述することができますが、ARG1と ARG2を展開し両者が等しくない場合、真と判定さ れTEXT-IF-TRUEが有効になります。

ifneq (ARG1, ARG2)
ifneq 'ARG1', 'ARG2'
ifneq "ARG1", "ARG2"

ifdefは、指定された変数名の変数が定義済みの場 合、真と判定されます。ifndefはその逆です。

ifdef VARIABLE-NAME
ifndef VARIABLE-NAME

VARIABLE-NAMEに変数が指定された場合、変数を展 開した後の文字列を変数名として使用します。

BAR = true
FOO = BAR
ifdef $(FOO)
BAZ = yes
endif

とした場合、変数FOOは「BAR」に展開され、それが 変数名として用いられます。変数BARは定義されて いるので、ifdef $(FOO)は真として判定され、 BAZ = yes行が有効になります。

6.1.3.7. make動作の実際

「Armadillo入門編」の「開発の基本的な流れ」で使用したsin.cとmakefileがどのように動 作しているのか、改めて見てみます。

図6.6「基本的なmakefile」では、Cソースファイル sin.cから実行ファイルsinが生成されます。

CROSS := arm-linux-gnueabihf 1

ifneq ($(CROSS),) 2
CROSS_PREFIX    := $(CROSS)-
endif

CC = $(CROSS_PREFIX)gcc 3
CFLAGS = -Wall -Wextra -O2
LDFLAGS = -lm

TARGET = sin 4

all: $(TARGET) 5

sin: sin.o 6
        $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@ 7

clean: 8
        $(RM) *~ *.o $(TARGET) 9

%.o: %.c 10
        $(CC) $(CFLAGS) -c -o $@ $<

図6.6 基本的なmakefile


1

デフォルトでCROSSをarm-linux-gnueabihfとして定義し、クロスコンパイルを行います。

2

CROSS変数が空でなければ、CROSS_PREFIXを定義します。

3

暗黙のルールで使用される変数CCとCFLAGS、LDFLAGSを明示的に定義し、オーバーライドしています。これによって、gccの前にCROSS_PREFIXが付きます。

4

ファイル名が変わっても使いまわせるように、実行ファイルの名前を変数で定義します。

5

デフォルトゴール(一般的にallという名前を付けます)は、TARGETに依存します。

6

sinは、sin.oに依存します。

7

$@、$^は自動変数、CC、LDFLAGS、LDLIBSは暗黙のルールで使用される変数です。

8

cleanターゲットは、依存ファイルがないので必ず実行されます。

9

生成したファイルや中間ファイルをすべて削除します。

10

Cプログラムのコンパイルを行うパターンルールを定義しています。

図6.6「基本的なmakefile」をMakefile という名前で保存し、Cソースコードをsin.c として同じディレクトリに置いてからmakeすると、ARM (Armadillo)用の実行 ファイルが生成されます。

[ATDE ~]$ make CROSS=arm-linux-gnueabihf-
arm-linux-gnueabihf-gcc -Wall -Wextra -O2   -c -o sin.o sin.c
arm-linux-gnueabihf-gcc -lm sin.o  -o sin
[ATDE ~]$ file sin
sin: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=49b950072b23d33d3e92be67f9eee226cb45779b, not stripped

make cleanで、生成されたすべてのファイルを削 除できます。

[ATDE ~]$ ls
Makefile  sin  sin.c  sin.o
[ATDE ~]$ make clean
rm -f *~ *.o
[ATDE ~]$ ls
Makefile  sin.c

また、コマンドライン引数による変数定義を使ってmake CROSS=として変数 CROSSをオーバーライドすると、ホストPC用の実行ファイルを生成できます。 この例のようにすることで、同じmakefile、同じソースファイルから異なるア ーキテクチャ用の実行ファイルを簡単に生成できるわけです。

[ATDE ~]$ make CROSS=
gcc -Wall -Wextra -O2   -c -o sin.o sin.c
gcc -lm sin.o  -o sin
[ATDE ~]$ ./sin
sin(0.5) = 0.479426

図6.6「基本的なmakefile」では、1つのソースファイルから1つの実行 ファイルを生成しています。これを、複数のソースから1つの実行ファイルを 生成したり、複数の実行ファイルを生成するように変更してみます。

sin.cとcos.cとtan.c から実行ファイルtrigonometricが生成され、 asin.cとacos.cとatan.c 実行ファイルinvtrigonometricが生成されるようにする場 合、このようになります。

CROSS   := arm-linux-gnueabihf

ifneq ($(CROSS),)
CROSS_PREFIX    := $(CROSS)-
endif

CC = $(CROSS_PREFIX)gcc
CFLAGS = -Wall -Wextra -O2
LDFLAGS =

TARGET = trigonometric invtrigonometric

all: $(TARGET)

hellotrigonometric: sin.o cos.o tan.o
        $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@

invtrigonometric: asin.o acos.o atan.o
        $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@

clean:
        $(RM) *~ *.o $(TARGET)

%.o: %.c
        $(CC) $(CFLAGS) -c -o $@ $<

図6.7 複数ファイルを扱うmakefile


6.2. C言語プログラミングの復習

実践的なプログラミングの話題に入る前に、C言語でプログラムを作成する際に 気をつけるべきことについて復習しておきます。Linuxシステム独特の話題もあ りますので、他のOS上でのC言語プログラミングに慣れた方も確認してみてくだ さい。

6.2.1. コマンドライン引数の扱いと終了ステータス

標準的なCプログラムでのmain関数は、以下のどちらかの形で定義しなければ なりません。

int main(void);
int main(int argc, char *argv[]);

戻り値はint型として規定されています。引数は取らないか、またはargc, argvの2個を取ります[29]

Cソースをコンパイルして生成したプログラムをシェルから実行すると、自身 のコマンド名と渡されたコマンドラインパラメータがmain関数の引数として渡 ります。パラメータをつけずコマンド名のみで実行した場合はargcは1で、 argvはコマンド名の文字列へのポインタです。パラメータをつけるとargcは (1+パラメータ数)となり、argvはコマンド名、パラメータ1、パラメータ2…と いった形の文字列配列になります。

main関数の戻り値は、コマンドの終了ステータスになります。シェルの世界で は0が真、0以外のすべての値を偽として扱いますので、プログラムが正常に終 了した場合、main関数の戻り値は0であるべきです。

終了ステータスを表現するためのマクロが、ヘッダファイル stdlib.hで定義されています。正常終了、つま り0となる値としてEXIT_SUCCESSが、異常終了のための値としてEXIT_FAILURE が用意されています。本書のサンプルプログラムでは、終了ステータスとして これらを使用しています。

[注記]その他の終了ステータス

stdlib.hで定義されている終了ステータス以外の 終了ステータスとしては、BSD由来のものが sysexits.hで定義されています。

プログラムの終了には、exit関数を呼ぶ方法もあります。

void exit(int status);

このexit関数に渡すstatusが終了ステータスであり、main関数の戻り値と同様 の扱いです[30]

main関数周りの動作を実際に見てみましょう。コマンドに渡したパラメータを 順番に表示するプログラムです。

#include <stdio.h>

int main(int argc, char *argv[])
{
        int i;

        for (i = 0; i < argc; i++)
                printf("%d: '%s'\n", i, argv[i]);

        return 0;
}

図6.8 すべてのパラメータを表示するプログラム(show_arg.c)


[ATDE ~]$ ./show_arg 1 "with space"
0: './show_arg'
1: '1'
2: 'with space'

図6.9 show_argの実行結果


argv[0]にはコマンド名が格納されています。argv[1]以降は、与えたパラメ ータが順に格納されます。シェルからスペースを含む文字列をパラメータとし て渡したい場合は、ダブルクォートかシングルクォートで囲みます。

シェルから呼ぶように作られたコマンドの多くは、「-」(ハイフン)始まりな どのオプション指定に対応しています。このようなオプションの解析を、すべ て自前で実装するのはかなり手間のかかることです。これを楽にしてくれるラ イブラリ関数が存在します。

getopt関数は、「-」で始まるショートオプションの解析を助けてくれます。 getopt_long関数は、「-」始まりのショートオプションと「--」始まりのロン グオプションの両方を扱うことができます。ここではgetopt_longを使ってみ ます。

[ATDE ~]$ ./greeting --name Alice
Hello, Alice!
[ATDE ~]$ ./greeting --name=Bob --time morning
Good morning, Bob!
[ATDE ~]$ ./greeting -n Charlie -t evening --german
Gute Nacht, Charlie!
[ATDE ~]$ ./greeting -nDave -g
Hallo, Dave!

図6.10 greetingの動作


一見してわかるとおり、-nまたは --nameで指定した名前に対して挨拶を表示するプロ グラムです。-tまたは --timeで時刻を指定することができ、それによって 挨拶文が変化します。-gまたは --germanを指定すると、挨拶文がドイツ語になりま す。

作成したコマンドに指定できるオプションを形式的に表記すると、次のように なります。

greeting <-n|--name NAME> [-t|--time TIME] [-g|--german]

このプログラムのソースコードが、図6.11「greeting.c」です。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>

enum time {
        TIME_MORNING,
        TIME_DAYTIME,
        TIME_NIGHT,
        TIME_UNKNOWN,
};

enum lang {
        LANG_ENGLISH,
        LANG_GERMAN,
};

static void usage(const char *prg)
{
        printf("usage: %s <-n|--name NAME> [-t|--time TIME] [-g|--german]\n", prg);
}

int main(int argc, char *argv[])
{
        int c;
        char *name = NULL;
        enum time time = TIME_UNKNOWN;
        enum lang lang = LANG_ENGLISH;
        char *greeting;

        while (1) {
                int option_index = 0;
                static struct option long_options[] = {
                        /* name,        has_arg,           flag, val*/
                        {"name",        required_argument, NULL, 'n'},
                        {"time",        required_argument, NULL, 't'},
                        {"german",      no_argument,       NULL, 'g'},
                        {0,             0,                 0,    0},
                };

                c = getopt_long(argc, argv, "n:t:g",
                                long_options, &option_index);
                if (c == -1)
                        break;

                switch (c) {
                case 'n':
                        name = strdup(optarg);
                        if (name == NULL)
                                exit(EXIT_FAILURE);
                        break;
                case 't':
                        if (strcmp(optarg, "morning") == 0)
                                time = TIME_MORNING;
                        else if (strcmp(optarg, "daytime") == 0)
                                time = TIME_DAYTIME;
                        else if (strcmp(optarg, "evening") == 0)
                                time = TIME_NIGHT;
                        else
                                time = TIME_UNKNOWN;
                        break;
                case 'g':
                        lang = LANG_GERMAN;
                        break;
                default:
                        usage(argv[0]);
                        exit(EXIT_SUCCESS);
                }
        }

        if (name == NULL) {
                /* NAME が指定されなかった */
                usage(argv[0]);
                return EXIT_FAILURE;
        }

        switch (time) {
        case TIME_MORNING:
                greeting =
                        (lang == LANG_ENGLISH) ? "Good Morning" : "Guten Morgen";
                break;
        case TIME_DAYTIME:
                greeting =
                        (lang == LANG_ENGLISH) ? "Good Afternoon" : "Guten Tag";
                break;
        case TIME_NIGHT:
                greeting =
                        (lang == LANG_ENGLISH) ? "Good Night" : "Gute Nacht";
                break;
        default:
        case TIME_UNKNOWN:
                greeting =
                        (lang == LANG_ENGLISH) ? "Hello" : "Hallo";
                break;
        }

        printf("%s, %s!\n", greeting, name);

        free(name);

        return EXIT_SUCCESS;
}

図6.11 greeting.c


6.2.2. 終了処理

C言語を使用してプログラムを記述する際、プロセスを正常に終了する方法に は、以下の3種類があります。

  1. main関数から戻る
  2. exit関数を呼ぶ
  3. _exit関数を呼ぶ

また、Linuxシステムで動作するプロセスは、正常に終了する方法以外に、シ グナルを受けて終了する場合があります。

main関数から戻るか、exit関数によってプロセスが終了した場合、所定の終 了処理が行われます。

まず、atexit関数やon_exit関数によって登録された関数が、それらが登録さ れた順番とは逆順に呼ばれます。

次に、オープン中の標準入出力[31]ストリームがすべて クローズされます。ストリームがクローズされると、バッファされている出力 データはすべてフラッシュされ、ファイルに書き出されます。

また、tmpfile関数によって作成されたファイルは削除されます。

exit関数は、これらの処理を行ったあと、_exit関数を呼びます。

_exit関数内では、そのプロセスがオープンしたディスクリプタがすべてクロー ズされます。

さらに、Linuxシステムの場合、exitや_exit関数で行われる処理の他に、プロ セス終了時にカーネルが資源の回収を行います。すなわち、プロセスがオ ープンしているすべてのディスクリプタをクローズし、使用していたメモリなど を開放します。

プロセスがシグナルを受信し、そのシグナルに対する動作がプロセスを終了さ せるものであった場合、プロセスは直ちに終了します。シグナルは、自プロセ ス以外のプロセスから送られることもありますし、プログラム中でabort関数 やkill関数で自プロセスへシグナルを送ることもできます。

これらの終了処理やカーネルによる資源の回収処理があるため、アプリケーショ ンプログラム内ではmallocしたメモリ領域は必ずfreeしなけばいけないという ことはありません。終了処理で行われることを把握した上で、資源の後始末を 明示的にプログラム中に記述せず、それらに任せるという方法もあります。

6.2.3. エラー処理

C言語でプログラムを記述する際、エラー処理を怠りがちです。世にあるC言語 プログラミングの参考書では、サンプルコードをシンプルに書くために、あえ てエラー処理を書いていない場合が多いので、それらを参考にしてコードを記 述すると、つい、エラー処理を忘れてしまいます。

しかし、実際に使用するプログラムでは、必ずエラー処理を行うコードを記述 してください。特に組み込みシステムでは、PCやサーバーなどと比較して、振 動や温湿度などの外部環境が厳しい環境で動作することが多いので、単純な処 理でもエラーが発生することがあります。

システムコールまたはライブラリコールのエラーを検出する一般的な方法は、 関数の戻り値をチェックすることです。システムコールといくつかのライブラ リコールは、エラーが発生した場合errnoを設定します。errnoの値を確認する ことによって、エラーの発生要因を知ることができます。

システムコールやライブラリコールの戻り値や、それらが設定するerrnoの値 は、manページで確認できます。

例えば、openシステムコールのmanページの返り値のセクションには、「 open()とcreat()は新しいファイル・ディスクリプタを返す。エラーが発生し た場合は-1を返す(その場合はerrnoが適切に設定される)。」と記述されてい ます。また、エラーのセクションには、openシステムコールが返す可能性のあ るerrnoの値と、どのような時に設定されるのかが記述されています。

システムコールのmanページをみるコマンドは、 man 2 関数名です。 ライブラリコールの場合は、 man 3 関数名となり ます。manページの内容をよく確認し、エラーが発生した場合の処理を忘れず に記述してください。

エラー処理の例として、ファイルの内容を読み込み、標準出力に表示するプロ グラムのソースコードを図6.12「fdump.c」に示します。

このプログラムは、open、close、read、writeの4つのシステムコールを使用 します。エラーが発生した際には、perror関数でerrnoに応じたエラーメッセ ージを表示します。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
        int fd;
        char buf[1024];
        ssize_t len;
        int ret;

        if (argc < 2) {
                printf("usage: %s <file name>\n", argv[0]);
                return EXIT_SUCCESS;
        }

        /*
         * open() は新しいファイル・ディスクリプタを返す。エラーが発生
         * した場合は -1 が返され、errno が適切に設定される。
         */
        fd = open(argv[1], O_RDONLY);
        /* エラーが発生した? */
        if (fd == -1) {
                /* errno に応じたエラーメッセージを出力する */
                perror("open");
                return EXIT_FAILURE;
        }

        ret = EXIT_SUCCESS;
        for(;;) {
                /*
                 * 成功した場合、読み込んだバイト数を返す (0 はファイル
                 * の終りを意味する)。エラーの場合は、-1 が返され、
                 * errno が適切に設定される。
                 */
                len = read(fd, buf, sizeof(buf));
                if (len <= 0) {
                        /* ファイルの終わりに達した? */
                        if (len == 0)
                                break;

                        /* エラーが発生した */
                        perror("read");
                        ret = EXIT_FAILURE;
                        break;
                }

                /*
                 * 成功した場合、書き込まれたバイト数が返される (ゼロは
                 * 何も書き込まれなかったことを示す)。エラーならば -1
                 * が返され、errno が適切に設定される。
                 */
                len = write(1, buf, len);
                /* エラーが発生した? */
                if (len == -1) {
                        perror("write");
                        ret = EXIT_FAILURE;
                        break;
                }
        }

        /*
         * close() は成功した場合は 0 を返す。エラーが発生した場合は
         * -1 を返して、 errno を適切に設定する。
         */
        if (close(fd) == -1) {
                perror("close");
                ret = EXIT_FAILURE;
        }

        return ret;
}

図6.12 fdump.c


fdumpの実行結果を以下に示します。引数にファイル名を指定して実行すると 、ファイルの内容を表示します。正常に終了したときの終了ステータスは0 (EXIT_SUCCESS)になります。/var/log/message は読み込み権限のないファイルなので、これを引数に指定してfdumpを実行す ると、openシステムコールで失敗します。異常終了時には、エラーメッセージ を表示して終了します。その時の終了コードは1(EXIT_FAILURE)になります。

[ATDE ~]$ ./fdump /etc/hostname
atde7
[ATDE ~]$ echo $?
0
[ATDE ~]$ ./fdump /var/log/messages
open: Permission denied
[ATDE ~]$ echo $?
1
[ATDE ~]$ ls -l /var/log/messages
-rw-r----- 1 root adm 1012996  4月 10 10:12 /var/log/messages

図6.13 fdumpの実行結果


6.2.4. 共通ヘッダファイル

次章からは、より実際に近いプログラムを目的別に取り上げていきます。

以降のプログラムでは、ソースコードを見やすくし、また何のエラーが起きた かを明確に可視化するために、エラー処理のための共通のヘッダファイル exitfail.hを使用することにします。

#ifndef EXITFAIL_H
#define EXITFAIL_H

#include <errno.h>

#include <stdarg.h>
#include <stdlib.h>
#include <string.h>

#ifdef MAIN_C
static char *progname = NULL;

#define exitfail_init()                                                 \
        (progname = (strrchr(argv[0], '/') ? : (argv[0] - 1)) + 1)

/**
 * プログラムエラー終了関数
 * @param format 書式フォーマットと引数群
 */
void exitfail(const char *format, ...)
{
        va_list va;

        va_start(va, format);
        fprintf(stderr, "%s: ", progname);
        vfprintf(stderr, format, va);
        va_end(va);

        exit(EXIT_FAILURE);
}
#else
extern void exitfail(const char *__format, ...);
#endif /* MAIN_C */

#define exitfail_errno(msg) exitfail(msg " - %s\n", strerror(errno))

#endif /* EXITFAIL_H */

図6.14 エラー内容表示とFAILURE終了するためのヘッダ(exitfail.h)


main関数を含んだソースでは、#defineマクロ定義でMAIN_Cを定義してこのヘ ッダをインクルードします。exitfail関数は、printf関数と同じ引数フォーマ ット形式で好きな内容のエラーメッセージを表示できます。exitfail_errnoは 、ライブラリ関数の呼び出しでerrnoが更新されるタイプのエラー発生直後に 呼び出す専用のものです。引数として渡した文字列(通常は関数名を想定して います)と、errnoを意味のある文章に変換したエラー文字列をセットで表示し ます。どちらの関数も、関数名どおりにexitしてEXIT_FAILUREを返します。

6.3. ファイルの取り扱い

まずは、ファイルを扱う例です。一般的なCのテキストであっても最初の方で 取り上げられるもので、基本的にはそれほど大きくは違いません。但し、PCの ように高速だったり、ふんだんなメモリが積まれているわけではありませんか ら、無駄な処理をしないようにしたり、メモリリークを起こさないように一層 の注意が必要といえます。

6.3.1. テキストファイルを扱う

テキストファイルを扱うサンプルプログラムを紹介します。ここではComma Separated Values(CSV)ファイルと呼ばれる、データをカンマで区切った形式 のものを扱ってみます。日本郵便が公開している住所の郵便番号(ローマ字 )(CSV 形式)[32]を処理する例としてみました。

以下のような仕様を満たすものとします。

  1. CSVファイルを中身を整形して表示するアプリケーション
  2. コマンド引数として、CSVファイル(郵便番号データ)を指定する
  3. 行頭にインデックス番号を表示し、その後スペース区切りで各項目を表示する
  4. 表示データが流れてしまわないように、画面サイズを超える場合は一時停止する
  5. 最後にデータ総数を表示する

郵便番号データCSVファイルの一行は、以下のように構成されています [33]

01101,"0600035","KITA5-JOHIGASHI","CHUO-KU SAPPORO-SHI","HOKKAIDO",0,0,1,0,0,0

先頭から順に、以下の内容を表しています。

全国地方公共団体コード,"郵便番号","町域名","市区町村名","都道府県名",フラグ1,フラグ2,フラグ3,フラ
グ4,フラグ5,フラグ6

フラグにはそれぞれ意味があるのですが、今回のプログラムに関して処理する 必要のあるものは1つだけです。フラグ4が0であって同じ郵便番号が複数行続 く場合は町域名が長いため複数行に分割されたデータとなる、という仕様 です。

01224,"0660005","KYOWA(88-2.271-10.343-2.404-1.427-","CHITOSE-SHI","HOKKAIDO",1,0,0,0,0,0
01224,"0660005","3.431-12.443-6.608-2.641-8.814.842-","CHITOSE-SHI","HOKKAIDO",1,0,0,0,0,0
01224,"0660005","5.1137-3.1392.1657.1752-BANCHI)","CHITOSE-SHI","HOKKAIDO",1,0,0,0,0,0

この時は、複数行を一つのデータとして扱う対応を行うことにします。

ここまでの仕様に基づいたコードは、次のようになりました。

#define _GNU_SOURCE /* strchrnul関数使用のために必要 */

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

#define MAIN_C
#include "exitfail.h"

/* 表示する文字数 */
#define DISP_WIDTH  60 /* 幅 */
#define DISP_HEIGHT 17 /* 高さ */

/* CSVデータ各要素のサイズ */
#define ZIPCODE_LEN 8
#define STREET_LEN  512
#define CITY_LEN    64
#define PREF_LEN    16
#define FLAG_NUM    6
/* CSVデータ1行の要素数 */
#define COLUMN_NUM  (5 + FLAG_NUM)

/* CSVデータ構造体 */
typedef struct {
        long code;                 /* 全国地方公共団体コード */
        char zipcode[ZIPCODE_LEN]; /* 郵便番号 */
        char street[STREET_LEN];   /* 町域名 */
        char city[CITY_LEN];       /* 市区町村名 */
        char pref[PREF_LEN];       /* 都道府県名 */
        long flag[FLAG_NUM];       /* フラグ配列 */
} csvline_t;

#define BASENAME(p) ((strrchr((p), '/') ? : ((p) - 1)) + 1)

/**
 * 行表示関数
 * @param pcsvline 表示するCSVデータ構造体へのポインタ(NULLの場合は表示済データの総数を表示)
 */
static void printline(csvline_t *pcsvline)
{
        static int count = 0;   /* 表示したデータ総数 */
        static int line = 0;    /* 一画面中に表示した行数 */
        int disp_width = DISP_WIDTH, disp_height = DISP_HEIGHT, newline;
        char buf[1024];

        /* CSVデータの各要素を表示 */
        if (pcsvline) {
                /* 郵便番号が入っていない場合は表示しない */
                if (!pcsvline->zipcode[0])
                        return;

                /* 各要素を表示フォーマットに展開 */
                snprintf(buf, sizeof(buf),
                         "%6d %05ld %s %s %s %s %ld %ld %ld %ld %ld %ld",
                         count++, pcsvline->code, pcsvline->zipcode,
                         pcsvline->street, pcsvline->city, pcsvline->pref,
                         pcsvline->flag[0], pcsvline->flag[1],
                         pcsvline->flag[2], pcsvline->flag[3],
                         pcsvline->flag[4], pcsvline->flag[5]);
        }
        /* CSVデータの総数を表示 */
        else {
                /* データ総数を表示フォーマットに展開 */
                sprintf(buf, "Count: %6d", count);
        }

        /* 今回追加される表示行数を計算 */
        newline = (strlen(buf) + (disp_width - 1)) / disp_width;
        /* 1画面を超える場合、入力があるまで一時停止 */
        if (line + newline >= disp_height) {
                getchar();
                /* 表示行数を初期化 */
                line = 0;
        }
        /* 実際に表示する */
        printf("%s\n", buf);
        /* 表示行数を更新 */
        line += newline;
}

/**
 * main関数
 * @param argc 引数なしの場合はusage表示のみ
 * @param argv 第1引数として読み込みCSVファイル名を指定
 * @return exit値
 */
int main(int argc, char *argv[])
{
        FILE *pcsvfile; /* CSVファイルポインタ */
        csvline_t csvline; /* CSVデータ */
        char buf[256], *pbuf, *pcol[COLUMN_NUM];
        int i;

        exitfail_init();

        /* 引数が指定されなかった場合、usage表示して終了 */
        if (argc < 2) {
                printf("Usage: %s <csvfile>\n", BASENAME(argv[0]));
                return EXIT_SUCCESS;
        }

        /* CSVファイルオープン */
        pcsvfile = fopen(argv[1], "r");
        if (!pcsvfile)
                exitfail_errno("fopen");

        /* CSVデータを初期化 */
        memset(&csvline, 0, sizeof(csvline));
        /* CSVファイルから1行読み込む */
        while (fgets(buf, sizeof(buf), pcsvfile)) {
                /* 各要素へのポインタ配列を初期化 */
                memset(pcol, 0, sizeof(pcol));
                /* 次のカンマ(または行末)を見つけてトークン化 */
                pbuf = strtok(buf, ",\n");
                for (i = 0; i < COLUMN_NUM && pbuf; i++) {
                        /* 前後のダブルクォートは除去する */
                        if (*pbuf == '"')
                                *strchrnul(++pbuf, '"') = '\0';
                        /* 要素へのポインタを保持 */
                        pcol[i] = pbuf;
                        /* 次のカンマ(または行末)を見つけてトークン化 */
                        pbuf = strtok(NULL, ",\n");
                }
                /* 要素数が不足している場合、次行にスキップ */
                if (i < COLUMN_NUM)
                        continue;

                /* 新しいデータの場合 */
                if (strcmp(pcol[1], csvline.zipcode) ||
                    strtol(pcol[8], NULL, 10)) {
                        /* 保持済みのデータを表示 */
                        printline(&csvline);

                        /* CSVデータ各要素を保持 */
                        memset(&csvline, 0, sizeof(csvline));
                        csvline.code = strtol(pcol[0], NULL, 10);
                        strncpy(csvline.zipcode, pcol[1],
                                sizeof(csvline.zipcode) - 1);
                        strncpy(csvline.street, pcol[2],
                                sizeof(csvline.street) - 1);
                        strncpy(csvline.city, pcol[3],
                                sizeof(csvline.city) - 1);
                        strncpy(csvline.pref, pcol[4],
                                sizeof(csvline.pref) - 1);
                        for (i = 0; i < FLAG_NUM; i++)
                                csvline.flag[i] = strtol(pcol[5 + i], NULL, 10);
                }
                /* 既存データへの追加の場合 */
                else
                        /* 町域名の続きを追加 */
                        strncat(csvline.street, pcol[2],
                                (sizeof(csvline.street) -
                                 strlen(csvline.street)) - 1);
        }
        /* 保持済みのデータを表示 */
        printline(&csvline);

        /* CSVファイルクローズ */
        fclose(pcsvfile);

        /* データ総数を表示 */
        printline(NULL);

        return EXIT_SUCCESS;
}

図6.15 CSVファイルの内容を表示するプログラム(dispcsv1.c)


main関数から見ていきます。CSVファイルの操作は基本的に標準Cライブラリ関 数で行っていますので、難しいところはないと思います。データはfgets関数で 一行ずつ読み込み、strtok関数でカンマ区切りをトークン単位に分解。トーク ンがダブルクォートで始まっている場合は、これを外します。

ここのところでstrchrnulという、標準Cライブラリにない関数を使用しています。 ダブルクォートを見つけるだけならstrchr関数でよいのですが、この関数は検 索文字が見つからなかったときにNULLを返します。これを考慮すると

        p = strchr(++pbuf, '"');
        if (p)
                *p  = '\0';

のように処理しなくてはなりません。

ここでman strchrとすると、似たような関数とし て以下のような説明が見つけられます。

SYNOPSIS
       #define _GNU_SOURCE
       #include <string.h>

       char *strchrnul(const char *s, int c);

DESCRIPTION
       The strchrnul() function is like strchr() except that if c is not found
       in s, then it returns a pointer to the null  byte  at  the  end  of  s,
       rather than NULL.

strchrnul関数は、(strchrとは違い)未発見時に終端文字'\0'位置へのポイン タを返します。このためサンプルプログラムのように条件分岐が不要になりま す。なお上記manに説明されていますが、こうしたGNU拡張関数を使う場合はヘ ッダ(今回の場合string.h)インクルード前にGNU拡張関数を使うため GNU_SOURCEマクロを定義(#define)しておく必要があります。今回は小さな例 ですが、このようにmanには便利な情報が多く記載されており大変有用です。

トークン解析処理が終わると、これを数値変換や文字列コピーして保持します。 前述した複数行にわたる長い町域名に対応するため、複数回のループにまたがっ て一つのデータを処理することがあります。

こうして完成した一つのデータを表示しているのは、printline関数です。こ の関数では、データをsnprintfで読みやすい形の文字列に整形しています。大 量の表示データが流れていってしまわないように、画面サイズ(マクロ定義で 横60文字×縦17文字とされています)ごとに一時停止する処理を入れつつ、表示 を行います。また、最後に行われるデータ総数表示にもこの関数を使用(引数 pcsvlineにNULLを指定)しますので、このための分岐処理も入れてあります。

作成したアプリケーションdispcsv1にCSVファイル名を渡すと、データが整形 表示されます。

[armadillo ~]# ./dispcsv1 ken_all_rome.csv
     0 01101 0600000 IKANIKEISAIGANAIBAAI CHUO-KU SAPPORO-SHI HOKKAIDO 0 0 0 0 0 0
     1 01101 0640941 ASAHIGAOKA CHUO-KU SAPPORO-SHI HOKKAIDO 0 0 1 0 0 0
     2 01101 0600041 ODORIHIGASHI CHUO-KU SAPPORO-SHI HOKKAIDO 0 0 1 0 0 0
     3 01101 0600042 ODORINISHI(1-19-CHOME) CHUO-KU SAPPORO-SHI HOKKAIDO 1 0 1 0 0 0
     4 01101 0640820 ODORINISHI(20-28-CHOME) CHUO-KU SAPPORO-SHI HOKKAIDO 1 0 1 0 0 0
     5 01101 0600031 KITA1-JOHIGASHI CHUO-KU SAPPORO-SHI HOKKAIDO 0 0 1 0 0 0
     6 01101 0600001 KITA1-JONISHI(1-19-CHOME) CHUO-KU SAPPORO-SHI HOKKAIDO 1 0 1 0 0 0
     7 01101 0640821 KITA1-JONISHI(20-28-CHOME) CHUO-KU SAPPORO-SHI HOKKAIDO 1 0 1 0 0 0

図6.16 dispcsv1の実行結果


リターンを入力すると次に進みます。途中で終了したいときは、Ctrl+Cを入力してください。

6.3.2. 設定ファイルに対応する

アプリケーションの設定を保存するとき、.iniや.confといった設定ファイルを 用いることがあります。設定ファイルも普通はテキストファイルですので標準 Cライブラリ関数を駆使して作成することができますが、こうした目的のために 特別な機能が用意されたライブラリを使うと、短いコードで効率的に扱うこと が可能です。ここではGLibというライブラリを使ってconfファイルを使用する 例を紹介します。

以下のような機能を加えてみます。

  1. dispcsv2.confファイルを参照して、動作の設定を可能にする
  2. 表示一時停止判定用の画面サイズを設定できるようにする
  3. 表示一時停止を行うかどうか設定できるようにする
  4. データ総数の表示を行うかどうか設定できるようにする
  5. 各データの条件を設定して一致/部分一致したもののみを表示できるようにする
  6. 文字列の条件比較においては大文字小文字を同一視する
  7. dispcsv2.confファイルが存在しない場合、初期状態が設定されたconfファイルを自動作成する

サンプルプログラムは、先ほど作ったものに手を加えたものです。機能実装の ために追加したコードがほとんどで、大きく構造を変更はしていません。

#define _GNU_SOURCE /* strchrnul関数使用のために必要 */

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

#include <glib.h>

#define MAIN_C
#include "exitfail.h"

/* 表示する文字数 */
#define DISP_WIDTH  60 /* 幅 */
#define DISP_HEIGHT 17 /* 高さ */

/* CSVデータ各要素のサイズ */
#define ZIPCODE_LEN 8
#define STREET_LEN  512
#define CITY_LEN    64
#define PREF_LEN    16
#define FLAG_NUM    6
/* CSVデータ1行の要素数 */
#define COLUMN_NUM  (5 + FLAG_NUM)

/* CSVデータ構造体 */
typedef struct {
        long code;                 /* 全国地方公共団体コード */
        char zipcode[ZIPCODE_LEN]; /* 郵便番号 */
        char street[STREET_LEN];   /* 町域名 */
        char city[CITY_LEN];       /* 市区町村名 */
        char pref[PREF_LEN];       /* 都道府県名 */
        long flag[FLAG_NUM];       /* フラグ配列 */
} csvline_t;

#define BASENAME(p) ((strrchr((p), '/') ? : ((p) - 1)) + 1)

/* conf設定ファイル内で使用するキーワードの定義 */
#define GROUP_DISPLAY "dispaly"
#define KEY_WIDTH     "Width"
#define KEY_HEIGHT    "Height"
#define GROUP_CONTROL "control"
#define KEY_PAUSE     "Pause"
#define KEY_COUNT     "Count"
#define GROUP_DATA    "data"
#define KEY_CODE      "Code"
#define KEY_ZIPCODE   "Zipcode"
#define KEY_STREET    "Street"
#define KEY_CITY      "City"
#define KEY_PREF      "Pref"
#define KEY_FLAG      "Flag"

/* conf設定保持用構造体 */
typedef struct {
        struct { /* 表示設定グループ */
                gint width;  /* 表示幅 */
                gint height; /* 表示高さ */
        } display;
        struct { /* 制御設定グループ */
                gboolean pause; /* 一時停止の有無 */
                gboolean count; /* 総数表示の有無 */
        } control;
        struct { /* データ条件設定グループ */
                gint code;      /* 全国地方公共団体コード */
                gchar *zipcode; /* 郵便番号(部分一致) */
                gchar *street;  /* 町域名(部分一致) */
                gchar *city;    /* 市区町村名(部分一致) */
                gchar *pref;    /* 都道府県名(部分一致) */
                gint *flag;     /* フラグ配列 */
                gsize flag_len; /* フラグ配列要素数 */
        } data;
} config_t;
/* conf設定保持用領域ポインタ */
static config_t *pconf;

/**
 * confファイル読み込み関数
 * @param conffilename confファイル名
 */
static void readconf(char conffilename[])
{
        GKeyFile *keyfile;
        GError *error = NULL;
        static gint flag[FLAG_NUM];
        gsize len;
        gchar *pdata;
        FILE *pconffile;
        int i;

        /* conf設定保持用領域確保 */
        pconf = g_slice_new(config_t);

        /* キーファイル確保 */
        keyfile = g_key_file_new();

        /* confからキーファイル読み込み、失敗した場合は新規作成 */
        if (!g_key_file_load_from_file(keyfile, conffilename,
                                       G_KEY_FILE_KEEP_COMMENTS |
                                       G_KEY_FILE_KEEP_TRANSLATIONS,
                                       &error)) {
                /* デフォルト設定 */
                pconf->display.width = DISP_WIDTH;
                pconf->display.height = DISP_HEIGHT;
                pconf->control.pause = TRUE;
                pconf->control.count = TRUE;
                pconf->data.code = -1;
                pconf->data.zipcode = "";
                pconf->data.street = "";
                pconf->data.city = "";
                pconf->data.pref = "";
                for (i = 0; i < FLAG_NUM; i++)
                        flag[i] = -1;
                pconf->data.flag = flag;
                pconf->data.flag_len = FLAG_NUM;
                /* キーファイル書き込み */
                g_key_file_set_integer(keyfile, GROUP_DISPLAY, KEY_WIDTH,
                                       pconf->display.width);
                g_key_file_set_integer(keyfile, GROUP_DISPLAY, KEY_HEIGHT,
                                       pconf->display.height);
                g_key_file_set_boolean(keyfile, GROUP_CONTROL, KEY_PAUSE,
                                       pconf->control.pause);
                g_key_file_set_boolean(keyfile, GROUP_CONTROL, KEY_COUNT,
                                       pconf->control.count);
                g_key_file_set_integer(keyfile, GROUP_DATA, KEY_CODE,
                                       pconf->data.code);
                g_key_file_set_string(keyfile, GROUP_DATA, KEY_ZIPCODE,
                                      pconf->data.zipcode);
                g_key_file_set_string(keyfile, GROUP_DATA, KEY_STREET,
                                      pconf->data.zipcode);
                g_key_file_set_string(keyfile, GROUP_DATA, KEY_CITY,
                                      pconf->data.city);
                g_key_file_set_string(keyfile, GROUP_DATA, KEY_PREF,
                                      pconf->data.pref);
                g_key_file_set_integer_list(keyfile, GROUP_DATA, KEY_FLAG,
                                            pconf->data.flag,
                                            pconf->data.flag_len);
                /* キーファイルからテキストデータ取得 */
                pdata = g_key_file_to_data(keyfile, &len, &error);
                if (!pdata)
                        g_error(error->message);

                /* 新規ファイルを作成してテキストデータ書き込み */
                pconffile = fopen(conffilename, "w");
                if (!pconffile)
                        exitfail_errno("fopen");
                if (fwrite(pdata, len, 1, pconffile) < 1)
                        exitfail_errno("fwrite");
                fclose(pconffile);
        }
        /* conf読み込みに成功した場合、設定を保持 */
        else {
                pconf->display.width =
                        g_key_file_get_integer(keyfile, GROUP_DISPLAY,
                                               KEY_WIDTH, NULL);
                pconf->display.height =
                        g_key_file_get_integer(keyfile, GROUP_DISPLAY,
                                               KEY_HEIGHT, NULL);
                pconf->control.pause =
                        g_key_file_get_boolean(keyfile, GROUP_CONTROL,
                                               KEY_PAUSE, NULL);
                pconf->control.count =
                        g_key_file_get_boolean(keyfile, GROUP_CONTROL,
                                               KEY_COUNT, NULL);
                pconf->data.code =
                        g_key_file_get_integer(keyfile, GROUP_DATA,
                                               KEY_CODE, NULL);
                pconf->data.zipcode =
                        g_key_file_get_string(keyfile, GROUP_DATA,
                                              KEY_ZIPCODE, NULL);
                pconf->data.street =
                        g_key_file_get_string(keyfile, GROUP_DATA,
                                              KEY_STREET, NULL);
                pconf->data.city =
                        g_key_file_get_string(keyfile, GROUP_DATA,
                                              KEY_CITY, NULL);
                pconf->data.pref =
                        g_key_file_get_string(keyfile, GROUP_DATA,
                                              KEY_PREF, NULL);
                pconf->data.flag =
                        g_key_file_get_integer_list(keyfile, GROUP_DATA,
                                                    KEY_FLAG,
                                                    &pconf->data.flag_len,
                                                    NULL);
        }

        /* キーファイル開放 */
        g_key_file_free(keyfile);
}

/**
 * 行表示関数
 * @param pcsvline 表示するCSVデータ構造体へのポインタ(NULLの場合は表示済データの総数を表示)
 */
static void printline(csvline_t *pcsvline)
{
        static int count = 0;   /* 表示したデータ総数 */
        static int line = 0;    /* 一画面中に表示した行数 */
        int disp_width = DISP_WIDTH, disp_height = DISP_HEIGHT, newline;
        char buf[1024];
        unsigned int i;

        /* CSVデータの各要素を表示 */
        if (pcsvline) {
                /* 郵便番号が入っていない場合は表示しない */
                if (!pcsvline->zipcode[0])
                        return;

                /* 全国地方公共団体コード条件設定があり、一致しなかったら表示しない */
                if (pconf->data.code >= 0 && pcsvline->code != pconf->data.code)
                        return;
                /* 郵便番号条件設定があり、部分一致しなかったら表示しない */
                if (pconf->data.zipcode[0] != '\0' &&
                    !strcasestr(pcsvline->zipcode, pconf->data.zipcode))
                        return;
                /* 町域名条件設定があり、部分一致しなかったら表示しない */
                if (pconf->data.street[0] != '\0' &&
                    !strcasestr(pcsvline->street, pconf->data.street))
                        return;
                /* 市区町村名条件設定があり、部分一致しなかったら表示しない */
                if (pconf->data.city[0] != '\0' &&
                    !strcasestr(pcsvline->city, pconf->data.city))
                        return;
                /* 都道府県条件設定があり、部分一致しなかったら表示しない */
                if (pconf->data.pref[0] != '\0' &&
                    !strcasestr(pcsvline->pref, pconf->data.pref))
                        return;
                for (i = 0; i < FLAG_NUM && i < pconf->data.flag_len; i++) {
                        /* フラグ条件設定があり、一致しなかったら表示しない */
                        if (pconf->data.flag[i] >= 0 &&
                            pcsvline->flag[i] != pconf->data.flag[i])
                                return;
                }

                /* 各要素を表示フォーマットに展開 */
                snprintf(buf, sizeof(buf),
                         "%6d %05ld %s %s %s %s %ld %ld %ld %ld %ld %ld",
                         count++, pcsvline->code, pcsvline->zipcode,
                         pcsvline->street, pcsvline->city, pcsvline->pref,
                         pcsvline->flag[0], pcsvline->flag[1],
                         pcsvline->flag[2], pcsvline->flag[3],
                         pcsvline->flag[4], pcsvline->flag[5]);
        }
        /* CSVデータの総数を表示 */
        else {
                /* 総数表示無効なら表示しない */
                if (!pconf->control.count)
                        return;
                /* データ総数を表示フォーマットに展開 */
                sprintf(buf, "Count: %6d", count);
        }

        /* 今回追加される表示行数を計算 */
        disp_width = pconf->display.width;
        disp_height = pconf->display.height;
        newline = (strlen(buf) + (disp_width - 1)) / disp_width;
        /* 1画面を超える場合、入力があるまで一時停止 */
        if (line + newline >= disp_height) {
                /* 一時停止有効なら */
                if (pconf->control.pause)
                        getchar();
                /* 表示行数を初期化 */
                line = 0;
        }
        /* 実際に表示する */
        printf("%s\n", buf);
        /* 表示行数を更新 */
        line += newline;
}

/**
 * main関数
 * @param argc 引数なしの場合はusage表示のみ
 * @param argv 第1引数として読み込みCSVファイル名を指定
 * @return exit値
 */
int main(int argc, char *argv[])
{
        FILE *pcsvfile; /* CSVファイルポインタ */
        csvline_t csvline; /* CSVデータ */
        char buf[256], *pbuf, *pcol[COLUMN_NUM];
        int i;

        exitfail_init();

        /* 引数が指定されなかった場合、usage表示して終了 */
        if (argc < 2) {
                printf("Usage: %s <csvfile>\n", BASENAME(argv[0]));
                return EXIT_SUCCESS;
        }

        /* confファイル名を作成 */
        snprintf(buf, sizeof(buf), "%s.conf", argv[0]);
        /* confファイルを読み込み */
        readconf(buf);

        /* CSVファイルオープン */
        pcsvfile = fopen(argv[1], "r");
        if (!pcsvfile)
                exitfail_errno("fopen");

        /* CSVデータを初期化 */
        memset(&csvline, 0, sizeof(csvline));
        /* CSVファイルから1行読み込む */
        while (fgets(buf, sizeof(buf), pcsvfile)) {
                /* 各要素へのポインタ配列を初期化 */
                memset(pcol, 0, sizeof(pcol));
                /* 次のカンマ(または行末)を見つけてトークン化 */
                pbuf = strtok(buf, ",\n");
                for (i = 0; i < COLUMN_NUM && pbuf; i++) {
                        /* 前後のダブルクォートは除去する */
                        if (*pbuf == '"')
                                *strchrnul(++pbuf, '"') = '\0';
                        /* 要素へのポインタを保持 */
                        pcol[i] = pbuf;
                        /* 次のカンマ(または行末)を見つけてトークン化 */
                        pbuf = strtok(NULL, ",\n");
                }
                /* 要素数が不足している場合、次行にスキップ */
                if (i < COLUMN_NUM)
                        continue;

                /* 新しいデータの場合 */
                if (strcmp(pcol[1], csvline.zipcode) ||
                    strtol(pcol[8], NULL, 10)) {
                        /* 保持済みのデータを表示 */
                        printline(&csvline);

                        /* CSVデータ各要素を保持 */
                        memset(&csvline, 0, sizeof(csvline));
                        csvline.code = strtol(pcol[0], NULL, 10);
                        strncpy(csvline.zipcode, pcol[1],
                                sizeof(csvline.zipcode) - 1);
                        strncpy(csvline.street, pcol[2],
                                sizeof(csvline.street) - 1);
                        strncpy(csvline.city, pcol[3],
                                sizeof(csvline.city) - 1);
                        strncpy(csvline.pref, pcol[4],
                                sizeof(csvline.pref) - 1);
                        for (i = 0; i < FLAG_NUM; i++)
                                csvline.flag[i] = strtol(pcol[5 + i], NULL, 10);
                }
                /* 既存データへの追加の場合 */
                else
                        /* 町域名の続きを追加 */
                        strncat(csvline.street, pcol[2],
                                (sizeof(csvline.street) -
                                 strlen(csvline.street)) - 1);
        }
        /* 保持済みのデータを表示 */
        printline(&csvline);

        /* CSVファイルクローズ */
        fclose(pcsvfile);

        /* データ総数を表示 */
        printline(NULL);

        return EXIT_SUCCESS;
}

図6.17 CSVファイルの内容を表示するプログラムのconfファイル対応版 (dispcsv2.c)


GLibの詳細なAPI説明については、以下のURLや市販の書籍などを参照してくだ さい。

GLib Reference Manual

ここでは簡単に流れを説明するに留めます。main関数から見ていくと、冒頭で confファイル名を作成してからreadconf関数を呼んでおり、この中でconfファ イルから設定を読み込んでいます。

readconf関数ではまず必要なメモリ領域を確保し、 g_key_file_load_from_file関数でconfファイルを読み込みます。この戻り値で 0が返って来たときはファイルがなかったものとしてデフォルト設定を使用し、 g_key_file_to_dataでテキストデータに変換してから新規confファイルとして 書き込みます。

confファイルが読み込めたときは、pconfから示される領域に各設定を保持しま す。このGLibのconfファイルは、グループによって分類されるキーに対して各 値が設定される形式になっています。例えば一つ目の項目の場合、グループ displayのキーWidthについて1つの数値が設定されています。その後の項目のよ うに1つの文字列や真偽値、複数の数値・文字列を設定するキーを作成すること も可能です。

読み込まれ保持した設定は、printline関数内で表示の制御に使用しています。 ここは事前に決めた仕様どおりに動作を変更しているだけですから、コード内 容を見てください。

GLibを使う時は、makefileにも注意する必要があります。GLib用のヘッダファ イルやライブラリファイルは、標準Cライブラリ向けのものとは違う特別なデ ィレクトリに配置されるため、これをgccに教えてあげなくてはならないので す。それらがどこにあるかわかれば、適切にmakefileに設定してgccに渡るよ うにすれば良いのですが、ATDEにも入っているpkg-configというツールを使っ てこれを簡略化できます。

もし、GLib(ARMアーキテクチャ用)やpkg-configがインストールされていない場合は aptでインストールできます。

[ATDE ~]$ sudo apt install pkg-config libglib2.0-dev:armhf

図6.18 pkg-configとGLibのインストール


CROSS   := arm-linux-gnueabihf

ifneq ($(CROSS),)
CROSS_PREFIX            := $(CROSS)-
PKGCONFIG_LIBDIR        := PKG_CONFIG_LIBDIR=/usr/lib/$(CROSS)/pkgconfig
endif

CC      = $(CROSS_PREFIX)gcc
CFLAGS  = -O2 -Wall -Wextra -I../common

PKGCONFIG_CFLAGS        = `$(PKGCONFIG_LIBDIR) pkg-config --cflags glib-2.0`
PKGCONFIG_LIBS          = `$(PKGCONFIG_LIBDIR) pkg-config --libs glib-2.0`

TARGET  = dispcsv2

all: $(TARGET)

dispcsv2: dispcsv2.c
        $(CC) $(CFLAGS) $(PKGCONFIG_CFLAGS) -o $@ $< $(PKGCONFIG_LIBS)

図6.19 dispcsv2のためのMakefile


指定されたCROSS(=アーキテクチャ名)からPKGCONFIG_LIBDIRを作っています。こ れが、目的のクロス環境pkgconfig情報があるパスになります。この PKGCONFIG_LIBDIRが定義された状態でpkg-configコマンドを実行すると、CFLAGS 用のオプション(-I<dir>のヘッダファイルパス)や、 LIBS(ライブラリ名)を教えてくれるのです。こうしてGLibを使う場合も、それ なりにシンプルにmakefileを書くことができます。

これを使ってmakeし、プログラムを実行してみます。

[armadillo ~]# ./dispcsv2 ken_all_rome.csv
     0 01101 0600000 IKANIKEISAIGANAIBAAI CHUO-KU SAPPORO-SHI HOKKAIDO 0 0 0 0 0 0
     1 01101 0640941 ASAHIGAOKA CHUO-KU SAPPORO-SHI HOKKAIDO 0 0 1 0 0 0
     2 01101 0600041 ODORIHIGASHI CHUO-KU SAPPORO-SHI HOKKAIDO 0 0 1 0 0 0
     3 01101 0600042 ODORINISHI(1-19-CHOME) CHUO-KU SAPPORO-SHI HOKKAIDO 1 0 1 0 0 0
     4 01101 0640820 ODORINISHI(20-28-CHOME) CHUO-KU SAPPORO-SHI HOKKAIDO 1 0 1 0 0 0
     5 01101 0600031 KITA1-JOHIGASHI CHUO-KU SAPPORO-SHI HOKKAIDO 0 0 1 0 0 0
     6 01101 0600001 KITA1-JONISHI(1-19-CHOME) CHUO-KU SAPPORO-SHI HOKKAIDO 1 0 1 0 0 0
     7 01101 0640821 KITA1-JONISHI(20-28-CHOME) CHUO-KU SAPPORO-SHI HOKKAIDO 1 0 1 0 0 0

図6.20 dispcsv2の実行結果


1回目の動作は先ほどのものとまったく変わりませんが、Ctrl+Cで終了すると dispcsv2.confファイルができています。

[dispaly]
Width=60
Height=17

[control]
Pause=true
Count=true

[data]
Code=-1
Zipcode=
Street=
City=
Pref=
Flag=-1;-1;-1;-1;-1;-1;

角括弧で囲われた単語がグループ、その下にイコール記号を使って値が設定 されている単語がキーになります。以下のように動作を変更させてみます。

  1. 画面サイズは80x24
  2. 一時停止しない
  3. 町域名にKOKUBUNJIを含んだもののみ出力する
[dispaly]
Width=80
Height=24

[control]
Pause=false
Count=true

[data]
Code=-1
Zipcode=
Street=kokubunji
City=
Pref=
Flag=-1;-1;-1;-1;-1;-1;

テキストエディタでこのようにdispcsv2.confを 変更して実行すると、以下のように動作します。

dispcsv2.confを編集したdispcsv2の実行結果. 

[armadillo ~]# ./dispcsv2 ken_all_rome.csv
     0 09216 3290417 KOKUBUNJI SHIMOTSUKE-SHI TOCHIGI 0 0 0 0 0 0
     1 12219 2900071 KITAKOKUBUNJIDAI ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     2 12219 2900073 KOKUBUNJIDAICHUO ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     3 12219 2900072 NISHIKOKUBUNJIDAI ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     4 12219 2900074 HIGASHIKOKUBUNJIDAI ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     5 12219 2900075 MINAMIKOKUBUNJIDAI ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     6 14215 2430413 KOKUBUNJIDAI EBINA-SHI KANAGAWA 0 0 1 0 0 0
     7 15222 9420088 BISHAMONKOKUBUNJI JOETSU-SHI NIIGATA 0 0 0 0 0 0
     8 15224 9520304 KOKUBUNJI SADO-SHI NIIGATA 0 0 0 0 0 0
     9 27127 5310064 KOKUBUNJI KITA-KU OSAKA-SHI OSAKA 0 0 1 0 0 0
    10 28201 6710234 MIKUNINOCHO KOKUBUNJI HIMEJI-SHI HYOGO 0 0 0 0 0 0
    11 28209 6695341 HIDAKACHO KOKUBUNJI TOYOKA-SHI HYOGO 0 0 0 0 0 0
    12 31201 6800155 KOKUFUCHO KOKUBUNJI TOTTORI-SHI TOTTORI 0 0 0 0 0 0
    13 31203 6820943 KOKUBUNJI KURAYOSHI-SHI TOTTORI 0 0 0 0 0 0
    14 33203 7080843 KOKUBUNJI TSUYAMA-SHI OKAYAMA 0 0 0 0 0 0
    15 35206 7470021 KOKUBUNJICHO HOFU-SHI YAMAGUCHI 0 0 0 0 0 0
    16 37201 7690105 KOKUBUNJICHO KASHIHARA TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    17 37201 7690102 KOKUBUNJICHO KOKUBU TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    18 37201 7690104 KOKUBUNJICHO SHIMMYO TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    19 37201 7690101 KOKUBUNJICHO NII TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    20 37201 7690103 KOKUBUNJICHO FUKE TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    21 46215 8950073 KOKUBUNJICHO SATSUMASENDAI-SHI KAGOSHIMA 0 0 0 0 0 0
Count:     22

6.3.3. バイナリファイルを扱う

バイナリファイルを扱う例として、Windowsで使われるBMP形式の画像ファイル を表示してみます。とはいっても、グラフィック画面は使用しません。コンソー ル上のみで実行できるように、エスケープシーケンスを使用したカラー対応の アスキーアート(文字を使った擬似画像)表示サンプルプログラムです。

#ifndef BITMAP_H
#define BITMAP_H

#include <stdint.h>

/* ビットマップファイルヘッダ構造体(オリジナル) */
/*
typedef struct tagBITMAPFILEHEADER {
        uint16_t bfType;
        uint32_t bfSize;        // アラインメント不正(2番地から始まる4バイト変数)
        uint16_t bfReserved1;
        uint16_t bfReserved2;
        uint32_t bfOffBits;     // アラインメント不正(10番地から始まる4バイト変数)
} BITMAPFILEHEADER, *PBITMAPFILEHEADER;
*/

/* ビットマップファイルヘッダ構造体 */
typedef struct tagBITMAPFILEHEADER {
        uint16_t bfType;
        uint16_t bfSize_l;      // Size下位16ビット
        uint16_t bfSize_h;      // Size上位16ビット
        uint16_t bfReserved1;
        uint16_t bfReserved2;
        uint16_t bfOffBits_l;   // OffBits下位16ビット
        uint16_t bfOffBits_h;   // OffBits上位16ビット
} BITMAPFILEHEADER, *PBITMAPFILEHEADER;

/* ビットマップファイルヘッダメンバ取得マクロ */
/* SizeとOffBitsは上位16bitと下位16bitを別々に取得して32bitに合成する */
#define BITMAPFILEHEADER_TYPE(pbf)      (((PBITMAPFILEHEADER)(pbf))->bfType)
#define BITMAPFILEHEADER_SIZE(pbf)                                      \
        (((uint32_t)((PBITMAPFILEHEADER)(pbf))->bfSize_h << 16) |       \
         ((uint32_t)((PBITMAPFILEHEADER)(pbf))->bfSize_l      ))
#define BITMAPFILEHEADER_OFFBITS(pbf)                                   \
        (((uint32_t)((PBITMAPFILEHEADER)(pbf))->bfOffBits_h << 16) |    \
         ((uint32_t)((PBITMAPFILEHEADER)(pbf))->bfOffBits_l      ))

/* ビットマップファイルタイプ識別子 */
#define BF_TYPE (*(uint16_t *)"BM")

/* ビットマップ情報ヘッダ構造体 */
typedef struct tagBITMAPINFOHEADER {
        uint32_t biSize;
        int32_t  biWidth;
        int32_t  biHeight;
        uint16_t biPlanes;
        uint16_t biBitCount;
        uint32_t biCompression;
        uint32_t biSizeImage;
        int32_t  biXPelsPerMeter;
        int32_t  biYPelsPerMeter;
        uint32_t biClrUsed;
        uint32_t biClrImportant;
} BITMAPINFOHEADER, *PBITMAPINFOHEADER;

/* ビットマップ情報ヘッダメンバ取得マクロ */
#define BITMAPINFOHEADER_SIZE(pbi)      (((PBITMAPINFOHEADER)(pbi))->biSize)
#define BITMAPINFOHEADER_WIDTH(pbi)     (((PBITMAPINFOHEADER)(pbi))->biWidth)
#define BITMAPINFOHEADER_HEIGHT(pbi)    (((PBITMAPINFOHEADER)(pbi))->biHeight)
#define BITMAPINFOHEADER_PLANES(pbi)    (((PBITMAPINFOHEADER)(pbi))->biPlanes)
#define BITMAPINFOHEADER_BITCOUNT(pbi)  (((PBITMAPINFOHEADER)(pbi))->biBitCount)
#define BITMAPINFOHEADER_COMPRESSION(pbi)       \
        (((PBITMAPINFOHEADER)(pbi))->biCompression)
#define BITMAPINFOHEADER_SIZEIMAGE(pbi) \
        (((PBITMAPINFOHEADER)(pbi))->biSizeImage)
#define BITMAPINFOHEADER_XPELSPERMETER(pbi)     \
        (((PBITMAPINFOHEADER)(pbi))->biXPelsPerMeter)
#define BITMAPINFOHEADER_YPELSPERMETER(pbi)     \
        (((PBITMAPINFOHEADER)(pbi))->biYPelsPerMeter)
#define BITMAPINFOHEADER_CLRUSED(pbi)   (((PBITMAPINFOHEADER)(pbi))->biClrUsed)
#define BITMAPINFOHEADER_CLRIMPORTANT(pbi)      \
        (((PBITMAPINFOHEADER)(pbi))->biClrImportant)

#endif /* BITMAP_H */

図6.21 BMPファイル形式構造定義ヘッダファイル(bitmap.h)


#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAIN_C
#include "exitfail.h"

/* ビットマップファイル形式用ヘッダ */
#include "bitmap.h"

/* 読み込み可能なビットマップファイルの条件(固定値) */
#define BMP_PLANES      1  /* プレーン数 */
#define BMP_BITCOUNT    24 /* 色深度 */
#define BMP_COMPRESSION 0  /* 圧縮方式 */

/* 読み込む最大画素サイズ */
#define MAX_WIDTH    480 /* 幅 */
#define MAX_HEIGHT   272 /* 高さ */
/* キャラクタ化する画素単位 */
#define PIXEL_WIDTH  8  /* 幅 */
#define PIXEL_HEIGHT 16 /* 高さ */
/* 表示するキャラクタ個数 */
#define DISP_WIDTH   (MAX_WIDTH / PIXEL_WIDTH)   /* 幅 */
#define DISP_HEIGHT  (MAX_HEIGHT / PIXEL_HEIGHT) /* 高さ */

/* 色変換用境界値 */
#define BOUNDARY_FG (0x55 * PIXEL_WIDTH * PIXEL_HEIGHT) /* 前景 */
#define BOUNDARY_BG (0xaa * PIXEL_WIDTH * PIXEL_HEIGHT) /* 背景 */

/* 色変換用構造体 */
/* 同一キャラクタに当たる複数の画素について各色を加算していき、
   境界値と比較して色変換を行う */
typedef struct {
        uint16_t r; /* 赤 */
        uint16_t g; /* 緑 */
        uint16_t b; /* 青 */
} color_t;

#define BASENAME(p)   ((strrchr((p), '/') ? : ((p) - 1)) + 1)

/**
 * main関数
 * @param argc 引数なしの場合はusage表示のみ
 * @param argv 第1引数として読み込みビットマップファイル名を指定
 * @return exit値
 */
int main(int argc, char *argv[])
{
        FILE *pbmpfile; /* ビットマップファイルポインタ */
        BITMAPFILEHEADER bf; /* ビットマップファイルヘッダ */
        uint32_t offbits; /* ファイル先頭から画素データへのオフセットサイズ */
        BITMAPINFOHEADER bi; /* ビットマップ情報ヘッダ */
        int32_t bmp_width, bmp_height; /* ビットマップファイル画素サイズ */
        color_t pixels[DISP_HEIGHT][DISP_WIDTH]; /* 色変換用画素データ蓄積 */
        uint8_t buf[MAX_WIDTH][3], *ptmp; /* 画素データ読み込み用バッファ */
        int dx, dy, px, py; /* 画素シーク位置 */

        exitfail_init();

        /* 引数が指定されなかった場合、usage表示して終了 */
        if (argc < 2) {
                printf("Usage: %s <bmpfile>\n", BASENAME(argv[0]));
                return 0;
        }

        /* ビットマップファイルオープン */
        pbmpfile = fopen(argv[1], "r");
        if (!pbmpfile)
                exitfail_errno("fopen");

        /* ビットマップファイルヘッダ読み込み */
        if (fread(&bf, sizeof(bf), 1, pbmpfile) < 1)
                exitfail_errno("fread");
        /* ビットマップファイルタイプが正しい識別子か */
        if (BITMAPFILEHEADER_TYPE(&bf) != BF_TYPE)
                exitfail("Bad bitmap file header type\n");
        /* ビットマップヘッダサイズが十分か */
        offbits = BITMAPFILEHEADER_OFFBITS(&bf);
        if (offbits < sizeof(bf) + sizeof(bi))
                exitfail("Bad bitmap header size\n");

        /* ビットマップ情報ヘッダ読み込み */
        if (fread(&bi, sizeof(bi), 1, pbmpfile) < 1)
                exitfail_errno("fread");
        /* ビットマップ情報ヘッダサイズが十分か */
        if (BITMAPINFOHEADER_SIZE(&bi) < sizeof(bi))
                exitfail("Bad bitmap info header size\n");
        /* 読み込み可能なビットマップファイルか */
        if (BITMAPINFOHEADER_PLANES(&bi) != BMP_PLANES ||
            BITMAPINFOHEADER_BITCOUNT(&bi) != BMP_BITCOUNT ||
            BITMAPINFOHEADER_COMPRESSION(&bi) != BMP_COMPRESSION)
                exitfail("Bad bitmap type\n");
        /* ビットマップサイズが十分か */
        bmp_width = BITMAPINFOHEADER_WIDTH(&bi);
        bmp_height = BITMAPINFOHEADER_HEIGHT(&bi);
        if (bmp_width < MAX_WIDTH || bmp_height < MAX_HEIGHT)
                exitfail("Bad bitmap size\n");

        /* 画素データ蓄積配列をゼロクリア */
        memset(pixels, 0, sizeof(pixels));

        /* 読み込む最初の画素(MAX_HEIGHT-1行目)までシーク */
        if (fseek(pbmpfile, offbits + 3 * bmp_width * (bmp_height - MAX_HEIGHT),
                  SEEK_SET) < 0)
                exitfail_errno("fseek");
        /* 最終行から順に画素データを蓄積 */
        for (dy = DISP_HEIGHT - 1; dy >= 0; dy--)
                for (py = PIXEL_HEIGHT - 1; py >= 0; py--) {
                        /* 画素データを1行分読み込む */
                        if (fread(buf, sizeof(buf), 1, pbmpfile) < 1)
                                exitfail_errno("fread");
                        /* 画素データを加算していく */
                        for (dx = 0; dx < DISP_WIDTH; dx++)
                                for (px = 0; px < PIXEL_WIDTH; px++) {
                                        ptmp = buf[PIXEL_WIDTH * dx + px];
                                        pixels[dy][dx].r += (uint16_t)ptmp[2];
                                        pixels[dy][dx].g += (uint16_t)ptmp[1];
                                        pixels[dy][dx].b += (uint16_t)ptmp[0];
                                }
                        /* 次行先頭画素までシーク */
                        if (fseek(pbmpfile, 3 * bmp_width - sizeof(buf),
                                  SEEK_CUR) < 0)
                                exitfail_errno("fseek");
                }

        /* ビットマップファイルクローズ */
        fclose(pbmpfile);

        /* 先頭行から順にキャラクタを描画していく */
        for (dy = 0; dy < DISP_HEIGHT; dy++) {
                printf("\n");
                for (dx = 0; dx < DISP_WIDTH; dx++) {
                        /* 境界値と比較して
                           色付けエスケープシーケンスとキャラクタを出力 */
                        printf("\x1b[3%dm\x1b[4%dm%c",
                               ((pixels[dy][dx].r > BOUNDARY_FG) ? 1 : 0) |
                               ((pixels[dy][dx].g > BOUNDARY_FG) ? 2 : 0) |
                               ((pixels[dy][dx].b > BOUNDARY_FG) ? 4 : 0),
                               ((pixels[dy][dx].r > BOUNDARY_BG) ? 1 : 0) |
                               ((pixels[dy][dx].g > BOUNDARY_BG) ? 2 : 0) |
                               ((pixels[dy][dx].b > BOUNDARY_BG) ? 4 : 0),
                               ((dx ^ dy) & 1) ? '_' : '/');
                }
                /* 色付けをクリアするエスケープシーケンスを出力 */
                printf("\x1b[0m");
        }
        /* 入力があるまで一時停止 */
        getchar();

        return 0;
}

図6.22 BMP形式画像ファイルのコンソール表示プログラム(dispbmp.c)


bitmap.hは、BMPファイル形式のヘッダを解析す るための構造体を定義したヘッダファイルです。 dispbmp.cのmain関数と平行して、見ていきます 。

main関数は、まず引数で指定されたbmpファイルをオープンし、そして BITMAPFILEHEADER構造体分のデータを読み込んでヘッダの中身を解析していき ます。まずはTYPEが正しくBMであることから順次チェックしていくのですが、 2番目の要素であるSIZEの読み込みで少々困ったことになります。

BITMAPFILEHEADER構造体の先頭の要素であるbfTypeが2バイトであり、次の bfSizeが4バイトであるため、構造体メンバとして直接参照しようとするとア ドレス2から4バイト参照することになり、ARMアーキテクチャでは正しく値を 読むことができません。このため、このサンプルではbfSizeメンバを4バイトと して直接定義せず、bfSize_l/bfSize_hと上位下位2バイトずつのメンバとして 分断させ、別々に読み込んだものを一つの4バイトとして扱うためのサポートマ クロBITMAPFILEHEADER_SIZEを用意する方法を取りました。こうすることで、呼 び出し側は分断されたデータであることを意識することなく、要素の比較がで きます。

ヘッダ2種類の解析の後、その情報に基づいた形で画像データを読み込んでいき ます。アスキーアート化するため、いくつかの並んだ画素を一つとしてRGB別に 配列に蓄えていき、この値の大きさで出力する色を決定します。色付けの決定 方法は、ここでは本筋から外れますので省略します。

最終段の出力は、printfで行っています。色付けはエスケープシーケンスとい う手法を用いており、0x1bに相当するコントロールコードの後に前景/背景と色 を指定するための情報を付加して出力します。

サンプルBMPファイルのmadillon.bmpを指定して 実行すると、以下のように出力されます。

[ATDE ~]$ ./dispbmp midomadillo.bmp
dispbmp の実行結果

図6.23 dispbmp の実行結果


6.4. デバイスの操作

「ファイルの種類」で説明したように、Linuxシステムを含むUNIXシ ステムでは、すべてをファイルとして表現します。

Armadilloというハードウェアが持つ、シリアルインターフェースやGPIO、LED 、スイッチなどのデバイスも例外ではありません。Linuxカーネルは、これら のデバイスをファイルとして扱えるように抽象化します。

本章では、このようなファイルを扱う方法について説明します。

6.4.1. デバイスファイルを使う

デバイスを抽象化したファイルで最も一般的なものは、デバイスファイル [34]です。

デバイスファイルには、キャラクタデバイスとブロックデバイスの2種類があり ます。それぞれの特徴は、「ファイルの種類」を参照してください。

デバイスファイルは、通常、/devディレクトリ 以下にあります。ls -lを実行したときに、一番左 に表示される文字がcのファイルがキャラクタデバイスで、bがブロックデバイ スです。

デバイスファイルをC言語で扱うには、通常ファイルと同様に、 open、close、read、writeシステムコールを使用します。

また、デバイスファイル特有のシステムコールとして、ioctlがあります。 ioctlでは、デバイスのパラメータを変更するなど、通常のread/write操作と は馴染まないデバイスへの操作を行うために使用されます。ioctlの使い方は 、対象となるデバイスによって異なります。

デバイスファイルを扱う例は、「シリアルポートの入出力」で説明します。

6.4.2. sysfsファイルシステムを使う

sysfsファイルシステムは、procファイルシステムに似た特殊ファイルシステ ムです。ユーザーランドアプリケーションは、sysfsファイルシステムを通し て、カーネル内部のデータ構造にアクセスできます。sysfsファイルシステム が提供するファイルのいくつかは、物理的なデバイスに対応しており、それら に対して読み書きすることで、デバイスを制御することができます。

sysfsファイルシステムは、通常、/sysディレク トリにマウントします。

sysfsを扱う例として、LEDの制御について説明します。

LEDは、LinuxシステムではLEDクラスとして汎用化されています。LEDクラスと して登録されたLEDに対する操作は、 /sys/class/leds/以下のディレクトリによって 行います。/sys/class/leds/(LED名)/brightness という名前のファイルに対して、0という文字を書き込むとLEDが消灯します。 また、1を書き込むと点灯します。詳しい仕様は「Armadilloシリーズソフ トウェアマニュアル」を参照してください。

Armadillo-600シリーズでは、red、green、yellowの3個のLEDをLEDクラスとし て扱えます。

シェルからLEDを点灯/消灯する例を、以下に示します。

[armadillo ~]# echo 1 > /sys/class/leds/red/brightness    1
[armadillo ~]# echo 0 > /sys/class/leds/red/brightness    2

図6.24 シェルからLEDを点灯/消灯する


1

red LED を点灯します

2

red LED を消灯します

C言語でのsysfsファイルシステムのファイルの扱いは、通常ファイルと同じで す。LEDを扱う簡単な例を、図6.25「LEDの点灯/消灯を行うプログラム(led_on_off.c)」に示します。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <limits.h>

#define LED_CLASS_PATH "/sys/class/leds"

int main(int argc, char *argv[])
{
        int fd;
        char path[PATH_MAX];
        ssize_t len;

        if (argc < 3) {
                printf("usage: %s <led name> <brightness>\n", argv[0]);
                return EXIT_SUCCESS;
        }

        snprintf(path, PATH_MAX, "%s/%s/brightness",
                 LED_CLASS_PATH, argv[1]);

        fd = open(path, O_WRONLY);
        if (fd == -1) {
                perror("open");
                return EXIT_FAILURE;
        }

        len = write(fd, argv[2], strlen(argv[2]));
        if (len == -1) {
                perror("write");
                return EXIT_FAILURE;
        }

        close(fd);

        return EXIT_SUCCESS;
}

図6.25 LEDの点灯/消灯を行うプログラム(led_on_off.c)


[armadillo ~]# ./led_on_off red 1       1
[armadillo ~]# ./led_on_off red 0       2
[armadillo ~]# ./led_on_off green 1     3
[armadillo ~]# ./led_on_off green 0     4

図6.26 led_on_off の実行例


1

red LED を点灯します

2

red LED を消灯します

3

green LED を点灯します

4

green LED を消灯します

6.5. シリアルポートの入出力

シリアルポートで入出力を行うプログラムを作ってみます。

Linuxでは、テキストデータを扱うコンソール端末用として、行単位に文字を 扱ったり自動的に変換したりしたりしてくれるカノニカルモードがあるのです が、意図したデータをそのまま転送したい場合には不都合です。バイナリデー タも自由に扱えるようにするには非カノニカルモードに設定する必要がありま す。ここでは非カノニカルモードを使います。

6.5.1. シリアルエコーサーバー

シリアルポートから受け取ったデータをそのまま返してくれる、エコーサーバ ーを作ってみます。

  1. 9600bpsで接続する。
  2. 8bitデータ、パリティなし、フロー制御なし

まずはこの条件だけの、シンプルなものにします。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <termios.h>
#include <unistd.h>

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

#define MAIN_C
#include "exitfail.h"

#define SERIAL_BAUDRATE B9600

#define BUF_SIZE 256

static int serial_fd = -1; /* シリアルポートファイルディスクリプタ */
static struct termios old_tio; /* 元のシリアルポート設定 */

static int terminated = 0; /* 終了シグナル発生フラグ */

#define BASENAME(p)   ((strrchr((p), '/') ? : ((p) - 1)) + 1)
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

#define __unused __attribute__((unused))

/**
 * 終了シグナルハンドラ
 * @param signum シグナル番号(不使用)
 */
static void terminate_sig_handler(__unused int signum)
{
        /* 終了シグナル発生を記録 */
        terminated = 1;
}

/**
 * シグナルハンドラ設定関数
 * @param sig_list ハンドラを設定するシグナルのリスト
 * @param num シグナルリストの要素数
 * @param handler 設定するハンドラ関数
 */
static void set_sig_handler(int sig_list[], ssize_t num, __sighandler_t handler)
{
        struct sigaction sa;
        int i;

        /* ハンドラ関数を設定 */
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = handler;

        /* 各シグナルに対して関連付け */
        for(i = 0; i < num; i++)
                if (sigaction(sig_list[i], &sa, NULL) < 0)
                        exitfail_errno("sigaction");
}

/**
 * シリアルポート設定復元関数
 */
static void restore_serial(void)
{
        int ret;

        /* シリアルポートの設定を元に戻す */
        ret = tcsetattr(serial_fd, TCSANOW, &old_tio);
        if (ret < 0)
                exitfail_errno("tcsetattr");
}

/**
 * シリアルポート設定関数
 * @param fd 設定するシリアルポートファイルディスクリプタ
 */
static void setup_serial(int fd)
{
        struct termios tio;
        int ret;

        /* 現在のシリアルポートの設定を退避する */
        ret = tcgetattr(fd, &old_tio);
        if (ret)
                exitfail_errno("tcgetattr");

        /* 終了時に設定を復元するための関数を登録 */
        if (atexit(restore_serial))
                exitfail_errno("atexit");

        /* 新しいシリアルポートの設定 */
        memset(&tio, 0, sizeof(tio));
        tio.c_iflag = IGNBRK | IGNPAR; /* ブレーク文字無視/パリティなし */
        tio.c_cflag = CS8 | CLOCAL | CREAD; /* フロー制御なし/8bit/非モデム/受信可 */
        tio.c_cc[VTIME] = 0; /* キャラクタ間タイマー無効 */
        tio.c_cc[VMIN] = 1; /* 最低1文字送信/受信するまでブロックする */
        ret = cfsetspeed(&tio, SERIAL_BAUDRATE); /* 入出力ボーレート */
        if (ret < 0)
                exitfail_errno("cfsetspeed");

        /* バッファ内のデータをフラッシュ */
        ret = tcflush(fd, TCIFLUSH);
        if (ret < 0)
                exitfail_errno("tcflush");

        /* 新しいシリアルポート設定を適用 */
        ret = tcsetattr(fd, TCSANOW, &tio);
        if (ret)
                exitfail_errno("tcsetattr");
}

/**
 * main関数
 * @param argc 引数なしの場合はusage表示のみ
 * @param argv 第1引数としてシリアルデバイス名を指定
 * @return exit値
 */
int main(int argc, char *argv[])
{
        int terminate_sig_list[] = { /* 終了シグナル種類 */
                SIGHUP, SIGINT, SIGQUIT, SIGPIPE, SIGTERM
        };
        char buf[BUF_SIZE];
        ssize_t ret, len, wrlen;

        exitfail_init();

        /* 引数が指定されなかった場合、usage表示して終了 */
        if (argc < 2) {
                printf("Usage: %s <device>\n", BASENAME(argv[0]));
                return EXIT_SUCCESS;
        }

        /* シリアルポートを読み書き可能な非制御端末としてオープン */
        serial_fd = open(argv[1], O_RDWR | O_NOCTTY);
        if (serial_fd < 0)
                exitfail_errno("open");

        /* 終了シグナルに対してハンドラを設定 */
        set_sig_handler(terminate_sig_list, ARRAY_SIZE(terminate_sig_list),
                        terminate_sig_handler);

        /* シリアルポートを設定 */
        setup_serial(serial_fd);

        /* 終了シグナルが発生していない限りループ */
        while (!terminated) {
                /* シリアルポートから読み込み */
                ret = read(serial_fd, buf, BUF_SIZE);
                if (ret < 0) {
                        /* シグナル発生時はリトライ */
                        if (errno == EINTR)
                                continue;
                        exitfail_errno("read");
                }
                len = ret;

                /* すべてのデータを書き込むまでループ(終了シグナル発生で中断) */
                for (wrlen = 0; wrlen < len && !terminated; wrlen += ret) {
                        /* シリアルポートに書き込み */
                        ret = write(serial_fd, buf + wrlen, len - wrlen);
                        if (ret < 0) {
                                if (errno == EINTR) {
                                        /* シグナル発生時はリトライ */
                                        ret = 0;
                                        continue;
                                }
                                exitfail_errno("write");
                        }
                }
        }

        return EXIT_SUCCESS;
}

図6.27 シリアルエコーサーバー(serial_echo_server1.c)


ポイントがいくつかあります。まず先に、シリアルポートの設定を行っている ところを見てみます。setup_serial関数がそれですが、ここでstruct termios 構造体tioのメンバに対して値を書き込んでいるところが重要です。c_iflagや c_cflagに対して、8bit/パリティなし/フロー制御なしであること、また非カ ノニカルモードにすることを意識して、適切な値を書かなければなりません。 ボーレートの設定については、それらとは別にcfsetspeed関数を使います。

tcsetattr関数で、作成したtio状態を実際にシリアルデバイスに反映させるの ですが、ここで1点重要なことがあります。この設定はこのプログラム内のみ ならずシステム全体に影響してしまうものであり、プログラムが正常に終了し たり、また不正な終了や(Ctrl+C入力によるような)強制終了が発生した場合、 初期状態に戻ってはくれません。つまり、行儀よくバグの少ないコードを書こ うとするならば、どのような終了時であっても初期状態に戻してあげるような 注意が必要なのです。

このプログラムでは、atexit関数とシグナルハンドラを使ってこれを実現して います。setup_serial関数内で呼ばれているatexit関数は、exitされる時 (main関数からのreturn時も含みます)に呼ばれて欲しいハンドラ関数を登録す るためのものです。これを使ってrestore_serial関数が登録されていますので 、終了時には必ずシリアルポート設定の状態が復帰されます。また、いくつか の不正/強制終了に対応するために、set_sig_handleでシグナルハンドラを登 録しています。

もう1つのポイントは、read/write関数のエラー処理でしょう。これらの関数 は、シグナルが発生された時に中断して、戻り値-1となることがあります。こ のプログラムの目的としては、これを致命的なエラーとして扱うのは適切では ありません。このため、errnoがEINT(シグナル発生による中断を表す)であっ た場合はcontinueしてリトライするような作りにしています。なお、この際に Ctrl+Cによる中断シグナル(SIGINT)などであった場合には、グローバル変数 terminatedをチェックしてきちんとループを抜けるようにしてあります。

空いている(現在コンソールとして使っていない)シリアルポートと、PCの空いて いるシリアルポート(もちろんUSBシリアルデバイスでも構いません)をクロス ケーブルで接続し、PC側ではTera Termを立ち上げます。ここでの設定は、プ ログラムに合わせ9600bps/8bit/パリティなし/フロー制御なしとします。

この状態で、シリアルポート名を引数として 図6.28「serial_echo_server1の実行例」を実行します。

[armadillo ~]# ./serial_echo_server1 /dev/ttymxc2

図6.28 serial_echo_server1の実行例


Tera Termから入力した文字が、そのまま返ってくるのが確認できます。

6.5.2. 改行コードの違いを吸収する

先ほどのシリアルエコーサーバーをWindows版Tera Termで試すと、改行した際 に次の行に行かず、現在入力している行の先頭から上書きされるような状態に なってしまいます。これは、LinuxとWindowsで改行を表すコードが異なるため です。

Linuxでは、改行コードとしてLF(ラインフィード)、バイナリで表すと0x0a [35]を使用します。これに対し、Windowsでは改行コードとしてCR(キャリッジ リターン)+LF、バイナリで表すと0x0d[36], 0x0aという連続2文字を使用します 。さらにTera Termのデフォルトの状態は、改行コードとしてCRの1文字のみを 送出する状態になっています[37]

先ほどのプログラムをちょっと改造して、この違いを吸収してみましょう。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <termios.h>
#include <unistd.h>

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

#define MAIN_C
#include "exitfail.h"

#define SERIAL_BAUDRATE B9600

#define BUF_SIZE  256
#define READ_SIZE (BUF_SIZE / 2)

static int serial_fd = -1; /* シリアルポートファイルディスクリプタ */
static struct termios old_tio; /* 元のシリアルポート設定 */

static int terminated = 0; /* 終了シグナル発生フラグ */

#define BASENAME(p)   ((strrchr((p), '/') ? : ((p) - 1)) + 1)
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

#define __unused __attribute__((unused))

/**
 * 終了シグナルハンドラ
 * @param signum シグナル番号(不使用)
 */
static void terminate_sig_handler(__unused int signum)
{
        /* 終了シグナル発生を記録 */
        terminated = 1;
}

/**
 * シグナルハンドラ設定関数
 * @param sig_list ハンドラを設定するシグナルのリスト
 * @param num シグナルリストの要素数
 * @param handler 設定するハンドラ関数
 */
static void set_sig_handler(int sig_list[], ssize_t num, __sighandler_t handler)
{
        struct sigaction sa;
        int i;

        /* ハンドラ関数を設定 */
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = handler;

        /* 各シグナルに対して関連付け */
        for(i = 0; i < num; i++)
                if (sigaction(sig_list[i], &sa, NULL) < 0)
                        exitfail_errno("sigaction");
}

/**
 * シリアルポート設定復元関数
 */
static void restore_serial(void)
{
        int ret;

        /* シリアルポートの設定を元に戻す */
        ret = tcsetattr(serial_fd, TCSANOW, &old_tio);
        if (ret < 0)
                exitfail_errno("tcsetattr");
}

/**
 * シリアルポート設定関数
 * @param fd 設定するシリアルポートファイルディスクリプタ
 */
static void setup_serial(int fd)
{
        struct termios tio;
        int ret;

        /* 現在のシリアルポートの設定を退避する */
        ret = tcgetattr(fd, &old_tio);
        if (ret)
                exitfail_errno("tcgetattr");

        /* 終了時に設定を復元するための関数を登録 */
        if (atexit(restore_serial))
                exitfail_errno("atexit");

        /* 新しいシリアルポートの設定 */
        memset(&tio, 0, sizeof(tio));
        tio.c_iflag = IGNBRK | IGNPAR; /* ブレーク文字無視/パリティなし */
        tio.c_cflag = CS8 | CLOCAL | CREAD; /* フロー制御なし/8bit/非モデム/受信可 */
        tio.c_cc[VTIME] = 0; /* キャラクタ間タイマー無効 */
        tio.c_cc[VMIN] = 1; /* 最低1文字送信/受信するまでブロックする */
        ret = cfsetspeed(&tio, SERIAL_BAUDRATE); /* 入出力ボーレート */
        if (ret < 0)
                exitfail_errno("cfsetspeed");

        /* バッファ内のデータをフラッシュ */
        ret = tcflush(fd, TCIFLUSH);
        if (ret < 0)
                exitfail_errno("tcflush");

        /* 新しいシリアルポート設定を適用 */
        ret = tcsetattr(fd, TCSANOW, &tio);
        if (ret)
                exitfail_errno("tcsetattr");
}

/**
 * main関数
 * @param argc 引数なしの場合はusage表示のみ
 * @param argv 第1引数としてシリアルデバイス名を指定
 * @return exit値
 */
int main(int argc, char *argv[])
{
        int terminate_sig_list[] = { /* 終了シグナル種類 */
                SIGHUP, SIGINT, SIGQUIT, SIGPIPE, SIGTERM
        };
        char buf[BUF_SIZE];
        ssize_t ret, len, wrlen;
        int lf_flag = 0; /* LF挿入処理発生フラグ */
        int i;

        exitfail_init();

        /* 引数が指定されなかった場合、usage表示して終了 */
        if (argc < 2) {
                printf("Usage: %s <device>\n", BASENAME(argv[0]));
                return EXIT_SUCCESS;
        }

        /* シリアルポートを読み書き可能な非制御端末としてオープン */
        serial_fd = open(argv[1], O_RDWR | O_NOCTTY);
        if (serial_fd < 0)
                exitfail_errno("open");

        /* 終了シグナルに対してハンドラを設定 */
        set_sig_handler(terminate_sig_list, ARRAY_SIZE(terminate_sig_list),
                        terminate_sig_handler);

        /* シリアルポートを設定 */
        setup_serial(serial_fd);

        /* 終了シグナルが発生していない限りループ */
        while (!terminated) {
                /* シリアルポートから読み込み */
                ret = read(serial_fd, buf, READ_SIZE);
                if (ret < 0) {
                        /* シグナル発生時はリトライ */
                        if (errno == EINTR)
                                continue;
                        exitfail_errno("read");
                }
                len = ret;

                /* LF挿入処理直後のLFの場合、1文字無視させる */
                if (lf_flag) {
                        if (buf[0] == '\n')
                                memmove(buf, buf + 1, --len);
                        lf_flag = 0;
                }
                /* データ最終文字がCRだった場合のために1文字潰しておく */
                buf[len] = '\0';
                /* データを最後まで検査 */
                for (i = 0; i < len; i++) {
                        /* CRを発見 */
                        if (buf[i] == '\r')
                                /* 直後がLFではない */
                                if (buf[++i] != '\n') {
                                        /* LF挿入処理 */
                                        if (i < len) {
                                                /* まだデータがあるなら後ろにずらす */
                                                memmove(buf + i + 1, buf + i,
                                                        len - i);
                                        }
                                        else
                                                /* LF挿入処理発生を保持 */
                                                lf_flag = 1;
                                        buf[i] = '\n';
                                        len++;
                                }
                }

                /* すべてのデータを書き込むまでループ(終了シグナル発生で中断) */
                for (wrlen = 0; wrlen < len && !terminated; wrlen += ret) {
                        /* シリアルポートに書き込み */
                        ret = write(serial_fd, buf + wrlen, len - wrlen);
                        if (ret < 0) {
                                if (errno == EINTR) {
                                        /* シグナル発生時はリトライ */
                                        ret = 0;
                                        continue;
                                }
                                exitfail_errno("write");
                        }
                }
        }

        return EXIT_SUCCESS;
}

図6.29 改行コード変換を行うシリアルエコーサーバー(serial_echo_server2.c)


大きく追加された箇所は、main関数後半のwhileループ内です。CRを受け取り 、その次がLFでなかった場合はLFを補ってあげることで、Windows上でも改行 状態として見えるように改変するロジックが入っています。

なお、このLF挿入処理が発生した場合は、データサイズが大きくなっていく( 最大で元データの2倍)ことになるため、READ_SIZEを定義してread時点ではバ ッファの半分までしか使用しないように変更しています。

6.5.3. より効率的な入出力方法

ここまでのプログラムは、readしたものを一旦バッファに蓄えてから、必ずバ ッファ内のすべてをwriteして、またreadするというシンプルなつくりでした。 受信と送信が等速であるような理想的な環境であればこれでも構いませんが、 接続相手や機器仕様によってはそう決め付けられないことも多く、その場合は この手順は効率的とは言えません。

これを改善するためのアプローチとして、複数のプロセスを動作させて送受信 を平行動作させる方法もありますが、今回のようなケースでは中間となるバッ ファの扱い方に工夫を凝らさなければならず、大げさとも言えます。

整理してみると、解決したい問題となるのは以下のような状況です。

  1. 送信に時間がかかり待たされる状態なのに、次の受信データが来てしまっている。
  2. 送信ができないので受信待ち状態に入ったら、送信の方が先にできるようになった。

どちらも待ち状態に入ってしまい、融通が利かなくなってしまうことが問題点 です。であれば、送受信ができない状態になったら即座に中断し、先に可能に なったものから優先的に処理するというアプローチであれば解決できそうです。 selectというシステムコールを使用して、このような実装が可能です。

#include <sys/types.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <termios.h>
#include <unistd.h>

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

#define MAIN_C
#include "exitfail.h"

#define SERIAL_BAUDRATE B9600

#define BUF_SIZE  256
#define READ_SIZE (BUF_SIZE / 2)

static int serial_fd = -1; /* シリアルポートファイルディスクリプタ */
static struct termios old_tio; /* 元のシリアルポート設定 */

static int terminated = 0; /* 終了シグナル発生フラグ */

#define BASENAME(p)   ((strrchr((p), '/') ? : ((p) - 1)) + 1)
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

#define __unused __attribute__((unused))

/**
 * 終了シグナルハンドラ
 * @param signum シグナル番号(不使用)
 */
static void terminate_sig_handler(__unused int signum)
{
        /* 終了シグナル発生を記録 */
        terminated = 1;
}

/**
 * シグナルハンドラ設定関数
 * @param sig_list ハンドラを設定するシグナルのリスト
 * @param num シグナルリストの要素数
 * @param handler 設定するハンドラ関数
 */
static void set_sig_handler(int sig_list[], ssize_t num, __sighandler_t handler)
{
        struct sigaction sa;
        int i;

        /* ハンドラ関数を設定 */
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = handler;

        /* 各シグナルに対して関連付け */
        for(i = 0; i < num; i++)
                if (sigaction(sig_list[i], &sa, NULL) < 0)
                        exitfail_errno("sigaction");
}

/**
 * シリアルポート設定復元関数
 */
static void restore_serial(void)
{
        int ret;

        /* シリアルポートの設定を元に戻す */
        ret = tcsetattr(serial_fd, TCSANOW, &old_tio);
        if (ret < 0)
                exitfail_errno("tcsetattr");
}

/**
 * シリアルポート設定関数
 * @param fd 設定するシリアルポートファイルディスクリプタ
 */
static void setup_serial(int fd)
{
        struct termios tio;
        int ret;

        /* 現在のシリアルポートの設定を退避する */
        ret = tcgetattr(fd, &old_tio);
        if (ret)
                exitfail_errno("tcgetattr");

        /* 終了時に設定を復元するための関数を登録 */
        if (atexit(restore_serial))
                exitfail_errno("atexit");

        /* 新しいシリアルポートの設定 */
        memset(&tio, 0, sizeof(tio));
        tio.c_iflag = IGNBRK | IGNPAR; /* ブレーク文字無視/パリティなし */
        tio.c_cflag = CS8 | CLOCAL | CREAD; /* フロー制御なし/8bit/非モデム/受信可 */
        tio.c_cc[VTIME] = 0; /* キャラクタ間タイマー無効 */
        tio.c_cc[VMIN] = 0; /* 送信/受信時にブロックしない */
        ret = cfsetspeed(&tio, SERIAL_BAUDRATE); /* 入出力ボーレート */
        if (ret < 0)
                exitfail_errno("cfsetspeed");

        /* バッファ内のデータをフラッシュ */
        ret = tcflush(fd, TCIFLUSH);
        if (ret < 0)
                exitfail_errno("tcflush");

        /* 新しいシリアルポート設定を適用 */
        ret = tcsetattr(fd, TCSANOW, &tio);
        if (ret)
                exitfail_errno("tcsetattr");
}

/**
 * main関数
 * @param argc 引数なしの場合はusage表示のみ
 * @param argv 第1引数としてシリアルデバイス名を指定
 * @return exit値
 */
int main(int argc, char *argv[])
{
        int terminate_sig_list[] = { /* 終了シグナル種類 */
                SIGHUP, SIGINT, SIGQUIT, SIGPIPE, SIGTERM
        };
        fd_set fds_org, rdfds, wrfds, *prdfds, *pwrfds;
        int nfds;
        char buf[BUF_SIZE];
        ssize_t ret, len, rdlen, wrlen;
        int lf_flag = 0; /* LF挿入処理発生フラグ */
        int i;

        exitfail_init();

        /* 引数が指定されなかった場合、usage表示して終了 */
        if (argc < 2) {
                printf("Usage: %s <device>\n", BASENAME(argv[0]));
                return EXIT_SUCCESS;
        }

        /* シリアルポートを読み書き可能な非制御端末としてオープン */
        serial_fd = open(argv[1], O_RDWR | O_NOCTTY);
        if (serial_fd < 0)
                exitfail_errno("open");

        /* 終了シグナルに対してハンドラを設定 */
        set_sig_handler(terminate_sig_list, ARRAY_SIZE(terminate_sig_list),
                        terminate_sig_handler);

        /* シリアルポートを設定 */
        setup_serial(serial_fd);

        /* selectのため、シリアルポートの設定されたfdセットを作成しておく */
        FD_ZERO(&fds_org);
        FD_SET(serial_fd, &fds_org);
        nfds = serial_fd + 1;

        len = 0;
        /* 終了シグナルが発生していない限りループ */
        while (!terminated) {
                /* バッファに空きがある場合、読み込み可能を待つ */
                if (len < READ_SIZE) {
                        rdfds = fds_org;
                        prdfds = &rdfds;
                }
                else
                        prdfds = NULL;
                /* バッファにデータがある場合、書き込み可能を待つ */
                if (len > 0) {
                        wrfds = fds_org;
                        pwrfds = &wrfds;
                }
                else
                        pwrfds = NULL;
                /* 読み書きが可能になるまで待つ */
                ret = select(nfds, prdfds, pwrfds, NULL, NULL);
                if (ret < 0) {
                        /* シグナル発生時はリトライ */
                        if (errno == EINTR)
                                continue;
                        exitfail_errno("select");
                }

                /* 書き込み可能になった */
                if (pwrfds && FD_ISSET(serial_fd, pwrfds)) {
                        /* シリアルポートに書き込み */
                        ret = write(serial_fd, buf, len);
                        if (ret < 0) {
                                /* シグナル発生時はリトライ */
                                if (errno == EINTR)
                                        continue;
                                exitfail_errno("write");
                        }
                        wrlen = ret;

                        /* 書き込んだ分を捨てて、残りデータを前にずらす */
                        if (wrlen < len)
                                memmove(buf, buf + wrlen, len - wrlen);
                        len -= wrlen;
                }

                /* 読み込み可能になった */
                if (prdfds && FD_ISSET(serial_fd, prdfds)) {
                        /* シリアルポートから読み込み */
                        ret = read(serial_fd, buf + len, READ_SIZE - len);
                        if (ret < 0) {
                                /* シグナル発生時はリトライ */
                                if (errno == EINTR)
                                        continue;
                                exitfail_errno("read");
                        }
                        rdlen = ret;

                        /* LF挿入処理直後のLFの場合、1文字無視させる */
                        if (lf_flag) {
                                if (buf[len] == '\n')
                                        memmove(buf + len, buf + len + 1,
                                                --rdlen);
                                lf_flag = 0;
                        }
                        /* データ最終文字がCRだった場合のために1文字潰しておく */
                        buf[len + rdlen] = '\0';
                        /* データを最後まで検査 */
                        for (i = len; i < len + rdlen; i++) {
                                /* CRを発見 */
                                if (buf[i] == '\r')
                                        /* 直後がLFではない */
                                        if (buf[++i] != '\n') {
                                                /* LF挿入処理 */
                                                if (i < len + rdlen) {
                                                        /* まだデータがあるなら
                                                           後ろにずらす */
                                                        memmove(buf + i + 1,
                                                                buf + i,
                                                                len + rdlen - i);
                                                }
                                                else
                                                        /* LF挿入処理発生を保持 */
                                                        lf_flag = 1;
                                                buf[i] = '\n';
                                                rdlen++;
                                        }
                        }
                        len += rdlen;
                }
        }

        return EXIT_SUCCESS;
}

図6.30 改行コード変換を行うシリアルエコーサーバー(serial_echo_server3.c)


main関数後半部分のwhileループ内の構造が、やや大きく変わりました。まず 、送受信の可能な条件を考えてみます。受信はバッファが一杯の時はできず、 送信はバッファにデータがない時はできません。これを加味して、select関数 に適切な引数を渡します。

select関数は、fd_set型で指定されたデバイスに対して、送信・受信ができる ようになるまで待ってくれます。プログラム内で、変数prdfdsとしているもの で受信側、変数pwrfdsとしているもので送信側、それぞれに入っているデバイ ス群を監視します。

送受信どちら側(あるいは両方)が空いたかについては、FD_ISSETというマクロ で渡したfd_setの変化をチェックすることで判定できます。なお、select関数 もreadやwriteと同様、シグナルにより中断して-1を返すことがある点につい ても注意してください。

実行結果は、見た目上は先ほど作ったものと変わりありません。しかしながら 、こちらの方がより効率的であり、構造的にもわかりやすく見えるのではない かと思います。

6.6. ネットワークを使う

LinuxなどのUNIX系OSでは、ネットワーク通信を行なうためにソケットという 概念を用いた仕組みを使います。ソケットはファイルと同じように扱うことが できるので、シリアルポートの時と同様にreadやwriteといった関数でデータ 送受信を行なうことができます。

6.6.1. TCP/IP

Ethernet上で単純にネットワーク通信を行うと、送信したデータの紛失、化け、 到達順番の入れ替わりなどが発生する可能性があります。TCP/IPはこれらの問題を 吸収し、信頼性の高い通信を提供するためのプロトコルです。例えばデータが紛失した 場合、データの再送を行うことで信頼性を確保します。 TCP/IPは高い信頼性が必要なネットワーク通信で標準的に使われており、HTTPやFTPなど 多くのネットワーク通信プロトコルの基盤となっています。

TCP/IPでの通信手順を模式化すると、次の図のようになります。サーバーと クライアントは、通信を行なう前に通信経路を確立する必要があります。

TCP/IPプログラムの基本的な流れ

図6.31 TCP/IPプログラムの基本的な流れ


通信経路を糸電話に例えると、次のようになります。

  1. 糸電話で話をする人(ソケット)を作成する
  2. サーバーは、クライアントから糸電話を投げてもらう場所を指定する(名前を付ける)
  3. サーバーは、投げてもらうまで待つ(要求を待つ)
  4. クライアントはサーバーに糸電話を投げる(接続要求を出す)
  5. サーバーは糸電話を受け取る(要求を受け付ける)
  6. 話をする(データの送信/受信)
  7. 話が終わったら糸電話で話をした人は帰る(ソケットの破棄)

6.6.2. TCP/IPでHello!

ソケットとTCP/IPを使って、簡単なサーバーアプリケーションを作ってみます 。まずはこんな仕様にしてみます。

  1. サーバーが接続を待つポートは、コマンドライン引数で指定する。
  2. クライアントからはtelnetアプリケーションで接続でき、接続すると Hello!と表示される。

こんなプログラムになります。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAIN_C
#include "exitfail.h"

#define BASENAME(p)   ((strrchr((p), '/') ? : ((p) - 1)) + 1)

/**
 * main関数
 * @param argc 引数なしの場合はusage表示のみ
 * @param argv 第1引数として接続待ちポートを指定
 * @return exit値
 */
int main(int argc, char *argv[])
{
        in_port_t listen_port;
        static int listen_fd, accept_fd; /* ソケットファイルディスクリプタ */
        struct sockaddr_in server_addr, client_addr;
        socklen_t addr_len;
        char message[] = "Hello!\r\n";
        ssize_t ret;

        exitfail_init();

        /* 引数が指定されなかった場合、usage表示して終了 */
        if (argc < 2) {
                printf("Usage: %s <port>\n", BASENAME(argv[0]));
                return EXIT_SUCCESS;
        }

        ret = strtoul(argv[1], NULL, 10);
        if (ret == LONG_MIN || ret == LONG_MAX)
                exitfail_errno("strtol");
        if (ret < 49152 || 65535 < ret)
                exitfail("Specify the port 49152-65535\n");
        listen_port = ret;

        /* 接続待ち用のソケットを作成 */
        listen_fd = socket(AF_INET, SOCK_STREAM, 0);
        if (listen_fd < 0)
                exitfail_errno("open");

        /* アドレスとポートを割り当て */
        memset(&server_addr, 0, sizeof(server_addr));
        server_addr.sin_family = AF_INET; /* IPv4インターネットプロトコル */
        server_addr.sin_addr.s_addr = INADDR_ANY; /* 任意のアドレス */
        server_addr.sin_port = htons(listen_port); /* 接続を待つポート */
        addr_len = sizeof(server_addr);
        ret = bind(listen_fd, (struct sockaddr *)&server_addr, addr_len);
        if (ret < 0)
                exitfail_errno("bind");

        /* クライアントからの接続を待つ */
        ret = listen(listen_fd, SOMAXCONN);
        if (ret < 0)
                exitfail_errno("listen");

        /* クライアントからの接続を受け付けて、入出力用のソケットを作成 */
        accept_fd = accept(listen_fd, (struct sockaddr *)&client_addr,
                           &addr_len);
        if (accept_fd < 0)
                exitfail_errno("accept");

        /* これ以上の接続を受け付けないため、接続待ち用ソケットを破棄 */
        close(listen_fd);

        /* メッセージを送信 */
        ret = write(accept_fd, message, strlen(message));
        if (ret < 0)
                exitfail_errno("write");

        /* 入出力用のソケットを破棄 */
        close(accept_fd);

        return EXIT_SUCCESS;
}

図6.32 ネットワークでHello!を返すサーバー (network_hello_server.c)


ソケットは2つ作られます。1つ目は、クライアントからの接続を待つためのソ ケットです。こちらはsocket関数で作成し、sockaddr_in型の変数server_addr に接続待ちするポートを入れてbind、それからlisten関数で接続を待つ動作に 入ります。

クライアントから接続があると、accept関数で新たに入出力用のソケットが作 られます。プログラムの作り方次第では複数のクライアントを待つこともでき るのですが、このサーバーは1接続のみとしますので、ここで接続待ち用のソ ケットは破棄してしまっています。

入出力用のソケットができてしまえば、後はシリアルポートの時と同じように read/writeできます。ここではHello!メッセージだけ表示して、ソケットを閉 じてプログラムを終了しています。

Armadillo上でこのネットワークサーバーを動作させます。この例では、ポー トは65432を指定してみました。

[armadillo ~]# ./network_hello_server 65432

図6.33 network_hello_serverの実行結果


PCからtelnetで接続してみます。ATDE上からでも、WindowsのDOSプロンプトな どからでも構いません。接続コマンドは同じです。telnetのパラメータとして 、サーバー(Armadillo)のIPアドレス(ここでは例として、192.168.1.100であ るとします)と接続ポートを指定します。

network_hello_serverへのtelnet. 

[ATDE ~]$ telnet 192.168.1.100 65432
Trying 192.168.1.100...
Connected to 192.168.1.100.
Escape character is '^]'.
Hello!
Connection closed by foreign host.

このようにHello!と表示されてから、すぐに切断されます。

[注記]ソケットの再利用

同じポートの指定でnetwork_hello_serverを何度も実行すると、エラーメッセージが 表示されて起動できないことがあります。

[armadillo ~]# ./network_hello_server 65432
./network_hello_server: bind: Address already in use

bindしようとしたアドレスが使用中です、というエラーメッセージです。サー バーの終了時に、ソケットはクローズしているのに…これは、TCP/IPを使用し ていることに起因しています。

TCP/IPは送信データの到達を保証するものであるため、データが紛失した際は 再送しなければなりません。このプログラムのように、送信後すぐにソケット をクローズしてしまった場合であっても、その後に再送する可能性があるため 、システムはしばらく(2~4分程度)ソケットを破棄しないまま保持し続けます 。このようなソケットを、TIME_WAIT状態であるといいます。なお、(接続しに 行った側である)クライアントが先にソケットをクローズした場合は、 TIME_WAIT状態にはなりません。

6.6.3. ネットワークエコーサーバー

Hello!を改造して、シリアルポート送受信で作成したエコーサーバーのネット ワーク版を作成してみます。

#include <sys/types.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAIN_C
#include "exitfail.h"

#define BUF_SIZE  256

#define BASENAME(p)   ((strrchr((p), '/') ? : ((p) - 1)) + 1)

/**
 * main関数
 * @param argc 引数なしの場合はusage表示のみ
 * @param argv 第1引数として接続待ちポートを指定
 * @return exit値
 */
int main(int argc, char *argv[])
{
        in_port_t listen_port;
        static int listen_fd, accept_fd; /* ソケットファイルディスクリプタ */
        struct sockaddr_in server_addr, client_addr;
        socklen_t addr_len;
        char message[] = "Hello!\r\n";
        fd_set fds_org, rdfds, wrfds, *prdfds, *pwrfds;
        int nfds;
        char buf[BUF_SIZE];
        ssize_t ret, len, wrlen;

        exitfail_init();

        /* 引数が指定されなかった場合、usage表示して終了 */
        if (argc < 2) {
                printf("Usage: %s <port>\n", BASENAME(argv[0]));
                return EXIT_SUCCESS;
        }

        ret = strtoul(argv[1], NULL, 10);
        if (ret == LONG_MIN || ret == LONG_MAX)
                exitfail_errno("strtol");
        if (ret < 49152 || 65535 < ret)
                exitfail("Specify the port 49152-65535\n");
        listen_port = ret;

        /* 接続待ち用のソケットを作成 */
        listen_fd = socket(AF_INET, SOCK_STREAM, 0);
        if (listen_fd < 0)
                exitfail_errno("open");

        /* アドレスとポートを割り当て */
        memset(&server_addr, 0, sizeof(server_addr));
        server_addr.sin_family = AF_INET; /* IPv4インターネットプロトコル */
        server_addr.sin_addr.s_addr = INADDR_ANY; /* 任意のアドレス */
        server_addr.sin_port = htons(listen_port); /* 接続を待つポート */
        addr_len = sizeof(server_addr);
        ret = bind(listen_fd, (struct sockaddr *)&server_addr, addr_len);
        if (ret < 0)
                exitfail_errno("bind");

        /* クライアントからの接続を待つ */
        ret = listen(listen_fd, SOMAXCONN);
        if (ret < 0)
                exitfail_errno("listen");

        /* クライアントからの接続を受け付けて、入出力用のソケットを作成 */
        accept_fd = accept(listen_fd, (struct sockaddr *)&client_addr,
                           &addr_len);
        if (accept_fd < 0)
                exitfail_errno("accept");

        /* これ以上の接続を受け付けないため、接続待ち用ソケットを破棄 */
        close(listen_fd);

        /* メッセージを送信 */
        ret = write(accept_fd, message, strlen(message));
        if (ret < 0)
                exitfail_errno("write");

        /* selectのため、ソケットの設定されたfdセットを作成しておく */
        FD_ZERO(&fds_org);
        FD_SET(accept_fd, &fds_org);
        nfds = accept_fd + 1;

        len = 0;
        /* 無限ループ */
        for (; ; ) {
                /* バッファに空きがある場合、読み込み可能を待つ */
                if (len < BUF_SIZE) {
                        rdfds = fds_org;
                        prdfds = &rdfds;
                }
                else
                        prdfds = NULL;
                /* バッファにデータがある場合、書き込み可能を待つ */
                if (len > 0) {
                        wrfds = fds_org;
                        pwrfds = &wrfds;
                }
                else
                        pwrfds = NULL;
                /* 読み書きが可能になるまで待つ */
                ret = select(nfds, prdfds, pwrfds, NULL, NULL);
                if (ret < 0) {
                        /* シグナル発生時はリトライ */
                        if (errno == EINTR)
                                continue;
                        exitfail_errno("select");
                }

                /* 書き込み可能になった */
                if (pwrfds && FD_ISSET(accept_fd, pwrfds)) {
                        /* ソケットに書き込み */
                        ret = write(accept_fd, buf, len);
                        if (ret < 0) {
                                /* シグナル発生時はリトライ */
                                if (errno == EINTR)
                                        continue;
                                exitfail_errno("write");
                        }
                        wrlen = ret;

                        /* 書き込んだ分を捨てて、残りデータを前にずらす */
                        if (wrlen < len)
                                memmove(buf, buf + wrlen, len - wrlen);
                        len -= wrlen;
                }

                /* 読み込み可能になった */
                if (prdfds && FD_ISSET(accept_fd, prdfds)) {
                        /* ソケットから読み込み */
                        ret = read(accept_fd, buf + len, BUF_SIZE - len);
                        if (ret <= 0) {
                                if (ret == 0)
                                        break; /* 終了 */
                                /* シグナル発生時はリトライ */
                                if (errno == EINTR)
                                        continue;
                                exitfail_errno("read");
                        }
                        len += ret;
                }
        }

        /* 入出力用のソケットを破棄 */
        close(accept_fd);

        return EXIT_SUCCESS;
}

図6.34 ネットワークエコーサーバー (network_echo_server1.c)


ソースに追加されたselect, read, writeを使う入出力部の基本構造は、 シリアルポートの時とまったく一緒です。

先ほどと同じようにサーバーを実行してみます。

[armadillo ~]# ./network_echo_server1 65432

図6.35 network_echo_server1の実行結果


PCからtelnetで接続してみます。

network_echo_server1へのtelnet. 

[ATDE ~]$ telnet 192.168.1.100 65432
Trying 192.168.1.100...
Connected to 192.168.1.100.
Escape character is '^]'.
Hello!
abc
abc
defg
defg
^]

telnet> quit
Connection closed.

先ほどと違い、Hello!が表示された後にソケットがクローズされません。 その後はエコーサーバーですので、入力したものがそのまま返ってきます。

telnet実行直後に表示されているように、Ctrlキー を押しながら]キーを押すとエスケープすることがで きます。「telnet> 」というプロンプトが表示されるので、quitと入力して telnetコマンドを終了することができます [38]

このように動作するのはATDEからtelnetした場合です。実は、他の telnetアプリケーションから接続した場合、それぞれちょっとずつ動作が 違ってきてしまいます。

例えばWindowsのDOSプロンプトからtelnetコマン ドで接続した時は、以下のようになりました[39]

C:\> telnet 192.168.1.100 65432
Microsoft Telnet クライアントへようこそ

エスケープ文字は 'Ctrl+]' です

Hello!
aabbcc

ddeeffgg

^]

Microsoft Telnet> quit

図6.36 network_echo_server1へのtelnet(Windows)


このように、1文字打つたびに文字が返ってきています。

これは、単純にtelnetクライアントアプリケーションの挙動が違うだけです。 ATDE (つまりLinux)のtelnetクライアントは(改行が入力されるたびに)行単位 でデータを送信し、Windowsのtelnetクライアントは1文字入力するたびにデー タを送信しているということになります。

また、Tera Termにもtelnet機能があります。こちらの場合は(標準の設定では )打ち込んだものがそのまま表示はされません。これは設定の問題なので良い のですが、シリアルエコーサーバーの時と同じように、CRやLFの改行コードの 違いによる問題も発生します。

WindowsのtelnetやTera Term相手の時も、Linuxのtelnetの時と同じように 表示するようにサーバーアプリケーションを改造してみます。

#include <sys/types.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAIN_C
#include "exitfail.h"

#define BUF_SIZE  256
#define READ_SIZE (BUF_SIZE / 2)

#define BASENAME(p)   ((strrchr((p), '/') ? : ((p) - 1)) + 1)

/**
 * main関数
 * @param argc 引数なしの場合はusage表示のみ
 * @param argv 第1引数として接続待ちポートを指定
 * @return exit値
 */
int main(int argc, char *argv[])
{
        in_port_t listen_port;
        static int listen_fd, accept_fd; /* ソケットファイルディスクリプタ */
        struct sockaddr_in server_addr, client_addr;
        socklen_t addr_len;
        char message[] = "Hello!\r\n";
        fd_set fds_org, rdfds, wrfds, *prdfds, *pwrfds;
        int nfds;
        char buf[BUF_SIZE];
        ssize_t ret, len, rdlen, wrlen;
        int lf_flag = 0; /* LF挿入処理発生フラグ */
        int i;

        exitfail_init();

        /* 引数が指定されなかった場合、usage表示して終了 */
        if (argc < 2) {
                printf("Usage: %s <port>\n", BASENAME(argv[0]));
                return EXIT_SUCCESS;
        }

        ret = strtoul(argv[1], NULL, 10);
        if (ret == LONG_MIN || ret == LONG_MAX)
                exitfail_errno("strtol");
        if (ret < 49152 || 65535 < ret)
                exitfail("Specify the port 49152-65535\n");
        listen_port = ret;

        /* 接続待ち用のソケットを作成 */
        listen_fd = socket(AF_INET, SOCK_STREAM, 0);
        if (listen_fd < 0)
                exitfail_errno("open");

        /* アドレスとポートを割り当て */
        memset(&server_addr, 0, sizeof(server_addr));
        server_addr.sin_family = AF_INET; /* IPv4インターネットプロトコル */
        server_addr.sin_addr.s_addr = INADDR_ANY; /* 任意のアドレス */
        server_addr.sin_port = htons(listen_port); /* 接続を待つポート */
        addr_len = sizeof(server_addr);
        ret = bind(listen_fd, (struct sockaddr *)&server_addr, addr_len);
        if (ret < 0)
                exitfail_errno("bind");

        /* クライアントからの接続を待つ */
        ret = listen(listen_fd, SOMAXCONN);
        if (ret < 0)
                exitfail_errno("listen");

        /* クライアントからの接続を受け付けて、入出力用のソケットを作成 */
        accept_fd = accept(listen_fd, (struct sockaddr *)&client_addr,
                           &addr_len);
        if (accept_fd < 0)
                exitfail_errno("accept");

        /* 接続待ち用のソケットは不要になったので破棄 */
        close(listen_fd);

        /* メッセージを送信 */
        ret = write(accept_fd, message, strlen(message));
        if (ret < 0)
                exitfail_errno("write");

        /* selectのため、ソケットの設定されたfdセットを作成しておく */
        FD_ZERO(&fds_org);
        FD_SET(accept_fd, &fds_org);
        nfds = accept_fd + 1;

        len = 0;
        /* 無限ループ */
        for (; ; ) {
                /* バッファに空きがある場合、読み込み可能を待つ */
                if (len < READ_SIZE) {
                        rdfds = fds_org;
                        prdfds = &rdfds;
                }
                else
                        prdfds = NULL;
                /* バッファにデータがあり改行が含まれていた場合、書き込み可能を待つ */
                if (len > 0 && memchr(buf, '\n', len)) {
                        wrfds = fds_org;
                        pwrfds = &wrfds;
                }
                else
                        pwrfds = NULL;
                /* 読み書きが可能になるまで待つ */
                ret = select(nfds, prdfds, pwrfds, NULL, NULL);
                if (ret < 0) {
                        /* シグナル発生時はリトライ */
                        if (errno == EINTR)
                                continue;
                        exitfail_errno("select");
                }

                /* 書き込み可能になった */
                if (pwrfds && FD_ISSET(accept_fd, pwrfds)) {
                        /* ソケットに書き込み */
                        ret = write(accept_fd, buf, len);
                        if (ret < 0) {
                                /* シグナル発生時はリトライ */
                                if (errno == EINTR)
                                        continue;
                                exitfail_errno("write");
                        }
                        wrlen = ret;

                        /* 書き込んだ分を捨てて、残りデータを前にずらす */
                        if (wrlen < len)
                                memmove(buf, buf + wrlen, len - wrlen);
                        len -= wrlen;
                }

                /* 読み込み可能になった */
                if (prdfds && FD_ISSET(accept_fd, prdfds)) {
                        /* ソケットから読み込み */
                        ret = read(accept_fd, buf + len, BUF_SIZE - len);
                        if (ret <= 0) {
                                if (ret == 0)
                                        break; /* 終了 */
                                /* シグナル発生時はリトライ */
                                if (errno == EINTR)
                                        continue;
                                exitfail_errno("read");
                        }
                        rdlen = ret;

                        /* LF挿入処理直後のLFの場合、1文字無視させる */
                        if (lf_flag) {
                                if (buf[len] == '\n')
                                        memmove(buf + len, buf + len + 1,
                                                --rdlen);
                                lf_flag = 0;
                        }
                        /* データ最終文字がCRだった場合のために1文字潰しておく */
                        buf[len + rdlen] = '\0';
                        /* データを最後まで検査 */
                        for (i = len; i < len + rdlen; i++) {
                                /* CRを発見 */
                                if (buf[i] == '\r')
                                        /* 直後がLFではない */
                                        if (buf[++i] != '\n') {
                                                /* LF挿入処理 */
                                                if (i < len + rdlen) {
                                                        /* まだデータがあるなら
                                                           後ろにずらす */
                                                        memmove(buf + i + 1,
                                                                buf + i,
                                                                len + rdlen - i);
                                                }
                                                else
                                                        /* LF挿入処理発生を保持 */
                                                        lf_flag = 1;
                                                buf[i] = '\n';
                                                rdlen++;
                                        }
                        }
                        len += rdlen;
                }
        }

        /* 入出力用のソケットを破棄 */
        close(accept_fd);

        return EXIT_SUCCESS;
}

図6.37 ネットワークエコーサーバー (network_echo_server2.c)


Windows用telnet対応のための変更点は1点だけ。write可能を待つかどうか判 定する際に、memchr関数によるデータ内容の確認を行っています。改行文字が 来るまでは、writeされないようにしているわけです。

そして、Tera Termのtelnetでの改行文字問題の修正ですが、これはシリアル エコーサーバーでの図6.30「改行コード変換を行うシリアルエコーサーバー(serial_echo_server3.c)」の対策とまった く一緒です。

同じtelnetアプリケーションとはいえ、このように(見た目の)動作に差が出る こともあります。より汎用的で完成度の高いものを作ろうとすれば多くの試験 が必要で、対策コストも決して小さくないということは心に留めておくべきで しょう。

6.7. プログラムをデバッグする

C言語で開発したプログラムをデバッグする際、printfを埋め込む いわゆるprintデバッグがよく行われます。printデバッグも デバッグ手法の1つではありますが、プログラムの規模が大きくなると これだけでデバッグするのは効率が悪く、かつメモリリークなどの重大なバグは 見逃してしまう可能性もあります。

ここではより効率的なC言語プログラムのデバッグ手法を紹介します。

6.7.1. gdbによるデバッグ

gdbとはデバッグを行うためのツールで、デバッガと呼ばれるものの1つです。 プログラムを1行ずつ実行したり、ブレークポイントを設定した行で処理を中断できたり、 変数に格納されている値を確認できたりと、様々なことができます。

ここでは、gdbの基本的な使い方について説明します。

デバッグ対象のプログラムとして、1から100までの数を全て加算する 簡単なものを用意しました。

#include <stdio.h>
#include <stdlib.h>

int sum(int a)
{
    int i, sum = 0;
    for (i = 1; i < a; i++) {
        sum += i;
    }
    return sum;
}

int main(int argc, char *argv[])
{
    int s;

    s = sum(100);
    printf("%d\n", s);

    return EXIT_SUCCESS;
}

図6.38 1から100までの和を計算するプログラム (sum.c)


このソースコードを -g -O0 オプションを付けてコンパイルします。

[armadillo ~]# gcc -g -O0 sum.c -o sum

-gオプションはgdbでデバッグできるようにするために必要なオプションです。 -O0オプションはコンパイラによる最適化をなしにするためのオプションです。 最適化されてしまうとデバッグしづらくなってしまいます。

このプログラムを実行すると以下のように表示されます。

[armadillo ~]# ./sum
4950

図6.39 sumの実行結果


期待していた結果である5050とは違った値となりました。 gdbを使ってデバッグを行います。

gdbがインストールされていない場合、aptでインストールできます。

[armadillo ~]# apt install gdb

gdbを使ってプログラムを起動します。

[armadillo ~]# gdb sum
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "arm-linux-gnueabihf".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from sum...done.
(gdb)

(gdb)プロンプトが表示されました。今後はこのプロンプトにコマンドを入力して デバッグを進めます。

startと入力するとmain関数まで進んで実行が止まります。

(gdb) start
Temporary breakpoint 1 at 0x610: file sum.c, line 17.
Starting program: /root/work/developers_guide/c/gdb/sum

Temporary breakpoint 1, main (argc=1, argv=0xbefffd84) at sum.c:17
17          s = sum(100);
(gdb)

ソースコードの17行目の処理で止まっているこを示しています。

nextと入力すると次の行へ進んで実行が止まります。

(gdb) next
18          printf("%d\n", s);

ここで、printコマンドを使うと変数sに入っている値を表示することができます。

(gdb) print s
$1 = 4950

プログラムの実行を再開するにはcontinueと入力します。

(gdb) continue
Continuing.
4950
[Inferior 1 (process 1812) exited normally]

プログラムが最後まで実行されました。 gdbを終了するにはquitと入力します。

(gdb) quit
[armadillo ~]#

プログラムが最後まで実行されても、再度startと入力すると 最初から実行することができます。

(gdb) start
Temporary breakpoint 2 at 0x400610: file sum.c, line 17.
Starting program: /root/work/developers_guide/c/gdb/sum

Temporary breakpoint 2, main (argc=1, argv=0xbefffd84) at sum.c:17
17          s = sum(100);

ここで、nextではなくstepと入力するとsum関数の中へ入ることができます。nextも stepも1行ずつ実行するコマンドですが、nextは関数の中には入らず、stepは関数の中に入ります。

(gdb) step
sum (a=100) at sum.c:6
6           int i, sum = 0;

sum関数の中に入って処理が止まりました。

目的の行がある場合、その行にたどり着くまでnextやstepを入力し続けるのは 大変なので、目的の行にブレークポイントを設定し、そこまで一気に実行してみます。

listと入力するとソースコードが表示されるので、目的の行が何行目なのかを確認します。

(gdb) list
1       #include <stdio.h>
2       #include <stdlib.h>
3
4       int sum(int a)
5       {
6           int i, sum = 0;
7           for (i = 1; i < a; i++) {
8               sum += i;
9           }
10          return sum;
[警告]

ソースコード内にコメントがある場合はコメントも表示されます。 この時、日本語のコメントがあった場合、環境によっては文字化けが 発生することがありますが、gdbの実行には影響ありません。

breakコマンドで8行目にブレークポイントを設定します。

(gdb) break 8
Breakpoint 3 at 0x4005e2: file sum.c, line 8.
(gdb) continue
Continuing.

Breakpoint 3, sum (a=100) at sum.c:8
8               sum += i;
(gdb) print sum
$2 = 0
(gdb) print i
$3 = 1

ブレークポイントを設定し、continueするとブレークポイントの 行で処理が止まります。そこで、変数sumとiの値を表示しています。

displayコマンドを使うと、変化していく変数の値を常に表示できます。

(gdb) display sum
1: sum = 0
(gdb) display i
2: i = 1
(gdb) continue
Continuing.

Breakpoint 3, sum (a=100) at sum.c:8
8               sum += i;
1: sum = 1
2: i = 2
(gdb) continue
Continuing.

Breakpoint 3, sum (a=100) at sum.c:8
8               sum += i;
1: sum = 3
2: i = 3
(gdb) continue
Continuing.

Breakpoint 3, sum (a=100) at sum.c:8
8               sum += i;
1: sum = 6
2: i = 4

for文を抜けるまでcontinueし続けるのは大変なので、 一度ブレークポイントを削除し、関数のreturn文の所に設定し直します。

ブレークポイントはdeleteコマンドで削除できますが、この時は ソースコードの行数ではなく、ブレークポイント番号を指定する必要があります。

(gdb) delete 3
(gdb) break 10
Breakpoint 4 at 0x4005f8: file sum.c, line 10.
(gdb) continue
Continuing.

Breakpoint 4, sum (a=100) at sum.c:10
10          return sum;
1: sum = 4950
2: i = 100

ブレークポイント番号3を削除し、10行目にブレークポイントを設定し直して そこまで処理を進めました。その時の、変数sumとiの値が表示されています。

ここで、変数iの値に注目すると100となっていることから、forループは 99回しか実行されておらず、forループの終了条件に問題があることがわかりました。

このように、gdbを使うとプログラムの実行の中断や変数の値の確認などができ、 デバッグの効率化が見込めます。

最後に、ここでの説明で使用したgdbのコマンド一覧と、使用はしていませんが 有用なコマンドを示します。またほとんどのコマンドは、省略形でも使うことができます。

表6.3 使用したコマンド

コマンド 省略形 動作

start

-

実行を開始しmain関数で停止する

step

s

1行ずつ実行し、関数の場合は中に入る

next

n

1行ずつ実行し、関数には入らない

print

p

変数の値やアドレスを表示する

continue

c

実行を再開する

quit

q

gdbを終了する

list

l

ソースコードを表示する

break n

b n

n行目にブレークポイントを設定する

delete n

d n

n番目のブレークポイントを削除する

display

-

常に変数の値やアドレスを表示する


表6.4 使用していないが有用なコマンド

コマンド 省略形 動作

run

r

実行を開始する

finish

fin

関数をreturnまで実行する

return n

-

nをreturnし、強制的に関数から抜ける

help

h

helpを表示する


ここで紹介したコマンド以外にも多くのコマンドが用意されています。 詳細に関しては、gdbのhelpコマンドおよび公式ドキュメント[40] を参照してください。

6.7.2. straceでシステムコールをトレースする

プログラムが呼び出すシステムコールをトレース(追跡)することで、 デバッグの役に立てることができます。

ここでは、システムコールをトレースする方法を 簡単に説明します。

システムコールとは、open、close、write、readなどの関数をコールして Linuxカーネルが提供している機能をユーザー空間から使う仕組みです。

システムコールをトレースするにはstraceというコマンドを使います。

ここでは「gdbによるデバッグ」で紹介したプログラムを straceでトレースします。

一番簡単なやり方は、straceの引数としてプログラムを渡すだけです。

[armadillo ~]# strace ./sum
execve("./sum", ["./sum"], [/* 14 vars */]) = 0
brk(NULL)                               = 0x197c000
uname({sysname="Linux", nodename="armadillo", ...}) = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=30847, ...}) = 0
mmap2(NULL, 30847, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb6fde000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/arm-linux-gnueabihf/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\3\0\0\0\0\0\0\0\0\3\0(\0\1\0\0\0\331e\1\0004\0\0\0"..., 5
12) = 512
lseek(3, 900076, SEEK_SET)              = 900076
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 2
920) = 2920
lseek(3, 896524, SEEK_SET)              = 896524
read(3, "A4\0\0\0aeabi\0\1*\0\0\0\0057-A\0\6\n\7A\10\1\t\2\n\3\f"..., 53) = 53
fstat64(3, {st_mode=S_IFREG|0755, st_size=902996, ...}) = 0
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb6
fdc000
mmap2(NULL, 972112, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb6
ed1000
mprotect(0xb6faa000, 61440, PROT_NONE)  = 0
mmap2(0xb6fb9000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRI
TE, 3, 0xd8000) = 0xb6fb9000
mmap2(0xb6fbc000, 9552, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOU
S, -1, 0) = 0xb6fbc000
close(3)                                = 0
set_tls(0xb6fdcb80, 0xb6fdd258, 0xb6fe7050, 0xb6fdcb80, 0xb6fe7050) = 0
mprotect(0xb6fb9000, 8192, PROT_READ)   = 0
mprotect(0x4ce000, 4096, PROT_READ)     = 0
mprotect(0xb6fe6000, 4096, PROT_READ)   = 0
munmap(0xb6fde000, 30847)               = 0
fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(207, 16), ...}) = 0
ioctl(1, TCGETS, {B115200 opost isig icanon echo ...}) = 0
brk(NULL)                               = 0x197c000
brk(0x199e000)                          = 0x199e000
write(1, "4950\n", 54950
)                   = 5
exit_group(0)                           = ?
+++ exited with 0 +++

openやreadなどのほかにも、多くのシステムコールが使われていることが確認できます。

特定のシステムコールのみを表示したい場合は-eオプションを使います。例えば openのみトレースしたい場合は以下のように指定します。

[armadillo ~]# strace -e open ./sum
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib/arm-linux-gnueabihf/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
4950
+++ exited with 0 +++

複数のシステムコールを指定したい場合は、-e trace=として、システムコールをカンマ区切りで指定します。 openとcloseの2つを指定すると以下のように表示されます。

[armadillo ~]# strace -e trace=open,close ./sum
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0
open("/lib/arm-linux-gnueabihf/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0
4950
+++ exited with 0 +++

-cオプションを使うと、システムコールの統計情報を出力することができます。

[armadillo ~]# strace -c ./sum
4950
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 43.27    0.000074          74         1           write
 29.82    0.000051          17         3           brk
 16.37    0.000028          28         1           ioctl
 10.53    0.000018           6         3           fstat64
  0.00    0.000000           0         3           read
  0.00    0.000000           0         2           open
  0.00    0.000000           0         2           close
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         2           lseek
  0.00    0.000000           0         3         3 access
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         1           uname
  0.00    0.000000           0         4           mprotect
  0.00    0.000000           0         5           mmap2
  0.00    0.000000           0         1           set_tls
------ ----------- ----------- --------- --------- ----------------
100.00    0.000171                    33         3 total

この統計情報で、コール回数やエラーの有無などの情報も確認できます。

このようにstraceを使ってシステムコールをトレースし結果を確認することができます。

コール回数が異常に多い場合や、エラーが発生しているような場合にそのシステムコールを 使用している個所を重点的に調べることで、バグの発見につなげることができます。

straceの使い方の詳細については、straceのmanページを参照してください。

6.7.3. メモリ破壊やメモリリークのデバッグ

C言語によるプログラミングにおいて、発見しづらいバグとして メモリ破壊やメモリリークがあります。

これらのバグがあっても、プログラムは正常動作することもあり 見逃されがちですが、長時間稼働するようなプログラムにおいては、 致命的な問題を引き起こす可能性もあります。

ここではツールを用いたメモリ破壊やメモリリークの検出方法を簡単に説明します。

6.7.3.1. Electric Fenceを使ったメモリ破壊検出

メモリ破壊とは確保したメモリ領域以外にアクセスしてしまうことで 発生します。

ここではメモリ破壊検出ツールであるElectric Fenceを使います。

Electric Fenceはaptでインストールすることができます。

[armadillo ~]# apt install electric-fence

メモリ破壊を行うプログラムを以下の通り準備します。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int *p;

    p = (int *) malloc(sizeof(int) * 5);
    p[5] = 100;
    free(p);

    return EXIT_SUCCESS;
}

図6.40 メモリ破壊を行うプログラム (mem_corruption.c)


変数pにint型のサイズで5つ分の領域を確保していますが、 p[5]へのアクセスしており、これは範囲外のアクセスとなります。 仮にp[5]が必要な領域を参照していた場合、その領域を書き換えて しまっていることになります。

しかしコンパイルして、実行すると正常に終了します。

[armadillo ~]# gcc -g -O0 mem_corruption.c -o mem_corruption
[armadillo ~]# ./mem_corruption
[armadillo ~]#

このように、偶然にも動いてしまうため、メモリ破壊は発見しづらいバグです。

Electric Fenceを使うためには、コンパイルするときにlibefence.soを リンクするだけです。

[armadillo ~]# gcc -g -O0 mem_corruption.c -o mem_corruption -lefence
[armadillo ~]# ./mem_corruption

  Electric Fence 2.2 Copyright (C) 1987-1999 Bruce Perens <bruce@perens.com>
Segmentation fault

実行すると、Segmentation faultが発生してプログラムが終了しました。 gdbと組み合わせると、どこで発生しているのかも確認することができます。

[armadillo ~]# gdb mem_corruption

(省略)

(gdb) run
Starting program: /root/work/developers_guide/c/gdb/mem_corruption
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/arm-linux-gnueabihf/libthread_db.so.1".

  Electric Fence 2.2 Copyright (C) 1987-1999 Bruce Perens <bruce@perens.com>

Program received signal SIGSEGV, Segmentation fault.
0x00400732 in main (argc=1, argv=0xbefffd44) at mem_corruption.c:9
9           p[5] = 100;
(gdb)

このように、メモリ破壊を検出することができます。Electric Fenceの詳細については manページ(man efence)を参照してください。

6.7.3.2. MemWatchを使ったメモリリーク検出

Electric Fenceではメモリ破壊は検出できますが、メモリリークを 検出することはできません。

ここでは、メモリリーク検出ツールであるMemWatchを使います。

MemWatchはソースコードとして提供されており、デバッグしたい プログラムと一緒にコンパイルすることで動作します。

MemWatchのソースコードはGitHubからダウンロードしてくることができます。

https://github.com/502110983/MemWatch

メモリリークが発生するプログラムを以下の通り準備します。 MemWatchのヘッダであるmemwatch.hをインクルードする必要がある点に 注意してください。

#include <stdio.h>
#include <stdlib.h>
#include "memwatch.h"

int main(int argc, char *argv[])
{
    int *p1, *p2;

    p1 = (int *) malloc(sizeof(int));
    p2 = (int *) malloc(sizeof(int));

    p2 = p1;

    free(p1);
    free(p2);

    return EXIT_SUCCESS;
}

図6.41 メモリリークが発生するプログラム (mem_leak.c)


変数p2として確保した領域が解放されないままになっており、 かつp1として確保した領域が二重に解放されています。

このプログラムをmemwatch.cと一緒にコンパイルします。コンパイルする時に -DMEMWATCH -DNW_STDIOというオプションを設定します。

[armadillo ~]# gcc -DMEMWATCH -DMW_STDIO mem_leak.c memwatch.c -o mem_leak
[armadillo ~]# ./mem_leak
MEMWATCH detected 2 anomalies
[armadillo ~]#

実行すると、memwatch.logというファイルができるので、このファイルを確認します。

[armadillo ~]# cat ./memwatch.log

============= MEMWATCH 2.71 Copyright (C) 1992-1999 Johan Lindh =============

Started at Wed Apr 29 21:58:47 2020

Modes: __STDC__ 64-bit mwDWORD==(unsigned long)
mwROUNDALLOC==8 sizeof(mwData)==32 mwDataSize==32

double-free: < 4 > mem_leak.c(15), 0x8261d8 was freed from mem_leak.c(14)

Stopped at Wed Apr 29 21:58:47 2020

unfreed: < 2 > mem_leak.c(10), 4 bytes at 0x826210        {FE FE FE FE .. .. .. ..
 .. .. .. .. .. .. .. .. ....}

Memory usage statistics (global):
 N)umber of allocations made: 2
 L)argest memory usage      : 8
 T)otal of all alloc() calls: 8
 U)nfreed bytes totals      : 4

このファイルの以下の出力に注目すると、メモリの二重解放と 未解放があることがわかります。

ソースコード15行目で二重解放
double-free: < 4 >  mem_leak.c(15), 0x8261d8 was freed from mem_leak.c(14)

ソースコード10行目で確保した領域が未解放
unfreed: < 2 > mem_leak.c(10), 4 bytes at 0x826210

このように通常であれば発見しづらいメモリリークを発見することができます。

MemWatchの詳細についてはMemWatchのソースコード一式に含まれる READMEとFAQを参照してください。



[24] GCCと大文字で書いた場合は、通常GNU Compiler Collection(C/C++、Objective-C/C++、Java、Fortran、Adaを扱える統合コ ンパイラパッケージ全体)を指します。

[25] 新しいアーキテクチャでは、以前のアーキテクチャに存在したイン ストラクションがすべて使用可能です。

[26] 一般的には、大文字始 まりのMakefileがよく使用されるようです。

[27] マクロとも呼ばれます。

[28] 日本では通常、シャープと呼ばれる記号。

[29] これ以外に独自拡張的な仕様として、環境 変数を使用するためにint main(int argc, char *argv[], char *envp[]);と3個の引数を取る場合があります。

[30] シェルで扱える終了ステータスは1バイト長なので、実 際には0xffでマスクされた値が返ることになります。

[31] man 3 stdio参照

[32] このデータは「郵便事業株式会社は著作権を主張しま せん。自由に配布していただいて結構です。」とされています。 http://www.post.japanpost.jp/zipcode/dl/readme.html(2020年4月現在の URL)

[33] 詳細なデータファイルの形式説明については、日本郵便のページを 参照。http://www.post.japanpost.jp/zipcode/dl/readme.html(2020年4月 現在のURL)

[34] スペシャルファイル(特殊ファイル)やデバイスノードとも呼ばれま す

[35] C言語コード中でキャラクタ文字表現する際の\nにあたります 。

[36] C言語コード中でキャラクタ 文字表現する際の\rにあたります。

[37] Tera Termの「設定」-「端末」メニュ ーをクリックするとすると、改行コードの「送信」として「CR」か「CR+LF」 を選べることが確認できます。

[38] この場合は接続したクライアント側がソケットをクローズすること になるため、TIME_WAIT状態にはなりません。

[39] Windows 10のDOSプロ ンプトからtelnetコマンドを使用して確認しまし た。