4: 適切なコードの作成
コミュニティ指向の設計プロセスについて語るべきことは多いですが、カーネル開発プロジェクトの証明はその成果としてのコードにあります。他の開発者による検証を受けてメインライン・ツリーにマージされるのはコードです。すなわち、プロジェクトの最終的な成功を決定するのはコードの品質です。本セクションではこのコーディング・プロセスについて考察します。まず、カーネル開発者が陥りやすい多くの過ちについて見ていきます。次に、物事を正しく行うこと、またそのために役立つツールに焦点を移して説明します。
4.1: 落とし穴
- コーディング・スタイル
カーネルには長い間、Documentation/CodingStyleに記述されている標準のコーディング・スタイルがありました。その間、そのファイルに記述された方法は、たかだか「助言」として位置付けられてきました。その結果、カーネル内にはこのコーディング・スタイルのガイドラインに合わない相当量のコードが含まれるようになりました。このようなコードの存在は、カーネル開発者にとって二つの危険性につながります。
そのひとつの危険性は、カーネルのコーディング標準は強制されるものではなく、問題にはならないという考え方です。実際、コードが標準に従って書かれていない場合、新しいコードのカーネルへの追加は非常に困難なものとなります。またレビューを行う前にコードの再フォーマットを要求する開発者も多く存在します。Linuxカーネルのように巨大化したコードベースでは、各開発者がすべてのコードの内容を素早く理解できるよう、コードの均質性が要求されます。そのため、特異なフォーマットのコードが許される余地はありません。
場合により、Linuxカーネルのコーディング・スタイルが、開発者の属する企業で義務付けられているスタイルと矛盾することがあります。そのような場合、コードをマージ可能にするためには、Linuxカーネルのスタイルを用いなければなりません。コードをカーネルにマージするということは、様々な面でその管理をある程度放棄することを意味しており、これにはコードのフォーマット方法も含まれています。
もうひとつの誤りは、カーネル内にある既存コードはコーディング・スタイルの修正が必要だと仮定することにあります。開発者の中にはこのLinux開発プロセスに慣れるため、またはカーネルのchangelogに自分の名前を入れるため(またはその両方)の方法として再フォーマット用パッチの作成を行うことができます。しかし、単なるコーディング・スタイルの修正は開発コミュニティからはノイズとして見られ、冷たい対応を受けがちです。そのため、この種のパッチは極力避けるべきです。別の理由で作業を行いながらコードのスタイルを修正することは当然なことですが、単にコーディング・スタイルの修正のためだけにこれを行うべきではありません。
また、コーディング・スタイル文書は、決して違反してはならない絶対的な法律として読むべきものでもありません。スタイルに違反すべき妥当な理由がある場合(たとえば80文字の制限内に収めるために分割してしまうと極めて読みにくくなる行の場合など)は、そうするべきです。
- アブストラクション・レイヤ
コンピュータ・サイエンスの教授は学生に対し、柔軟性と情報隠蔽のためアブストラクション・レイヤを最大限に活用するよう教えます。たしかにLinuxカーネルではアブストラクション(抽象化)を多用しています。数百万行のコードが関係するプロジェクトではこれなしに生き残る方法はありません。しかし、経験によれば、過度な、または中途半端なアブストラクションは中途半端な最適化と同様に有害なことが分かっています。アブストラクションは必要とされるレベルまで使用し、それ以上には多用しないことが必要です。
簡単な例として、すべての呼び出し元から常にゼロとして渡される引数を持つ関数を考えます。その引数は、その関数が提供する柔軟性の拡張として、誰かが必要とすることになる場合を想定して保持しておくこともできます。しかしその頃には、この余分な引数を使用するコードは、それが1回も使用されなかったため気付かれないような微妙な形で壊れている可能性が高いものです。あるいは、その追加の柔軟性がまさに必要になった場合でも、プログラマーの当初の期待に一致した形でその引数が使われるとは限りません。カーネル開発者は未使用の引数を削除するパッチを定期的に提出します。一般的に言えば、この引数は、最初から無かった方が良かったということです。 ハードウェアへのアクセスを隠すアブストラクション・レイヤは多くのドライバを複数のOS上で使えるようにするためによく使われますが、これらは特に問題視されます。そのようなレイヤはコードを見えにくくし、また性能上のペナルティが課される場合もあるため、Linuxカーネルには含めるべきではありません。
一方、別のカーネル・サブシステムから大量のコードをコピーしている場合、その中の一部のコードを取り出して別のライブラリにするか、またはその機能を上位レベルで具体化することについて考慮すべきではないかと質問すべき時です。ひとつのカーネル内で同じコードを複製することは意味がありません。
- 全般的な #ifdef およびプリプロセッサの使用
一部のCプログラマーにとって、Cプリプロセッサはソースファイルに大きな柔軟性を持たせる効率的な方法と考えられ、強力な誘惑になると思われます。しかし、プリプロセッサはCではなく、これを多用したコードは読みにくくなり、またコンパイラによる正当性チェックもより困難になります。プリプロセッサの多用は、ほとんどすべての場合において、クリーンアップの必要なコードの徴候です。
#ifdef を用いた条件付きコンパイルは実に強力な機能であり、カーネル内で使われています。しかし、 #ifdef ブロックが大量にばらまかれたコードを見たいとは思いません。原則として、#ifdef の使用は可能な限りヘッダー・ファイルに限定すべきです。条件付きコンパイルが行われるコードを、コードが存在しない場合は単純に空になる関数に限定することもできます。これによりコンパイラは最適化によりその空の関数へのコールを簡単に除外することができます。その結果、大幅にクリーン化されたコードが作成され、容易に理解できるようになります。
Cプリプロセッサのマクロは、数式が副作用を伴って複数回評価されることや、型の安全性がないこと等、多くの害をもたらします。マクロを定義したいという誘惑に駆られた場合には、代替案としてインライン関数の作成を検討してみてください。結果としてのコードは同じですが、インライン関数は読みやすく、その引数を複数回評価することもなく、またコンパイラが引数および戻り値の型検査を行えるようになります。
- インライン関数
しかし、インライン関数にもそれ自身の危険性があります。プログラマーは、関数呼び出しを避けつつソースファイルをインライン関数で満たすことに、見かけ上の効率の良さに満足してしまう可能性があります。しかし、これらの関数は実際には性能を低下させることがあります。これらのコードは各呼び出しで複製されるため、コンパイル後のカーネルのサイズを膨張させてしまいます。これがプロセッサの命令キャッシュに対する圧力となり、実行速度が劇的に低下します。原則として、インライン関数は非常に小さいこと、また少ない使い方が必要です。結局のところ、関数コールの負担はそれほど大きくありません。多数のインライン関数を作成することは、中途半端な最適化の古典的な例です。
一般に、カーネルのプログラマーは危険を承知でキャッシュの影響を無視します。初心者向けのデータ構造のクラスで教えられる古典的な処理時間とメモリスペースのトレードオフは、最新のハードウェアの場合には当てはまらない場合が多いでしょう。大きなプログラムはコンパクトなプログラムよりも実行速度が低下する、という意味で「スペースは時間」と言われます。
- ロック処理
2006年5月、Devicescapeネットワーク・スタックが派手なファンファーレと共にGPLでリリースされ、メインライン・カーネルへのマージすべく提供されました。この貢献のニュースは歓迎されました。すなわちLinuxにおける無線ネットワーキング対応は未だ標準以下と考えられており、Devicescapeスタックはこの状況の改善を約束するものでした。しかし、このコードは2007年6月になるまで、実際にメインラインにはマージされませんでした(2.6.22)。一体、何が起きたのでしょうか?
このコードには、企業内の閉じた環境で開発されたことを示す多くの徴候がありました。しかし、特に大きなひとつの問題は、このコードがマルチプロセッサ・システムで動作するようには設計されていなかったことでした。このネットワーキング・スタック(現在のmac80211)をマージできるようにするため、ロック処理のための改修が必要でした。
昔はマルチプロセッサ・システムで必要な並行処理の問題は考えなくてもLinuxカーネルのコード開発が可能でした。しかし今では、たとえばこの文書でさえもデュアル・コアのラップトップで執筆しています。またシングルプロセッサ・システムの場合でも、応答性の改善を図るためにはカーネル内の並行処理レベルを高めることが必要です。ロック処理を考慮せずにカーネル・コードを書くことのできた時代は遠く過ぎ去ってしまいました。
複数のスレッドから同時にアクセスされる可能性のあるリソース(データ構造、ハードウェア・レジスタ等)はすべて、ロックによる保護が必要です。新しいコードはこの要件を念頭に置いて書かれますが、ロック処理を後から追加するという改修は非常に困難な仕事です。カーネル開発者は時間をかけて、利用可能なロック・プリミティブを十分に理解してから、その作業に適したツールを選定することが必要です。並行処理に対する注意が足りないと見られるコードは、メインラインへのマージまで困難な道を歩むことになります。
- リグレッション
最後に言及する価値のある危険性がこれです。大きな改善をもたらす可能性のある変更は魅力的ですが、それにより既存のユーザーが使用中の何かが壊れることがあります。この種の変更は「リグレッション(退行)」と呼ばれていますが、リグレッションはメインライン・カーネルで最も歓迎されないものとなっています。いくつかの例外を除いて、リグレッションを起こした変更は、修正がタイムリーに行われない場合、却下されます。最初からリグレッションを避けることが大事です。
その変更より問題が発生する人の数よりも、多くの人に良い効果をもたらすものであれば、リグレッションも正当化されるという議論がよくあります。つまり、1個のシステムを壊すかわりに10個のシステムに新機能を提供するものであれば、その変更を行ってはどうかというものです。この質問に対する最善の回答は、2007年7月にLinusが表明しています。
そうすると、新しい問題を発生させても、そのバグ修正を行わないということになる。このようなやり方は狂気の沙汰であり、それでは真の進歩があったのか、まったく誰にも分からなくなる。それは2歩前進して1歩後退なのか、それとも1歩進んで2歩後退なのか?
(http://lwn.net/Articles/243460/ )
特に歓迎されないタイプのリグレッションは、ユーザー空間ABIに対する何らかの変更です。一度ユーザー空間に提供したインタフェースは、永久にサポートしなければなりません。この事実が、ユーザー空間インタフェースの作成を特に困難なものとしています。すなわち、互換性のない変更方法は許されないため、最初から正しく作成することが必要です。このことから、ユーザー空間のインタフェースについては相当量の考察、明確な文書化、および広範囲なレビューが常に要求されます。
4.2: コードチェック用ツール
少なくとも今のところ、エラーのないコードを書くことはほとんど誰もが達成できない理想です。しかし、我々が望むことができるのは、コードがメインライン・カーネルにマージされる前に、このようなエラーを可能な限り多くとらえて修正することです。カーネル開発者達はこの目的のため、多岐にわたる問題を自動的に捕捉できる、すばらしいツールのセットを編成しました。コンピュータが捕捉した問題が、後でユーザーを悩ます問題とならないよう、当然ながら可能な限り自動化ツールを使うべきです。
最初のステップは、単にコンパイラが発生する警告に注意することです。最新バージョンの gcc は、多数の潜在的エラーの検出(と警告)が可能です。非常に多くの場合、これらの警告は実際の問題を示唆しています。規則として、レビュー用に提出されるコードはコンパイラの警告が発生しないものでなければなりません。警告が出ないようにするに際して、その真の原因の把握に努めること、原因に対処せず警告だけが無くなるような修正は避けるよう、十分に注意します。
また、コンパイラのすべての警告がデフォルトで有効になっているとは限らないことに注意が必要です。すべての警告を有効にするには、「make EXTRA_CFLAGS=-W」でカーネルのビルドを行います。
カーネルには、デバッグ機能を有効にするいくつかの設定オプションがあり、そのほとんどは「kernel hacking」サブメニューにあります。開発やテストに使うカーネルの場合、これらオプションのいくつかをONにしておくことが必要です。
- 特に、以下のものはONにしておくことが必要です。 ENABLE_WARN_DEPRECATED、ENABLE_MUST_CHECK、およびFRAME_WARNは、非推奨インタフェースの使用や、関数からの重要な戻り値を無視するといった問題に対する一連の警告を有効にするものです。これら警告の出力は冗長なこともありますし、カーネルの他の部分からの警告については気にする必要はありません。
- DEBUG_OBJECTSは、カーネルに生成される各種オブジェクトの生成から消滅までを追跡し、異常が発生したときに警告するコードを追加します。複雑なオブジェクトを作成してエクスポートするようなサブシステムを追加する場合、このオブジェクト・デバッグ・インフラの追加を考慮してください。
- DEBUG_SLABは、メモリーの割当/使用に関係する各種のエラーを発見します。このオプションは開発用カーネルで使うべきです。
- DEBUG_SPINLOCK、DEBUG_SPINLOCK_SLEEP、およびDEBUG_MUTEXESは、多くの一般的なロック・エラーを発見します。
その他、多くのデバッグ・オプションがありますが、その一部について以下で検討します。一部のオプションは性能に重大な影響があるため、常時使用すべきではありません。しかし、多少の時間をかけて利用可能なオプションについて学習すれば、すぐにその何倍も元が取れるようになります。
重いデバッグ・ツールのひとつに、ロック処理チェッカー「lockdep」があります。このツールは、システム内のすべてのロック(spinlockまたはmutex)について、その獲得と開放、ロック獲得の順序、現在の割込環境、その他の追跡を行います。ロックが常に同じ順序で獲得されることや、すべての状態に同じ割込条件が適用されていること等の確認を行います。言い換えると、lockdep はシステムがデッドロックする可能性のある多くのシナリオを発見することができます。ユーザーに展開済のシステムの場合、この種の問題の発生は開発者にとっても、ユーザーにとっても非常な苦痛を伴うものです。lockdepを使うことで、これらを前もって自動的に発見できます。何らかの重要なロック処理を含むコードについては、メインラインへの提出前にlockdepを有効にして実行することが必要です。
まじめなカーネル・プログラマーであれば、疑いの余地なく、エラーに終わる可能性のあるすべての処理(メモリー割り当てなど)の戻りステータスをチェックするはずです。しかし実際には、そのエラーの結果としての実行される障害回復パスについてはテストが疎かになりがちです。未テストのコードは不正な場合が多く、これらエラー処理パスのすべてを数回実行することにより自分のコードに対する自信が深まります。
Linuxカーネルには、特にメモリー割り当てに関係する部分について、まさにこれを行う障害挿入(フォールトインジェクション)の仕組みが用意されています。障害挿入を有効とし、メモリー割り当て失敗のパーセンテージを指定して、メモリー割り当てを失敗させることができます。また挿入する障害をコード内の特定の範囲に限定することができます。障害挿入を有効に設定してシステムを動作させることにより、プログラマーは不具合が発生したときのコードの反応の仕方を確認することができます。この機能の使用法の詳細についてはDocumentation/fault-injection/fault-injection.text を参照してください。
その他の種類のエラーは、「sparse」という静的分析ツールで発見することができます。このsparseは、ユーザー空間とカーネル空間のアドレスの混同、数値のビッグエンディアンとリトルエンディアンの混用、ビット・フラグのセットが予期されている時に整数値を渡した場合等についてプログラマーに警告を与えます。このsparseは別にインストールする必要があり(ディストリビュータのパッケージに含まれていない場合、http://www.kernel.org/pub/software/devel/sparse/で入手可能です)、インストール後に、自分のコードのmakeコマンドに「C=1」を追加して実行します。
その他の移植時の障害については、そのコードを他のアーキテクチャー用にコンパイルして発見するのが最善の方法です。例えば、S/390システムまたはBlackfin開発ボードを持っていない場合でも、コンパイルのステップを実行することは可能です。様々なx86システム用のクロス・コンパイラは以下のサイトにあります。
http://www.kernel.org/pub/tools/crosstool/
多少の時間をかけて各コンパイラをインストールし使ってみることは、将来の困惑を避ける上で役立ちます。
4.3: ドキュメンテーション
カーネル開発におけるドキュメントの作成はルールというよりは、むしろ例外といえる程でした。たとえそうであっても、適切な文書を作成することで新しいコードのカーネルへのマージが容易になり、他の開発者も楽になり、ユーザーにも役立ちます。多くのケースにおいて、文書の添付は基本的な義務となってきています。
あらゆるパッチについて、その最初の資料は changelog(更新履歴)です。このログには、解決される問題、解決方式、パッチ作成に関与した人々、性能への何らかの影響、およびそのパッチの理解に必要なその他の情報について記述することが必要です。
新たなユーザー空間インタフェースを追加するコード(sysfsファイルや/procファイルを含む)の場合、ユーザー空間の開発者がその内容を把握できるインタフェース資料を含めることが必要です。この資料の形式や提供が必要な情報の内容についてはDocumentation/ABI/READMEを参照してください。
Documentation/kernel-parameters.txtファイルには、Linuxカーネルのすべてのブートタイム・パラメータが記述されています。新規パラメータを追加するパッチの場合、このファイルに該当する項目を追加することが必要です。
新しいコンフィギュレーション・オプションがある場合、必ずそのオプションの内容と、ユーザーがそのオプションを選択する場合について明確に説明するヘルプ・テキストを添付しなければなりません。
多くのサブシステムの内部API情報は特別なフォーマットのコメントで文書化されており、これらのコメントは「kernel-doc」スクリプトを通じ、様々な方法で抽出してフォーマット化することができます。kerneldocコメントを持つサブシステムを対象とした開発を行う場合、既存のコメントを維持しつつ、必要に応じ外部から利用可能な関数についてコメントの追加を行うことが必要です。そのように文書化されていない部分であっても、将来のためにkerneldocコメントを追加しても何ら害はありません。実際、これはカーネル開発の初心者にとっては役に立つ行為です。これらコメントのフォーマットやkerneldocテンプレートの作成方法については、Documentation/kernel-doc-nano-HOWTO.txtファイルに示されています。
誰でも既存のカーネル・コードを読み進めてみると、多くの場合はコメントがないことの方に気が付きます。繰り返しになりますが、従来に比べて新しいコードに対する期待度は高まっています。コメントのないコードのマージはより一層困難になります。とは言うものの、過剰なほど詳細にコメントされたコードも望まれません。コードはそれ自体による読み取りが可能なものとすべきであり、コメントはより微妙な内容について説明するものです。
特定の内容については常にコメントが必要です。メモリーバリアを使う場合、そのバリアが必要な理由を説明する1行の追加が必要です。データ構造のロッキング・ルールについては、一般にどこかで説明することが必要です。一般に、主要なデータ構造については分かりやすい資料が必要です。コードの個々のビット間の、明白でない依存関係についても指摘しておくべきです。コードの整理係が誤って「クリーンアップ」したくなるような部分があれば、その部分がそのように設計されている理由を述べるコメントが必要です。その他、必要な部分にコメントします。
4.4: 内部APIの変更
カーネルがユーザー空間に対し提供するバイナリ・インタフェースは、極めて厳しい状況の場合以外、維持されます。しかし、カーネルの内部プログラミング・インタフェースは非常に流動的であり、その必要性が発生したときには変更することが可能です。カーネルAPI周辺に手を入れる必要性を感じたとき、または自分のニーズに合わないため特定のカーネル機能を使わないと決めたような場合、それはそのAPIの変更が必要な徴候かもしれません。カーネル開発者は、そのような変更を行う権限を持っています。
もちろん、これには多少の問題点があります。APIの変更は可能ですが、そのためには十分な正当化が必要です。そのため、内部APIを変更するパッチを作成する場合、その変更がどのような内容なのか、なぜ必要なのかを説明する文書を付けなければなりません。この種の変更は大きなパッチの中に埋めてしまわず、別々のパッチに分けることが必要です。
もうひとつの問題点として、一般に内部APIを変更する開発者には、その変更により影響を受けるカーネル・ツリー内のコードの修正が求められるということがあります。広く使用されている関数の場合、この仕事は文字通り数百件から数千件もの変更につながり、またその多くは他の開発者が行っている作業との間でコンフリクトを発生させる可能性が高くなります。言うまでもなくこれは大きな作業であり、その正当化理由が堅固なものであるという確信が必要です。
非互換の発生するAPI変更を行う場合、可能な限り更新されていないコードもコンパイラにかけることが必要です。これにより、ツリー内でそのインタフェースが使用される部分をすべて特定することができます。また、これによりツリー外のコードを作成した開発者に対しても、対応すべき変更があるということが警告されます。ツリー外のコードへの対応はカーネル開発者が心配すべきことではありませんが、必要以上にツリー外の開発者を困らせる必要はありません。



