Manjusaka

Manjusaka

sk_buffのマイナーなフィールドについて話しましょう: nohdr

今日は興味深い問題に遭遇しました。「nohdr フィールドは実際にはどのように使用されるのか」ということで、ここに簡単な水文を書いて記録しておきます。

本文#

前提条件#

まず最初に、どれほどマイナーなフィールドを紹介しても、SKBUFF に関連する場合は、まず sk_buff の簡単な紹介を行う必要があります。

要するに、sk_buff は Linux のネットワークサブシステムの中核データ構造であり、リンク層からデータパケットの操作まで、すべて sk_buff を介して行われます。

sk_buff を完全に説明することは、基本的には Linux のネットワークシステムを完全に説明することに等しいため、完全に説明することは不可能です。この人生では絶対に不可能です!

いくつかのキーポイントについて簡単に話しましょう。これらは、本文で言及されるマイナーフィールド「nohdr」の重要なフィールドを理解するのに役立つかもしれません。

まず最初に、最も重要な 3 つのフィールド:datamacnh です。それぞれ、現在の sk_buff のデータ領域の開始アドレス、L2 ヘッダーの開始アドレス、L3 ヘッダーの開始アドレスを表しています。図を使って理解しやすくしましょう。

sk_buff 三剣士

図を見た方は少し理解できるかもしれませんが、実際にはカーネル内部でも、ネットワークリクエストを処理するために、ポインタのオフセットを使用して段階的に新しいヘッダーを追加していくという方法で処理されます。これは私たちの直感と一致しています。また、L3 ヘッダーの開始アドレスを知っている場合、IP などの L3 プロトコルのヘッダーの長さは固定です。したがって、L4 のオフセットを計算し、手動で処理することができます。

Bingo、カーネルには tcphdr のデータ構造(IP の場合は iphdr )があります。オフセットに基づいて、手動でキャストして処理することができます。ただし、詳細な手順については後で説明します。

次に、重要な 2 つのフィールド、lendata_len です。これらのフィールドはどちらもデータの長さを示していますが、簡単に言えば、len は現在の sk_buff のすべてのデータの長さを表しています(つまり、現在のプロトコルのヘッダーとペイロードを含む)、data_len は現在の有効なデータの長さを表しています(つまり、現在のプロトコルのペイロードの長さ)。

OK、前提条件はここまでです。

nohdr について#

花は 2 つ咲き、それぞれが異なる意味を持っています。sk_buff のいくつかの準備知識について話した後、ここで「nohdr」というフィールドについて話しましょう。正直なところ、このフィールドは本当にマイナーです。

まず、公式には次のように説明されています。

'nohdr' フィールドは TCP セグメンテーションオフロード('TSO' の略)のサポートに使用されます。この機能をサポートするほとんどのデバイスは、パケットスニッファなどからこれらの変更が見えないように、送信パケットの TCP および IP ヘッダーにいくつかの細かい変更を加える必要があります。そのために、この 'nohdr' フィールドとデータ領域の参照カウントの特別なビットを使用して、デバイスがパケットヘッダーの変更を行う前にデータ領域を置き換える必要があるかどうかを追跡します。

うーん、この文章は少しわかりにくいですね。まず最初に、TSO については皆さんが一定の理解を持っていると思います。ネットワークカードを使用して大きなデータパケットをセグメント化するためのものです(具体的な Linux の GSO/TSO の実装については、別の記事で詳しく説明します)。そのため、このような場合、ネットワークカードはヘッダーの一部を少し変更してパケットの分割を完了する必要があります。

しかし、L4 レイヤーのパケットに関しては、ヘッダーの変更に関心がなく、ペイロードに関心がある場合があります。では、どうすればいいのでしょうか。ここで「nohdr」が役立ちます。

ここで、「nohdr」が有効になるためには、別のフィールド「dataref」と組み合わせる必要があります。dataref はカウントフィールドであり、具体的な意味は、現在のデータフィールドがいくつの sk_buff によって参照されているかを示しています。ここでは 2 つのケースがあります。

  1. nohdr が 0 の場合、dataref の値はデータ領域の参照カウントです。
  2. nohdr が 1 の場合、上位 16 ビットはデータ領域のペイロードデータ領域の参照カウントであり、下位 16 ビットはデータ領域の参照カウントです。

公式には次のように説明されています。

/* We divide dataref into two halves. The higher 16 bits hold references * to the payload part of skb->data. The lower 16 bits hold references to * the entire skb->data. It is up to the users of the skb to agree on * where the payload starts.

* * All users must obey the rule that the skb->data reference count must be * greater than or equal to the payload reference count.

* * Holding a reference to the payload part means that the user does not * care about modifications to the header part of skb->data.

*/ 
#define SKB_DATAREF_SHIFT 16 #define SKB_DATAREF_MASK ((1 << SKB_DATAREF_SHIFT) - 1)

実際には、なぜこのように設計されているのかはそれほど難しくありません。まず最初に、カーネル内部でパケットを取得する場合、ヘッダーの具体的な内容に関心を持たず、ペイロードに関心を持つ場合があります。また、ペイロードへの参照カウントについても、正確性を保証するために個別に処理する必要があります。これにより、データがまだ処理されていない場合にカーネルがデータスライスを事前に解放しないようになります。もちろん、この場合、データ領域の参照カウントがペイロードの参照カウントよりも大きいことを確認する必要があります(これは約束を守らない場合、カーネルがダンプされる結果になるかもしれません)。

最後に、カーネルは dataref を使用して、適切なタイミングでデータ領域のメモリスペースを解放します。解放条件は次のいずれかを満たす必要があります。

  1. !skb->cloned: skb がクローンされていない場合
  2. !atomic_sub_return (skb->nohdr ? (1 << SKB_DATAREF_SHIFT) + 1 : 1, &skb_shinfo (skb)->dataref) つまり、nohdr が 1 の場合は dataref-(1 << SKB_DATAREF_SHIFT) + 1) を使用してデータ領域を解放するかどうかを判断します。nohdr が 0 の場合は dataref-1 を使用してデータ領域を解放するかどうかを決定します。

まとめ#

水文はほぼ以上です。「nohdr」は本当にマイナーなフィールドです。この水文のいくつかの参照は、電車の中で調べたものですので、記事には記載していません。だいたいこんな感じです。問題を解くために戻ります...

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。