カーネルのCコードが技術的に間違っていますか?

カーネルのCコードが技術的に間違っていますか?

インターネットでは、次のような複数のスレッドを見つけることができます。

http://www.gossamer-threads.com/lists/linux/kernel/972619

人々は、-O0を使用してLinuxを構築できないと文句を言い、これがサポートされていないと言いました。 Linuxは自動的に機能をインライン化し、デッドコードを削除し、成功したビルドに必要なタスクを実行するためにGCCの最適化に頼っています。

私は少なくともいくつかの3.xカーネルについてこれを直接確認しました。私が試したことは、-O0でコンパイルされた場合、数秒のビルド時間の後に終了します。

これは一般に許容されるコーディング慣行と見なされますか?コンパイラの最適化(自動インラインなど)は、信頼できるほど十分に予測可能ですか?少なくとも1つのコンパイラだけを扱うときは? GCCの将来のバージョンがデフォルトの最適化(-O2や-Osなど)を使用して現在のLinuxカーネルのビルドを中断する可能性はどのくらいですか?

もっと賢明なポイント:3.xカーネルは最適化なしでコンパイルできないので、技術的に間違ったCコードと見なすべきですか?

答え1

いくつかの(しかし関連する)質問を組み合わせます。そのうちのいくつかは実際にはトピック(コーディング標準など)とは関係がないので無視します。

カーネルが「技術的に間違ったCコード」かどうかから始めましょう。私は答えがカーネルが占める特別な場所を説明しているので、ここから始めました。これは残りの部分を理解するために重要です。

カーネルのCコードが技術的に間違っていますか?

答えは間違いなく「間違っている」です。

Cプログラムが間違っていると見なす方法はいくつかあります。まず、いくつかの簡単な質問を取り上げます。

  • C構文に従わない(つまり、構文エラーのある)プログラムは正しくありません。カーネルは、C構文にさまざまなGNU拡張を使用します。 C標準に関する限り、これは構文エラーです。 (もちろんGCCではそうではありません。-std=c99 -pedanticまたはこれに似てコンパイルしてみてください...)
  • 設計された目的を実行しないプログラムは正しくありません。カーネルは巨大なプログラムであり、変更ログをすばやく確認しても確かに巨大なプログラムではないことを証明できます。それとも、私たちが言いたいのと同じようにバグがあります。

Cでの最適化とはどういう意味ですか?

[注:このセクションには、実際の規則に対する非常に不正確な修正が含まれています。詳細については、標準を参照してスタックオーバーフローを検索してください。 ]

今より多くの説明が必要です。 C標準では、特定のコードが特定の動作を生成する必要があると規定しています。また、構文的に有効な特定のC項目には「未定義の動作」があると言います。 1つの(残念ながら一般的な!)例は、配列の終わりを超えてアクセスすることです(たとえば、バッファオーバーフロー)。

未定義の動作は非常に強力です。プログラムに少しでも含まれていれば、C標準は、もはやプログラムがどのような振る舞いを見せるのか、コンパイラがそれに直面したときにどの出力を生成するのか気にしません。

ただし、プログラムで定義された動作のみが含まれていても、Cはまだコンパイラに多くの空き容量を許可します。簡単な例として(注:私の例では、簡潔にするために行などを省略しました#include):

void f() {
    int *i = malloc(sizeof(int));
    *i = 3;
    *i += 2;
    printf("%i\n", *i);
    free(i);
}

もちろん、5が印刷され、その後に改行文字が続くはずです。これがC標準が要求するものです。

このプログラムをコンパイルして出力を逆アセンブルすると、メモリを取得するためにmallocが呼び出され、返されたポインタがどこか(おそらくレジスタ)に格納され、値3がそのメモリに格納され、次にそのメモリに2が追加されます。予想できます。メモリ(おそらくロード、追加、保存も必要です)を実行し、メモリをスタックにコピーし、ドット"%i\n"文字列をスタックに挿入して関数をprintf呼び出します。かなり多くのことがあります。ただし、代わりに次のような書き込みが表示されることがあります。

/* Note that isn't hypothetical; gcc 4.9 at -O1 or higher does this. */
void f() { printf("%i\n", 5) }

問題は次のとおりです。 C規格ではこれを許可しています。 C 標準は、次の事項にのみ関心があります。結果、実装方法ではなく。

これがC言語最適化のすべてです。コンパイラは、C標準が必要とする結果を得るためのよりスマートな方法(フラグに応じて一般的に小さいかより速い)を提示します。 GCC-ffast-mathオプションなど、いくつかの例外がありますが、そうでない場合、最適化レベルは技術的に正しいプログラム(つまり、定義された動作のみを含むプログラム)の動作を変更しません。

定義されたアクションのみを使用してカーネルを作成できますか?

引き続きサンプルプログラムを確認しましょう。コンパイラが変換するバージョンではなく、私たちが書くバージョンです。私たちが最初にすることは、mallocメモリを取得するために呼び出すことです。 C標準は何をすべきかを教えてくれますmallocが、それがどのように行われるのかを教えてくれません。

malloc速度ではなく明確さを目的とした実装を見てみると、大きなメモリチャンクを得るためにいくつかのシステムコール(mmapwithなど)を実行することがわかります。MAP_ANONYMOUSブロックのどの部分が使用され、どの部分が使用可能かを示すいくつかのデータ構造を内部的に保持します。少なくとも要求されたサイズと同じ大きさの空きブロックを見つけ、要求された量だけ切り捨て、そのブロックへのポインタを返します。また、完全にCで書かれており、定義された動作のみが含まれています。スレッドセーフな場合は、一部のpthread呼び出しを含めることができます。

さて、最後に何が起こっているのか見てみると、mmapいろいろな興味深いものが見えます。まず、システムにマッピングに使用できる十分なRAMおよび/またはスワップスペースがあることを確認するために、いくつかのチェックを実行します。次に、ブロックを入れる空きアドレス空間を探します。その後、ページテーブルと呼ばれるデータ構造を編集し、プロセスで一連のインラインアセンブリ呼び出しを実行できます。実際には、物理​​メモリのいくつかの空きページ(たとえば、物理DRAMモジュールの実際のビット)を見つけることもできます。このプロセスでは、他のメモリを強制的に交換する必要があるかもしれません。要求されたブロック全体に対してこれを行わない場合は、そのメモリに最初にアクセスしたときに発生するように設定します。ほとんどの作業は、インラインアセンブリ、さまざまなマジックアドレスの作成などを介して行われます。また、特に交換が必要な場合は、コアの大部分を使用することにも注意してください。

インラインアセンブリ、マジックアドレスの作成などはC仕様の外にあります。これは驚くべきことではありません。 Cは、1970年代初頭にCが発明されたときに想像できなかった多くのアーキテクチャを含む、さまざまなシステムアーキテクチャで実行できます。マシン固有のコードを隠すことは、カーネル(およびある程度Cライブラリ)の重要な部分です。

もちろん、サンプルプログラムに戻ると明らかに似printfていることがわかります。標準Cですべての書式設定などを行う方法は非常に明確ですが、実際にはモニターに表示されます。それとも別のプログラムにパイプしますか?今回もカーネル(そしておそらくX11やWayland)は多くの魔法を実行します。

カーネルが実行する他の作業を考えると、多くの部分がCの外側にあります。たとえば、カーネルはディスク(Cはディスク、PCIeバス、またはSATAについては不明)のデータを物理メモリ(Cはmallocのみを知り、DIMM、MMUなどは不明)に読み込み、実行可能にします(Cは何も知らない)既知のプロセッサ実行ビットに対して)関数として呼び出されます(Cの外部だけでなく、非常に許可されていません)。

カーネルとコンパイラの関係

以前の内容を覚えている場合、プログラムに定義されていない動作が含まれている場合、C標準に関する限り、すべてが間違っています。ただし、カーネルには未定義の動作を含める必要があります。したがって、カーネルとコンパイラの間には、少なくともカーネル開発者がC標準に違反しても、カーネルが機能することを確実にするのに十分な関係が必要です。少なくともLinuxの場合、カーネルはGCCの内部動作についてある程度知っておく必要があります。

割れる確率はどのくらいですか?

将来のGCCバージョンでは、カーネルが破損する可能性があります。このようなことが以前にも何度も起こったので、私は自信を持って話すことができます。もちろん、GCCの厳格なエイリアスの最適化のようなものは、カーネルに加えて多くのものを壊します。

また、Linuxカーネルが依存するインラインは自動インラインではなく、カーネル開発者が手動で指定することに注意してください。 -O0を使用してカーネルをコンパイルし、いくつかのマイナーな問題を解決した後は、ほとんどが動作すると報告する人がたくさんいます。 (それらの1つはあなたがリンクしたスレッドにもあります)。ほとんどのカーネル開発者は、-O0副作用として最適化を要求し、コンパイルする理由がないと考え、いくつかのトリックが機能し、誰もテストに使用しないためサポートされ-O0ません。

たとえば、次の項目以上にコンパイルしてリンクします-O1が、次の項目にはリンクしません-O0

void f();

int main() {
    int x = 0, *y;
    y = &x;

    if (*y)
        f();
    return 0;
}

f()最適化により、gccは絶対に呼び出されずに無視されることを確認できます。最適化がないと、gccは呼び出しを保存し、リンカは失敗しますf()。カーネル開発者は、カーネルコードを読みやすくするために同様の動作に頼っています。

答え2

~からGentoo GCC最適化Wiki

セクション 2.3: -O フラグ

-O 以下は -O 変数です。これは全体的な最適化レベルを制御します。これにより、コードのコンパイルに時間がかかり、特に最適化レベルを上げると、より多くのメモリを使用できるようになります。

-O設定には、-O0、-O1、-O2、-O3、-Os、-Og、および-Ofastの7つがあります。 /etc/portage/make.confでは、そのうちの1つだけを使用する必要があります。

-O0に加えて、各-O設定は複数の追加フラグを有効にするため、GCCマニュアルの最適化オプションに関する章を読んで、各-Oレベルで有効になっているフラグとその機能の説明を確認してください。

各最適化レベルを確認してみましょう。

-O0:このレベル(つまり、「O」の後にゼロが続く文字)は、最適化を完全にオフにし、CFLAGSまたはCXXFLAGSに-Oレベルが指定されていない場合のデフォルト値です。これにより、コンパイル時間が短縮され、デバッグ情報が向上する可能性がありますが、一部のアプリケーションは最適化を有効にしないと正しく機能しません。このオプションは、デバッグ目的を除いては推奨されません。
-O1: 最も基本的な最適化レベルです。コンパイラは、コンパイル時間をあまり費やすことなく、より高速で小さなコードを生成しようとします。非常に基本的ですが、常に作業を完了する必要があります。
-O2: -O1より一歩進んだものです。特別な要件がない限り、これは推奨される最適化レベルです。 -O1によって有効にされたフラグに加えて、-O2はより多くのフラグを有効にします。 -O2を使用すると、コンパイラはサイズに影響を与えたり、コンパイル時間をあまり費やすことなくコードのパフォーマンスを向上させようとします。
-O3: 可能な限り最高の最適化レベルです。コンパイル時間とメモリ使用量の面で高価な最適化が可能です。 -O3でコンパイルしてもパフォーマンスの向上は保証されず、実際には大きなバイナリとメモリ使用量が増えるため、多くの場合システムの速度が遅くなる可能性があります。 -O3は複数のパッケージを壊すこともできます。したがって、-O3 の使用はお勧めできません。
-Os:このオプションはコードサイズを最適化します。生成されたコードのサイズを大きくしないすべての-O2オプションを有効にします。ディスクの記憶領域が非常に限られているか、またはCPUキャッシュが小さいコンピュータに役立ちます。
-Og:GCC 4.8では、新しい一般的な最適化レベル-Ogが導入されました。合理的なレベルのランタイムパフォーマンスを提供しながら、高速コンパイルと優れたデバッグ環境の要件を満たしています。全体的な開発経験は、基本最適化レベル-O0よりも優れている必要があります。 -Ogは-gを意味せず、デバッグを妨げる可能性がある最適化を無効にするだけです。
-Ofast: -O3 と -ffast-math、-fno-protect-parens および -fstack-arrays で構成される GCC 4.7 の新機能です。このオプションは厳格な規格準拠に違反するため、推奨されません。前述したように、-O2 が推奨される最適化レベルです。パッケージのコンパイルが失敗し、-O2を使用しない場合は、このオプションを使用して再構築してみてください。代替方法として、CFLAGS および CXXFLAGS を -O1 または -O0 -g2 -ggdb (エラー報告および可能性のある問題を確認するため) などの低い最適化レベルに設定してみてください。

最適化ではなく-O0について具体的に質問しました。上記を読むと、O0はデバッグ専用であることがわかります。 menuconfigを使用したことがある場合は、カーネルデバッグを有効または無効にするオプションがあることに気付くでしょう。有効にすると、このオプションはO0が情報を提供するのとほぼ同じ方法でデバッグ情報を出力します。また、システム全体が単一の最適化設定で構築またはコンパイルされること、つまりO0でカーネルをコンパイルし、O2でシステムの残りの部分をコンパイルできないことを見落としたかもしれません。


GCC バージョン間の旧バージョンとの互換性に関しては、GCC は、あるバージョンで -O フラグを有効にすることが新しいバージョンで -O 設定と同じであるため、常にバージョン間の互換性を維持します。 GCC4.7および-Ofastオプションに関する上記の注意事項を参照してください。このオプションは 4.7 以降でのみ使用できますが、4.7 の -O2 = すべてのバージョンの -O2 です。

関連情報