今日は興味深い問題に遭遇しました。「nohdr フィールドは実際にはどのように使用されるのか」ということで、ここに簡単な水文を書いて記録しておきます。
本文#
前提条件#
まず最初に、どれほどマイナーなフィールドを紹介しても、SKBUFF に関連する場合は、まず sk_buff の簡単な紹介を行う必要があります。
要するに、sk_buff は Linux のネットワークサブシステムの中核データ構造であり、リンク層からデータパケットの操作まで、すべて sk_buff を介して行われます。
sk_buff を完全に説明することは、基本的には Linux のネットワークシステムを完全に説明することに等しいため、完全に説明することは不可能です。この人生では絶対に不可能です!
いくつかのキーポイントについて簡単に話しましょう。これらは、本文で言及されるマイナーフィールド「nohdr」の重要なフィールドを理解するのに役立つかもしれません。
まず最初に、最も重要な 3 つのフィールド:data
、mac
、nh
です。それぞれ、現在の sk_buff のデータ領域の開始アドレス、L2 ヘッダーの開始アドレス、L3 ヘッダーの開始アドレスを表しています。図を使って理解しやすくしましょう。
図を見た方は少し理解できるかもしれませんが、実際にはカーネル内部でも、ネットワークリクエストを処理するために、ポインタのオフセットを使用して段階的に新しいヘッダーを追加していくという方法で処理されます。これは私たちの直感と一致しています。また、L3 ヘッダーの開始アドレスを知っている場合、IP などの L3 プロトコルのヘッダーの長さは固定です。したがって、L4 のオフセットを計算し、手動で処理することができます。
Bingo、カーネルには tcphdr
のデータ構造(IP の場合は iphdr
)があります。オフセットに基づいて、手動でキャストして処理することができます。ただし、詳細な手順については後で説明します。
次に、重要な 2 つのフィールド、len
と data_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 つのケースがあります。
- nohdr が 0 の場合、dataref の値はデータ領域の参照カウントです。
- 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 を使用して、適切なタイミングでデータ領域のメモリスペースを解放します。解放条件は次のいずれかを満たす必要があります。
- !skb->cloned: skb がクローンされていない場合
- !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」は本当にマイナーなフィールドです。この水文のいくつかの参照は、電車の中で調べたものですので、記事には記載していません。だいたいこんな感じです。問題を解くために戻ります...