ロック lint を使用するには、以下の 3 つの手順が必要です。
ロック lint を使用するための環境の設定
解析対象となるソースコードのコンパイルと、ロック lint データベースファイル (.ll ファイル)の作成
lock_lint コマンドを使ったロック lint セッションの実行
この項の後半では、これらの手順について解説します。
図 5-1 に、ロック lint を使った一連のタスクの制御フローを示します。
ロック lint を利用することで、システム実行のために整備すべきアサーションを改善できます。質の高いアサーションを整備することで、ロック lint は既存のソースコードおよび新しいソースコードの正当性を評価できるようになります。
ロック lint インタフェースは、シェルで実行される lock_lint コマンドと lock_lint サブコマンドから構成されます。デフォルトでは、ロック lint は環境変数 $SHELL によって指定されるシェルを使用します。あるいは、lock_lint start コマンドで使用するシェルを指定することによって、ロック lint は任意のシェルを実行できます。以下の例では、Korn シェルでロック lint セッションを開始しています。
% lock_lint start /bin/ksh
ロック lint は子シェル上で見られる LL_CONTEXT という環境変数を作成します。初期設定のために準備したシェルを使用する場合、ホームディレクトリの .ll_init ファイルを、lock_lint コマンドが読み込むようにアレンジし、カレントディレクトリの .ll_init ファイル (存在する場合は) を実行させることができます。csh を使用する場合は、.cshrc ファイルに以下のコードを挿入することで、こうした操作を実行できます。
if ($?LL_CONTEXT) then if (-x $(HOME)/.ll_init) source $(HOME)/.ll_init endif
ロック lint を同じファイルに対して実行したいと考えるほかのユーザーが、同じシェルを使うとは限らないため、.cshrc に現在の作業ディレクトリのファイルを読み込ませないようにすることをお薦めします。ユーザーの $(HOME)/.ll_init を使用するのは 1 人だけであるため、ロック lint セッション中にプロンプトを変更し、使用するエイリアスを定義できるように、$(HOME)/.ll_init を読み込む形式とすべきです。以下の 〜/.ll_init バージョンは csh に対してこの操作を実行します。
# Cause analyze subcommand to save state before analysis. alias analyze "lock_lint save before analyze;¥ lock_lint analyze" # Change prompt to show we are in lock_lint. set prompt="lock_lint‾$prompt"
「start」も参照してください。
サブコマンドを実行する場合は、目的に応じて、パイプ、リダイレクト、逆引用符 (``) などを利用できます。たとえば、以下のコマンドは、ロック foo がすべてのグローバル変数を保護することをアサートしています (グローバル変数の公式名称はコロンで始まります)。
% lock_lint assert foo protects `lock_lint vars | grep ^:`
一般的に、サブコマンドは grep や sed などのフィルタとともに簡単に利用できるように設定されます。特に、各変数や関数に対して 1 行分の情報を出力する vars や funcs には、こうした機能は欠かせません。各行には、変数や関数の (定義されたあるいは派生した) 属性が含まれます。以下の例は、構造体 bar のどのメンバーが、メンバー lock によって保護されるかを示します。
% lock_lint vars -a `lock_lint members bar` | grep =bar::lock
シェルインタフェースを使用するため、ユーザーコマンドのログは、シェルの履歴 (history) 機能を利用することで得られます (history のレベルは、.ll_init ファイルにおいて、大きめに設定する必要があるかもしれません)。
$TMPDIR が設定されていない限り、ロック lint は /var/tmp に一時利用ファイルを出力します。
.ll ファイルを生成するように メークファイルを修正するには、まずは .c から .o を作成するための規則を使って、.c から .ll を作成するルールを記述します。たとえば、以下の例から、その次の例を記述できます。
# Rule for making .o from .c in ../src. %.o: ../src/%.c $(COMPILE.c) -o $@ $<
# Rule for making .ll from .c in ../src. %.ll: ../src/%.c cc $(CFLAGS) $(CPPFLAGS) $(FOO) $<
上記の例では、コンパイラオプション (CFLAGS と CPPFLAGS) 用のメークマクロに -Zll フラグが指定されていなければなりません。
接尾辞規則を使用する場合、.ll をサフィックスとして定義する必要があります。そのため、ユーザーによっては、% ルールの使用を好むかもしれません。
適切な .o ファイルが make 変数 FOO_OBJS に含まれている場合、以下の行によって FOO_LLS を作成できます。
FOO_LLS = ${FOO_OBJS:%.o=%.ll}
あるいは、それらがサブディレクトリ ll にある場合は、以下のようになります。
FOO_LLS = ${FOO_OBJS:%.o=ll/%.ll}
サブディレクトリ ll/ に .ll ファイルを保存したい場合、以下のラベルにより、メークファイルに自動的にこのファイルを作成させることができます。
.INIT: @if [ ! -d ll ]; then mkdir ll; fi
ロック lint でソースコードを解析するには、まずは Sun WorkShop ANSI C コンパイラの -Zll オプションを使ってこれをコンパイルする必要があります。次に、コンパイラはコンパイルされた各 .c ファイルに 1 つずつロック lint データベースファイル (.ll ファイル) を作成します。そして、この .ll ファイルは、後ほど load サブコマンドによってロック lint に読み込みます。
ときには、ロック lint は、解析中に意味のある結果を返すために、単純化したコードを必要とする場合があります。こうしたシンプルな表示を提供できるようにするために、-Zll オプションは自動的にマクロ __lock_lint を定義します。
__lock_lint の使用法については、 「ロック lint の制限事項」 の項で詳しく解説します。
ロック lint インタフェースは、lock_lint コマンドおよびそのコマンドと一緒に指定することが可能な一連のサブコマンドから構成されます。
lock_lint [subcommand]
この例の subcommand の部分には、ソースコードを解析し、データ競合およびデッドロックを調べるために使用される一連のサブコマンドを指定します。サブコマンドの詳細については、付録 A 「ロック lint コマンドリファレンス」を参照してください。
ロック lint セッションにおける最初のサブコマンドは、いつも start でなければなりません。このサブコマンドが、ロック lint の利用状況に応じてユーザーが選択したサブシェルを起動します。ロック lint セッションはサブシェル内から開始されるため、このサブシェルを終了することでセッションは終了します。たとえば、C シェルを使っている場合、ロック lint を終了するには、コマンド exit を使用します。
ロック lint の状態は、読み込まれる一連のデータベースと指定されたアサーションから構成されます。そして、この状態の修正と解析の再実行を繰り返すことで、潜在的なデータ競合およびデッドロックを最適化するための情報を入手できます。1 つの状態に対して、解析はつねに 1 度だけ可能です。状態を再確立し、その状態に修正を加え、再度解析を試みるための手段としては、save、restore および refresh の各サブコマンドが提供されています。
ソースコードに注釈を挿入し、それをコンパイルして .ll ファイルを作成します。
「ソースコードへの注釈の挿入」の項を参照してください。
load サブコマンドを使って .ll ファイルを読み込みます。
assert サブコマンドを使って、関数および変数を保護するロックについてのアサーションを設定します。
これらの指定は、ソースコードへの注釈の挿入を使ってロック lint に伝えることもできます。「ソースコードへの注釈の挿入」の項を参照してください。
assert order サブコマンドを使って、デッドロックを回避するためには、ロックをどのような順番で獲得するかについてのアサーションを指定します。
これらの指定は、ソースコードへの注釈の挿入を使ってロック lint に伝えることもできます。「ソースコードへの注釈の挿入」を参照してください。
どの関数がルートであるかについてロック lint が正しく認識しているかをチェックします。
funcs -o サブコマンドがルート関数をルート (基点) として提示しない場合は、declare root サブコマンドを使って修正してください。funcs -o が非ルート関数をルートとして提示する場合は、declare... targets サブコマンドを使ってその関数を関数ターゲットとしてリストアップしている可能性が考えられます。ルート関数の詳細については、「declare root func」を参照してください。
assert rwlock サブコマンドを使って、階層的ロック関係 (存在する場合。ただし、まれです) を記述します。
これらの指定は、ソースコードへ注釈の挿入を使ってロック lint に伝えることもできます。「ソースコードへの注釈の挿入」 を参照してください。
ignore サブコマンドを使って、解析から除外したい関数および変数を無視します。
ignore コマンドの使用はできるだけ控えてください。このサブコマンドに相当するソースコードへの注釈の挿入 (例:NO_COMPETING_THREADS_NOW) もできる限り使用しないでください。
analyze サブコマンドを使って解析を実行します。
エラーを処理します。
この操作には、#ifdef__lock_lint (「ロック lint の制限事項」を参照) を使ったソースの修正、あるいは手順 3、4、6、7 を遂行するためのソースコードへの注釈の挿入 (「ソースコードへの注釈の挿入」を参照) などが含まれます。
ロック lint を解析以前の状態に戻し、必要ならば解析を再実行してください。
エラーは順番に処理することがベストです。順番が逆になると、関数のエントリに際して保持されないロックの問題、あるいは保持されていないのに解放されてしまうロックの問題によって、正常に保護されない変数についての誤ったメッセージが多数生じてしまいます。
analyze -v サブコマンドを使った解析を実行して、上記の手順を繰り返します。
analyze サブコマンドからのエラーがなくなったら、いかなるロックからも正常に保護されない変数をチェックします。
以下のコマンドを使用してください。
lock_lint vars -h | fgrep ¥*
適切なアサーションを使った解析を再実行し、どこで、適切にロックされないまま変数がアクセスされているかを見つけてください。
まったく同じ状態に対して analyze を 2 回実行することはできないので、analyze を実行する前に、save サブコマンドを使ってロック lint の状態を保存しておくことをお薦めします。また、ほかのアサーションを追加する前に、refresh または restore を使ってその状態を復元してください。analyze 前に自動的に保存を実行する、analyze 用のエイリアスを設定しておくと良いでしょう。
ロック lint は、C コンパイラによって作成される一連のデータベースによって、解析対象となるソースについての情報を獲得します。各ソースファイルのロック lint データベースは、独立したファイルに保存されます。一連のソースファイルを分析するには、load サブコマンドを使用してそれらに関連するデータベースファイルを読み込みます。files サブコマンドは、読み込まれたデータベースファイルによって表されるソースファイルのリストの表示に使用できます。いったんファイルが読み込まれると、ロック lint は、すべての関数、グローバルデータ、関連ソースファイルにおいて参照される外部関数についての情報を得るようになります。
解析フェーズの一部として、ロック lint は、読み込まれたすべてのソースに対してコールグラフを作成します。定義された関数についての情報は、funcs サブコマンドを介して利用可能となります。ロック lint が解析対象であるコードに対する正確なコールグラフを持つということは、意味のある解析を行う上でとても重要です。
読み込まれたファイルから呼び出されることのない関数はすべてルート関数と呼ばれます。読み込まれたモジュール内で呼び出される関数であっても (たとえば、関数がライブラリのエントリポイントであり、ライブラリ内でも呼び出される場合など)、ルート関数として扱いたいこともあります。そのためには、declare root サブコマンドを使用してください。また、ignore サブコマンドを実行することによって、コールグラフから関数を削除することも可能です。
ロック lint は、関数ポインタに対するすべての参照およびそれらに対して行なわれる関数の割り当て状況について知っています。現在読み込まれているファイルの関数ポインタについての情報は、funcptrs サブコマンドを介して利用できます。関数ポインタを経由して行われる呼び出しについての情報は、pointer calls サブコマンドを介して利用できます。もしも、ロック lint が発見できないような関数ポインタの割り当てが存在する場合、それらは declare ... targets サブコマンドによって指定されている可能性があります。
デフォルトでは、ロック lint は可能性のあるすべての実行パスの検証を行おうとします。そして、コードが関数ポインタを使用している場合、コードの通常の動作においては、実行パスの多くが実際にはたどられない可能性があります。これにより、実際には起こらないデッドロックが報告されてしまうことがあります。こうした事態を防ぐには、disallow および reallow サブコマンドを使用して、決して使用されることのない実行パスをロック lint に知らせてください。また、reallows および disallows サブコマンドを使用することによって、現時点での制限事項を出力できます。
ロック lint データベースにも、ソースコード内でアクセスされるすべてのグローバル変数についての情報が含まれます。これらの変数についての情報は、vars サブコマンドを介して利用できます。
ロック lint のジョブの 1 つは、変数のアクセスが整合性を保って保護されているかどうかを確認することです。特定の変数についてそのアクセスが問題にならない場合は、ignore サブコマンドによって、検証の対象から除外できます。
また、次に示すソースコードへの注釈の挿入のどれか1つを利用することも可能です。
SCHEME_PROTECTS_DATA |
READ_ONLY_DATA |
DATA_READABLE_WITHOUT_LOCK |
NOW_INVISIBLE_TO_OTHER_THREADS |
NOW_VISIBLE_TO_OTHER_THREADS |
詳細は、 「ソースコードへの注釈の挿入」を参照してください。
ソースコードへの注釈の挿入を利用することで、コード中のロックに関するアサーションを効率的に調整できます。アサーションには、保護、順序、副作用の 3 種類があります。
保護アサーションは、特定のロックによって何が保護されているかを説明します。以下に示すソースコードへの挿入を使用してください。
MUTEX_PROTECTS_DATA |
RWLOCK_PROTECTS_DATA |
SCHEME_PROTECTS_DATA |
DATA_READABLE_WITHOUT_LOCK |
RWLOCK_COVERS_LOCK |
assert サブコマンドのバリエーションは、特定のロックがデータまたは関数の一部分を保護していることをアサートするために使用されます。さらに別のバリエーションである assert ... covers は、特定のロックがほかのロックを保護していることをアサートします (これは階層的ロッキングスキームに使用されます)。
順序アサーションは、特定のロックが取得されるべき順序を指定します。ソースコードへの注釈の挿入 LOCK_ORDER または assert order サブコマンドによって、ロックの順序を指定できます。
副作用アサーションは、ある関数が特定のロックの解放または取得の副作用を持つことをアサートします。 以下に示すソースコードへの注釈の挿入を使用してください。
MUTEX_ACQUIRED_AS_SIDE_EFFECT |
READ_LOCK_ACQUIRED_AS_SIDE_EFFECT |
WRITE_LOCK_ACQUIRED_AS_SIDE_EFFECT |
LOCK_RELEASED_AS_SIDE_EFFECT |
LOCK_UPGRADED_AS_SIDE_EFFECT |
LOCK_DOWNGRADED_AS_SIDE_EFFECT |
NO_COMPETING_THREADS_AS_SIDE_EFFECT |
COMPETING_THREADS_AS_SIDE_EFFECT |
assert side effect サブコマンドによって副作用を指定することも可能です。ときには、外部関数についての副作用アサーションを作成したいこともあり、その場合、ロックは読み込まれたモジュールから可視ではありません (たとえば、外部関数のモジュールに対して静的である場合など)。このようなケースでは、declare サブコマンドの形式を使用することで、ロックを“作成”できます。
ロック lint の主たる役割は、データ競合やデッドロックに通じる可能性のある整合性に欠けたロックの使用状況を報告することです。ロックの使用状況の解析は、analyze サブコマンドの使用時に行われます。その結果として、以下の問題が報告されます。
ロックに副作用を生じさせたり、ロックの副作用についてのアサーションに違反する関数 (たとえば、相互排他ロックの状態をロックから非ロックへと変化させる関数など)。関数がエントリにおいてロックを獲得した後、あるリターンポイントでその解放に失敗した場合などに、意図しない副作用はよく起こります。関数の中のその経路は、副作用としてロックを獲得したと言われます。この種の問題はデータ競合とデッドロックの両方につながる可能性があります。
ロックについて整合性に欠ける副作用を持ち (つまり、関数に異なるパスが存在するということ)、異なる副作用を生じる関数。これはロック lint の制限事項 (「「ロック lint の制限事項」」を参照) であり、よくあるエラーの原因です。ロック lint はこうした関数を扱うことができず、つねにエラーとしてのみ報告し、正しく解釈することはありません。たとえば、ある関数から戻ったとき、何回かに 1 度、その関数において獲得されたロックの解除を忘れてしまうこともあります。
関数のエントリにおいて、どのロックが保持されるべきかについてのアサーションの違反。この問題はデータ競合につながる可能性があります。
変数がアクセスされた時点で、ロックが保持されるべきかについてのアサーションの違反。この問題はデータ競合につながる可能性があります。
ロックが獲得される順序について指定するアサーションの違反。この問題はデッドロックにつながる可能性があります。
特定の条件変数上のすべての待機に対して、同じ (あるいはアサートされた) 相互排他ロックが使用できない。
アサーションおよびロックと関連したソースコード解析に関するその他の問題。
解析の後、ロック lint のサブコマンドによって以下の操作が可能となります。
その他のロックに関する不整合部分の発見。
適切な declare、assert、ignore サブコマンドの作成。これらの操作は、ロック lint の状態を復元した後、再度解析を行う前に指定できます。
そうしたサブコマンドの 1 つが order であり、ロックが獲得された順序に関する調査に使用できます。ロック lint が潜在的なデッドロックをより正確に診断できるように、ロックの順序に関する問題を理解し、そうした順序についてのアサーションを作成するためには、こうした情報は有効です。
さらに、同様のサブコマンドには vars があります。vars サブコマンドは、変数の読み取りまたは書き込み時 (存在する場合)、どのロックが整合性を保持するかについて報告します。保護に関する規約がもともとドキュメント化されていなかったり、古くなってしまっているコードにおいて、保護規約を確定するためには、こうした情報は有効です。
ロック lint の解析力にも限界はあります。こうした限界の最大の要因は、ロック lint がユーザーの変数の値を知らないという現実にあります。
ロック lint は、ありがちな原因を無視したり、想定を単純化することで、さまざまな種類の問題を解決します。その他の問題も、アプリケーションの中で条件付きでコンパイルされたコードを使用することで回避できます。-Zll オプションを使ってコンパイルを行うと、コンパイラはつねにプリプロセッサマクロ __lock_lint を定義します。このマクロを利用することで、コードがあいまいになることをかなり回避できます。
ロック lint は以下の問題についての推論ができません。
関数ポインタがどの関数を指示しているか。こうした関数の割り当てをロック lint は推論することができません (「declare」を参照) 。関数ポインタに新しい割り当てを追加するには、declare サブコマンドが利用できます。
関数ポインタによる呼び出しに注目する際、ロック lint はその関数ポインタの、可能性のあるすべての値に対応する呼び出し経路をテストします。実行されない呼び出しシーケンスの存在が明らかな場合、あるいはその疑いがある場合は、disallow および reallow サブコマンドを使って、どのシーケンスを実行するかを指定してください。
if (x) pthread_mutex_lock(&lock1);
この場合、2 つの実行パス (1 つはロックを保持し、もう 1 つはロックを保持しない) が作成され、unlock 呼び出しはおそらく副作用メッセージを生成する原因となります。__lock_lint マクロを利用し、ロックは無条件で行われるよう、ロック lint に強制的に扱わせることによって、こうした問題を処理できる場合もあります。
#ifdef __lock_lint pthread_mutex_lock(&lock1); #else if (x) pthread_mutex_lock(&lock1); #endif
以下のようなコードの解析ではロック lint にはなんの問題も生じません。
if (x) { pthread_mutex_lock(&lock1); foo(); pthread_mutex_unlock(&lock1); }
この場合、実行パスは 1 つだけしかなく、そのパスに沿ってロックが獲得および解放され、副作用は起こりません。
構造の要素が使用されているコードにおいて、どの変数およびロックが使用されているか (「ロックの逆転」を参照してください)。
struct foo* p; pthread_mutex_lock(p->lock); p->bar = 0;
配列のどの要素がアクセスされているか。これは前のケースと類似的に扱われます。つまり、インデックスは無視されます。
longjmps に関する一切。
いつループから抜けるか、あるいは再帰を脱出するか (そのため、自身がループしていることが判明するとすぐに、あるいは 1 度の再帰の後、パスに沿った処理は中止されます)。
その他のロック lint の問題点には以下のものがあります。
ロック lint は、相互排他ロックおよび読み取り書き込みロックの使用状況についてのみ解析を行います。ロック lint は、条件変数とともに使用されている相互排他ロックの限られた整合性のチェックを実行します。しかし、ロック lint はセマフォおよび条件変数を、ロックとは認識しません。こうした解析においても、ロック lint がその意味を理解できる対象は限られています。
ロック lint が 2 つの変数が同じ変数である、あるいは 1 つの変数を 2 つの異なる変数であると思い込んでしまう状況があります (「ロックの逆転」を参照)。
(ポインタを介した) スレッド間での自動変数の共有は可能ですが、ロック lint は自動変数は非共有であると想定し、通常はそれらを無視します (ロック lint の対象となるのは、それらが関数ポインタの場合のみです)。
ロック lint は、ロックの副作用に整合性のない関数について不具合を報告します。関数がそうした副作用を持つ持たないに関わらず、ロック lint に単純化したコードを与えるためには、#ifdef およびアサーションが使用されなければなりません。
解析中、ロック lint は rw_upgrade と呼ばれるロック操作についてのメッセージを生成する場合もあります。そうした呼び出しは現実には存在しませんが、ロック lint は以下のようなコードをその次のコードのように書き直します。
if (rw_tryupgrade(&lock1)) { ... }
if () { rw_tryupgrade(&lock1); ... }
rw_tryupgrade() が起こる場所では、ロック lint は常にそれが成功しているものとみなします。
すでに保持されているロックを獲得しようとした場合、ロック lint はエラーとしてフラグを立てます。しかし、ロックに名前未定の場合 (たとえば、foo::lock)、この名前は単独のロックではなく、一連のロックを参照するため、こうしたエラーは抑制されます。しかし、名前未定のロックが常に同じロックを参照する場合は、ロック lint がこの種の潜在的デッドロックを報告できるように、declare one サブコマンドを使ってください。
これらのロックから独自のロックを構築した場合 (たとえば、再帰的な相互排他はときに通常の相互排他から構築されます)、ロック lint はそれらについて知ることはありません。一般的には、#ifdef を使って、通常の相互排他が操作されているかのようにロック lint に提示します。名前未定のロックが再帰的にロックされても、エラーが生成されることはないので、再帰的なロックに対しては、名前未定のロックを使用してください。以下に例を示します。
void get_lock() { #ifdef __lock_lint. struct bogus *p; pthread_mutex_lock(p->lock); #else <the real recursive locking code> #endif }