當應用程序調用send()等一系列系統調用向UDP套接字寫數據時,最終會調用到UDP的udp_sendmsg(),這篇筆記就以該函數爲入口分析下UDP對發送數據包的處理過程。
1. 基本特性
在分析代碼之前,有必要對一些UDP的寫操作特性做特別的說明,否則會看的暈頭轉向。
1.1 MSG_MORE標記
UDP數據報不像TCP,它是有邊界的,即發送端的一個UDP數據報會完整的也被接收端以一個UDP數據報的方式接收。
然而,並非一次寫操作對應一個UDP數據報,應用程序可以通過指定MSG_MORE或者設置UDP_CORK選項的方式來將多次寫操作的數據合併成一個UDP數據報。在寫操作時,如果設置了MSG_MORE,表示還有更多數據要發送,應用期望內核收到設置了該標記的數據時先不要發送給IP,而是將其緩存,如果後面連續收到設定了該標記的數據,那麼將這些數據組合成到同一個UDP報文。類似的,在使能和關閉UDP_CORK選項期間發送的所有數據也要組合成一個UDP報文發送給IP。
應用程序在使用這種方式的時候必須要注意多次組合的數據最好不要超過MTU,否則IP層就不得不將這些要組合的數據分成多個IP數據包發送出去,這樣會造成性能的下降。
2. UDP發送入口udp_sendmsg()
@iocb: 爲異步IO預留擴展,當前不支持
@sk:傳輸控制塊
@msg:包含了用戶空間要發送的數據
@len:要發送的數據長度
int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t len)
{
struct inet_sock *inet = inet_sk(sk);
struct udp_sock *up = udp_sk(sk);
int ulen = len;
struct ipcm_cookie ipc;
struct rtable *rt = NULL;
int free = 0;
int connected = 0;
__be32 daddr, faddr, saddr;
__be16 dport;
u8 tos;
int err, is_udplite = IS_UDPLITE(sk);
//corkreq表示是否需要等待其它數據,將這些報文組合成一個UDP報文
int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;
int (*getfrag)(void *, char *, int, int, int, struct sk_buff *);
//UDP首部長度字段只有16bit,所以一個數據包大小不能超過0xFFFF
if (len > 0xFFFF)
return -EMSGSIZE;
/*
* Check the flags.
*/
//UDP不支持帶外數據,所以不能設置MSG_OOB
if (msg->msg_flags&MSG_OOB) /* Mirror BSD error message compatibility */
return -EOPNOTSUPP;
ipc.opt = NULL;
//pending標記和前面說的MSG_MORE標記有關。當設置MSG_MORE標記的數據到達時,UDP會將待
//發送的數據暫存到發送隊列中,這些數據就處於pending狀態,等應用指定要發送數據時,會將
//數據發送給IP,然後清空發送隊列,這時退出pending狀態。
if (up->pending) {
/*
* There are pending frames.
* The socket lock must be held while it's corked.
*/
lock_sock(sk)
/*
* 再判斷一次是因爲了lock_sock()可能會導致進程休眠。內核中有許多地方使用這樣的方式編程。
* 因爲大部分情況下pending標記是沒有的,這樣的話就不會進入到這裏,這種編程方式就可以省掉
* 一個lock_sock(比較複雜、耗時)電泳,僅當設置了pending後,才加鎖並再檢查一次,這樣就
* 能在大部分情況下不用鎖,少數情況下加鎖,這種方法是內核中常用的提升效率的策略。
*/
if (likely(up->pending)) {
//pengding的值只能是0或者AF_INET
if (unlikely(up->pending != AF_INET)) {
release_sock(sk);
return -EINVAL;
}
//因爲已經有掛起的數據,所以可以不用再次進行地址、路由的選擇,直接跳轉到do_append_data
//處追加數據即可。因爲如果有pending標記,下面需要做的工作在處理第一個數據包時已經處理過了
goto do_append_data;
}
release_sock(sk);
}
//ulen表示要發送的UDP報文長度,這裏在數據長度的基礎上再加上UDP首部長度8個字節
ulen += sizeof(struct udphdr);
//下面這段邏輯是確定目的端IP地址和端口號
//msg_name不爲空,表示調用系統調用時用戶空間程序指定了目的端地址信息,這種
//情況下校驗指定參數並設置地址族、目的地址和目的端口
if (msg->msg_name) {
//目的地址長度必須是IPv4地址
struct sockaddr_in * usin = (struct sockaddr_in*)msg->msg_name;
if (msg->msg_namelen < sizeof(*usin))
return -EINVAL;
//地址族必須是AF_INET或者AF_UNSPEC
if (usin->sin_family != AF_INET) {
if (usin->sin_family != AF_UNSPEC)
return -EAFNOSUPPORT;
}
//目的IP和目的端口
daddr = usin->sin_addr.s_addr;
dport = usin->sin_port;
//目的端口不能爲0
if (dport == 0)
return -EINVAL;
} else {
//調用發送相關係統調用時沒有指定目的地址情況處理
//如果在該UDP套接字上沒有執行過connect()系統調用,所以內核不知道要將該數據包發給誰,
//這種情況返回需要建立連接的錯誤碼
if (sk->sk_state != TCP_ESTABLISHED)
return -EDESTADDRREQ;
//應用程序有調用過connect(),這種情況下目的端地址信息會被保存在inet_sock結構中
daddr = inet->daddr;
dport = inet->dport;
/* Open fast path for connected socket.
Route will not be used, if at least one option is set.
*/
//由於已經連接過,所以連接標記置1
connected = 1;
}
ipc.addr = inet->saddr;
ipc.oif = sk->sk_bound_dev_if;
//如果發送數據時指定了控制信息(sendmsg()系統調用),用的比較少,先忽略
if (msg->msg_controllen) {
err = ip_cmsg_send(msg, &ipc);
if (err)
return err;
if (ipc.opt)
free = 1;
connected = 0;
}
if (!ipc.opt)
ipc.opt = inet->opt;
saddr = ipc.addr;
ipc.addr = faddr = daddr;
//源路由選項相關處理,先忽略
if (ipc.opt && ipc.opt->srr) {
if (!daddr)
return -EINVAL;
faddr = ipc.opt->faddr;
connected = 0;
}
tos = RT_TOS(inet->tos);
/*
* 如果設置了SOCK_LOCALROUTE或者發送時設置了MSG_DONTROUTE標記,再或者IP選項中存在嚴格源站選路
* 選項,則說明目的地址或下一跳必然位於本地子網中。此時需要設置tos中的RTO_ONLINK標記,表示
* 後續查找路由時與目的地直連。
*/
if (sock_flag(sk, SOCK_LOCALROUTE) ||
(msg->msg_flags & MSG_DONTROUTE) ||
(ipc.opt && ipc.opt->is_strictroute)) {
tos |= RTO_ONLINK;
connected = 0;
}
//多播地址處理,忽略
if (ipv4_is_multicast(daddr)) {
if (!ipc.oif)
ipc.oif = inet->mc_index;
if (!saddr)
saddr = inet->mc_addr;
connected = 0;
}
//對於已經連接的情況,之前一定已經查詢過路由了,這裏需要檢查該路由是否依然有效
if (connected)
rt = (struct rtable*)sk_dst_check(sk, 0);
//如果需要,這裏查詢路由表
if (rt == NULL) {
//查詢條件有:輸出設備接口、源和目的IP、TOS、源和目的端口
struct flowi fl = {
.oif = ipc.oif,
.nl_u = {
.ip4_u = {
.daddr = faddr,
.saddr = saddr,
.tos = tos
}
},
.proto = sk->sk_protocol,
.uli_u = {
.ports = {
.sport = inet->sport,
.dport = dport
}
}
};
security_sk_classify_flow(sk, &fl);
//查詢路由表
err = ip_route_output_flow(&init_net, &rt, &fl, sk, 1);
//路由查詢失敗、發送失敗
if (err) {
if (err == -ENETUNREACH)
IP_INC_STATS_BH(IPSTATS_MIB_OUTNOROUTES);
goto out;
}
err = -EACCES;
//路由結果爲廣播但是該socket不允許廣播,發送失敗
if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST))
goto out;
//如果是已連接套接字,那麼將路由信息設置到套接字,下次檢查即可,不用重複查詢,見上文
if (connected)
sk_dst_set(sk, dst_clone(&rt->u.dst));
}
//MSG_CONFIRM表示該報文要求接收端的數據鏈路層進行確認,用的很少,忽略
if (msg->msg_flags&MSG_CONFIRM)
goto do_confirm;
back_from_confirm:
saddr = rt->rt_src;
if (!ipc.addr)
daddr = ipc.addr = rt->rt_dst;
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 "udp cork app bug 2\n");
err = -EINVAL;
goto out;
}
/*
* Now cork the socket to pend data.
*/
//將一些重要信息暫存到inet->cork中,以備可能存在的後續發送過程使用
inet->cork.fl.fl4_dst = daddr;
inet->cork.fl.fl_ip_dport = dport;
inet->cork.fl.fl4_src = saddr;
inet->cork.fl.fl_ip_sport = inet->sport;
//下面就要將待發送數據放入發送隊列了,先設置pending標記
up->pending = AF_INET;
do_append_data:
//up->len變量記錄了當前該傳輸控制塊上已經pending的字節數,這裏將ulen累加到該變量上
up->len += ulen;
//根據是否爲UDPlite選用不同的拷貝函數,這兩個協議公用一套函數,但是因爲校驗和計算方法
//有差別,而且可能需要在拷貝過程中順便計算校驗和(這樣可以避免再次遍歷數據),所以這裏需要區分
getfrag = is_udplite ? udplite_getfrag : ip_generic_getfrag;
//ip_append_data()很重要,而且足夠複雜,它屬於IP提供給上層協議使用的一個發送接口,目前
//主要有UDP和raw套接字使用,該函數後面會單獨分析,這裏只需要知道如下幾點:
//1. 該函數將要發送的數據按照MTU大小分割成若干個方便IP處理的片段,每個片段一個skb;並且這些
// skb會放入到套接字的發送緩衝區中;
//2. 該函數只是組織數據包,並不執行發送動作,如果需要發送,需要由調用者主動調用ip_push_frames()
//3. 處理成功返回0,失敗返回錯誤碼
err = ip_append_data(sk, getfrag, msg->msg_iov, ulen,
sizeof(struct udphdr), &ipc, rt,
corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
//數據包處理失敗,將所有數據包清空,見下文
if (err)
udp_flush_pending_frames(sk);
//數據包處理沒有問題,並且沒有啓用MSG_MORE特性,那麼直接將發送隊列中的數據發送給IP。
//對於大多數應用都是走了該分支,即一次寫操作對應一個UDP數據包,這種UDP套接字相當於
//沒有發送緩衝區
else if (!corkreq)
err = udp_push_pending_frames(sk);
//這種情況不大可能發生,除非應用程序指定要發送的數據長度爲0
else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
up->pending = 0;
release_sock(sk);
out:
//釋放對路由緩存的引用
ip_rt_put(rt);
if (free)
kfree(ipc.opt);
//處理過程沒有錯誤,返回已發送的字節數
if (!err)
return len;
/*
* 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(UDP_MIB_SNDBUFERRORS, is_udplite);
}
return err;
do_confirm:
//確認處理,用的很少,忽略
dst_confirm(&rt->u.dst);
if (!(msg->msg_flags&MSG_PROBE) || len)
goto back_from_confirm;
err = 0;
goto out;
}
注:上面的代碼中涉及許多IP選項的相關處理,這裏先忽略它們,在實際中,這些選項用的也很少。
2.1 udp_push_pending_frames()
UDP使用該函數將發送隊列中的數據包發送給IP,它實際上是ip_push_pending_frames()的包裝,代碼如下:
/*
* Push out all pending data as one UDP datagram. Socket is locked.
*/
//如註釋:該函數會將當前所有pending的數據包作爲一個UDP數據報發送出去
static int udp_push_pending_frames(struct sock *sk)
{
struct udp_sock *up = udp_sk(sk);
struct inet_sock *inet = inet_sk(sk);
struct flowi *fl = &inet->cork.fl;
struct sk_buff *skb;
struct udphdr *uh;
int err = 0;
int is_udplite = IS_UDPLITE(sk);
__wsum csum = 0;
/* Grab the skbuff where UDP header space exists. */
//獲取發送隊列中第一個SKB的指針,注意是獲取,並不會將該skb從發送隊列上摘除
//發送隊列中此時可能有多個skb,每個skb攜帶的數據爲一個MTU大小,這是由前面的
//ip_append_data()處理好的,方面IP層的後續處理
if ((skb = skb_peek(&sk->sk_write_queue)) == NULL)
goto out;
/*
* Create a UDP header
*/
//組裝UDP首部各個字段
uh = udp_hdr(skb);
uh->source = fl->fl_ip_sport;
uh->dest = fl->fl_ip_dport;
uh->len = htons(up->len);
uh->check = 0;
//計算數據包的校驗和
if (is_udplite) /* UDP-Lite */
csum = udplite_csum_outgoing(sk, 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_outgoing(sk, skb, fl->fl4_src,fl->fl4_dst, up->len);
goto send;
} else /* `normal' UDP */
csum = udp_csum_outgoing(sk, skb);
/* add protocol-dependent pseudo-header */
//僞首部校驗和計算
uh->check = csum_tcpudp_magic(fl->fl4_src, fl->fl4_dst, up->len,
sk->sk_protocol, csum);
if (uh->check == 0)
uh->check = CSUM_MANGLED_0;
send:
//調用IP協議的push()函數將數據包組織成一個IP報文發送出去。這些數據包雖然可能會由多個片段組成,
//而且每個片段都達到了MTU大小,但是它們公用一個ipid,表明它們屬於同一個IP報文,只是分段了而已
//該函數在IP協議的發送部分再分析
err = ip_push_pending_frames(sk);
out:
//無論成功與否,發送隊列中不再有數據,所以清空len和pending標記
up->len = 0;
up->pending = 0;
if (!err)
UDP_INC_STATS_USER(UDP_MIB_OUTDATAGRAMS, is_udplite);
return err;
}
2.2 udp_flush_pending_frames()
該函數用於將發送隊列中的所有數據都丟棄,並且清除pending標記。
/*
* Throw away all pending data and cancel the corking. Socket is locked.
*/
static void udp_flush_pending_frames(struct sock *sk)
{
struct udp_sock *up = udp_sk(sk);
if (up->pending) {
up->len = 0;
up->pending = 0;
//skb的刪除由該函數完成
ip_flush_pending_frames(sk);
}
}
/*
* Throw away all pending data on the socket.
*/
void ip_flush_pending_frames(struct sock *sk)
{
struct sk_buff *skb;
//刪除發送隊列中的數據
while ((skb = __skb_dequeue_tail(&sk->sk_write_queue)) != NULL)
kfree_skb(skb);
//由於處於pending狀態時,inet_sk的cork字段保存了一些緩存信息,所以也需要清除
ip_cork_release(inet_sk(sk));
}
static void ip_cork_release(struct inet_sock *inet)
{
//主要是路由和IP選項
inet->cork.flags &= ~IPCORK_OPT;
kfree(inet->cork.opt);
inet->cork.opt = NULL;
if (inet->cork.rt) {
ip_rt_put(inet->cork.rt);
inet->cork.rt = NULL;
}
}
3. 小結
從上面的代碼分析過程中可以看出,UDP的發送過程還是相當直接的,它幾乎不緩存應用寫入的數據,直接將這些數據組裝成UDP數據報,然後丟給IP處理。