內核版本:2.6.34
UDP報文接收
UDP報文的接收可以分為兩個部分:協議棧收到udp報文,插入相應隊列中;用戶 調用recvfrom()或recv()系統調用從隊列中取出報文,這裡的隊列就是sk->sk_receive_queue,它是報文中轉的紐帶,兩部 分的聯系如下圖所示。
第一部分:協議棧如何收取udp報文的。
udp模塊的注冊在inet_init()中,當收到的是udp報文,會 調用udp_protocol中的handler函數udp_rcv()。
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0) printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");
udp_rcv() -> __udp4_lib_rcv() 完成udp報文接收,初始化udp的校驗和,並不驗證校驗和的正確性。
if (udp4_csum_init(skb, uh, proto)) goto csum_error;
在udptable中以套接字的[saddr, sport, daddr, dport]查找相應的sk,在上一篇中已詳細講過” sk的查找”,這裡報文的source源端口相當於源主機的端口,dest目的端口相當於本地端口。
sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
如果udptable中存在相應的sk,即有 socket在接收,則通過udp_queue_rcv_skb()將報文skb入隊列,該函數稍後分析,總之,報文會被放到sk- >sk_receive_queue隊列上,然後sock_put()減少sk的引用計算,並返回。之後的接收工作的完成將有賴於用戶的操作。
if (sk != NULL) { int ret = udp_queue_rcv_skb(sk, skb); sock_put(sk); if (ret > 0) return -ret; return 0; }
當沒有在udptable中找到sk時,則本機沒有socket會接收它,因此要發送icmp不可達報文,在此之前,還要驗證校驗 和udp_lib_checksum_complete(),如果校驗和錯誤,則直接丟棄報文;如果校驗和正確,則會增加mib中的統計,並發送icmp端 口不可達報文,然後丟棄該報文。
if (udp_lib_checksum_complete(skb)) goto csum_error; UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE); icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0); kfree_skb(skb);
udp_queue_rcv_skb() 報文入隊列
sock_woned_by_user()判斷sk- >sk_lock.owned的值,如果等於1,表示sk處於占用狀態,此時不能向sk接收隊列中添加skb,執行else if部分, sk_add_backlog()將skb添加到sk->sk_backlog隊列上;如果等於0,表示sk沒被占用,執行if部分,__udp_queue_rcv_skb() 將skb添加到sk->sk_receive_queue隊列上。
bh_lock_sock(sk); if (!sock_owned_by_user(sk)) rc = __udp_queue_rcv_skb(sk, skb); else if (sk_add_backlog(sk, skb)) { bh_unlock_sock(sk); goto drop; } bh_unlock_sock(sk);
那麼何時sk會被占用?何時sk->sk_backlog上的skb被處理的?
創建socket時, sys_socket() -> inet_create() -> sk_alloc() -> sock_lock_init() -> sock_lock_init_class_and_name()初 始化sk->sk_lock_owned=0。
比如當銷毀socket時,udp_destroy_sock()會調用lock_sock()對sk加鎖,操作完後,調用 release_sock()對sk解鎖。
void udp_destroy_sock(struct sock *sk) { lock_sock(sk); udp_flush_pending_frames(sk); release_sock(sk); }
實際上,lock_sock()設置sk->sk_lock.owned=1;而release_sock()設置sk->sk_lock.owned=0,並處理sk_backlog隊 列上的報文,release_sock() -> __release_sock(),對於sk_backlog隊列上的每個報文,調用sk_backlog_rcv() -> sk->sk_backlog_rcv()。同樣是在socket的創建中,sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv()即 __udp_queue_rcv_skb(),這個函數的作用上面已經講過,將skb添加到sk_receive_queue,這樣,所有的sk_backlog上的報文轉 移到了sk_receive_queue上。簡單來說,sk_backlog隊列的作用就是,鎖定時報文臨時存放在此,解鎖時,報文移到 sk_receive_queue隊列。
第二部分:用戶如何收取報文
用戶可以調用sys_recvfrom()或sys_recv()來接收報文,所不同的是 ,sys_recvfrom()可能通過參數獲得報文的來源地址,而sys_recv()則不可以,但對接收報文並沒有影響。在用戶調用 recvfrom()或recv()接收報文前,發給該socket的報文都會被添加到sk->sk_receive_queue上,recvfrom()和recv()要做的 就是從sk_receive_queue上取出報文,拷貝到用戶空間,供用戶使用。
sys_recv() -> sys_recvfrom()
sys_recvfrom () -> sk->ops->recvmsg()
==> sock_common_recvmsg() -> sk->sk_prot->recvmsg()
==> udp_recvmsg()
sys_recvfrom()
調用sock_recvmsg()接收udp報文,存放在msg中,如果接收到報文,從內核到用戶空 間拷貝報文的源地址到addr中,addr是recvfrom()調用的傳入參數,表示報文源的地址。而報文的內容是在udp_recvmsg()中從 內核拷貝到用戶空間的。
err = sock_recvmsg(sock, &msg, size, flags); if (err >= 0 && addr != NULL) { err2 = move_addr_to_user((struct sockaddr *)&address, msg.msg_namelen, addr, addr_len); if (err2 < 0) err = err2; }
udp_recvmsg() 接收udp報文
這個函數有三個關鍵操作:
1. 取到數據包 -- __skb_recv_datagram()
2. 拷貝數據 -- skb_copy_datagram_iovec()或skb_copy_and csum_datagram_iovec()
3. 必要時計算校 驗和 – skb_copy_and_csum_datagram_iovec()
__skb_recv_datagram(),它會從sk->sk_receive_queue上取出一個skb,前面已經分析到,內核收到發往該socket 的報文會放在sk->sk_receive_queue。
skb = __skb_recv_datagram(sk, flags | (noblock ? MSG_DONTWAIT : 0), &peeked, &err);
如果沒有報文,有兩種情況:使用了非阻塞接收,且用戶接收時還沒有報文到來;使 用阻塞接收,但之前沒有報文,且在sk->sk_rcvtimeo時間內都沒有報文到來。沒有報文,返回錯誤值。
if (!skb) goto out;
len是recvfrom()傳入buf的大小,ulen是報文內容的長度,如果ulen > len,那麼只需要使用buf的 ulen長度就可以了;如果len < ulen,那麼buf不夠報文填充,只能對報文截斷,取前len個字節。
ulen = skb- >len - sizeof(struct udphdr); if (len > ulen) len = ulen; else if (len < ulen) msg->msg_flags |= MSG_TRUNC;
如果報文被截斷或使用UDP-Lite,那麼需要提前驗證校驗和, udp_lib_checksum_complete()完成校驗和計算,函數在下面具體分析。
if (len < ulen || UDP_SKB_CB(skb)- >partial_cov) { if (udp_lib_checksum_complete(skb)) goto csum_copy_err; }
如果報文不用驗證校驗和,那麼執行if部分,調用skb_copy_datagram_iovec()直接拷貝報文到buf中就可以了;如果 報文需要驗證校驗和,那麼執行else部分,調用skb_copy_and_csum_datagram_iovec()拷貝報文到buf,並在拷貝過程中計算校 驗和。這也是為什麼在內核收到udp報文時為什麼先驗證校驗和再處理的原因,udp報文可能很大,校驗和的計算可能很耗時,將 其放在拷貝過程中可以節約開銷,當然它的代價是一些校驗和錯誤的報文也會被添加到socket的接收隊列上,直到用戶真正接收 時它們才會被丟棄。
if (skb_csum_unnecessary(skb)) err = skb_copy_datagram_iovec(skb, sizeof(struct udphdr), msg->msg_iov, len); else { err = skb_copy_and_csum_datagram_iovec(skb, sizeof(struct udphdr), msg->msg_iov); if (err == -EINVAL) goto csum_copy_err; }
拷貝地址到msg->msg_name中,在sys_recvfrom()中msg->msg_name=&address,然後address會從內核拷貝 給用戶空間的addr。
if (sin) { sin->sin_family = AF_INET; sin->sin_port = udp_hdr(skb)->source; sin->sin_addr.s_addr = ip_hdr(skb)->saddr; memset(sin->sin_zero, 0, sizeof(sin->sin_zero)); }
下面來重點看核心操作的三個函數:
__skb_recv_datagram() 從sk_receive_queue上取一個skb
核心代 碼段如下,skb_peek()從sk->sk_receive_queue中取出一個skb,如果有的話,則返回skb,作為用戶此次接收的報文,當然 還有對skb的後續處理,但該函數只是取出一個skb;如果還沒有的話,則使用wait_for_packet()等待報文到來,其中參數timeo 代表等待的時間,如果使用非阻塞接收的話,timeo會設置為0(即當前沒有skb的話則直接返回,不進行等待),否則設置為sk- >sk_rcvtimeo。
do { …… skb = skb_peek(&sk->sk_receive_queue); if (skb) { *peeked = skb->peeked; if (flags & MSG_PEEK) { skb->peeked = 1; atomic_inc(&skb->users); } else __skb_unlink(skb, &sk->sk_receive_queue); } if (skb) return skb; …… } while (!wait_for_packet(sk, err, &timeo));
skb_copy_datagram_iovec() 拷貝skb內容到msg中
拷貝可以分三部分:線性地址空間的拷貝,聚合/發散地址空間的拷貝,非線性地址空間的拷貝。第二部分需要硬件的支持,這 裡討論另兩部分。
在skb的buff中的是線性地址空間,在skb的frag_list上的是非線性地址空間;當沒有分片發生的,用線性 地址空間就足夠了,但是當報文過長而分片時,第一個分片會使用線性地址空間,其余的分片將被鏈到skb的frag_list上,即非 線性地址空間,具體可以參考”ipv4模塊”中分片部分。
拷貝報文內容時,就要將線性和非線性空間的內容都拷貝過去。下 面是拷貝線性地址空間的代碼段,start是報文的線性部分長度(skb->len-skb->datalen),copy是線性地址空間的大小, offset是相對skb的偏移(即此次拷貝從哪裡開始),以udp報文為例,這幾個值如下圖所示。memcpy_toiovec()拷貝內核到to中, 要注意的是它改變了to的成員變量。
int start = skb_headlen(skb); int i, copy = start - offset; if (copy > 0) { if (copy > len) copy = len; if (memcpy_toiovec(to, skb->data + offset, copy)) goto fault; if ((len -= copy) == 0) return 0; offset += copy; }
下面 是拷貝非線性地址空間的代碼段,遍歷skb的frag_list鏈表,對上面的每個分片,拷貝內容到to中,這裡start, end的值不重要 ,重要的是它們的差值end-start,表示了當前分片frag_iter的長度,使用skb_copy_datagram_iovec()拷貝當前分片內容,即 把每個分片都作為單獨報文來處理。不過對於分片,感覺只有拷貝的第一部分和第二部分,在IP層分片重組時,並沒有將分片鏈 在分片的frag_list上的情況,而都鏈在頭分片的frag_list上。
skb_walk_frags(skb, frag_iter) { int end; end = start + frag_iter->len; if ((copy = end - offset) > 0) { if (copy > len) copy = len; if (skb_copy_datagram_iovec(frag_iter, offset - start, to, copy)) goto fault; if ((len -= copy) == 0) return 0; offset += copy; } start = end; }
還是以一個例子來說明,主機收到一個udp報文,內容長度為4000 bytes,MTU是1500,傳入buff數組大小也為4000。 根據MTU,報文會會被分成三片,分片IP報內容大小依次是1480, 1480, 1040。每個分片都有一個20節字的IP報文,第一個分片 還有一個8節字的udp報頭。接收時數據拷貝情況如下:
分片一是第一個分片 ,包含UDP報文,在拷貝時要跳過,因為使用的是udp socket接收,只要報文內容就可以了。三張圖片代表了三次調用 skb_copy_datagram_iovec()的情況,iov是存儲內容的buff,最終結果是三個分片共4000字節拷貝到了iov中。
memcpy_toiovec()函數需要注意,不僅因為它改變了iovec的成員值,還因為最後的iov++。在udp socket的接收recvfrom() 中,msg.msg_iov = &iov,而iov定義成struct iovec iov,即傳入參數iov實際只有一個的空間,那麼在iov++後,iov將指 向非法的地址。這裡只考慮udp使用時的情況,memcpy_toiovec()調用的前一句是,這裡len是接收buff的長度:
if (copy > len) copy = len;
而memcpy_toiovec()中又有int copy = min_t(unsigned int, iov->iov_len, len),這裡len是上面 傳入的copy,iov_len是接收buff長度,這兩句保證了函數中copy值與len相等,即完成一次拷貝後,len-=copy會使len==0,雖 然iov++指向了非法內存,但由於while(len > 0)已退出,所以不會使用iov做任何事情。其次,函數中的iov++並不會對參數 iov產生影響,即函數完成iov還是傳入的值。最後,拷貝完後會修改iov_len和iov_base的值,iov_len表示可用長度,iov_base 表示起始拷貝位置。
int memcpy_toiovec(struct iovec *iov, unsigned char *kdata, int len) { while (len > 0) { if (iov->iov_len) { int copy = min_t(unsigned int, iov->iov_len, len); if (copy_to_user(iov->iov_base, kdata, copy)) return -EFAULT; kdata += copy; len -= copy; iov->iov_len -= copy; iov->iov_base += copy; } iov++; } return 0; }
skb_copy_and_csum_datagram_iovec() 拷貝skb內容到msg中,同時計算校驗和
這個函數提高了校驗和計 算效率,因為它合並了拷貝與計算操作,這樣只要一次遍歷操作就可以了。與skb_copy_datagram_iovec()相比,它在每次拷貝 skb內容時,計算下這次拷貝內容的校驗和。
csum = csum_partial(skb->data, hlen, skb->csum); if (skb_copy_and_csum_datagram(skb, hlen, iov->iov_base, chunk, &csum)) goto fault;
UDP報文發送
發送時有兩種調用方式:sys_send()和sys_sendto(),兩者的區別在於sys_sendto()需 要給入目的地址的參數;而sys_send()調用前需要調用sys_connect()來綁定目的地址信息;兩者的後續調用是相同的。如果調 用sys_sendto()發送,地址信息在sys_sendto()中從用戶空間拷貝到內核空間,而報文內容在udp_sendmsg()中從用戶空間拷貝 到內核空間。
sys_send() -> sys_sendto()
sys_sendto() -> sock_sendmsg() -> __sock_sendmsg() -> sock->ops->sendmsg()
==> inet_sendmsg() -> sk->sk_prot->sendmsg()
==> udp_sendmsg()
udp_sendmsg()的核心流程如下圖所示,只列出了核心的函數調用了參數賦值,大致步驟是: 獲取信息 -> 獲取路由項rt -> 添加數據 -> 發送數據。
udp_sock結構體中的 pending用於標識當前udp_sock上是否有待發送數據,如果有的話,則直接goto do_append_data繼續添加數據;否則先要做些初 始化工作,再才添加數據。實際上,pending!=0表示此調用前已經有數據在udp_sock中的,每次調和sendto()發送數據時, pending初始等於0;在添加數據時,設置up->pending = AF_INET。直到最後調用udp_push_pending_frames()將數據發送給 IP層或skb_queue_empty(&sk->sk_write_queue)發送鏈表上為空,這時設置up->pending = 0。因此,這裡可以看到 ,報文發送時pending值的變化:
通常使用sendto()發送都是一次調用對應一個報文,即pending=0->AF_INET->0; 但如果調用sendto()時參數用到了MSG_MORE標志,則pending=0->AF_INET,直到調用sendto()時未使用MSG_MORE標志,表示 此次發送數據是最後一部分數據時,pending=AF_INET->0。
if (up->pending) { 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); }
如果pending=0沒有待發送數據,執行初始化操作:報文長度、地址信息、路由項。
ulen初始為sendto()傳入的數 據長度,由於是第一部分數據(如果沒有後續數據,則就是報文),ulen要添加udp報頭的8字節。
ulen += sizeof(struct udphdr);
這段代碼獲取要發送數據的目的地址和端口號。一種情況是調用sendto()發送數據,此 時目的的信息以參數傳入,存儲在msg->msg_name中,因此從中取出daddr和dport;另一種情況是調用connect(), send()發 送數據,在connect()調用時綁定了目的的信息,存儲在inet中,並且由於是調用了connect(),sk->sk_state會設置為 TCP_ESTABLISHED。以後調用send()發送數據時,無需要再給入目的信息參數,因此從inet中取出dadr和dport。而connected表 示了該socket是否已綁定目的。
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; connected = 1; }
下一步是獲取路由項rt,如果已連接(調用過connect),則路由信息在connect()時已獲取,直接拿就可以了;如果未 連接或拿到的路由項已被刪除,則需要重新在路由表中查找,還是使用ip_route_output_flow()來查找,如果是連接狀態的 socket,則要用新找到的rt來更新socket,當然,前提條件是之前的rt已過期。
if (rt == NULL) { …… err = ip_route_output_flow(net, &rt, &fl, sk, 1); …… if (connected) sk_dst_set(sk, dst_clone(&rt->u.dst)); }
存儲信息daddr, dport, saddr, sport到cork.fl中,它們會在生成udp報頭和計算udp校驗和時用到。up- >pending=AF_INET標識了數據添加的開始,下面將開始數據的添加工作。
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->inet_sport; up->pending = AF_INET;
如果pending!=0或執行完初始化操作,則直接執行添加數據操作:
up->len表示要發送數據的總長度,包括udp報頭,因此每發送一部分數據就要累加它的長度,在發送後up->len被清0。然 後調用ip_append_data()添加數據到sk->sk_write_queue,它會處理數據分片等問題,在 ”ICMP模塊” 中有詳細分析過。
up->len += ulen; getfrag = is_udplite ? udplite_getfrag : ip_generic_getfrag; err = ip_append_data(sk, getfrag, msg->msg_iov, ulen, sizeof(struct udphdr), &ipc, &rt, corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
ip_append_data()添加數據正確會返回0,否則 udp_flush_pending_frames()丟棄將添加的數據;如果添加數據正確,且沒有後續的數據到來(由MSG_MORE來標識),則 udp_push_pending_frames()將數據發送給IP層,下面將詳細分析這個函數。最後一種情況是當sk_write_queue上為空時,它觸 發的條件必須是發送多個報文且sk_write_queue上為空,而實際上在ip_append_data過後sk_write_queue不會為空的,因此正常 情況下並不會發生。哪種情況會發生呢?重置pending值為0就是在這裡完成的,三個條件語句都會將pending設置為0。
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;
數據已經處理完成,釋放取到的路由項rt,如果有IP選項,也釋放它。如果發送數據成功,返 回發送的長度len;否則根據錯誤值err進行錯誤處理並返回err。
ip_rt_put(rt); if (free) kfree(ipc.opt); if (!err) return len; 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;
在 “ICMP模塊” 中往IP層發送數據使用的是ip_push_pending_frames()。而在UDP模塊中往IP層發送數 據使用的是ip_push_pending_frames()。而在UDP模塊中往IP層發送數據的udp_push_pending_frames()只是對 ip_push_pending_frames()的封裝,主要是增加對UDP的報頭的處理。同理,udp_flush_pending_frames()也是,只是它更簡單 ,僅僅重置了up->len和up->pending的值,重置後可以開始一個新報文。那麼udp_push_pending_frames()封裝了哪些處 理呢。
udp_push_pending_frames() 發送數據給IP層
設置udp報頭,包括源端口source,目的端口dest,報文長度len 。
uh = udp_hdr(skb); uh->source = fl->fl_ip_sport; uh->dest = fl->fl_ip_dport; uh->len = htons(up->len); uh->check = 0;
計算udp報頭中的校驗和,包括了偽報頭、udp報頭和報文內容。
if (is_udplite) 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); uh->check = csum_tcpudp_magic(fl->fl4_src, fl->fl4_dst, up->len, sk->sk_protocol, csum);
將報文發送給IP層,這個函數已經分析過了。
err = ip_push_pending_frames(sk);
同樣,在發送完報文 後,重置len和pending的值,以便開始下一個報文發送。
up->len = 0; up->pending = 0;