eBPF ソケット レベル リダイレクトのカーネル実装の詳細を図解

eBPF ソケット レベル リダイレクトのカーネル実装の詳細を図解

前回の記事「eBPF を使用してソケット レベルのリダイレクトを実装する」では、eBPF のソケット レベルのリダイレクトのアプリケーション シナリオについて全体的に紹介しました。マシン上にループバック デバイスを介して相互にデータを送受信する必要がある 2 つのプロセスがある場合、ebpf を使用して、送信プロセス側でローカル マシンの基礎となる TCP/IP プロトコル スタックをスキップし、宛先プロセスのソケットに直接渡すことで、カーネル内のデータ処理パスと時間を短縮できます。

このプロセスは図 1 に示されています。この記事では、図 1 の右側のカーネルの実装の詳細を詳しく見ていきます。

図1: ebpfを使用してソケットレベルのリダイレクトを行い、TCP/IPスタックとloデバイスをスキップする

まずは全体像を見て、その全体像から重要な知識ポイントを一つずつ分析していきます。

図 2: ebpf を使用したソケット レベル リダイレクトのグローバル詳細

1. 準備

1. bpf_sock_opsをsock_hashマップに挿入する

私たちの話は、図 3 に示すように、bpf_sock_ops を sock ハッシュ マップに挿入することから始まります。ここにいくつかのコード スニペットを示します。完全なコンパイル可能で実行可能なコードは、https://github.com/LanceHBZhang/socket-acceleration-with-ebpf にあります。さらに、ebpf プログラムの完全なインストール プロセスには cgroup も含まれますが、これについては詳しく説明しません。

次のコードでは、特別なマップ タイプ BPF_MAP_TYPE_HASH を使用します。この記事では、これを sock_hash とも呼びます。 KV タイプのデータを格納し、その値は実際にはデータ構造 struct bpf_sock_ops に対応します。 bpf_sock_ops を保存するだけでなく、ユーザーが作成した sk_msg タイプの bpf プログラムをこのタイプのマップにアタッチして、データを受信するソケットを見つけることもできます。アタッチステートメントについては、github コードを参照してください。

静的インライン
void bpf_sock_ops_ipv4 (構造体bpf_sock_ops * skops )
{
構造体 sock_keyキー= {};
整数戻り値;

extract_key4_from_ops ( skops& key ) を実行します。

ret = sock_hash_update ( skops& sock_ops_map& keyBPF_NOEXIST );
戻り値が0場合
printk ( "sock_hash_update() が失敗しました、戻り値: %d\n" , ret );
}

printk ( "sockmap: op %d、port %d --> %d\n" ,
skops -> opskops -> local_portbpf_ntohl ( skops -> Remote_port ));
}

__section ( "sockops" )
int bpf_sockmap (構造体bpf_sock_ops * skops )
{
スイッチ( skops -> op ) {
BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CBの場合:
BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CBの場合:
if ( skops -> family == 2 ) { //AF_INET
bpf_sock_ops_ipv4 (スコップ);
}
壊す;
デフォルト
壊す;
}
0を返します
}

次のコードで sock_ops タイプの bpf 関数 bpf_sockmap() を cgroupv2 のルート パスにアタッチすると、アクティブまたはパッシブ接続の確立などのソケット イベントが発生したときに、bpf 関数 bpf_sockmap() が呼び出されます。このプロセスは、図 3 の実行ポイント 1 に示されています。具体的には、1 で行われるのは 3 ウェイ ハンドシェイク (SYN / SYNC-ACK / ACK) です。 3ウェイハンドシェイクなので、当然通信する双方に関係するため、bpf_sockmap() 関数内の bpf_sock_ops_ipv4(skops) が2回呼び出されます。

bpf_sockmap() が行うことは非常にシンプルです。ソース IP / ソース ポート / 宛先 IP / 宛先ポート / ファミリをキーとして使用し、struct bpf_sock_ops オブジェクトを sock_hash に格納します。このプロセスは、図 3 の実行ポイント 1.2 に示されています。 bpf_sockmap() が ebpf に関連していることを示すために、 ebpf のロゴを 2 か所に描きました。

上記のコードでは、 sock_hash_update() 関数呼び出しが sock_has マップを更新しているように見えますが、実際にはカーネル内で実行される処理の方が重要であり、TCP プロトコル関連の関数を正確かつ動的に置き換えます。

図3: sock_hashマップにsockを挿入する

2. 正確な動的プロトコールの交換

カーネル プロトコル スタックに関連するデータ構造に注目していれば、次の図に示すように、struct file / struct socket / struct sock / struct proto などのいくつかの重要な役割に必ず遭遇するでしょう。

ソケットは、デザインパターンでよく使用されるアダプタのようなものです。一方では、アプリケーション層に面した struct ファイルに適応し、他方では、 struct sock を参照してネットワーク プロトコル スタックを直列に接続します。

この図をよく見ると、 struct sock が魂であり、その内容からそれを垣間見ることができることがわかります。 struct sock には非常に重要な部分があります。それは、ネットワーク プロトコルへの参照であり、先ほど見た sk_prot です。なぜそれが重要なのでしょうか? sk_prot がポインターとして指す構造体 tcp_prot には、recvmsg や sendmsg など、TCP プロトコルに不可欠な一連の関数が含まれているため、この記事ではこれらに焦点を当てる必要があります。名前から使用シナリオもわかります。TCP 層でデータの送受信に使用されます。

もちろん、struct tcp_prot に加えて、sk_prot は struct udp_prot/ping_prot/raw_prot を指すこともできます。

図4: ファイル/ソケット/sock/操作 (画像出典: 開発内部パワー育成WeChatアカウント)

では、ebpf はここで何をするのでしょうか?それはとても賢いですね。これは、struct proto 内の recvmsg/sendmsg 関数を動的に置き換えます。たとえば、recvmsg は元の tcp_recvmsg から tcp_bpf_recvmsg に置き換えられ、tcp_sendmsg は tcp_bpf_sendmsg に置き換えられます。

静的void tcp_bpf_rebuild_protos (構造体 proto prot [ TCP_BPF_NUM_CFGS ]、構造体proto *ベース)
{
prot [ TCP_BPF_BASE ] = *ベース;
プロトコル[ TCP_BPF_BASE ]。閉じる= sock_map_close ;
プロトコル[ TCP_BPF_BASE ]。受信メッセージ= tcp_bpf_recvmsg ;
プロトコル[ TCP_BPF_BASE ]。 sock_is_readable = sk_msg_is_readable ;

prot [ TCP_BPF_TX ] = prot [ TCP_BPF_BASE ];
prot [ TCP_BPF_TX ] .sendmsg = tcp_bpf_sendmsg ;
prot [ TCP_BPF_TX ] .sendpage = tcp_bpf_sendpage ;

prot [ TCP_BPF_RX ] = prot [ TCP_BPF_BASE ];
プロトコル[ TCP_BPF_RX ]。 tcp_bpf_recvmsg_parserは、次のようになります。

prot [ TCP_BPF_TXRX ] = prot [ TCP_BPF_TX ];
プロトコル[ TCP_BPF_TXRX ]。 tcp_bpf_recvmsg_parserは、次のようになります。
}

静的int __init tcp_bpf_v4_build_proto ( void )
{
tcp_bpf_rebuild_protos ( tcp_bpf_prots [ TCP_BPF_IPV4 ]、およびtcp_prot );
0を返します
}
遅い_initcall ( tcp_bpf_v4_build_proto );

int tcp_bpf_update_proto (構造体sock * sk構造体sk_psock * psockbool復元)
{
// ...
/* sk_clone_lock() のロックレス読み取りとペアになります */
WRITE_ONCE ( sk -> sk_prot& tcp_bpf_prots [ファミリ][構成] );
0を返します
}

単純な置き換えは実際には賢明ではありません。私の次兄は、ここでの置き換えは「正確な動的置き換え」なので巧妙だと言いました。まず、なぜ精密置換と呼ばれるのでしょうか?考えてみてください。すべてのサービスがローカルプロセス間の通信にループバックを使用する必要はありません。さらに、プロセス間通信がこのように実行される場合でも、すべてのシナリオでソケット レベルのリダイレクトを使用する必要があるわけではありません。そのため、交換操作は広く使用できず、必要な場合にのみ交換できます。いわゆる「動的置換」とは、カーネルのコンパイル時に直接置換されるのではなく、必要なときに置換されることを意味します。

では、この「必要な時」とはいつなのでしょうか?

答えは、bpf_sock_ops が sock_hash に格納されるときです。これは、図 3 に関係するプロセスです。システム関数 bpf_sock_hash_update が呼び出されると、カーネルは net/core/sock_map.c にある sock_hash_update_common 関数を呼び出し、これにより、呼び出しチェーン内の置換関数 tcp_bpf_update_proto() の呼び出しが完了します。実際の置換の結果、sk->sk_prot は置換されたバージョン、つまり tcp_bpf_prots[family][config] を保存しますが、tcp_bpf_prots は非常に早い段階で初期化されています。

ここでの置換操作は、実際にソケット レベルのリダイレクトを使用する必要がある sock にのみ関連し、他の sock には影響しないことを再度強調します。もちろん、交換した靴下は実際には一足です。図 3 では、envoy プロセスとプロセス B はそれぞれ通信プロセスに参加する独自の sock を持っているため、独自の recvmsg/sendmsg を置き換える必要があることがお分かりでしょう。

2. sk_psock

図 3 では、TX キューと RX キューから独立した新しいキュー ingress_msg も確認できます。通信する双方のソケット層にはこのようなキューがあります。キューに一時的に保存されるデータは、構造体 struct sk_msg によって表されます。 sk_msg には、これまでよく知られている skb が含まれているため、その具体的な定義は省略します。次のデータ送受信プロセスの説明では、ingress_msg キューがどのように機能するかを確認します。

このキューは、構造体 struct sk_psock {} にあります。 sk_psock には、その仲間の sock/eval/cork なども含まれています。

カーネル コードには、sk_psock の読み取りと書き込みを行う psock->eval ステートメントが多数あります。さらに、この構造体には関数ポインタ psock_update_sk_prot があり、これは前のセクションで説明した関数 tcp_bpf_update_proto() を指していることがわかります。

構造体sk_psock {
構造体sock * sk ;
構造体sock * sk_redir ;
u32バイトを適用します
u32コルクバイト;
u32評価;
構造体sk_msg *コルク;
構造体sk_psock_progsプログラム;
#IS_ENABLEDの場合( CONFIG_BPF_STREAM_PARSER )
構造体strparser strp ;
#終了
構造体sk_buff_head ingress_skb ;
構造体list_head ingress_msg ;
spinlock_tイングレスロック;
符号なしロング状態;
構造体 list_headリンク;
spinlock_tリンクロック;
refcount_t参照カウント;
void ( * saved_unhash )( struct sock * sk );
void ( * saved_close )( struct sock * sk , long timeout );
void ( * saved_write_space )( struct sock * sk );
void ( * saved_data_ready )(構造体sock * sk );
int ( * psock_update_sk_prot )( struct sock * skstruct sk_psock * psockbool復元);
構造体proto * sk_proto ;
構造体ミューテックスwork_mutex ;
構造体sk_psock_work_state作業状態;
構造体work_struct作業;
構造体 rcu_work rwork ;
}

3. データの送信

プロセス B との TCP 接続が正常に確立されると、プロセス envoy は図 5 の 2.1 に示すようにデータの書き込みを開始します。

通常の状況では、write() システム コールによって渡されたデータは、最終的に TCP 層の tcp_sendmsg() によって処理されます。しかし、「prot の正確な動的置換」のセクションで、tcp_sendmsg() が tcp_bpf_sendmsg() に置き換えられたと述べたことを覚えていますか?つまり、ここでの主役は実際には tcp_bpf_sendmsg() です。

図5: データ送信プロセス

図 5 では、tcp_bpf_sendmsg() が実行するいくつかの重要な点を強調しています。

1. 実行ポイント2.3: ebpプログラムを実行する

ebpf プログラムは実際にはかなり前に準備して sock_hash にアタッチする必要があります (このプロセスについては、上記に添付されている github コードを参照してください)。プログラムのエントリ ポイントは非常にシンプルです: bpf_redir()。また、struct sk_msg_md から送信元 IP / 送信元ポート / 宛先 IP / 宛先ポート / ファミリを抽出し、それをキーとして通信相手の struct sock である sock_hash 内のリダイレクト先を探し、psock->sk_redir に格納します。

静的インライン
void extract_key4_from_msg (構造体sk_msg_md * msg構造体sock_key *キー)
{
キー-> sip4 =メッセージ-> remote_ip4 ;
キー-> dip4 =メッセージ-> local_ip4 ;
キー- >ファミリー= 1 ;

キー-> dport = ( bpf_htonl ( msg -> local_port ) >> 16 );
key -> sport = FORCE_READ ( msg -> remote_port ) >> 16 ;
}

__section ( "sk_msg" )
int bpf_redir (構造体sk_msg_md *メッセージ)
{
構造体 sock_keyキー= {};
extract_key4_from_msg (メッセージ&キー)
msg_redirect_hash ( msg& sock_ops_map& keyBPF_F_INGRESS );
SK_PASSを返します
}

コード内の msg_redirect_hash() という名前は少し誤解を招きます。一見すると、リダイレクト処理はここで完了したと思いました。実際には、マップの検索とリダイレクトが許可されているかどうかの確認という 2 つの操作のみが実行されます。本当の楽しみはまだこれからです。コードは長くないので、ここにすべて貼り付けます。

 BPF_CALL_4 ( bpf_msg_redirect_hashstruct sk_msg *msgstruct bpf_map *mapvoid *keyu64flags )
{
構造体sock * sk ;

if (ありそうにない(フラグ& ~ ( BPF_F_INGRESS )))
SK_DROPを返します

sk = __sock_hash_lookup_elem (マップキー);
if (可能性は低い( ! sk || ! sock_map_redirect_allowed ( sk )))
SK_DROPを返します

msg- > flags =フラグ;
msg- > sk_redir = sk ;
SK_PASSを返します
}

2. 実行ポイント2.4: sk_msgをキューに入れる

ここで、ingress_msg キューが初めて動作するのを確認します。

struct sk_psock {} には eval というメンバーがあります。このキーワードから、評価結果に関係するものだと推測できるでしょう。では評価の対象は誰でしょうか? 2.3 で実行する必要があるのは ebpf プログラムです。 2.3 での実行結果は、後で使用するために psock->eval に格納されます。

実行結果は __SK_PASS / __SK_REDIRECT / __SK_DROP の 3 つになります。 psock->eval が、注目する __SK_REDIRECT と等しい場合、ポイント 2.4 の実行プロセスが開始され、sk_msg がキュー psock->ingress_msg に配置されます。

ここでの psock は依然として送信側、つまり envoy プロセスに属していることに注意してください。したがって、当然、ingress_msg キューも envoy プロセスに属します。

3. 実行ポイント2.5: 新しいタスクを開始する

実行ポイント 2.4 で sk_msg を psock->ingress_msg に入れた後、カーネルは他の関数の呼び出しを継続せず、schedule_work(&psock->work) を通じて非同期タスクを開始することを選択し、データ送信プロセスを終了します。

この選択とキューは非常に合理的です。まず、効率性のために、送信者 (特使) を待たせることはできません。 2 番目に、分離のために、キューは送信者と受信者の間の結合を解除するために使用されます。 3 番目は、受信側 (プロセス B) の処理速度に合わせるためです。プロセス B はこの時点で他の事項を処理している可能性があり、skb を消費する時間がないからです。

4. データの受信

前のセクションの実行ポイント 2.5 では、データ送信プロセスが最終的に非同期タスクを開始することを説明しました。ここでデータ送信プロセスが終了し、データ受信プロセスがここで開始されます。図 6 に、プロセス全体に含まれる主要なステップ (実行ポイント 3.1/3.2/4.1/4.2) を示しました。

図6: データ受信プロセス

1. 実行ポイント3.1: sk_mskgをデキューする

非同期タスクのエントリ ポイントは関数 sk_psock_backlog() であり、最初に envoy によって使用される psock ポインターを取得し、次に psock->ingress_skb から sk_msg を継続的に取り出して消費します。

次のコードでは、struct sk_psock *psock = container_of(work, struct sk_psock, work) は、work を通じて psock を取得するロジックであり、シンプルですがエレガントなプロセスです。

静的void sk_psock_backlog (構造体work_struct * work )
{
構造体sk_psock * psock = container_of ( work構造体sk_psockwork );
構造体sk_psock_work_state *状態= & psock -> work_state ;
構造体sk_buff * skb = NULL ;
//...

//...
while (( skb = skb_dequeue ( & psock -> ingress_skb ))) {
//...
始める
入力= skb_bpf_ingress ( skb );
skb_bpf_redirect_clear ( skb );
する{
ret = -EIO ;
if ( ! sock_flag ( psock -> skSOCK_DEAD ))
ret = sk_psock_handle_skb ( psock , skb ,オフ,
長さ入力);
//...
} while (長さ);

if ( !イングレス)
kfree_skb ( skb );
}
終わり
mutex_unlock ( & psock -> work_mutex );
}

2. 実行ポイント3.2: sk_msgを再度キューに追加する

上記のコードの skb_dequeue(&psock->ingress_skb) の行を見てみましょう。実際には、送信側の psock、つまり sk_msg 内のデータが消費されます。消費の結果はどうなりますか?実のところ、それは非常に簡単です。実行ポイント 3.2 では、sk_msg が受信側の psock->ingress_msg キューに詰め込まれます。

実際、このステップが完了すると、新しく開始されたタスクのミッションが完了したことがわかります。

3. 実行ポイント4.1と4.2: skbの処理

さて、この場所を見るのに十分な忍耐力があれば、次に何が起こるかはおそらく推測できるでしょう。実行ポイント 4.1 で呼び出される read() 操作は、実際には TCP プロトコルの recvmsg 関数に対応します。 recvmsg は単なる関数へのポインタです。実際の関数実装は tcp_bpf_recvmsg() です。私の次男は、この記事の冒頭にある「正確な動的置換」のセクションですでに基礎を築いています。

5. スペアタイヤ

図 2 をもう一度見ると、ブラザーが tcp_bpf_sendmsg() と tcp_bpf_recvmsg() からそれぞれ tcp_sendmsg() と tcp_recvmsg() に点線を引いていることがわかります。私は tcp_sendmsg() と tcp_recvmsg() をスペアパーツと呼んでいます。 tcp_bpf_sendmsg() と tcp_bpf_recvmsg() は、異常な状況を処理する際には従来の方法を使用するためです。

静的int tcp_bpf_recvmsg (構造体sock * sk構造体msghdr * msgsize_t len ​​、
int非ブロックintフラグint * addr_len )
{
// ...
psock = sk_psock_get ( sk );
if (ありそうにない( ! pso​​ck ))
tcp_recvmsg ( skmsglennonblockflagsaddr_len )を返します
}

図 7: 代替の tcp_sendmsg() と tcp_recvmsg()

<<:  Kubernetes で Minecraft を実行する

>>:  3 分で Python Web アプリケーションをデプロイします。クラウド開発について知りたいですか?

推薦する

検索後半:WeChatは大規模な検索を行うか?

先ほど終了したWeChatオープンクラスPROでは、WeChat検索には有望な未来があるという非常に...

Red Hat、ハイブリッドクラウドデータ管理プロバイダーNooBaaを買収

オープンソースソリューションのリーディングプロバイダーであるRed Hat, Inc. (NYSE:...

SEO テクニックを学んだら何ができるでしょうか?

私の一般的な答えは、電子商取引を行うこと、または SEO 関連サービスを提供することです。前者は S...

Virpus: 永久に 40% オフ / シアトル VPS / XenPV 仮想化 / 1Gbps ネットワーク

今年 10 月、virpus は最新の人気プロモーションを実施し、すべての Xen PV 仮想 VP...

サイトのコンバージョン率を改善できない主な理由について説明します。

ウェブサイトの成功は、安定した高いトラフィックと切り離せません。eコマースウェブサイトの場合、コンバ...

質疑応答プラットフォームが依然として外部リンクの宝庫であり得るかどうかの簡単な分析

かつて、質疑応答プラットフォームは、その重みと効果の両面で、すべてのオンライン マーケターのお気に入...

2023年のクラウドコンピューティングのトレンド

クラウド コンピューティングのトレンドは、人々や企業が有用だと感じるクラウド コンピューティング サ...

エッジ展開のビジネスケース

アプリケーション ワークロードをエッジ コンピューティング デプロイメントに移行するメリットを検討す...

SEO アカデミー: SEO 業界のための 10 のヒント

1.SEO は単なる補助ツールです。魔法のようなものだと考えないでください。 2. SEO 学習の初...

アリババが旅行ブランド「GoGo」を立ち上げ

アリババは昨日、北京で新たな旅行事業のサブブランド「QuAH」を立ち上げた。このブランドは2010年...

SparkプロジェクトによってもたらされたSEOの変更についてお話ししましょう

翻訳するスパーク計画はSEO業界にどのようなSEOの変化をもたらすのでしょうか?ご存知のとおり、スパ...

百度の25回目のアルゴリズムアップグレードが医療業界に与える影響とその意味

6月28日の事件以来、百度はユーザーエクスペリエンスとネットワーク環境の向上、そしてユーザーへのより...

アイ・レ・フオのショッピングガイドコミュニティへの変革は不確実な未来を秘めており、金色の飯碗を持つ乞食たちは

AiLeHuo が最初に作成されたとき、それは「Baidu の中間ページ プラットフォームの橋渡し」...

オンラインプロモーションチャネルとは何ですか?

一般的なオンラインチャネルは何ですか?オンライン チャネルにはさまざまな種類があり、詳しく言うと数十...