ロック lint は、相互排他および複数読み取り/単一書き込みロックの使い方を解析するコマンド行ユーティリティであり、こうしたロック技術の使い方に整合性がない部分を捜し出します。
この章は、以下の項目から構成されています。
マルチスレッドモデルにおいて、 プロセスは共通のアドレス空間およびその他のプロセス資源のほとんどを共有する 1 つまたは複数の制御のスレッドから構成されます。スレッドは、共有するデータに関連したロックを獲得し、解放しなければなりません。 こうした操作を行わないと、 データの競合が起こる可能性があります (こうした状況では、同じ入力を基に実行を繰り返したとしても、プログラムは異なる結果を生み出す可能性があります)。
データ競合は比較的起こりやすい問題の 1 つです。最初に適切なロックを獲得せずに、変数を利用しようとすると、簡単に起こります。その一方で、データ競合はその発見が困難です。一般的には、適切に保護されていないデータに対して、2 つのスレッドがほぼ同じタイミングでアクセスしようとしたときにのみ、その兆候は現れます。したがって、データ競合が問題の兆候を見せることなく、数カ月に渡って正常に動作することもあるのです。どんなに単純なマルチスレッドプログラムであっても、プログラムのすべての並列動作状況を徹底的にテストすることは困難であるため、従来のテストおよびデバッグ方法ではデータ競合を十分に防ぐことはできませんでした。
ほとんどのプロセスは複数の資源を共有しています。アプリケーション内での動作は、複数の資源の利用を必要とします。これはつまり、プロセスの実行前には、各資源に対するロックを取得しておく必要があるということです。異なる操作が共通の一連の資源を利用し、ロックを取得する順番に整合性がない場合、デッドロックの可能性が生じます。たとえば、2 つのスレッドが異なる資源のロックを獲得した後、各スレッドがもう一方のスレッドが所有する資源のロックを獲得しようとする際に、最も単純な デッドロックが起こります。
ロックおよびその使用法の解析において、ロック lint は一般的なデータ競合 (変数にアクセスする際に、適切なロックを獲得できないなど) の原因を検出します。
表 5-1、 表 5-2 および 表 5-3 に、ロック lint が認識する Solaris および POSIX libthread API の ルーチンの一覧を示します。
表 5-1 排他ロック
Solaris |
POSIX |
Kernel (Solaris のみ) |
---|---|---|
mutex_lock mutex_unlock mutex_trylock |
pthread_mutex_lock pthread_mutex_unlock pthread_mutex_trylock |
mutex_enter mutex_exit mutex_tryenter |
表 5-2 読み取り書き込みロック
Solaris |
Kernel (Solaris のみ) |
---|---|
rw_rdlock, rw_wrlock rw_unlock rw_tryrdlock, rw_trywrlock
|
rw_enter rw_exit rw_tryenter rw_downgrade rw_tryupgrade |
表 5-3 条件変数
Solaris |
POSIX |
Kernel (Solaris のみ) |
---|---|---|
cond_broadcast cond_wait
cond_timedwait
cond_signal |
pthread_cond_broadcast pthread_cond_wait
pthread_cond_timedwait
pthread_cond_signal |
cv_broadcast cv_wait cv_wait_sig cv_wait_sig_swap cv_timedwait cv_timedwait_sig cv_signal |
さらに、ロック lint は表 5-4 に示す型も認識します。
表 5-4 ロック構造
Solaris |
POSIX |
Kernel (Solaris のみ) |
---|---|---|
mutex_t rwlock_t |
pthread_mutex_t
|
kmutex_t krwlock_t |
ロック lint は、解析対象であるモジュールに関するいくつかの基本情報の報告を行います。
関数のロッキング副作用。未知の副作用がデータ競合やデッドロックにつながることがあります。
(少なくとも 1 つのロックによって) 整合性のある保護がなされていない変数へのアクセス。および、どのロックが変数を保護するかに関するアサーションに違反するアクセス。この情報は潜在的なデータ競合を指摘します。
サイクルおよび整合性のないロックの取得順序。この情報は潜在的なデッドロックを指摘します。
特定のロックによって保護されている変数。この情報は選択されている細分性 (どの変数がどのロックによって保護されているか) が適切かどうかを判断する手助けとなります。
ロック lint はアプリケーションについてのアサーションを指定するためのサブコマンドを提供します。解析段階において、ロック lint はアサーションの違反を報告します。
プログラムを拡張する際には、新しいアサーションを追加し、解析フェーズを 実行してそのアサーションに改善を加え、その結果確立された プログラムのロック規則を新しいコードが違反しないように留意すべきです。
コンパイラはロック lint が使用する情報を収集します。より明確に言うなら、コマンド行オプション -Zll を C コンパイラに指定することで、それぞれの .c ソースコードファイルに対して .ll ファイルを生成します。この .ll ファイルには、各関数の制御の流れ、および相互排他あるいは読み取り側/書き込み側ロックにおける変数または操作のアクセスについての情報が含まれます。
-Zll フラグを指定したコンパイルでは、.o ファイルは作成されません。
ロック lint との対話の操作には、ソースコードへの注釈の挿入とコマンド行インタフェースの 2 通りの方法が用意されています。
ソースコードへの注釈の挿入とは、ロック lint に情報を渡すためにソースコード中に置かれるアサーションおよび NOTE です。ロック lint は、コード中の特定のポイントにおけるロックの状態に関するアサーションを検査し、挿入された注釈はロック動作が正しいかどうかの検証や、不要なエラーメッセージの回避に利用されます。
詳細については、「ソースコードへの注釈の挿入」 の項を参照してください。
もう 1 つは、ロック lint サブコマンドを使用して、関連 .ll ファイルを読み込み、アサーションを指定する方法です。このロック lint とのインタフェースは、lock_lint コマンドおよび lock_lint コマンド行で指定する一連のサブコマンドから構成されます。
lock_lint サブコマンドの特色を以下に示します。
対応する注釈挿入を持たないいくつかの追加制御を実行することができる。
プログラム中の関数、変数、関数ポインタ、ロックについての便利なクエリーを数多く作成できる。
ロック lint サブコマンドは、コードを解析し、ロックによって確実に保護されていない変数を発見する手助けとなります。どの変数がロックによって保護され、関数の呼び出し時にはどのロックが確立されるかといった、アサーションを決定することができるでしょう。適切なアサーションの下で、解析を実行することによって、そのアサーションがどこで違反されるかが示されます。
付録 A 「ロック lint コマンドリファレンス」を参照してください。
ほとんどのプログラマは、コマンド行サブコマンドよりもソースコードへの注釈挿入を好むことが報告されています。しかし、両者の間には必ずしも 1 対 1 の対応関係は存在していないことも事実です。
ロック 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 }
注釈とは、ソースコード中に挿入されるひとまとまりの文字列です。注釈を利用することで、ロック lint 自身では推測できないプログラムに関するさまざまな事項をロック lint に伝え、その結果、極端に多量なフラグの出力を抑制したり、ロック lint に一定の条件下でのテストを行わせることができます。注釈は、ドキュメントコードにおいて、一般のコメントとほぼ同様の働きをします。ソースコードへの注釈の挿入には、アサーションと NOTE の 2 種類があります。
注釈は、付録 A 「ロック lint コマンドリファレンス」 で紹介するロック lint サブコマンドのいくつかと同じ働きをします。次の「ソースコードへの注釈の挿入を使用する理由」の項でも述べるように、これらのサブコマンドの代わりにソースコードへの注釈の挿入を利用するほうが一般的に望まれています。
ソースコードへの注釈の挿入を利用するには、いくつかの理由があります。一般的に、注釈はロック lint サブコマンドのスクリプトを利用するよりも望まれています。
注釈は説明の対象となるコードと組み合わせて記述されるためロック lint サブコマンドのスクリプトよりも一般的にメンテナンスが容易です。
注釈によって、関数中のいかなる場所においても、そのロック状態についてのアサーションを指定できます (アサーションを置いた位置が、チェックの行われる箇所となります)。一方、サブコマンドを使用する場合、どんなに細分性のある解析を行おうとしても、関数単位のアサーションの指定となります。
サブコマンドに記述される関数は変更される可能性があります。誰かが関数の名前を func1 から func2 に変更した場合、func1 と記述したサブコマンドはエラーとなるでしょう (たとえ動作したとしても、ほかの関数に func1 という名前が与えられている場合、さらに悪い事態が起こる可能性があります)。
NOTE (NO_COMPETING_THREADS_NOW) など、注釈によっては、対応するサブコマンドが存在しないものもあります。
注釈は、プログラムのドキュメント化という観点からも有効です。ロック lint をそれほど頻繁に使用しない場合も、注釈を使用しておけば、プログラムのドキュメント化には有用です。たとえば、変数を宣言するヘッダーファイルでは、どのロックあるいは規則が変数を保護しているかをドキュメント化できます。あるいは、ロックを獲得し、意図的にロックを解放せずに戻ってくるような関数においては、注釈によって、その意図を明確に宣言できます。
ロック lint はソースコードへの注釈の挿入のスキーマをほかのツールと共有します。Sun WorkShop ANSI C コンパイラをインストールする場合、ロック lint が理解できるすべての注釈名を含むファイル SUNW_SPRO-cc-ssbd も自動的にインストールされます。このファイルは、<installation_directory>/SUNWspro/SC5.0/lib/note に保存されています。
/usr/lib/note 以外の保存場所を指定するには、環境変数 NOTEPATH を以下のように指定してください。
setenv NOTEPATH $NOTEPATH:other_location
NOTEPATH のデフォルト値は以下のとおりです。
<installation_directory/SUNWSPRO/SC5.0/lib/note:/usr/lib/note>
ソースコードへの注釈の挿入を使用するには、ファイル note.h をソースまたはヘッダーファイルに含めておいてください。
#include <note.h>
ノート形式の注釈の多くは、引数として (ロックまたは変数の) 名前を受け付けます。名前は表 5-5 に示す構文で指定します。
表 5-5 ロック lint NOTE による名前の指定
構文 |
意味 |
---|---|
Var |
名前付き変数 |
Var.Mbr.Mbr... |
名前付き struct/union 変数のメンバー |
Tag |
このタグを使った、名前未定の struct/union |
Tag::Mbr.Mbr... |
名前未定の struct/union のメンバー |
Type |
この typedef を使った、名前未定の struct/union |
Type::Mbr.Mbr... |
名前未定の struct/union のメンバー |
C では、構造体のタグと型 (Type) は、独立した名前空間に保存されるため、ロック lint に関する限り、2 つの異なる struct が同じ名前を持つことが可能となっています。ロック lint が foo::bar を見つけると、まずはタグ foo の付いた struct を捜します。そして、見つからなかった場合は、Type foo を探し、それが struct を表していることを確認します。
しかし、ロック lint の厳密な動作では、特定の変数またはロックは確実に 1 つの名前によって認識されることが要求されます。そのため、struct に対してタグが提供されず、struct が typedef の一部として定義されている場合にのみ、Type が使用されます。
たとえば、以下の例において Foo は型 (Type) の名前として作用します。
typedef struct { int a, b; } Foo;
こうした制限によって、struct について知られた名前は必ず 1 つだけとなります。
名前引数は一般式を受け付けません。たとえば、以下の記述は正しくありません。
NOTE(MUTEX_PROTECTS_DATA(p->lock, p->a p->b))
しかし、注釈の中には (名前以外に) 式を受け入れるものもあります。それらは明確に区別されます。
多くの場合、注釈は名前のリストを引数として受け付けます。リストの要素は空白によって区切られます。リストの指定を単純化するため、こうしたリストを取るすべての注釈は、多くのシェルと同様のジェネレータメカニズムが理解されます。
Prefix{A B ...}Suffix
Prefix、Suffix、A、B、... の部分は、空白を含まない文字列ならば何でもかまいません。そのため、上記の表記は以下の表記に相当します。
PrefixASuffix PrefixBSuffix ...
たとえば、以下の表記は、
struct_tag::{a b c d}
次の冗長なテキストと同じ意味となります。
struct_tag::a struct_tag::b struct_tag::c struct_tag::d
この構造は次のように入れ子とすることも可能です。
foo::{a b.{c d} e}
これは以下と同じ意味になります。
foo::a
foo::b.c
foo::b.d
foo::ae
注釈がロックまたはほかの変数を参照する場合、そのロックまたは変数の宣言または定義はすでに出現している必要があります。
データの名前が構造体を表す場合、その構造体のすべての非ロック (相互排他または読み取り側/書き出し側) メンバーを参照します。そうしたメンバーの 1 つが構造体そのものである場合、その非ロックメンバーすべてを意味します。しかしロック lint は条件変数の抽象性を理解するため、それを構成するメンバーへと分けることはありません。
NOTE インタフェースは、コンパイル後のオブジェクトコードに影響を与えることなく、ソースコードへのロック lint 用の情報の挿入を可能にします。ノート形式の注釈の基本構文は、以下のどちらかです。
NOTE(NoteInfo)
または
_NOTE(NoteInfo)
そして、_NOTE よりも NOTE のほうが一般的に好まれています。ただし、複数の関連しないプロジェクトにおいて使用されるヘッダーファイルでは、混乱を避けるために _NOTE を使用してください。NOTE がすでに使用され、それを変更したくない場合は、_NOTE を使ってほかのマクロ (ANNOTATION など) を定義してください。たとえば、以下の内容を含むインクルードファイル (annotation.h とします) を定義できます。
#define ANNOTATION _NOTE #include <sys/note.h>
NOTE インタフェースに渡される NoteInfo は、書式上は以下のいずれかの形式に一致しなければなりません。
NoteName
NoteName(Args)
NoteName は、注釈の種類を示す単なる識別子です。トークンとして正しく、カッコのトークンが適合してさえいれば (閉じカッコが存在する)、Arg は何でもかまいません。それぞれの個別の NoteName は、引数に関して、独自の要求事項を持つことになります。
このマニュアルでは、特にことわり書きがない限り、NOTE は NOTE と _NOTE の両方を指します。
NOTE は、ソースコード中の、特定の定義済みの箇所においてのみ有効です。
トップレベル:すべての関数定義、type および struct 定義、変数宣言、その他の 構文の外側。以下に例を示します。
struct foo { int a, b; mutex_t lock; }; NOTE(MUTEX_PROTECTS_DATA(foo::lock, foo)) bar() {...}
宣言あるいはステートメント中のブロック内のトップレベル。この場合も、注釈はすべての type および struct 定義、変数宣言、その他の構文の外側でなければなりません。
foo() { ...; NOTE(...) ...; ...; }
NOTE() は、前述の場所でのみ使用されます。たとえば、以下の場所での使用は正しくありません。
a = b NOTE(...) + 1;
typedef NOTE(...) struct foo Foo;
for (i=0; NOTE(...) i<10; i++) ...
ノート形式の注釈はステートメントではありません。つまり、NOTE() は、ブロックを構成するために中括弧が使用されない限り、if/else/for/while 本文の内側では使用してはなりません。たとえば、以下の表記は構文エラーを引き起こします。
if (x)
NOTE(...)
以下の注釈は、関数定義の外側でも内側でも使用可能です。注釈中で記述される名前はすべて、もあらかじめ宣言されている必要があります。
NOTE(MUTEX_PROTECTS_DATA(Mutex, DataNameList))
NOTE(RWLOCK_PROTECTS_DATA(Rwlock, DataNameList))
NOTE(SCHEME_PROTECTS_DATA("description", DataNameList))
最初の 2 つの注釈は、指定されたデータがアクセスされる場合は、必ずロックが保持されるよう、ロック lint に指示します。
3 番目の注釈 SCHEME_PROTECTS_DATA は、データが相互排他ロックも読み取り書き込みロックのいずれも持たない場合に、データをどのように保護するかを説明します。スキーマに対して提供される description はただのテキストであり、プログラム的には重要ではありません。ロック lint は、指定されたデータを完全に無視することで対応します。description の部分は自由に指定できます。
上記の注釈の使用方法を説明するために、いくつかの例を示します。最初の例はとてもシンプルで、ロックが 2 つの変数を保護することを示しています。
mutex_t lock1; int a,b; NOTE(MUTEX_PROTECTS_DATA(lock1, a b))
次の例では、さまざまな可能性が示されています。struct foo のメンバーの一部は静的なロックで保護され、ほかのメンバーは foo 上のロックで保護されています。そして、foo のほかのメンバーは、その利用上の規則によって保護されています。
mutex_t lock1; struct foo { mutex_t lock; int mbr1, mbr2; struct { int mbr1, mbr2; char* mbr3; } inner; int mbr4; }; NOTE(MUTEX_PROTECTS_DATA(lock1, foo::{mbr1 inner.mbr1})) NOTE(MUTEX_PROTECTS_DATA(foo::lock, foo::{mbr2 inner.mbr2})) NOTE(SCHEME_PROTECTS_DATA("convention XYZ", inner.mbr3))
1 つのデータは 1 つの手段においてのみ保護が可能です。1 つのデータに対して、保護に関する複数の注釈が使用されている場合 (前記の 3 つ以外だけでなく、READ_ONLY_DATA も含みます)、後の注釈によって自動的に以前の注釈は上書きされます。これによって、1、2 の例外を除く全メンバーが同じ方法で保護されるという構造記述が容易になります。たとえば、以下の例の struct BAR のメンバーのほとんどは struct foo 上のロックによって保護されますが、ただ 1 つだけはグローバルロックによって保護されます。
mutex_t lock1; typedef struct { int mbr1, mbr2, mbr3, mbr4; } BAR; NOTE(MUTEX_PROTECTS_DATA(foo::lock, BAR)) NOTE(MUTEX_PROTECTS_DATA(lock1, BAR::mbr3))
NOTE(READ_ONLY_DATA(DataNameList))
この注釈は、関数定義の外側でも内側でも使用可能で、データをどう保護するべきかをロック lint に指示します。また、データは読み取り専用で、書き込み不可であると指示します。
非可視とみなされている間に、読み取り専用データが書き込まれる場合、エラーは検出されません。ほかのスレッドがデータにアクセスできない場合 (たとえばほかのスレッドがそれを認知していない場合など)、データは非可視とみなされます。
この注釈は、初期化され、その後変更されることが決してないデータと一緒に使用されることがよくあります。ほかのスレッドに対してデータが可視状態となる以前の実行時に初期化が行われる場合は、注釈を使って、その間はデータが非可視であることをロック lint に知らせてください。
ロック lint は、const データが読み取り専用であることを知っています。
NOTE(DATA_READABLE_WITHOUT_LOCK(DataNameList))
この注釈は、関数定義の外側でも内側でも使用可能で、保護用のロックを保持することなく、指定されたデータの読み取りが可能であることをロック lint に知らせます。修正するつもりがなければ、非保護データを参照することは不正ではないため、単独で存在する不可分な読み取り可能データ (これに対して、その値がまとめて使用されるデータセットもあります) に対してこの注釈は有用です。
NOTE(RWLOCK_COVERS_LOCKS(RwlockName, LockNameList))
この注釈は、関数定義の外側でも内側でも使用可能で、読み取り書き込みロックとその他のロックのセット間に階層関係が存在することをロック lint に伝えます。こうしたルールの下では、書き込みアクセス用のカバーロックを保持することで、そのカバーされたロックによって保護されるすべてのデータに対するスレッドアクセスを提供します。また、何らかのカバーされたロックを保持する場合、スレッドは読み取りアクセス用のカバーロックを必ず保持しなければなりません。
このように読み取り書き込みロックを使ってほかのロックをカバーする方法は、単に規則の 1 つです。つまり、特殊なロックの種類ではありません。しかし、ロック lint がこうしたカバーリング関係について知らされていないと、ロックは通常の表記方法に従って使用されるとみなしてしまい、その結果、エラーを生成します。
以下の例では、名前未定の foo 構造のメンバー lock が、名前未定の構造 bar と zot のメンバー lock をカバーしています。
NOTE(RWLOCK_COVERS_LOCKS(foo::lock, {bar zot}::lock))
NOTE(MUTEX_ACQUIRED_AS_SIDE_EFFECT(MutexExpr))
NOTE(READ_LOCK_ACQUIRED_AS_SIDE_EFFECT(RwlockExpr))
NOTE(WRITE_LOCK_ACQUIRED_AS_SIDE_EFFECT(RwlockExpr))
NOTE(LOCK_RELEASED_AS_SIDE_EFFECT(LockExpr))
NOTE(LOCK_UPGRADED_AS_SIDE_EFFECT(RwlockExpr))
NOTE(LOCK_DOWNGRADED_AS_SIDE_EFFECT(RwlockExpr))
NOTE(NO_COMPETING_THREADS_AS_SIDE_EFFECT)
NOTE(COMPETING_THREADS_AS_SIDE_EFFECT)
これらの注釈は、関数定義の外側でも内側でも使用可能です。各注釈は、関数が指定されたロックに対する指定された副作用を持っていること (つまり、関数の終了時において、ロックを関数への進入時とは異なる状態のままに意図的にしておくことなど) をロック lint に伝えます。最後の 2 つの注釈の場合は、副作用はロックについてのものではなく、並行性の状態についてのものとなります。
副作用として読み取り書き込みロックが獲得されることを記述する場合、そのロックが読み取りアクセス用または書き込みアクセス用のどちらかを指定する必要があります。
ロックが読み取り専用アクセス用に獲得された状態から読み取り書き込みアクセス用に獲得された状態に変化する場合、ロックは昇格したといいます。一方、逆方向の変化はロックの降格といいます。
ロック lint は、各関数について、ロックの副作用 (および並行性) を解析します。通常、関数は副作用を持たないとロック lint は考えます。そこで、コードにそうした効果を持たせる場合は、注釈を使ってその意図をロック lint に伝えなければなりません。そして、注釈に指定された副作用とは異なる副作用を持つことが判明すると、エラーメッセージが返されます。
この項で解説している注釈は、だいたい、関数の性質を指すものであり、コードの特定のポイントを指すものではありません。このため、これらの注釈は関数の先頭に記述することをお薦めします。たとえば、以下の 2 つの例の間に違いはありません。上の例の方が読みやすいということだけです。
foo() { NOTE(MUTEX_ACQUIRED_AS_SIDE_EFFECT(lock_foo)) ... if (x && y) { ... } }
foo() { ... if (x && y) { NOTE(MUTEX_ACQUIRED_AS_SIDE_EFFECT(lock_foo)) ... } }
関数がそうした副作用を持つ場合、その作用は関数内のどの経路においても同じでなければなりません。ロック lint は、指定された以外の副作用を持つ関数のパスの解析については拒否します。
NOTE(COMPETING_THREADS_NOW)
NOTE(NO_COMPETING_THREADS_NOW)
これらの 2 つの注釈は、関数定義の内側でのみ使用可能です。1 つ目の注釈は、コードのこのポイントの後に、このスレッドがアクセスするデータと同じデータにアクセスしようとするほかのスレッドが存在することをロック lint に伝えます。2 つ目の注釈は、もはやそうした状態ではないこと (ほかに実行中のスレッドはない、あるいは、実行中のいかなるスレッドもこのスレッドがアクセスしているデータにアクセスすることはないなど) を指定します。競合するスレッドが存在しなければ、ロック lint は、通常はそのデータを保護するロックを保持することなくそのコードがデータにアクセスするかどうかについて、不具合を報告することはありません。
これらの注釈は、追加のスレッドを 1 つも起動しないうちに、ロックを保持せずにデータを初期化する関数において有用です。こうした関数は、ほかのすべてのスレッドが終了するのを待った後、ロックを保持せずにデータにアクセスすることを許可されます。その具体例を以下に示します。
main() { <initialize data structures> NOTE(COMPETING_THREADS_NOW) <create several threads> <wait for all of those threads to exit> NOTE(NO_COMPETING_THREADS_NOW) <look at data structures and print results> }
NOTE が main() に存在する場合、ロック lint は、main() の開始時に、ほかのスレッドが実行中でないと想定します。main() に NOTE が含まれない場合は、その想定はしません。
競合スレッドが存在しているとすでに想定されている場合は、解析中に注釈 COMPETING_THREADS_NOW に出会っても、ロック lint は警告を発しません。たとえば、単純な入れ子構造の場合がそうです。注釈は各使用時に異なる意味を示すことができるため、警告メッセージは表示されません (概念的には、スレッドの競合とは、コードの一部分が次のコードと異なる場合を意味します)。一方、先行する COMPETING_THREADS_NOW 注釈 (明示的でも非明示的でも) と一致しない NO_COMPETING_THREADS_NOW 注釈は、警告の原因となります。
NOTE(NOT_REACHED)
この注釈は、関数定義の内側でのみ使用可能で、コードの特定のポイントへの到達が不可能であるため、そのポイントにおいて保持されるロックの条件を無視するように、ロック lint に指示します。この注釈は、たとえば lint の注釈 /* NOTREACHED */ の使用方法のように、exit() に対するすべての呼び出しの後に使用する必要はありません。ただし、この注釈を exit() の定義もしくはそれと同様の定義 (主にロック lint ライブラリ) の中で使用してください。そうすれば、ロック lint は、そうした関数の呼び出しの後のコードには到達できないことがわかります。この注釈は、ロック lint ライブラリの外側ではあまり使用すべきではありません。その使用例 (ロック lint ライブラリ中) を以下に示します。
exit(int code) { NOTE(NOT_REACHED) }
NOTE(LOCK_ORDER(LockNameList))
この注釈は、関数定義の外側あるいは内側のいずれかで使用可能で、ロックが獲得されるべき順番を指定します。機能的には、assert order および order サブコマンドに類似しています。詳しくは、付録 A 「ロック lint コマンドリファレンス」を参照してください。
デッドロックを避けるために、ロック lint は、同時に複数のロックが保持されなければならない場合、常に、獲得される順序が明確になっていることを前提とします。注釈によってそうした順序がロック lint に知らされている場合、その順序に違反すると、通知メッセージが生成されます。
この注釈は複数回使用されることもありますが、その意味は適切に組み合わされます。たとえば、以下の注釈が与えられた場合、
NOTE(LOCK_ORDER(a b c))
NOTE(LOCK_ORDER(b d))
ロック lint は以下の順序を演繹的に推論します。
NOTE(LOCK_ORDER(a d))
この例では、c と d の順序について推論することは不可能です。
この順序の中に循環が存在する場合は、該当するエラーメッセージが生成されます。
NOTE(NOW_INVISIBLE_TO_OTHER_THREADS(DataExpr, ...))
NOTE(NOW_VISIBLE_TO_OTHER_THREADS(DataExpr, ...))
これらの注釈は関数定義の内側でのみ許可され、指定された式によって表される変数がほかのスレッドに対して可視状態であるかどうか、つまりは、ほかのスレッドがこの変数にアクセスできるかどうかをロック lint に伝えます。
また、通常は可視であると想定されている変数でも、この変数へのポインタを持ったスレッドが存在しないため、実際には不可視であることをロック lint に知らせるという注釈の使い方も一般的です。データをヒープの外に割り当てる場合など、こうした状況はしばしば起こります (ほかのスレッドにはまだその変数の構造が不可視であるため、ロックを保持せず安全に構造の初期化を行えます)。
Foo* p = (Foo*) malloc(sizeof(*p)); NOTE(NOW_INVISIBLE_TO_OTHER_THREADS(*p)) p->a = bar; p->b = zot; NOTE(NOW_VISIBLE_TO_OTHER_THREADS(*p)) add_entry(&global_foo_list, p);
関数の呼び出しが、変数を可視にしたり、不可視にしたりするという副作用を持つことは絶対にありません。関数から戻る時点で、関数が原因となって起こる可視性に関する一切の変化は逆転します。
NOTE(ASSUMING_PROTECTED(DataExpr, ...))
この注釈は関数定義の内側でのみ許可され、指定された式によって表される変数が以下に示すいずれかの方法によって保護されていると、この関数が想定しているということをロック lint に伝えます。
各変数に対して適切なロックが保持されている。
変数はほかのスレッドから不可視となっている。
呼び出しが行われる際に、競合するスレッドがない。
これらの条件のいずれも真でない場合、ロック lint はエラーを生成します。
f(Foo* p, Bar* q) { NOTE(ASSUMING_PROTECTED(*p, *q)) p->a++; ... }
ロック lint は、スレッドやロックに関連したいくつかのアサーションを認識します。(詳細については、「assert」のマニュアルページを参照してください)。
アサーションは、ステートメントが許可されている関数定義の内側でのみ作成が可能です。
ASSERT() はカーネルおよびドライバコードにおいて利用される一方、assert() はユーザー (アプリケーション) コードで利用されます。ただし、このマニュアルでは、特にことわり書きがない限り、いずれに対しても assert() を使用しています。
assert(NO_LOCKS_HELD);
このアサーションによって、コードのこのポイントに到達したなら、このテストを実行するスレッドによってロックは保持されるべきでないと、ロック lint は認識します。このアサーションに対する違反は、解析中に報告されます。ブロックを行うルーチンは、スレッドがブロックまたは終了するときには、保持されるロックがないように、このようなアサーションを使用できます。
また、このアサーションには、獲得されたいかなるロックもその位置では必ず解放させることを、コードを修正しようとする第三者に思い出させる働きもあります。
唯一必要なことは、このアサーションを、ブロックするリーフレベルの関数で使用することです。ブロックするほかの関数を呼び出すためだけに、呼び出す側の関数がブロックするのであれば、呼び出される側の関数のみにアサーションを含めておくだけで呼び出す側に含める必要はありません。そのため、このアサーションは、特にロック lint 用に記述されたライブラリのバージョン (lint ライブラリのように) において (libc など) もっとも頻繁に出現することになるでしょう。
ファイル synch.h は、まだ別途定義されていない場合は、NO_LOCKS_HELD を 1 と定義し、このアサーションが適合するようにします。つまり、実行時にはアサーションは効果的に無視されます。note.h または synch.h のいずれかをインクルードする前に (順番はどちらが先でもかまいません)、NO_LOCKS_HELD を定義することによって、デフォルトの実行時の状態を上書きできます。たとえば、コードの本体に a と b と呼ばれる 2 つのロックだけが使用されている場合は、以下の定義で十分でしょう。
#define NO_LOCKS_HELD (!MUTEX_HELD(&a) && !MUTEX_HELD(&b)) #include <note.h> #include <synch.h>
このようにしても、ロック lint によるアサーションのテストに影響はありません。ほかのロックが保持されていれば (a と b だけでなく)、ロック lint はその件について報告を行います。
assert(NO_COMPETING_THREADS);
このアサーションにより、コードのこのポイントに到達したなら、このコードを実行中のスレッドと競合するスレッドがほかにあってはいけないと、ロック lint は認識します。このアサーションに対する違反は (特定の NOTE 形式のアサーションによって提供される情報に基づき)、解析中に報告されます。保護用のロックを保持せずに変数にアクセスする関数は (同じデータにアクセスする関連スレッドはほかにはないとの想定の下で動作しています)、必ずそのようにマークされなければなりません。
デフォルトでは、このアサーションは実行時に無視されます (つまり、いつも成功するということです)。どのスレッドが競合するかという概念にはアプリケーションの情報が含まれるため、NO_COMPETING_THREADS は実行時についての一般的な意味を持つものではありません。たとえば、同じデバイスに対するドライバにおいて、実行中のスレッドがほかに存在しないことを示すため、こうしたアサーションを作成します。一般的な意味ではないため、synch.h は、まだ別途定義されていないならば、NO_COMPETING_THREADS を 1 と定義します。
しかし、note.h または synch.h のいずれかをインクルードする前に (インクルードする順番はどちらが先でもかまいません)、NO_COMPETING_THREADS を定義することによって、デフォルトの意味を上書きできます。たとえば、プログラムが num_threads という変数に実行中のスレッドの数のカウントを保存する場合は、以下の定義で十分です。
#define NO_COMPETING_THREADS (num_threads == 1) #include <note.h> #include <synch.h>
このようにしても、ロック lint によるアサーションのテストに影響はありません。
assert(MUTEX_HELD(lock_expr) && ...);
このアサーションは、カーネル内において広く利用されます。アサーションが有効状態にある場合は、実行時チェックを行います。同じ機能はユーザーコードにも存在します。
このコードは、ロック lint の解析中、有効なアサーションとともにコードが実際に実行されているときほぼ同じことを行います。つまり、実行中のスレッドが指定されたとおりのロックを保持しない場合、エラーを報告します。
スレッドライブラリは、いずれかのスレッドがロックを取得することをチェックするのみという、緩やかなテストを行います。一方、ロック lint はもっと強力なテストを実行します。
ロック lint は、MUTEX_HELD()、RW_READ_HELD()、RW_WRITE_HELD()、および RW_LOCK_HELD() マクロの使用、そしてその否定を認識します。こうしたマクロ呼び出しは、&& 演算子によって組み合わせることができます。たとえば、以下のアサーションで、ロック lint は、相互排他ロックが保持されておらず、読み取り書き込みロックが書き込み用に保持されていることをチェックします。
assert(p && !MUTEX_HELD(&p->mtx) && RW_WRITE_HELD(&p->rwlock));
ロック lint は以下のような式も認識します。
MUTEX_HELD(&foo) == 0