用 SystemTap 找出 TCP 如何決定 MSS 的值

fcamel
fcamel的程式開發心得
14 min readNov 12, 2017

先前在《TCP maximum segment size 是什麼以及是如何決定的》介紹 MSS 相關知識,最後在讀 Linux kernel原始碼的時候,有找到相關部份,但沒有往下深入。這篇透過SystemTap 嘗試追得更深入一些。

關於 SystemTap 的基本知識和安裝方法,寫在《用 SystemTap 找出送 SIGKILL 的 process》。若想進一步發揮更多功能,需要配合原始碼才行。

注意: 文內附的 kernel 原始碼連結是連到最新版 Linux kernel,方便讀者閱讀註解和函式的大概脈絡。而我是用 Ubuntu 上 4.4.0–98-generic,所以細節對到的行數會和最新版略有不同。

取得 kernel 原始碼

修改 /etc/apt/sources.list,將 deb-src ... 的註解取消掉,例如:

deb-src http://us.archive.ubuntu.com/ubuntu/ xenial main restricteddeb-src http://us.archive.ubuntu.com/ubuntu/ xenial-updates main restricted

更新 package 列表

$ apt-get update

列出可安裝的 linux-source 版本:

$ apt-cache search linux-source
linux-source - Linux kernel source with Ubuntu patches
linux-source-4.4.0 - Linux kernel source for version 4.4.0 with Ubuntu patches
linux-source-4.10.0 - Linux kernel source for version 4.10.0 with Ubuntu patches
linux-source-4.11.0 - Linux kernel source for version 4.11.0 with Ubuntu patches
linux-source-4.13.0 - Linux kernel source for version 4.13.0 with Ubuntu patches
linux-source-4.8.0 - Linux kernel source for version 4.8.0 with Ubuntu patches

找出安裝的 linux kernel 版本:

$ uname -r
4.4.0-98-generic

下載 linux kernel source codes 到目前目錄下:

$ apt-get source linux-source-4.4.0

觀察 MSS 的變化

從 net/ipv4/tcp_output.c 看到幾個相關的函式:

  • tcp_advertise_mss() 回傳 advertise MSS,作為的 MSS 的參考值之一。
  • tcp_current_mss() 回傳目前的 MSS。

先確認 MSS 的值隨著過程會不斷變化 :

$ cat tcp_current_mss.stp
global prog_name = "tcp_test";
probe begin {
printf("--- Ready ---\n");
}
probe kernel.function("tcp_current_mss").return {
if (execname() == prog_name) {
printf("> tcp_current_mss=%d\n", $return);
}
}
$ sudo stap tcp_current_mss.stp
--- Ready ---
> tcp_current_mss=21888
> tcp_current_mss=22912
...
> tcp_current_mss=30080
> tcp_current_mss=31104
> tcp_current_mss=32128

為方便過濾內容,我只顯示程式名稱 tcp_test 的內容。$return 是 “function(…).return” 才能使用的特殊變數。

執行 stap 後,另外執行測試程式 tcp_test。tcp_test 會接受來自一個 TCP 連線,然後和 client 互相傳送資料。從上可以看出 MSS 的值不斷變大。我是在本機執行 client ,所以 MSS 的值相當大。

從 net/ipv4/tcp_output.c 得知 tcp_current_mss() 會參考 tcp_sync_mss() 的值,要驗證是否 tcp_sync_mss() 確實有影響,可以加入 tcp_sync_mss() 的 probe:

...probe kernel.function("tcp_current_mss").return {
...
}
probe kernel.function("tcp_sync_mss").return {
if (execname() == prog_name) {
printf("> tcp_sync_mss=%d (pmtu=%d)\n", $return, @entry($pmtu));
}
}

pmtu 是 tcp_sync_mss() 帶的參數,是 Path MTU 的值,順便一起觀察。結果如下:

$ sudo stap tcp_current_mss.stp
--- Ready ---
> tcp_current_mss=21888
> tcp_sync_mss=22912 (pmtu=65535)
> tcp_current_mss=22912
> tcp_sync_mss=23936 (pmtu=65535)
> tcp_current_mss=23936
> tcp_sync_mss=24960 (pmtu=65535)
> tcp_current_mss=24960
> tcp_sync_mss=25984 (pmtu=65535)
> tcp_current_mss=25984
> tcp_sync_mss=27008 (pmtu=65535)
> tcp_current_mss=27008
> tcp_sync_mss=28032 (pmtu=65535)
> tcp_current_mss=28032
> tcp_sync_mss=29056 (pmtu=65535)
> tcp_current_mss=29056
> tcp_sync_mss=30080 (pmtu=65535)
> tcp_current_mss=30080
> tcp_sync_mss=31104 (pmtu=65535)
> tcp_current_mss=31104
> tcp_sync_mss=32128 (pmtu=65535)
> tcp_current_mss=32128
> tcp_sync_mss=32128 (pmtu=65535)

為什麼 tcp_sync_mss() 前面算出來的 MSS 比較小呢?從程式得知 tcp_sync_mss() 會呼叫 tcp_mtu_to_mss()、tcp_bound_to_half_wnd():

unsigned int tcp_sync_mss(struct sock *sk, u32 pmtu)
{
...
mss_now = tcp_mtu_to_mss(sk, pmtu);
mss_now = tcp_bound_to_half_wnd(tp, mss_now);
...
return mss_now;
}

tcp_bound_to_half_wnd() 會限制 MSS 上限在 window size的一半。window size 表示可以送的 bytes 數,所以這個限制很合理。

tcp_mtu_to_mss() 呼叫 __tcp_mtu_to_mss() 作了許多計算,但 tcp_sync_mss() 呼叫 tcp_mtu_to_mss() 的部份沒有可 probe 的點:

( kernel 4.4.0–98-generic 在 tcp_outut.c:1403 呼叫 tcp_mtu_to_mss() )

這裡透過 stap -L 找出可偵測的點,還有列出可觀察的變數。

所以這條線索就斷了。改試著找出 __tcp_mtu_to_mss() 可probe 的點:

有 probe 的點,但沒有可觀察的變數,所以也沒用。如此一來,想弄懂細節得仔細研究程式碼,這裡就先打住不追了。

觀察 TCP handshake 設定 MSS 的值

《TCP maximum segment size 是什麼以及是如何決定的》提到 TCP 會在 handshake 的時候在 SYN 封包的 options 裡設定 MSS,這樣對方就不會用超過這大小的 MSS。一樣來用 SystemTap 看看能否深入了解更多細節。

tcp_connect() 開啟連線送出 SYN 封包。其中關鍵的幾行:

3280   /* Send off SYN; include data in Fast Open. */
3281 err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
3282 tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);

一般情況應該不會用 fast open,加上 tcp_transmit_skb() 的註解看來滿符合目標:

/* This routine actually transmits TCP packets queued in by
* tcp_do_sendmsg(). This is used by both the initial
* transmission and possible later retransmissions.
* All SKB's seen here are completely headerless. It is our
* job to build the TCP header, and pass the packet down to
* IP so it can do the same plus pass the packet off to the
* device.
*
* We are working here with either a clone of the original
* SKB, or a fresh unique copy made by the retransmit engine.
*/
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
gfp_t gfp_mask)
{
...
}

所以就繼續看 tcp_transmit_skb(),發現關鍵字 TCPHDR_SYN,以此為線索找相關程式 :

930   inet = inet_sk(sk);
931 tp = tcp_sk(sk);
932 tcb = TCP_SKB_CB(skb);
933 memset(&opts, 0, sizeof(opts));
934
935 if (unlikely(tcb->tcp_flags & TCPHDR_SYN))
936 tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
937 else
938 tcp_options_size = tcp_established_options(sk, skb, &opts,
939 &md5);

再來看 tcp_syn_options() 裡和 MSS 相關的程式:

543 /* Compute TCP options for SYN packets. This is not the final
544 * network wire format yet.
545 */
546 static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,
547 struct tcp_out_options *opts,
548 struct tcp_md5sig_key **md5)
549 {
...
573 opts->mss = tcp_advertise_mss(sk);

不幸的是,tcp_advertise_mss() 已被 inline,也沒有可以 probe 的點:

tcp_syn_options() 也無法觀察 opts。不過 tcp_transmit_skb() 有很多可以 probe 的點,和程式碼對應來看:

993   tcp_options_write((__be32 *)(th + 1), tp, &opts);

993 行要輸出 TCP options 的內容,所以這時 opts->mss 應該就是最終結果,觀察程式如下:

$ cat tcp_syn_mss.stp
global prog_name = "tcp_test";
probe begin {
printf("--- Ready ---\n");
}
probe kernel.function("tcp_connect").call {
if (execname() == prog_name) {
printf("{ tcp_connect\n");
}
}
probe kernel.function("tcp_connect").return {
if (execname() == prog_name) {
printf("} tcp_connect\n");
}
}
probe kernel.statement("tcp_transmit_skb@net/ipv4/tcp_output.c:993") {
// Observable variables according to "stap -L":
// $sk:struct sock*
// $skb:struct sk_buff*
// $opts:struct tcp_out_options
// $tcp_header_size:unsigned int
// $md5:struct tcp_md5sig_key*
if (execname() == prog_name) {
printf("opts->mss=%d\n", $opts->mss);
}
}

執行結果:

$ sudo stap tcp_syn_mss.stp
--- Ready ---
{ tcp_connect
opts->mss=65495
} tcp_connect
opts->mss=0
opts->mss=0
opts->mss=0
...

由此確認在送 SYN 封包時 MSS 是設成下一站 MTU 減去 header size ( 65535 — 40 = 65495),其它封包不會帶有這個 option。

這如此一來,有興趣了解更多細節,可以看 tcp_advertise_mss() 和 tcp_output.c:993 之前的部份程式碼,縮小了要閱讀的範圍。

總結

  • my_prog_name == execname()pid() == target() ( 搭配 stap -x PID ) 過濾顯示資訊。
  • stap -L 找出可偵測的點和可觀察的變數,包含 stap -L ‘kernel.function(“…”).call’stap -L ‘kernel.function(“…”).return’stap -L ‘kernel.statement(“FUNCTION@PATH:*”)' 等。
  • 找不到可觀察的點,可以對照程式碼找參數傳遞的流程,找出後面可以觀察的點,再來觀察。

還有許多實用的技巧,比方說用 SystemTap 追踪函式呼叫流程,對 user space 程式也管用

參考資料

這幾篇有相當多有用的的資訊,推薦一讀:

還有 SystemTap 帶的 tapset 也有許多例子可參考。從原始碼編譯安裝的話, tapset 放在 /usr/local/share/systemtap/tapset/。

相關文章

--

--