1. 前言
本文分享了Linux內(nèi)核網(wǎng)絡(luò)數(shù)據(jù)包發(fā)送在UDP協(xié)議層的處理,主要分析了udp_sendmsg和udp_send_skb函數(shù),并分享了UDP層的數(shù)據(jù)統(tǒng)計(jì)和監(jiān)控以及socket發(fā)送隊(duì)列大小的調(diào)優(yōu)。
2. udp_sendmsg
這個(gè)函數(shù)定義在 net/ipv4/udp.c,函數(shù)很長(zhǎng),分段來看。
2.1 UDP corking
在變量聲明和基本錯(cuò)誤檢查之后,udp_sendmsg 所做的第一件事就是檢查 socket 是否“ 塞住”了(corked)。UDP corking 是一項(xiàng)優(yōu)化技術(shù),允許內(nèi)核將多次數(shù)據(jù)累積成單個(gè)數(shù)據(jù)報(bào)發(fā)送。在用戶程序中有兩種方法可以啟用此選項(xiàng):
使用 setsockopt 系統(tǒng)調(diào)用設(shè)置 socket 的 UDP_CORK 選項(xiàng)
程序調(diào)用 send,sendto 或 sendmsg 時(shí),帶 MSG_MORE 參數(shù)
udp_sendmsg 代碼檢查 up-》pending 以確定 socket 當(dāng)前是否已被塞?。╟orked),如果是, 則直接跳到 do_append_data 進(jìn)行數(shù)據(jù)追加(append)。
int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t len)
{
/* variables and error checking 。。。 */
fl4 = &inet-》cork.fl.u.ip4;
if (up-》pending) {
/*
* There are pending frames.
* The socket lock must be held while it‘s corked.
*/
lock_sock(sk);
if (likely(up-》pending)) {
if (unlikely(up-》pending != AF_INET)) {
release_sock(sk);
return -EINVAL;
}
goto do_append_data;
}
release_sock(sk);
}
2.2 獲取目的 IP 地址和端口
接下來獲取目的 IP 地址和端口,有兩個(gè)可能的來源:
如果之前 socket 已經(jīng)建立連接,那 socket 本身就存儲(chǔ)了目標(biāo)地址
地址通過輔助結(jié)構(gòu)(struct msghdr)傳入,正如我們?cè)?sendto 的內(nèi)核代碼中看到的那樣
具體邏輯:
/*
* Get and verify the address.
*/
if (msg-》msg_name) {
struct sockaddr_in *usin = (struct sockaddr_in *)msg-》msg_name;
if (msg-》msg_namelen 《 sizeof(*usin))
return -EINVAL;
if (usin-》sin_family != AF_INET) {
if (usin-》sin_family != AF_UNSPEC)
return -EAFNOSUPPORT;
}
daddr = usin-》sin_addr.s_addr;
dport = usin-》sin_port;
if (dport == 0)
return -EINVAL;
} else {
if (sk-》sk_state != TCP_ESTABLISHED)
return -EDESTADDRREQ;
daddr = inet-》inet_daddr;
dport = inet-》inet_dport;
/* Open fast path for connected socket.
Route will not be used, if at least one option is set.
*/
connected = 1;
}
UDP 代碼中出現(xiàn)了 TCP_ESTABLISHED!UDP socket 的狀態(tài)使用了 TCP 狀態(tài)來描述。上面的代碼顯示了內(nèi)核如何解析該變量以便設(shè)置 daddr 和 dport。
如果沒有 struct msghdr 變量,內(nèi)核函數(shù)到達(dá) udp_sendmsg 函數(shù)時(shí),會(huì)從 socket 本身檢索目的地址和端口,并將 socket 標(biāo)記為“已連接”。
2.3 Socket 發(fā)送:bookkeeping 和打時(shí)間戳
接下來,獲取存儲(chǔ)在 socket 上的源地址、設(shè)備索引(device index)和時(shí)間戳選項(xiàng)(例如SOCK_TIMESTAMPING_TX_HARDWARE, SOCK_TIMESTAMPING_TX_SOFTWARE, SOCK_WIFI_STATUS):
ipc.addr = inet-》inet_saddr;
ipc.oif = sk-》sk_bound_dev_if;
sock_tx_timestamp(sk, &ipc.tx_flags);
2.4 輔助消息(Ancillary messages)
除了發(fā)送或接收數(shù)據(jù)包之外,sendmsg 和 recvmsg 系統(tǒng)調(diào)用還允許用戶設(shè)置或請(qǐng)求輔助數(shù)據(jù)。用戶程序可以通過將請(qǐng)求信息組織成 struct msghdr 類型變量來利用此輔助數(shù)據(jù)。一些輔助數(shù)據(jù)類型記錄在IP man page中 。
輔助數(shù)據(jù)的一個(gè)常見例子是 IP_PKTINFO。對(duì)于 sendmsg,IP_PKTINFO 允許程序在發(fā)送數(shù)據(jù)時(shí)設(shè)置一個(gè) in_pktinfo 變量。程序可以通過填寫 struct in_pktinfo 變量中的字段來指定要在 packet 上使用的源地址。如果程序是監(jiān)聽多個(gè) IP 地址的服務(wù)端程序,那這是一個(gè)很有用的選項(xiàng)。在這種情況下,服務(wù)端可能想使用客戶端連接服務(wù)端的那個(gè) IP 地址來回復(fù)客戶端,IP_PKTINFO 非常適合這種場(chǎng)景。
setsockopt 可以在socket 級(jí)別設(shè)置發(fā)送包的 IP_TTL和 IP_TOS。而輔助消息允許在每個(gè)數(shù)據(jù)包級(jí)別設(shè)置 TTL 和 TOS 值。Linux 內(nèi)核會(huì)使用一個(gè)數(shù)組將 TOS 轉(zhuǎn)換為優(yōu)先級(jí),后者會(huì)影響數(shù)據(jù)包如何以及何時(shí)從 qdisc 中發(fā)送出去。
可以看到內(nèi)核如何在 UDP socket 上處理 sendmsg 的輔助消息:
if (msg-》msg_controllen) {
err = ip_cmsg_send(sock_net(sk), msg, &ipc,
sk-》sk_family == AF_INET6);
if (err)
return err;
if (ipc.opt)
free = 1;
connected = 0;
}
解析輔助消息的工作是由 ip_cmsg_send 完成的,定義在 net/ipv4/ip_sockglue.c 。傳遞一個(gè)未初始化的輔助數(shù)據(jù),將會(huì)把這個(gè) socket 標(biāo)記為“未建立連接的”。
2.5 設(shè)置自定義 IP 選項(xiàng)
接下來,sendmsg 將檢查用戶是否通過輔助消息設(shè)置了的任何自定義 IP 選項(xiàng)。如果設(shè)置了 ,將使用這些自定義值;如果沒有,那就使用 socket 中(已經(jīng)在用)的參數(shù):
if (!ipc.opt) {
struct ip_options_rcu *inet_opt;
rcu_read_lock();
inet_opt = rcu_dereference(inet-》inet_opt);
if (inet_opt) {
memcpy(&opt_copy, inet_opt,
sizeof(*inet_opt) + inet_opt-》opt.optlen);
ipc.opt = &opt_copy.opt;
}
rcu_read_unlock();
}
接下來,該函數(shù)檢查是否設(shè)置了源記錄路由(source record route, SRR)IP 選項(xiàng)。SRR 有兩種類型:寬松源記錄路由和嚴(yán)格源記錄路由。如果設(shè)置了此選項(xiàng),則會(huì)記錄第一跳地址并將其保存到 faddr,并將 socket 標(biāo)記為“未連接”。這將在后面用到:
ipc.addr = faddr = daddr;
if (ipc.opt && ipc.opt-》opt.srr) {
if (!daddr)
return -EINVAL;
faddr = ipc.opt-》opt.faddr;
connected = 0;
}
處理完 SRR 選項(xiàng)后,將處理 TOS 選項(xiàng),這可以從輔助消息中獲取,或者從 socket 當(dāng)前值中獲取。然后檢查:
是否(使用 setsockopt)在 socket 上設(shè)置了 SO_DONTROUTE,或
是否(調(diào)用 sendto 或 sendmsg 時(shí))指定了 MSG_DONTROUTE 標(biāo)志,或
是否已設(shè)置了 is_strictroute,表示需要嚴(yán)格的 SRR 任何一個(gè)為真,tos 字段的 RTO_ONLINK 位將置 1,并且 socket 被視為“未連接”:
tos = get_rttos(&ipc, inet);
if (sock_flag(sk, SOCK_LOCALROUTE) ||
(msg-》msg_flags & MSG_DONTROUTE) ||
(ipc.opt && ipc.opt-》opt.is_strictroute)) {
tos |= RTO_ONLINK;
connected = 0;
}
2.6 多播或單播(Multicast or unicast)
接下來代碼開始處理 multicast。這有點(diǎn)復(fù)雜,因?yàn)橛脩艨梢酝ㄟ^ IP_PKTINFO 輔助消息 來指定發(fā)送包的源地址或設(shè)備號(hào),如前所述。
如果目標(biāo)地址是多播地址:
將多播設(shè)備(device)的索引(index)設(shè)置為發(fā)送(寫)這個(gè) packet 的設(shè)備索引,并且
packet 的源地址將設(shè)置為 multicast 源地址
如果目標(biāo)地址不是一個(gè)組播地址,則發(fā)送 packet 的設(shè)備制定為 inet-》uc_index(單播), 除非用戶使用 IP_PKTINFO 輔助消息覆蓋了它。
if (ipv4_is_multicast(daddr)) {
if (!ipc.oif)
ipc.oif = inet-》mc_index;
if (!saddr)
saddr = inet-》mc_addr;
connected = 0;
} else if (!ipc.oif)
ipc.oif = inet-》uc_index;
2.7 路由
現(xiàn)在開始路由,UDP 層中處理路由的代碼以快速路徑(fast path)開始。如果 socket 已連接,則直接嘗試獲取路由:
if (connected)
rt = (struct rtable *)sk_dst_check(sk, 0);
如果 socket 未連接,或者雖然已連接,但路由輔助函數(shù) sk_dst_check 認(rèn)定路由已過期,則代碼將進(jìn)入慢速路徑(slow path)以生成一條路由記錄。首先調(diào)用 flowi4_init_output 構(gòu)造一個(gè)描述此 UDP 流的變量:
if (rt == NULL) {
struct net *net = sock_net(sk);
fl4 = &fl4_stack;
flowi4_init_output(fl4, ipc.oif, sk-》sk_mark, tos,
RT_SCOPE_UNIVERSE, sk-》sk_protocol,
inet_sk_flowi_flags(sk)|FLOWI_FLAG_CAN_SLEEP,
faddr, saddr, dport, inet-》inet_sport);
然后,socket 及其 flow 實(shí)例會(huì)傳遞給安全子系統(tǒng),這樣 SELinux 或 SMACK 這樣的系統(tǒng)就可以在 flow 實(shí)例上設(shè)置安全 ID。接下來,ip_route_output_flow 將調(diào)用 IP 路由代碼,創(chuàng)建一個(gè)路由實(shí)例:
security_sk_classify_flow(sk, flowi4_to_flowi(fl4));
rt = ip_route_output_flow(net, fl4, sk);
如果創(chuàng)建路由實(shí)例失敗,并且返回碼是 ENETUNREACH, 則 OUTNOROUTES 計(jì)數(shù)器將會(huì)加 1。
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
rt = NULL;
if (err == -ENETUNREACH)
IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
goto out;
}
這些統(tǒng)計(jì)計(jì)數(shù)器所在的源文件、其他可用的計(jì)數(shù)器及其含義,將在下面的 UDP 監(jiān)控部分分享。
接下來,如果是廣播路由,但 socket 的 SOCK_BROADCAST 選項(xiàng)未設(shè)置,則處理過程終止。如果 socket 被視為“已連接”,則路由實(shí)例將緩存到 socket 上:
err = -EACCES;
if ((rt-》rt_flags & RTCF_BROADCAST) &&
!sock_flag(sk, SOCK_BROADCAST))
goto out;
if (connected)
sk_dst_set(sk, dst_clone(&rt-》dst));
2.8 MSG_CONFIRM: 阻止 ARP 緩存過期
如果調(diào)用 send, sendto 或 sendmsg 的時(shí)候指定了 MSG_CONFIRM 參數(shù),UDP 協(xié)議層將會(huì)如下處理:
if (msg-》msg_flags&MSG_CONFIRM)
goto do_confirm;
back_from_confirm:
該標(biāo)志提示系統(tǒng)去確認(rèn)一下 ARP 緩存條目是否仍然有效,防止其被垃圾回收。 do_confirm 標(biāo)簽位于此函數(shù)末尾處:
do_confirm:
dst_confirm(&rt-》dst);
if (!(msg-》msg_flags&MSG_PROBE) || len)
goto back_from_confirm;
err = 0;
goto out;
dst_confirm 函數(shù)只是在相應(yīng)的緩存條目上設(shè)置一個(gè)標(biāo)記位,稍后當(dāng)查詢鄰居緩存并找到 條目時(shí)將檢查該標(biāo)志,我們后面一些會(huì)看到。此功能通常用于 UDP 網(wǎng)絡(luò)應(yīng)用程序,以減少不必要的 ARP 流量。此代碼確認(rèn)緩存條目然后跳回 back_from_confirm 標(biāo)簽。一旦 do_confirm 代碼跳回到 back_from_confirm(或者之前就沒有執(zhí)行到 do_confirm ),代碼接下來將處理 UDP cork 和 uncorked 情況。
2.9 uncorked UDP sockets 快速路徑:準(zhǔn)備待發(fā)送數(shù)據(jù)
如果不需要 corking,數(shù)據(jù)就可以封裝到一個(gè) struct sk_buff 實(shí)例中并傳遞給 udp_send_skb,離 IP 協(xié)議層更進(jìn)了一步。這是通過調(diào)用 ip_make_skb 來完成的。
先前通過調(diào)用 ip_route_output_flow 生成的路由條目也會(huì)一起傳進(jìn)來, 它將保存到 skb 里。
/* Lockless fast path for the non-corking case. */
if (!corkreq) {
skb = ip_make_skb(sk, fl4, getfrag, msg-》msg_iov, ulen,
sizeof(struct udphdr), &ipc, &rt,
msg-》msg_flags);
err = PTR_ERR(skb);
if (!IS_ERR_OR_NULL(skb))
err = udp_send_skb(skb, fl4);
goto out;
}
ip_make_skb 函數(shù)將創(chuàng)建一個(gè) skb,其中需要考慮到很多的事情,例如:
MTU
UDP corking(如果啟用)
UDP Fragmentation Offloading(UFO)
Fragmentation(分片):如果硬件不支持 UFO,但是要傳輸?shù)臄?shù)據(jù)大于 MTU,需要軟件做分片
大多數(shù)網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)程序不支持 UFO,因?yàn)榫W(wǎng)絡(luò)硬件本身不支持此功能。我們來看下這段代碼,先看 corking 禁用的情況。
2.9.1 ip_make_skb
定義在net/ipv4/ip_output.c,這個(gè)函數(shù)有點(diǎn)復(fù)雜。
構(gòu)建 skb 的時(shí)候,ip_make_skb 依賴的底層代碼需要使用一個(gè) corking 變量和一個(gè) queue 變量 ,skb 將通過 queue 變量傳入。如果 socket 未被 cork,則會(huì)傳入一個(gè)假的 corking 變量和一個(gè)空隊(duì)列。
現(xiàn)在來看看假 corking 變量和空隊(duì)列是如何初始化的:
struct sk_buff *ip_make_skb(struct sock *sk, /* more args */)
{
struct inet_cork cork;
struct sk_buff_head queue;
int err;
if (flags & MSG_PROBE)
return NULL;
__skb_queue_head_init(&queue);
cork.flags = 0;
cork.addr = 0;
cork.opt = NULL;
err = ip_setup_cork(sk, &cork, /* more args */);
if (err)
return ERR_PTR(err);
如上所示,cork 和 queue 都是在棧上分配的,ip_make_skb 根本不需要它。 ip_setup_cork 初始化 cork 變量。接下來,調(diào)用__ip_append_data 并傳入 cork 和 queue 變 量:
err = __ip_append_data(sk, fl4, &queue, &cork,
¤t-》task_frag, getfrag,
from, length, transhdrlen, flags);
我們將在后面看到這個(gè)函數(shù)是如何工作的,因?yàn)椴还?socket 是否被 cork,最后都會(huì)執(zhí)行它。
現(xiàn)在,我們只需要知道__ip_append_data 將創(chuàng)建一個(gè) skb,向其追加數(shù)據(jù),并將該 skb 添加 到傳入的 queue 變量中。如果追加數(shù)據(jù)失敗,則調(diào)用__ip_flush_pending_frame 丟棄數(shù)據(jù) 并向上返回錯(cuò)誤(指針類型):
if (err) {
__ip_flush_pending_frames(sk, &queue, &cork);
return ERR_PTR(err);
}
最后,如果沒有發(fā)生錯(cuò)誤,__ip_make_skb 將 skb 出隊(duì),添加 IP 選項(xiàng),并返回一個(gè)準(zhǔn)備好傳遞給更底層發(fā)送的 skb:
return __ip_make_skb(sk, fl4, &queue, &cork);
2.9.2 發(fā)送數(shù)據(jù)
如果沒有錯(cuò)誤,skb 就會(huì)交給 udp_send_skb,后者會(huì)繼續(xù)將其傳給下一層協(xié)議,IP 協(xié)議:
err = PTR_ERR(skb);
if (!IS_ERR_OR_NULL(skb))
err = udp_send_skb(skb, fl4);
goto out;
如果有錯(cuò)誤,錯(cuò)誤計(jì)數(shù)就會(huì)有相應(yīng)增加。后面的“錯(cuò)誤計(jì)數(shù)”部分會(huì)詳細(xì)介紹。
2.10 沒有被 cork 的數(shù)據(jù)時(shí)的慢路徑
如果使用了 UDP corking,但之前沒有數(shù)據(jù)被 cork,則慢路徑開始:
對(duì) socket 加鎖
檢查應(yīng)用程序是否有 bug:已經(jīng)被 cork 的 socket 是否再次被 cork
設(shè)置該 UDP flow 的一些參數(shù),為 corking 做準(zhǔn)備
將要發(fā)送的數(shù)據(jù)追加到現(xiàn)有數(shù)據(jù)
udp_sendmsg 代碼繼續(xù)向下看,就是這一邏輯:
lock_sock(sk);
if (unlikely(up-》pending)) {
/* The socket is already corked while preparing it. */
/* 。。。 which is an evident application bug. --ANK */
release_sock(sk);
LIMIT_NETDEBUG(KERN_DEBUG pr_fmt(“cork app bug 2
”));
err = -EINVAL;
goto out;
}
/*
* Now cork the socket to pend data.
*/
fl4 = &inet-》cork.fl.u.ip4;
fl4-》daddr = daddr;
fl4-》saddr = saddr;
fl4-》fl4_dport = dport;
fl4-》fl4_sport = inet-》inet_sport;
up-》pending = AF_INET;
do_append_data:
up-》len += ulen;
err = ip_append_data(sk, fl4, getfrag, msg-》msg_iov, ulen,
sizeof(struct udphdr), &ipc, &rt,
corkreq ? msg-》msg_flags|MSG_MORE : msg-》msg_flags);
2.10.1 ip_append_data
這個(gè)函數(shù)簡(jiǎn)單封裝了__ip_append_data,在調(diào)用后者之前,做了兩件重要的事情:
檢查是否從用戶傳入了 MSG_PROBE 標(biāo)志。該標(biāo)志表示用戶不想真正發(fā)送數(shù)據(jù),只是做路徑探測(cè)(例如,確定PMTU)
檢查 socket 的發(fā)送隊(duì)列是否為空。如果為空,意味著沒有 cork 數(shù)據(jù)等待處理,因此調(diào)用 ip_setup_cork 來設(shè)置 corking
一旦處理了上述條件,就調(diào)用__ip_append_data 函數(shù),該函數(shù)包含用于將數(shù)據(jù)處理成數(shù)據(jù)包的大量邏輯。
2.10.2 __ip_append_data
如果 socket 是 corked,則從 ip_append_data 調(diào)用此函數(shù);如果 socket 未被 cork,則從 ip_make_skb 調(diào)用此函數(shù)。在任何一種情況下,函數(shù)都將分配一個(gè)新緩沖區(qū)來存儲(chǔ)傳入的數(shù)據(jù),或者將數(shù)據(jù)附加到現(xiàn)有數(shù)據(jù)中。這種工作的方式圍繞 socket 的發(fā)送隊(duì)列。等待發(fā)送的現(xiàn)有數(shù)據(jù)(例如,如果 socket 被 cork) 將在隊(duì)列中有一個(gè)對(duì)應(yīng)條目,可以被追加數(shù)據(jù)。
這個(gè)函數(shù)很復(fù)雜,它執(zhí)行很多計(jì)算以確定如何構(gòu)造傳遞給下面的網(wǎng)絡(luò)層的 skb。
該函數(shù)的重點(diǎn)包括:
如果硬件支持,則處理 UDP Fragmentation Offload(UFO)。絕大多數(shù)網(wǎng)絡(luò)硬件不支持 UFO。如果你的網(wǎng)卡驅(qū)動(dòng)程序支持它,它將設(shè)置 NETIF_F_UFO 標(biāo)記位
處理支持分散/收集( scatter/gather)IO 的網(wǎng)卡。許多 卡都支持此功能,并使用 NETIF_F_SG 標(biāo)志進(jìn)行通告。支持該特性的網(wǎng)卡可以處理數(shù)據(jù) 被分散到多個(gè) buffer 的數(shù)據(jù)包;內(nèi)核不需要花時(shí)間將多個(gè)緩沖區(qū)合并成一個(gè)緩沖區(qū)中。避 免這種額外的復(fù)制會(huì)提升性能,大多數(shù)網(wǎng)卡都支持此功能
通過調(diào)用 sock_wmalloc 跟蹤發(fā)送隊(duì)列的大小。當(dāng)分配新的 skb 時(shí),skb 的大小由創(chuàng)建它 的 socket 計(jì)費(fèi)(charge),并計(jì)入 socket 發(fā)送隊(duì)列的已分配字節(jié)數(shù)。如果發(fā)送隊(duì)列已經(jīng) 沒有足夠的空間(超過計(jì)費(fèi)限制),則 skb 并分配失敗并返回錯(cuò)誤。我們將在下面的調(diào)優(yōu)部分中看到如何設(shè)置 socket 發(fā)送隊(duì)列大?。╰xqueuelen)
更新錯(cuò)誤統(tǒng)計(jì)信息。此函數(shù)中的任何錯(cuò)誤都會(huì)增加“discard”計(jì)數(shù)。我們將在下面的監(jiān)控部分中看到如何讀取此值
函數(shù)執(zhí)行成功后返回 0,以及一個(gè)適用于網(wǎng)絡(luò)設(shè)備傳輸?shù)?skb。
在 unorked 情況下,持有 skb 的 queue 被作為參數(shù)傳遞給上面描述的__ip_make_skb,在那里 它被出隊(duì)并通過 udp_send_skb 發(fā)送到更底層。
在 cork 的情況下,__ip_append_data 的返回值向上傳遞。數(shù)據(jù)位于發(fā)送隊(duì)列中,直到 udp_sendmsg 確定是時(shí)候調(diào)用 udp_push_pending_frames 來完成 skb,后者會(huì)進(jìn)一步調(diào)用 udp_send_skb。
2.10.3 Flushing corked sockets
現(xiàn)在,udp_sendmsg 會(huì)繼續(xù),檢查__ip_append_skb 的返回值(錯(cuò)誤碼):
if (err)
udp_flush_pending_frames(sk);
else if (!corkreq)
err = udp_push_pending_frames(sk);
else if (unlikely(skb_queue_empty(&sk-》sk_write_queue)))
up-》pending = 0;
release_sock(sk);
我們來看看每個(gè)情況:
如果出現(xiàn)錯(cuò)誤(錯(cuò)誤為非零),則調(diào)用 udp_flush_pending_frames,這將取消 cork 并從 socket 的發(fā)送隊(duì)列中刪除所有數(shù)據(jù)
如果在未指定 MSG_MORE 的情況下發(fā)送此數(shù)據(jù),則調(diào)用 udp_push_pending_frames,它將數(shù)據(jù)傳遞到更下面的網(wǎng)絡(luò)層
如果發(fā)送隊(duì)列為空,請(qǐng)將 socket 標(biāo)記為不再 cork
如果追加操作完成并且有更多數(shù)據(jù)要進(jìn)入 cork,則代碼將做一些清理工作,并返回追加數(shù)據(jù)的長(zhǎng)度:
ip_rt_put(rt);
if (free)
kfree(ipc.opt);
if (!err)
return len;
這就是內(nèi)核如何處理 corked UDP sockets 的。
2.11 Error accounting
如果:
non-corking 快速路徑創(chuàng)建 skb 失敗,或 udp_send_skb 返回錯(cuò)誤,或
ip_append_data 無法將數(shù)據(jù)附加到 corked UDP socket,或
當(dāng) udp_push_pending_frames 調(diào)用 udp_send_skb 發(fā)送 corked skb 時(shí)后者返回錯(cuò)誤
僅當(dāng)返回的錯(cuò)誤是 ENOBUFS(內(nèi)核無可用內(nèi)存)或 socket 已設(shè)置 SOCK_NOSPACE(發(fā)送隊(duì)列已滿)時(shí),SNDBUFERRORS 統(tǒng)計(jì)信息才會(huì)增加:
/*
* ENOBUFS = no kernel mem, SOCK_NOSPACE = no sndbuf space. Reporting
* ENOBUFS might not be good (it’s not tunable per se), but otherwise
* we don‘t have a good statistic (IpOutDiscards but it can be too many
* things)。 We could add another new stat but at least for now that
* seems like overkill.
*/
if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk-》sk_socket-》flags)) {
UDP_INC_STATS_USER(sock_net(sk),
UDP_MIB_SNDBUFERRORS, is_udplite);
}
return err;
我們接下來會(huì)在后面的數(shù)據(jù)監(jiān)控里看到如何讀取這些計(jì)數(shù)。
3. udp_send_skb
udp_sendmsg 通過調(diào)用 udp_send_skb 函數(shù)將 skb 送到下一網(wǎng)絡(luò)層,在本文中是 IP 協(xié)議層。這個(gè)函數(shù)做了一些重要的事情:
向 skb 添加 UDP 頭
處理校驗(yàn)和:軟件校驗(yàn)和,硬件校驗(yàn)和或無校驗(yàn)和(如果禁用)
調(diào)用 ip_send_skb 將 skb 發(fā)送到 IP 協(xié)議層
更新發(fā)送成功或失敗的統(tǒng)計(jì)計(jì)數(shù)器
首先,創(chuàng)建 UDP 頭:
static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4)
{
/* useful variables 。。。 */
/*
* Create a UDP header
*/
uh = udp_hdr(skb);
uh-》source = inet-》inet_sport;
uh-》dest = fl4-》fl4_dport;
uh-》len = htons(len);
uh-》check = 0;
接下來,處理校驗(yàn)和。有幾種情況:
首先處理 UDP-Lite 校驗(yàn)和
接下來,如果 socket 校驗(yàn)和選項(xiàng)被關(guān)閉(setsockopt 帶 SO_NO_CHECK 參數(shù)),它將被標(biāo)記為校 驗(yàn)和關(guān)閉
接下來,如果硬件支持 UDP 校驗(yàn)和,則將調(diào)用 udp4_hwcsum 來設(shè)置它。請(qǐng)注意,如果數(shù) 據(jù)包是分段的,內(nèi)核將在軟件中生成校驗(yàn)和,可以在 udp4_hwcsum 的源代碼中看到這一點(diǎn)
最后,通過調(diào)用 udp_csum 生成軟件校驗(yàn)和
if (is_udplite) /* UDP-Lite */
csum = udplite_csum(skb);
else if (sk-》sk_no_check == UDP_CSUM_NOXMIT) { /* UDP csum disabled */
skb-》ip_summed = CHECKSUM_NONE;
goto send;
} else if (skb-》ip_summed == CHECKSUM_PARTIAL) { /* UDP hardware csum */
udp4_hwcsum(skb, fl4-》saddr, fl4-》daddr);
goto send;
} else
csum = udp_csum(skb);
接下來,添加了偽頭 :
uh-》check = csum_tcpudp_magic(fl4-》saddr, fl4-》daddr, len,
sk-》sk_protocol, csum);
if (uh-》check == 0)
uh-》check = CSUM_MANGLED_0;
如果校驗(yàn)和為 0,則根據(jù) RFC 768,校驗(yàn)為全 1( transmitted as all ones (the equivalent in one’s complement arithmetic))。最后,將 skb 傳遞給 IP 協(xié)議層并增加統(tǒng)計(jì)計(jì)數(shù):
send:
err = ip_send_skb(sock_net(sk), skb);
if (err) {
if (err == -ENOBUFS && !inet-》recverr) {
UDP_INC_STATS_USER(sock_net(sk),
UDP_MIB_SNDBUFERRORS, is_udplite);
err = 0;
}
} else
UDP_INC_STATS_USER(sock_net(sk),
UDP_MIB_OUTDATAGRAMS, is_udplite);
return err;
如果 ip_send_skb 成功,將更新 OUTDATAGRAMS 統(tǒng)計(jì)。如果 IP 協(xié)議層報(bào)告錯(cuò)誤,并且錯(cuò)誤 是 ENOBUFS(內(nèi)核缺少內(nèi)存)而且錯(cuò)誤 queue(inet-》recverr)沒有啟用,則更新 SNDBUFERRORS。
接下來看看如何在 Linux 內(nèi)核中監(jiān)視和調(diào)優(yōu) UDP 協(xié)議層。
4. 監(jiān)控:UDP 層統(tǒng)計(jì)
兩個(gè)非常有用的獲取 UDP 協(xié)議統(tǒng)計(jì)文件:
/proc/net/snmp
/proc/net/udp
4.1 /proc/net/snmp
監(jiān)控 UDP 協(xié)議層統(tǒng)計(jì):
cat /proc/net/snmp | grep Udp:
要準(zhǔn)確地理解這些計(jì)數(shù),需要仔細(xì)地閱讀內(nèi)核代碼。一些類型的錯(cuò)誤計(jì)數(shù)并不是只出現(xiàn)在一種計(jì)數(shù)中,而可能是出現(xiàn)在多個(gè)計(jì)數(shù)中。
InDatagrams: Incremented when recvmsg was used by a userland program to read datagram. Also incremented when a UDP packet is encapsulated and sent back for processing.
NoPorts: Incremented when UDP packets arrive destined for a port where no program is listening.
InErrors: Incremented in several cases: no memory in the receive queue, when a bad checksum is seen, and if sk_add_backlog fails to add the datagram.
OutDatagrams: Incremented when a UDP packet is handed down without error to the IP protocol layer to be sent.
RcvbufErrors: Incremented when sock_queue_rcv_skb reports that no memory is available; this happens if sk-》sk_rmem_alloc is greater than or equal to sk-》sk_rcvbuf.
SndbufErrors: Incremented if the IP protocol layer reported an error when trying to send the packet and no error queue has been setup. Also incremented if no send queue space or kernel memory are available.
InCsumErrors: Incremented when a UDP checksum failure is detected. Note that in all cases I could find, InCsumErrors is incremented at the same time as InErrors. Thus, InErrors - InCsumErros should yield the count of memory related errors on the receive side.
UDP 協(xié)議層發(fā)現(xiàn)的某些錯(cuò)誤會(huì)出現(xiàn)在其他協(xié)議層的統(tǒng)計(jì)信息中。一個(gè)例子:路由錯(cuò)誤 。 udp_sendmsg 發(fā)現(xiàn)的路由錯(cuò)誤將導(dǎo)致 IP 協(xié)議層的 OutNoRoutes 統(tǒng)計(jì)增加。
4.2 /proc/net/udp
監(jiān)控 UDP socket 統(tǒng)計(jì):
cat /proc/net/udp
每一列的意思:
sl: Kernel hash slot for the socket
local_address: Hexadecimal local address of the socket and port number, separated by :。
rem_address: Hexadecimal remote address of the socket and port number, separated by :。
st: The state of the socket. Oddly enough, the UDP protocol layer seems to use some TCP socket states. In the example above, 7 is TCP_CLOSE.
tx_queue: The amount of memory allocated in the kernel for outgoing UDP datagrams.
rx_queue: The amount of memory allocated in the kernel for incoming UDP datagrams.
tr, tm-》when, retrnsmt: These fields are unused by the UDP protocol layer.
uid: The effective user id of the user who created this socket.
timeout: Unused by the UDP protocol layer.
inode: The inode number corresponding to this socket. You can use this to help you determine which user process has this socket open. Check /proc/[pid]/fd, which will contain symlinks to socket[:inode]。
ref: The current reference count for the socket.
pointer: The memory address in the kernel of the struct sock.
drops: The number of datagram drops associated with this socket. Note that this does not include any drops related to sending datagrams (on corked UDP sockets or otherwise); this is only incremented in receive paths as of the kernel version examined by this blog post.
打印這些計(jì)數(shù)的代碼在net/ipv4/udp.c。
5. 調(diào)優(yōu):socket 發(fā)送隊(duì)列內(nèi)存大小
發(fā)送隊(duì)列(也叫“寫隊(duì)列”)的最大值可以通過設(shè)置 net.core.wmem_max sysctl 進(jìn)行修改。
$ sudo sysctl -w net.core.wmem_max=8388608
sk-》sk_write_queue 用 net.core.wmem_default 初始化, 這個(gè)值也可以調(diào)整。
調(diào)整初始發(fā)送 buffer 大?。?/p>
$ sudo sysctl -w net.core.wmem_default=8388608
也可以通過從應(yīng)用程序調(diào)用 setsockopt 并傳遞 SO_SNDBUF 來設(shè)置 sk-》sk_write_queue 。通過 setsockopt 設(shè)置的最大值是 net.core.wmem_max。
不過,可以通過 setsockopt 并傳遞 SO_SNDBUFFORCE 來覆蓋 net.core.wmem_max 限制, 這需要 CAP_NET_ADMIN 權(quán)限。
每次調(diào)用__ip_append_data 分配 skb 時(shí),sk-》sk_wmem_alloc 都會(huì)遞增。正如我們所看到 的,UDP 數(shù)據(jù)報(bào)傳輸速度很快,通常不會(huì)在發(fā)送隊(duì)列中花費(fèi)太多時(shí)間。
6. 總結(jié)
本文重點(diǎn)分析了數(shù)據(jù)包在傳輸層(UDP協(xié)議)的發(fā)送過程,并進(jìn)行了監(jiān)控和調(diào)優(yōu),后面數(shù)據(jù)包將到達(dá) IP 協(xié)議層,下次再分享,感謝閱讀。
Reference:https://blog.packagecloud.io/eng/2017/02/06/monitoring-tuning-linux-networking-stack-sending-data
編輯:jq
-
LINUX內(nèi)核
+關(guān)注
關(guān)注
1文章
316瀏覽量
21644
原文標(biāo)題:Linux內(nèi)核網(wǎng)絡(luò)udp數(shù)據(jù)包發(fā)送(二)——UDP協(xié)議層分析
文章出處:【微信號(hào):gh_6fde77c41971,微信公眾號(hào):FPGA干貨】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論