Linux カーネルの記事 - プロセス間通信 (シグナル、パイプ、共有メモリ、ソケット)

シグナルのメカニズム
Linux オペレーティング システムでは、さまざまなイベントに応答するために、多くのシグナルも定義されています。kill -lコマンドを使用してすべてのシグナルを表示できます。

# kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

60種類以上。このうち共通の信号は
1: HUP、端末がプロセスを抜けて終了し、キャプチャ可能
2: INT、つまり crtl+c、フォアグラウンドプロセスを終了
8: FPE、算術演算エラー、オーバーフロー、除算0など。

9:9 KILL、プロセスを強制終了します。ブロック、キャプチャ、無視はできません。
15: TERM、プロセスは正常に終了し、通常はプロセスをキャプチャし、リソースの解放や終了などの余波に対処します。デフォルトでは kill は 15 です。

19 SIGSTOP この信号はフォアグラウンド プロセスを一時停止できます。これは、Ctrl+Z のショートカット キーを入力するのと同じです
(9、15 はブロック、キャプチャ、および無視できません)
。 20 SIGSTP も一時停止されたプロセスですが、ブロック、キャプチャできます。 、無視されました

14 ALARM クロック信号。alarm() 関数で信号を受信する時間を設定し、それを処理する処理関数を設定します。

17 CHILD シグナル: ゾンビプロセスを防止します。子プロセスが終了し、親プロセスに CHILD シグナルを送信します。待機せずに、このシグナルに対して処理関数の非同期 waitpid を設定できます。実際、デフォルトでは無視されており、対処する必要はなく、リソースを直接解放するだけです。

信号処理には 3 つの方法があり、
1 つ目はデフォルトです。用語の場合はデフォルトでクローズされます。たとえば、core のデフォルトでは、後で理由を解析しやすくするために、クローズした後にコア ファイルを書きます。

信号のキャプチャ: 信号を処理する信号処理関数を記述します(ID、信号処理関数)

シグナルを無視: sigchild など

(kill と stop ではキャプチャ、無視、ブロックする方法はありません)

**

信号処理の流れ

**
主に登録信号と送信信号が含まれます。
信号にデフォルトの動作を実行させたくない場合、1 つの方法は、signal 関数を通じて特定の信号に対応する信号処理関数を登録することです。
sighandler_t シグナル(intsignum, sighandler_t ハンドラー);

実際、signal はシステムコールではなく、gilbc 関数です。(malloc、brk、mmap と同じ)
実際には、システム コールは sigaction であり、実際にはシグナルをアクションに関連付けますが、このアクションは構造体 sigaction で表されます。
struct sigaction { __sighandler_t sa_handler; unsigned long sa_flags; __sigrestore_t sa_restorer; sigset_t sa_mask; /* 拡張性のために最後にマスク */ };他のメンバー変数を使用すると、信号処理の動作をより詳細に制御できます。そして、シグナル関数ではこれらを設定する機会は与えられません。たとえば、 sa_flags のデフォルト設定が SA_ONESHOT であるとはどういう意味ですか? ここで設定した信号処理機能は一度だけ動作することを意味します。私たちはこれを望んでいませんし、私が明示的にオフにするまでは確実に機能することを望んでいます。







SA_NOMASK は、この信号処理関数の実行中に、他の信号がある場合、同じ信号が到着しても、この信号処理関数が中断されることを意味します。あるデータ構造を操作する場合、同じ信号であるため同じインスタンスを操作する可能性が高く、この場合デッドロックや同期を考慮する必要があります。したがって、他の信号をブロックした方がよいでしょう

SA_RESTART、システムコール時にシグナルが到着します。この時点でシステムコールは自動的に再開され、呼び出し元は自分でコードを書く必要はありません。たとえば、文字の読み取りを中断し、再度文字を読み取る場合、ユーザーがそれ以上入力しないとそこで停止し、ユーザーは同じ文字を再度入力する必要があります。
したがって、関数 sigaction を使用し、ニーズに応じてパラメーターをカスタマイズすることをお勧めします。

カーネルでは、rt_sigaction が do_sigaction を呼び出して信号処理関数を設定します。各プロセスの task_struct には、配列である struct sighand_struct を指す sighand があり、添え字が信号、内部の内容が信号処理関数です。(これもプロセス メモリ空間に関連しており、そこには信号関連の処理関数があり、システム コールはカーネル空間にあります)

(要約すると、システム コールのコードはカーネル空間の最初の 1MB にあります。ルートは割り込み時にコードを見つけます。シグナルが割り込まれたときの処理関数は、プロセスのカーネル モード空間、具体的にはシグナルにあります。 task_struct の構造体部分。共通はカーネル モードになります)

(ソフト割り込みにはシグナル(異常)、システムコールがあり、これらはカーネルの初期化trap_init()に登録され、割り込み処理プロセスはシーンを保護する(カーネル状態のptg構造体に保存される) - システムコールまたは割り込み処理を実行関数 -
ハードウェアの復元 割り込みは IO によって生成される割り込みです。少し複雑です。真ん中に割り込み信号があります - 割り込みベクトル - グローバル割り込みベクトル - 割り込みハンドラー (駆動) を見つけます)

信号の送信

場合によっては、ターミナルに特定のキーの組み合わせを入力すると、シグナルがプロセスに送信されます。たとえば、Ctrl+C は SIGINT シグナルを生成し、Ctrl+Z は SIGSTOP シグナルを生成します。
場合によっては、ハードウェア例外によってシグナルが生成されることもあります。たとえば、0 で除算する命令が実行されると、CPU は例外を生成し、プロセスに SIGFPE を送信します。たとえば、プロセスが不正なメモリにアクセスすると、メモリ管理モジュールは例外を生成し、プロセスに SIGSEGV を送信します。
シグナルを送信する最も直接的な方法は、kill コマンドを使用してシグナルを送信することです。たとえば、kill -9 pid がプロセスにシグナルを送信してプロセスを強制終了できることは誰もが知っています。tkill を使用するか、特定のスレッドにシグナルを送信して、最後にdo_send_sig_info 関数を呼び出すこともできます。この関数で重要なのは sigpending 構造です。

struct sigpending には 2 つのメンバーがあり、1 つは受信されたシグナルを示すセット sigset_t と、どのシグナルが受信されたかを示すリンク リストですその構造は次のとおりです。

struct sigpending { struct list_head リスト; sigset_t 信号; };


32 未満のシグナルはコレクションに配置されますが、これは信頼性がありません。たとえば、SIGUSR1 は合計 5 つあり、A、B、C、D、E となります。
これら 5 つの信号が密集しすぎる場合。A が来ますが、信号処理機能がそれを処理する前に、B、C、D、E がすべて来ます。上記のロジックによれば、A が SIGUSR1 を sigset_t セットに入れたため、後の 4 つは失われます。別の場合、A が来て信号処理関数で処理されたとき、カーネルが信号処理関数を呼び出す前に、コレクション内のフラグをクリアします。このとき、再び B が来ても、B は入ります。コレクションを処理すれば、失われることはありません。

32 を超える信号はリンク リストにマウントされます。これは信頼性の高い信号です。

シグナル処理のタイミング
kill で送信したシグナルが task_struct 構造体に掛かったら、最後にcomplete_signalを呼び出す必要があります。signal_wake_up を呼び出してウェイクアップしてみます。
signal_wake_up_state には 2 つの主要なものがあります。1 つ目は、このスレッドに TIF_SIGPENDING を設定することです。これは、信号処理とプロセス スケジューリングが実際に一種のメカニズムを使用することを意味します。
プロセスを呼び出す必要がある場合、直接プロセスを駆動するのではなく、スケジューリング待ちを意味するフラグTIF_NEED_RESCHEDを立て、システムコールの終了または割り込み処理の終了を待ちます。カーネル状態からの状態呼び出し、スケジュール関数はスケジューリングを実行します。
シグナルも同様で、シグナルが到着したときに、シグナルを直接処理するのではなく、フラグ TIF_SIGPENDING を設定して、処理を待っているシグナルがすでに存在することを示します。同様に、システムコールが終了するか、割り込み処理が終了してカーネル状態がユーザ状態に戻ると、シグナルが処理される。

要約すると、
sianal と siaaction を使用してシグナル signal を登録すると、そのシグナルはプロセスの task_struct の signal 配列に配置されます。

kill などでシグナルを送信する場合、実際には send_sig_info が呼び出されます。非常に重要な構造が保留中です。シグナルの送信は実際にはプロセス task_struct の sigpending 構造体にシグナルをハングさせることであり、マウント セットが 32 未満の場合は信頼性が低い場合は失われ、32 を超える場合はマウント リストは信頼性があります。 。

次に、信号のタイミングを処理します。プロセスが実行中の場合、シグナル ハンドラーが直接実行されます。

ただし、プロセスはシステム コールまたはハードウェア割り込み (および IO 待機などの割り込み可能なスリープ状態) を実行している可能性があります。この場合、シグナル処理とプロセス スケジューリングはメカニズムです。最初にプリエンプション フラグまたはシグナル処理フラグを与えてから、プロセスをウェイクアップしようとします。プロセスがシグナルを見つけると、中断して戻ります。つまり、システム コールはシグナルによって中断されます。信号を処理する機会が戻ってきたら、ユーザー モードに入り、信号処理関数をユーザー モードにコピーして実行します。実行後は元の位置に戻ります。

(もちろん、システム コールまたはハードウェア割り込みがシグナルによって中断された後は、実行する操作をユーザー定義することも、アクションを実行することも、システム コールを再開することもできます)

また、中断不可能なスリープ状態の場合、システムがスリープを呼び出したときに信号をウェイクアップすることはできません。これを終了する方法はなく、kill -9 も機能しません。これは主に一部のカーネルの中断不可能なプロセスに使用され、アトミック性を保証しますか? 同様に、例えばキャラクタデバイスの読み込み時に割り込みが発生すると、デバイスが制御不能な状態になるため、割り込み不可に設定する必要があります。

匿名パイプの原理

パイプラインを作成するには、次の関数を渡す必要があります。

int Pipe(int fd[2])
ここでは、パイプ Pipe を作成し、パイプの両端を表す 2 つのファイル記述子を返します。1 つはパイプの読み取り記述子 fd[0] で、もう 1 つは書き込みです。パイプの記述子 fd[1]
ここに画像の説明を挿入

この関数は実際にはシステムコール Pipe2 を呼び出します。この関数の主な原理は、最初にパイプライン ファイルを作成することです。ファイル システム上にも作成されますが、これは単なる特殊なファイル システムであり、特殊な inode に対応する特殊なファイルを作成します。ファイルには独自の構造体 (inode、実際のデータ、対応するファイル操作を含む) があり、内部には特殊なファイルが含まれています。 i ノードはディスクではなくメモリ内のカーネル バッファを指します。fd_install を再度呼び出して、 2 つの fd を 2 つの struct ファイルに関連付けます。
ここでのファイルは実際にはパイプライン ファイルです。つまり、fd1 は読み取り用にファイルを開き、fd2 は書き込み用にファイルを開きます。

この時点ではまだプロセスなので、子プロセスをforkする必要があります。これにより、子プロセスは親プロセスのstruct file_structをコピーし、ここにfdの配列がコピーされますが、structファイルが指すto by fd には、同じファイルのコピーがまだ 1 つしかありません (これはパイプライン ファイルであるため)。次に、親プロセスは読み取り fd を閉じて書き込まれた fd のみを保持し、子プロセスは書き込まれた fd を閉じて読み取り fd のみを保持します。双方向のトラフィックが必要な場合は、2 つのパイプを作成する必要があります。
ここに画像の説明を挿入
しかし、殻の中にいる私たちには当てはまりません。シェルで A|B を実行する場合、A プロセスと B プロセスは両方ともシェルによって作成された子プロセスとなり、A と B の間に親子関係はありません。
したがって、 dup2(int oldfd, int newfd); を使用して古いファイル記述子を新しいファイル記述子に割り当て、 newfd の値が oldfd と同じになるようにします。
fd 配列では、最初の 3 つの項目は固定されており、0 番目の項目 STDIN_FILENO は標準入力、最初の項目 STDOUT_FILENO は標準出力、3 番目の項目 STDERR_FILENO はエラー出力を意味します。dup2(fd[1], STDOUT_FILENO), STDOUT_FILENO (最初の項目) は標準出力ではなく、実行シーンのパイプ ファイルを指します。その後、標準出力に書き込まれるものはすべてパイプ ファイルに書き込まれます。 。読み取り側も同様です。
ここに画像の説明を挿入

名前付きパイプは
主に、親族関係のない 2 つのプロセスが通信したいという問題を解決します。
Glibc のmkfifo
関数は、実際に ext4 ファイル システム上にファイルを作成するためにmknodat システム コール (名前付きパイプもデバイスであるため、mknod も使用されます) を呼び出しますが、メモリ内に特殊な i ノードを作成するためにinit_special_inode を呼び出します。パイプ ファイル構造、ストレージ i ノード、エンティティ (カーネル バッファ)、対応する操作。

概要:
匿名および名前付きは、実際にはカーネルのメモリ バッファ領域です。パイプラインとデバイスはすべてファイルであり、ユーザーの観点からはアクセス インターフェイスは一貫しています。違いは、それらの i ノードが特別であることです。この違いに従って、異なるファイル構造 (対応する操作、inode、ファイルを含む) に対応できます。たとえば、ファイルの読み取りはファイルの読み取り、
デバイスの読み取りは実際にはデバイスからドライバーを読み取ること、パイプラインの読み取りはカーネル バッファーからの読み取りです。私たちは気にしません。

なぜパイプがあるのでしょうか?
通信の場所としてディスク上の通常のファイルを使用しないのはなぜでしょうか?
欠点: 1: アクセス制御がありません。親プロセスの書き込みが非常に遅い場合、または親プロセスが終了する場合、子プロセスは終了せずに 0 を返し、その結果、子プロセスはリサイクルできなくなり、メモリ リークが発生します。2. 明らかに、ディスク ファイル アクセスが遅くなります

パイプラインはアクセス制御されたメモリ ファイルであり、速度が遅すぎたり速すぎたりするとブロックされます。片側を閉じるともう一方も止まります。

要約すると、オペレーティング システムは、統合されたオープンを通じてさまざまな種類のファイル (通常のファイル、ディレクトリ、デバイス、パイプライン) をどのように操作するのでしょうか? プロセスには、ファイル名が含まれる fd 配列があります。ファイルを作成するとき、オペレーティング システムは、名前とi ノード
構造間のマッピングを含む dentry を作成しますが、ファイルの種類によって i ノードは異なります( i ノードには、ファイルの種類、所有者、時間や保存場所などの変更情報が含まれます)。開いた場合は、inode によって異なります。さまざまなタイプのファイルの構造体 file_struct が作成され、さまざまなファイルの関連操作 (オープン操作、読み取り、書き込みなど) が記録されます( fd-inode-file_struct )

IPC共有メモリ

共有メモリ メカニズムを使用すると、2 つのプロセスは、自身のメモリ内の変数と同じように、共有メモリ内の変数にアクセスできますしかし、同時に問題も発生します。2 つのプロセスがメモリを共有すると、同時読み取りと書き込みの問題が発生するため、共有メモリの保護が必要になり、セマフォなどの同期メカニズムが必要になります

int shmget(key_t key, size_t size, int shmflag);
key は先ほど生成したキーで、
shmflag が IPC_CREAT の場合は新規作成を意味し、読み書き権限 0777 も指定できます。
共有メモリの場合、サイズを指定する必要があります。このアプリケーションのサイズはどれくらいですか? ベスト プラクティスは、複数のプロセスが共有する必要があるデータを構造体に配置し、ここでのサイズを構造体のサイズにすることです。このようにして、各プロセスはこのメモリを取得した後、型をこの構造体型に強制的に変換する限り、内部の共有データにアクセスできます。

共有メモリが生成された後の次のステップは、この共有メモリをプロセスの仮想アドレス空間にマップすることです:
void *shmat(int shm_id, const void *addr, int shmflg);
ここでの shm_id は、上で作成した共有メモリです。 id、addr は、マッピングがどこかにあることを指定します。指定しない場合、カーネルは自動的にアドレスを選択し、それを戻り値として返します。戻りアドレスを取得した後、ポインタを struct shm_data 構造体にキャストする必要があります。その後、このポインタによって設定されたデータとデータ長を使用できるようになります。
共有メモリが使い果たされたら、shmdt
int shmdt(const void *shmaddr);を通じて仮想メモリへのマッピングを解除できます。

セマフォセマフォ コレクションを作成する必要がありますが、これも xxxget を使用して作成されます: セマフォは semctl int semget(key_t key, int nsems, int semflg)
によって特定の値に初期化される必要があります; セマフォは多くの場合、特定のリソースの数を表します, セマフォを相互排除に使用する場合、セマフォは 1 に設定されることがよくあります。セマフォの場合、多くの場合、P 操作と V 操作という 2 つの操作が定義されます。このセマフォを使用して共有メモリ内の struct shm_data を保護し、同時に 1 つのプロセスだけがその構造を操作できるようにすることができます。

特定のカーネルメカニズムについては言及しません。

ネットワークシステムソケット

先ほどの通信は1台のマシン上の異なるプロセス間の通信であり、ソケットは異なるマシンのプロセス間の通信に使用されますが、
この部分の基礎知識を十分に習得する必要があり、ネットワーク通信は非常に重要です。
基本知識:
ここに画像の説明を挿入

ソケットのプログラミングは、ファイル記述子、つまりソケット ファイル記述子に基づく必要があります。ソケットファイル記述子を作成するには、socket(2) システムコールが使用されます。
intソケット(intドメイン、intタイプ、intプロトコル);
最初のパラメータは主にプロトコルUNIX、IPV4、IPV6で、2番目のパラメータはソケットのタイプ(バイトストリームとデータパケット、TCP、UDPの場合)、プロトコル:通常設定されます。 0にします。エラーが発生した場合は -1 を返し、それ以外の場合はソケット ファイル記述子を返します。


ソケットシステムコールによるbindで作成されたファイル記述子は直接利用できず、TCP/UDPプロトコルに関わるプロトコル、IP、ポートなどの基本要素は反映されず、bind(2)システムコールが反映されます。これらの要素をファイル記述子
#include
<sys/socket.h>
int binding(intソケット, const struct sockaddr *address, socklen_t address_len); 2 番目のパラメーターは、プロトコル、IP、ポート、およびその他の要素のフィールドを定義します。address_len: プロトコル アドレス構造の長さ。
バインドされたアドレスが間違っている場合、またはポートがすでに占有されている場合、バインド関数は確実にエラーを報告しますが、それ以外の場合は通常、エラーは返されません。

listen
関数はもう少し複雑です。ソケット システム コールを使用してソケットを作成する場合、それはアクティブ ソケット (クライアント ソケット) であるとみなされ、 listen(2) システム コールを呼び出すと、アクティブ ソケットがパッシブ ソケットに
変換され、カーネルが受け入れる必要があることを示します。このソケットに向けられた接続リクエスト。listen もう 1 つの重要なタスクは、未完了の接続キュー (半接続キュー) と完了した接続キュー (完全な接続キュー) を作成することです。カーネルは、リスニング ソケットごとにこれら 2 つのキューを維持します。スリーウェイ ハンドシェイクが完了していない接続は、不完全なキューに一時的に保存されます。スリーウェイ ハンドシェイクが完了し、サーバー呼び出しによってまだ処理されていない接続は、 accept システムコールは完了キューに格納されます。接続キュー。関数のプロトタイプは次のとおりです。#include <sys/socket.h> int listen(intソケット, int backlog);



同時実行性が高いシナリオのサーバー側プログラムの場合、バックログを適切に増やす必要があります(Nginx と Redis のデフォルトのバックログ値は 511)。バックログは実際には完全な接続キューのサイズであり、スリーウェイ ハンドシェイクは完了したが受け入れられていないことを意味します。しきい値を超えると、クライアントの syn リクエストは破棄されます。実際、完全な接続サイズは min(backlog, somaxconn) によって決まります。

カーネル分析:
1. listen では、引き続き sockfd_lookup_light を使用して、fd ファイル記述子に従って構造体のソケット構造を見つけます次に、struct ソケット構造体の ops の listen 関数を呼び出します(これは実際にはすべてがファイルであるという操作を反映しており、対応する i ノードも fd を通じて見つかりますが、カーネル メモリ内のディスク上にはなく、ファイルの関連情報と場所は i ノードを通じて見つかります。 fd には、対応するファイル構造レコードが存在します。対応する操作)
/2. ソケットが TCP_LISTEN 状態にない場合、inet_csk_listen_start を呼び出して listen 状態に入ります。
3. アクティブ ソケットからパッシブ ソケットへの変換は、実際には struct inet_connection_sock *icsk = inet_csk(sk); に対応し、強制的な型変換が行われます。struct inet_connection_sock の構造はさらに複雑です。これを開くと、さまざまなタイムアウト、輻輳制御などが行われたさまざまな状態のキューを確認できます。TCP は接続指向であると言います。つまり、クライアントとサーバーの両方が接続の状態を維持するための構造を持っており、この構造を指します。
4. reqsk_queue_alloc(&icsk->icsk_accept_queue); は、完全な接続キューを割り当てます。

accept
accept(2) システムコールは、完了した接続のキューの先頭から接続のサービスを試みるため、結果として生じるキューのギャップは、未完了の接続のキューからの接続で埋められます。この時点で完了した接続キューが空で、ソケット ファイル記述子がデフォルトのブロッキング モードである場合、プロセスは一時停止されます。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
ソケット: ソケットはファイル記述子をリッスンします。
addr: 接続されたピアプロセスのプロトコルアドレス。ピア情報を気にしない場合は、NULL に設定できます。
addrlen: アドレス構造の長さへのポインタ。addr パラメータが NULL に設定されている場合は、NULL に設定できます。
戻り値: エラーの場合は -1 を返し、それ以外の場合は接続されたソケット ファイル記述子を返します。

カーネル分析:
1. それらはすべて同じです sockfd_lookup_light を通じて、fd ファイル記述子に従って struct ソケット構造を見つけます
2. newsock = sock_alloc(); そして、それに基づいて新しいニュースックを作成します。これは接続ソケット
3 です。icsk_accept_queue が空の場合は、inet_csk_wait_for_connect を呼び出して待機します。待機中に、schedule_timeout を呼び出して CPU を放棄し、プロセス ステータスを TASK_INTERRUPTIBLE に設定します。割り込み可能なスリープ状態、シグナルによって
ウェイクアップ可能 CPU が再びウェイクアップすると、icsk_accept_queue が空かどうかを判断し、同時に signal_pending を呼び出して、処理できるシグナルがあるかどうかを確認します。icsk_accept_queue が空でなくなったら、inet_csk_wait_for_connect から戻り、キューから struct sock オブジェクトを取り出し、newsk に割り当てます。

connect (3 ウェイ ハンドシェイクの実装)
アクティブなソケットを作成する側 (クライアント) は、connect(2) システム コールを呼び出して、パッシブなソケット側 (サーバー) との接続を確立します。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
このうち、最初のパラメータは accept によって確立された接続 fd で、addr はサーバーの ip+port です。

3 ウェイ ハンドシェイクは通常、connect を呼び出すクライアントによって開始されます。完了後、完全な接続キューに入ります。1
. 手順は同じです。fd に従って、struct ソケット構造を見つけて、対応する conncet 操作を見つけます。
2. ソケットが SS_UNCONNECTED 状態にあることが判明した場合、tcp_v4_connect 関数が呼び出されます。主にルーティングの選択を行います。(クライアントにはバインドがないため、ネットワーク カードとポートが自動的に選択されます。) SYN を送信する前に、まずクライアント ソケットの状態を TCP_SYN_SENT に設定し、次に TCP の ISN 初期化シーケンス番号を初期化してから、tcp_connect を呼び出します。 tcp_connect には
、新しい構造体struct tcp_sockがあり、それを開くと、これが struct inet_connection_sock の拡張であることがわかります。 struct inet_connection_sock は、必須の型変換を通じてアクセスされる struct tcp_sock** の先頭にあり、そしてトリックは再び繰り返されます**。struct tcp_sock は、より多くの TCP 状態を維持します
。 3. inet_wait_for_connect は、クライアントがサーバーから ACK を受信するのを常に待ちますそして、サーバーが受け入れた後も待機していることがわかります。サーバーは tcp_v4_send_synack を通じて syn を受信します。特定の送信プロセスは気にしません。コメントからわかります。SYN を受信した後、SYN-ACK を返信します。返信が完了すると、サーバーは TCP_SYN_RECV
4 になります。tcp_send_ack を呼び出し、確認応答-ACK、クライアントは送信後に TCP_ESTABLISHED 状態になります。サーバーがそれを受信すると、ESTABLISHED になります。

Socket システム コールには、ファミリー、タイプ、およびプロトコルの 3 つのレベルのパラメータがあり、これら 3 つのレベルのパラメータを通じて、タイプ リンク リストが net_proto_family テーブルに見つかり、プロトコルの対応する操作がタイプ リンク リストに見つかります。この動作は 2 つの層に分かれており、TCP プロトコルの場合、第 1 層は inet_stream_ops 層、第 2 層は tcp_prot 層になります。

送信データパケット書き込み

ssize_t write(int fd, const void*buf, size_t nbytes);
write 関数は、buf の nbytes バイトの内容をファイル記述子 fd に書き込みます。成功すると書き込まれたバイト数を返します。失敗すると -1 を返します。 . そして errno 変数を設定します

これには、ネットワーク パケットを VFS 層から IP 層に送信し、その後 MAC 層に送信してソケットの書き込み操作を分析する方法が含まれます。

1. 最初のステップは統一することです。fd を介して対応する構造体ファイル構造を検索すると、write システム コールは最終的に、構造体ファイル構造が指す file_operations オペレーションを呼び出します: sock_write_iter; ここで、IP プロトコル レベルと TCP/UDP レベルの 2 つのレベルに従って、最後の呼び出しは tcp_sendmsg VFS -
Socket
2 です。 tcp_sendmsg の実装はまだ非常に複雑で、ここでいくつかの処理が行われます。1 つ目はユーザー モードからカーネル バッファにデータをコピーすること、2 つ目はデータを送信することです。
カーネルへのコピーはループであり、copid を宣言し、ループの最後に += copy をコピーして、各コピーの数を合計します。1 サイクルで行うことは次のとおりです
。MSS (MTU-TCP ヘッダー - IP ヘッダー) を計算します。コピーが 0 未満の場合、最後の構造体 sk_buff に保存する場所がないことを意味します。sk_stream_alloc_skb を呼び出し、構造体を再割り当てする必要があります。 sk_buff を呼び出してから skb_entail を呼び出し、新しく割り当てられた sk_buff をキューの最後に置きます (その中には最適化されたデータ構造がいくつかあります)。

3. ネットワークパケットを送信します。__tcp_push_pending_frames または tcp_push_one に関係なく、ネットワーク パケットを送信するために tcp_write_xmit が呼び出されますここでの主なロジックは、送信キューを処理するために使用されるループです。キューが空でない限り、キューは送信されます。
ループでは、TCP 層に関係する多くの送信アルゴリズム (セグメンテーション、輻輳制御、フロー制御など) がループの最後に tcp_transmit_skb を呼び出して、実際にネットワーク パケットを送信します。やることは2つあり、1つ目はTCPヘッダーを埋め、すべての設定が完了したらip_queue_xmit関数を呼び出して送信します。

4. IP 層から MAC 層まで、
ip_queue_xmit には 3 つのロジック部分があります。
最初の部分はルート、つまり送信したいパケットがどのネットワーク カードから送信されるかを選択することです。fib_table_lookup 関数は、このテーブル内を検索します。プレフィックスに従ってクエリを実行し、最長一致するものを見つけます。たとえば、192.168.2.0/24 と 192.168.0.0/16 は両方とも 192.168.2.100/24 と一致します。ただし、このエントリは 192.168.2.0/24 に対して使用する必要があります。これをより便利に行うために、トライ ツリーの構造を使用します。IP アドレスをバイナリに変換し、それを
トライ ツリー 2 の 2 番目の部分に置きます。これは、IP 層のヘッダーを準備し、それを埋めることです。コンテンツ。
3 3 番目の部分は、ip_local_out を呼び出して、nf_hook を含む IP パケットを送信することです。IP 層で行うべきことの 1 つは、iptables ルール
(nf_hook。これは何ですか? nf は Netfilter を意味します。これは Linux カーネルのメカニズムであり、ネットワークの送信および転送の主要なノードにフック関数を追加するために使用されます)。関数はデータ パケットをインターセプトし、データ パケットに介入できます。よく知られた実装はカーネル モジュール ip_tables です。ユーザー モードでは、コマンド ラインを使用してカーネルのルールに介入するクライアント プログラム iptables もあります。 iptable テーブルには主に 2 つのタイプが
あり、各テーブルにはいくつかのチェーンがあります. 要約すると、iptable に特定のルールがある場合、主に次の
3 つのチェーンを含む IP アドレス フィルタ テーブル処理フィルタリング機能をフィルタリングまたは変更します. INPUT チェーン: すべてのターゲット アドレスをフィルタリングしますこのマシンのデータ パケットです。FORWARD チェーン: このマシンを通過するすべてのデータ パケットをフィルタリングします。OUTPUT チェーン: このマシンによって生成されたすべてのデータ パケットをフィルタリングします。
nat テーブルは主にネットワーク アドレス変換を扱い、次の 3 つのチェーンを含む SNAT (送信元アドレスの変更) と DNAT (宛先アドレスの変更) を実行できます。PREROUTING チェーン: データ パケットの到着時に宛先アドレスを変更できます; OUTPUT チェーン: ローカルに生成されたデータ パケットの宛先アドレスを変更できます; POSTROUTING チェーン: データ パケットの出発時にデータ パケットのソース アドレスを変更できます。出力と
POSTROUTING チェーンを通過します。次に、ip_finish_output を呼び出します。MAC層に入ります。

4. MAC 層が実行する必要があるのは、ARP プロトコルです。MAC 層は MAC アドレスを取得するために ARP を必要とするため、同じネットワーク セグメントに属するネイバーを見つけるために ___neigh_lookup_noref を呼び出す必要があり、neigh_probe を呼び出して ARP を送信します。MAC アドレスを使用すると、dev_queue_xmit を呼び出してレイヤー 2 ネットワーク パケットを送信でき、__dev_xmit_skb を呼び出して要求をキューに入れます。ネットワーク パケットの送信によりソフト割り込みがトリガーされ、データがキューから取得され、ハードウェア ネットワーク カードの送信キューに入れられます。

要約すると、
VFS-SOCKET 層にはいくつかの層があります。 fd を通じて対応するファイル構造を見つけ、対応する書き込み操作tcp_sendmsgを呼び出します。これには 2 つのレベルが含まれています。

TCP 層に到着したら、最初にユーザー状態からカーネル バッファにデータをコピーし、次にデータを送信します。この層には、MSS、輻輳制御、フロー制御、TCP ヘッダー充填などの TCP 接続のいくつかの特性が含まれます。ip_queue_xmit関数を呼び出してIP 層に送信します

IP 層に到着したら、主に 3 つの作業を行う必要があります。1 つ目は、ルートを選択し、どのネットワーク カードから送信するかを選択し、トライ ツリーの構造を使用します。IP アドレスをバイナリに変換し、より高速なマッチングのためのトライ ツリー、2 つ目は IP 層ヘッダー情報の準備、3 つ目は iptables ルールによるもの、そしてip_finish_outputの呼び出しです。MAC層に入ります。

MAC に関して言えば、主に行うべきことは、ARP が MAC アドレスを見つけて、それを見つけた後に要求キューに入れ、ソフト割り込みをトリガーすることです。これにより、ネットワーク カードがデータをフェッチして送信します。

読み取り関数の分析 (実際には逆のプロセス)

ここに画像の説明を挿入

プロセスは大まかに次のとおりです。
1. ハードウェア ネットワーク カードがネットワーク パケットを受信した後。DMA テクノロジーはリング バッファに置かれ、NAPI を経由します (データの到着時間が不確実なため、通常の割り込み処理では処理直後は非効率になる可能性があります。そのため、NAPI は割り込みされ、データがなくなるまでポーリングし、その後戻って) データをバッファに入れ、リング バッファからカーネル構造体 sk_buff にデータを読み取るように CPU に通知します。
2. netif_receive_skb を呼び出してカーネル ネットワーク プロトコル スタックに入り、VLAN 上でレイヤー 2 ロジック処理を実行した後、ip_rcv を呼び出してレイヤー 3 IP レイヤーに入ります。
3. IP 層で iptables ルールが処理され、ip_local_deliver が呼び出されて上位の TCP 層に配信されます。TCP 層で tcp_v4_rcv を呼び出します。
4. TCP 層は主にいくつかのキューを処理します。現在のソケットが読み取られていない場合はバックログ キューに入れ、読み取り中でリアルタイムである必要がない場合はプリキュー キューに入れ、それ以外の場合は tcp_v4_do_rcv を呼び出し、シリアル番号が接続できる場合は、 put it sk_receive_queue キューに入れます; シリアル番号が接続できない場合は、一時的に out_of_order_queue キューに入れ、シリアル番号が接続できたら sk_receive_queue キューに入れます。(これが、http2 がキュー ヘッド ブロッキングの問題を依然として抱えている理由ですが、これは TCP 層にあり、受信キューに入る前にシリアル番号を接続する必要があります)

ここまででカーネルによるネットワークパケットの受信処理は終了し、次はユーザーモードでのネットワークパケットの読み取り処理となり、この処理はいくつかのレベルに分かれています。
VFS 層: read システム コールは、構造体ファイルを検索し、内部の file_operations の定義に従って sock_read_iter 関数を呼び出します。sock_read_iter 関数は sock_recvmsg 関数を呼び出します。次に、inet と TCP の 2 つのレベルの後、実際の読み取り関数 tcp_recvmsg 関数を呼び出します。tcp_recvmsg 関数は、receive_queue キュー、prequeue キュー、backlog キューを順番に読み取ります。

おすすめ

転載: blog.csdn.net/weixin_53344209/article/details/130733126