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 アプリケーションをデプロイします。クラウド開発について知りたいですか?

推薦する

自分専用のウェブサイトを構築する必要はまだあるのでしょうか?

かつて流行した「個人ウェブサイト」という言葉は、時が経つにつれて徐々に忘れ去られつつあります。インタ...

dogyun: 虎の年、香港クラウド199元/年、10台のコンピュータールームのクラウドサーバー30%割引、チャージ10%増し、抽選など。

Dogyunの虎年特別新年プロモーションが始まりました:(1)香港特別価格CN2+BGP年間VPSが...

ロシアのホスティングプロバイダー:htsの紹介、信頼性の高いVPS +専用サーバー

ロシアのホスティング会社、Hosting Telesystems (HTS) をご紹介します。同社は...

ocitysolutions-4.17ドル/512MB RAM/15GB HDD/150GB フロー

Ocitysolutions は、Mineocity ゲーム コンソール プロバイダーとして 201...

最も安い VPS: 2018 年にネットユーザーによって評価された最も安い VPS のリスト (レビューと評価付き)

最も安い VPS は、誰もが常に探して購入しています。動的で最も安い VPS、米国で最も安い VPS...

企業で働く SEO 担当者は、どうすれば SEO 目標を合理的に設定できるでしょうか?

SEO を行う人の中には、個人のウェブマスターではなく、企業にサービスを提供するタイプの人もいます。...

任正非のインタビュー全文2万字

1. 記者:まず、私は生放送の記者です。あなたは軍人なので時間を厳しく管理しています。私も時間を厳し...

ネットワークマーケティングで優れたウェブサイトコンテンツを作成する方法

顧客が企業のウェブサイトを閲覧する際、商品の選択から最終的な購入に至るまでのプロセスは、ウェブサイト...

#UKcn2gia# akkocloud: トリプルネットワーク UKcn2gia ライン VPS、最低 299 元/年、500Mbps の帯域幅

Akkocloud は UK VPS サービスを提供しており、これは中国本土向けに特別に最適化された...

Linode MarketplaceとMastodonを使って独自の分散型ソーシャルプラットフォームを構築しましょう

近年、ユーザーは分散型ソフトウェア サービスのオプションを求めるようになっています。オープンソースの...

hostyun 上のすべての AS9929 VPS は as4809+as9929 に変換されました。みんなでテストして検証してみましょう。

この二日間、Hostyun が、すべてのオリジナル AS9929 ネットワーク シリーズ VPS に...

「クラウド+フォグ」コンピューティングは、データエネルギーの「覚醒」の原動力となるのでしょうか?

スマート シティやスマート ホームなどの IoT アプリケーションの普及により、あらゆるもののインテ...

Docker をベースにした継続的デリバリープラットフォームの構築実践

[[212695]]スタートアップ企業であり DevOps エンジニアである私たちは、次のような問題...

edgenat: 香港データセンターの帯域幅は値上げなしで無料アップグレード、すべての VPS が 30% 割引、香港 CN2\韓国 CN2\米国 Unicom AS4837

edgenatは最新ニュースを正式に発表しました。香港データセンターは帯域幅を全面的にアップグレード...

多くのネットユーザーが、新浪微博のアカウントがハッキングされ、100人以上の有名人や団体をフォローしていたと報告した。

今月24日以来、多くのネットユーザーが新浪微博のアカウントがハッキングされたとネット上で報告している...