UDP協議是 User Datagram Protocol 的簡稱, 中文名是用戶數據報協議,是OSI(Open System Interconnection,開放式系統互聯) 參考模型中一種無連接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務,位于 TCP/IP協議 模型的 傳輸層,如下圖:

也就是說 UDP協議 是建立中 IP協議(網絡層)之上的,IP協議 用于區分網絡上不同的主機(IP協議源碼分析),而 UDP協議 用于區分同一臺主機上不同的進程發送(接收)的網絡數據,如下圖所示:

從上圖可以看出,UDP協議 通過 端口號 來區分不同進程的數據包。
下面我們來看看 UDP協議 的協議頭部,如下圖所示:

從上圖可知,UDP頭部 由四個字段組成:源端口、目標端口、數據包長度 和 校驗和。
源端口 用于指示本機的進程,而 目標端口 用于指示遠端的進程。數據包長度 表示這個 UDP 數據包總長度(包括UDP頭部和 數據長度),而 校驗和 用于校驗數據包在傳輸的過程中是否損壞了。
下面我們看看 UDP頭部 在內核中的表示方式,如下代碼:
struct udphdr {__u16 source; // 源端口__u16 dest; // 目標端口__u16 len; // 數據包長度__u16 check; // 校驗和};
可以看出,udphdr 結構的字段與 UDP頭部 結構圖中的字段一一對應。最后,我們來看看 UDP頭部 在數據包的具體位置,如下圖:

下面我們主要通過 UDP 數據包的發送和接收兩個過程來分析 UDP 在內核中的實現原理。
數據的發送是由應用層調用 send() 或者 write() 系統調用,將數據傳遞到傳輸層協議處理,如下圖:

從上圖可以看出,用戶態 的應用程序調用 send() 系統調用時會觸發調用 內核態 的 sys_send() 內核函數,而 sys_send() 最終會調用 inet_sendmsg() 函數發送數據。
inet_sendmsg() 函數會根據用戶使用的傳輸層協議選擇不同的數據發送接口,比如 UDP 協議就會使用 udp_sendmsg() 函數發送數據。
我們來分析一下 UDP 協議的發送接口 udp_sendmsg() 函數的實現,代碼如下(由于 udp_sendmsg() 函數的實現比較復雜,所以我們分段分析):
int udp_sendmsg(struct sock *sk, struct msghdr *msg, int len){int ulen = len + sizeof(struct udphdr);struct ipcm_cookie ipc;struct udpfakehdr ufh;struct rtable *rt = NULL;int free = 0;int connected = 0;u32 daddr;u8 tos;int err;
udp_sendmsg() 函數的參數含義如下:
sk:Socket 對象。
msg:要發送的數據實體,其類型為 msghdr 結構。
len:要發送的數據長度。
上面的代碼主要定義了一些局部變量,如:
ulen 變量就是要發送的數據總長度(UDP頭部長度和數據長度之和)。
rt 變量表示數據傳輸的路由信息,其類型為 rtable 結構。
ufh 變量是調用 IP 層 ip_build_xmit() 函數時的上下文,主要用于構建 UDP協議頭部 ,其類型為 udpfakehdr 結構。
我們接口分析 udp_sendmsg() 函數:
// 是否提供了接收數據的目標IP地址和端口if (msg->msg_name) {// 接收數據的目標IP地址和端口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 -EINVAL;}// 設置接收數據的目標IP地址和端口ufh.daddr = usin->sin_addr.s_addr;ufh.uh.dest = usin->sin_port;if (ufh.uh.dest == 0)return -EINVAL;} else {if (sk->state != TCP_ESTABLISHED)return -ENOTCONN;ufh.daddr = sk->daddr; // 使用綁定Socket的IP地址ufh.uh.dest = sk->dport; // 使用綁定Socket的端口connected = 1;}
上面的代碼主要完成以下幾個工作:
如果用戶發送數據時提供了目標 IP 地址和端口,就把用戶提供的目標 IP 地址和端口復制到 ufh 變量中。
否則就把綁定到 Socket 對象的目標 IP 地址和端口復制到 ufh 變量中,并且設置 connected 變量為 1。
我們繼續分析 udp_sendmsg() 函數,代碼如下:
if (connected)rt = (struct rtable*)sk_dst_check(sk, 0); // 獲取路由信息對象緩存if (rt == NULL) { // 如果路由信息對象還沒被緩存// 調用 ip_route_output() 函數獲取路由信息對象err = ip_route_output(&rt, daddr, ufh.saddr, tos, ipc.oif);if (err)goto out;err = -EACCES;if (rt->rt_flags & RTCF_BROADCAST && !sk->broadcast)goto out;if (connected)sk_dst_set(sk, dst_clone(&rt->u.dst)); // 設置路由信息對象緩存}
上面的代碼比較簡單,首先調用 sk_dst_check() 查看 路由信息對象 是否被緩存,如果已經緩存,那么直接使用此 路由信息對象。否則調用 ip_route_output() 函數獲取 路由信息對象,并且調用 sk_dst_set() 設置 路由信息對象 緩存。
路由信息對象 指明數據在傳送過程的 下一跳 主機的信息(通常為網關),有了 下一跳 主機的信息,就可以把數據轉發給 下一跳 主機,然后由 下一跳 主機繼續完成發送工作。
我們繼續分析 udp_sendmsg() 函數,代碼如下:
ufh.saddr = rt->rt_src; // 設置源IP地址if (!ipc.addr) // 如果沒有提供目標IP地址,使用路由信息的目標IP地址ufh.daddr = ipc.addr = rt->rt_dst;ufh.uh.len = htons(ulen);ufh.uh.check = 0;ufh.iov = msg->msg_iov;ufh.wcheck = 0;// 構建MAC頭部、IP頭部和UDP頭部并且下發給IP協議層err = ip_build_xmit(sk, (sk->no_check == UDP_CSUM_NOXMIT? udp_getfrag_nosum: udp_getfrag),&ufh, ulen, &ipc, rt, msg->msg_flags);out:ip_rt_put(rt);...return err;}
上面的代碼主要把 路由信息對象 的源IP地址復制到 ufh 變量中,然后調用 ip_build_xmit() 函數完成數據發送的后續工作。ip_build_xmit() 函數的第一個參數用于復制 UDP頭部 和負載數據到數據包的函數指針,IP 層通過調用此函數把 UDP頭部 和負載數據復制到數據包中。
ip_build_xmit() 函數是 IP 協議層的實現,這里就不作說明,可以參考此文章:IP協議源碼分析。
總的來說,udp_sendmsg() 函數的主要工作就是為要發送的數據包構建 UDP頭部,然后把數據包交由 IP 層完成接下來的發送操作,所以 UDP協議 的發送過程比較簡單。
當網卡設備接收到數據包后,會交由內核協議棧處理。內核協議棧對數據包的處理是由下至上,如下圖所示:

也就是說,物理層處理完數據包后會交由鏈路層處理,而鏈路層處理完交由網絡層處理,以此類推。
所以當網絡層(IP協議)處理完數據包后,會交由傳輸層處理,在本文中介紹的傳輸層協議是 UDP協議,所以這里主要介紹的是 UDP協議 對數據包的處理過程。
當 IP 協議層處理完數據包后,如果 IP 頭部的上層協議字段(protocol 字段)指明的是 UDP協議,那么就會調用 udp_rcv() 函數處理數據包。下面我們來分析一下 udp_rcv() 函數的實現,代碼如下:
int udp_rcv(struct sk_buff *skb, unsigned short len){struct sock *sk;struct udphdr *uh;unsigned short ulen;struct rtable *rt = (struct rtable*)skb->dst; // 路由信息對象u32 saddr = skb->nh.iph->saddr; // 遠端IP地址(源IP地址)u32 daddr = skb->nh.iph->daddr; // 本地IP地址(目標IP地址)uh = skb->h.uh; // UDP頭部...// 根據目標端口獲取對用的 Socket 對象sk = udp_v4_lookup(saddr, uh->source, daddr, uh->dest, skb->dev->ifindex);if (sk != NULL) {udp_queue_rcv_skb(sk, skb); // 把數據包添加到Socket對象的receive_queue隊列中sock_put(sk);return 0;}...}
udp_rcv() 函數主要完成兩個工作:
調用 udp_v4_lookup() 函數獲取目標端口對應的 Socket 對象。
調用 udp_queue_rcv_skb() 函數把數據包添加到 Socket 對象的 receive_queue 隊列中。
UDP協議 使用了一個名為 udp_hash 的哈希表來保存所有綁定了端口的 Socket 對象,當應用程序調用 bind() 系統調用為 Socket 對象綁定端口時,就會將此 Socket 對象添加到 udp_hash 哈希表中。
而 udp_v4_lookup() 函數就是根據目標端口從 udp_hash 哈希表中獲取對應的 Socket 對象,udp_v4_lookup() 函數實現如下:
__inline__ struct sock *udp_v4_lookup(u32 saddr, u16 sport, u32 daddr, u16 dport, int dif){struct sock *sk;read_lock(&udp_hash_lock); // 為udp_hash哈希表上鎖sk = udp_v4_lookup_longway(saddr, sport, daddr, dport, dif);if (sk)sock_hold(sk);read_unlock(&udp_hash_lock);return sk;}
udp_v4_lookup() 函數首先為 udp_hash 哈希表上鎖,然后調用 udp_v4_lookup_longway() 函數從 udp_hash 哈希表中獲取對應目標端口的 Socket 對象,udp_v4_lookup_longway() 函數的實現如下:
struct sock *udp_v4_lookup_longway(u32 saddr, // 源IP地址(遠端IP地址)u16 sport, // 源端口(遠端端口)u32 daddr, // 目標IP地址(本地IP地址)u16 dport, // 目標端口(本地端口)int dif){struct sock *sk, *result = NULL;unsigned short hnum = ntohs(dport);int badness = -1;// 根據目標端口從 udp_hash 哈希表中獲取對應的 Socket 對象for (sk = udp_hash[hnum&(UDP_HTABLE_SIZE - 1)]; sk != NULL; sk = sk->next) {if (sk->num == hnum) { // 對比目標端口是否匹配int score = 0;if (sk->rcv_saddr) { // 如果Socket設置了固定的本地接收IPif(sk->rcv_saddr != daddr) // 對比目標IP地址是否匹配continue;score++;}if (sk->daddr) { // 如果Socket設置了固定的遠端接收IPif(sk->daddr != saddr) // 對比源IP地址是否匹配continue;score++;}if (sk->dport) { // 如果Socket設置了固定的遠端接收端口if(sk->dport != sport) // 對比源端口是否匹配continue;score++;}if (sk->bound_dev_if) { // 如果Socket設置了固定的接收網絡設備if(sk->bound_dev_if != dif) // 對比接收設備是否匹配continue;score++;}if (score == 4) { // 完美匹配, 那么直接返回即可result = sk;break;} else if(score > badness) { // 否則使用分數最高的Socket對象result = sk;badness = score;}}}return result;}
udp_v4_lookup_longway() 函數的主要邏輯就是根據目標端口從 udp_hash 哈希表中獲取對應的 Socket 對象。
由于同一個端口有可能綁定了多個 Socket 對象,所以 udp_v4_lookup_longway() 函數查找 Socket 對象時使用了最優匹配,也就是說除了匹配目標端口外,還可能會匹配源 IP 地址、源端口和目標 IP 地址等。
找到目標端口對應的 Socket 對象后,就可以調用 udp_queue_rcv_skb() 函數把數據包添加到 Socket 對象的 receive_queue 隊列中,udp_queue_rcv_skb() 函數實現如下:
static int udp_queue_rcv_skb(struct sock * sk, struct sk_buff *skb){...if (sock_queue_rcv_skb(sk, skb) < 0) {...return -1;}return 0;}
從上面代碼可以看出,udp_queue_rcv_skb() 函數最終會調用 sock_queue_rcv_skb() 函數完成任務,所以我們來分析一下 sock_queue_rcv_skb() 函數的實現,代碼如下:
static inline int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb){...// 把數據包添加到Socket對象的receive_queue隊列skb_queue_tail(&sk->receive_queue, skb);if (!sk->dead)sk->data_ready(sk, skb->len); // 喚醒等待Socket對象就緒的進程return 0;}
sock_queue_rcv_skb() 通過調用 skb_queue_tail() 函數把 skb 數據包添加到 Socket 對象的 receive_queue 隊列中,并且喚醒等待 Socket 對象就緒的進程。
當把數據包添加到 Socket 對象的 receive_queue 隊列后,UDP協議 的接收工作就此完畢。
推薦閱讀
嵌入式Linux入門進階,請關注:一口Linux
后臺回復1024有驚喜!