基於Linux核心的UDP協議原始碼分析
當一個數據包(package)經過IP層的處理之後,最終呼叫ip_local_deliever()函式,這個函式會根據這個資料包(packet)的傳輸層頭兒確定其採用的傳輸協議,如果是UDP協議,將會呼叫udp_rcv()函式。至此,進入傳輸層的範圍。
UDP協議棧的報頭定義如下:
struct udphdr { unsigned short source;//源埠 unsigned short dest;//目的埠 unsigned short len;//資料包長度 unsigned short check;//檢驗和 };
udp_rcv()函式的原始碼如下:
/* * All we need to do is get the socket, and then do a checksum. */ int udp_rcv(struct sk_buff *skb, struct device *dev, struct options *opt, unsigned long daddr, unsigned short len, unsigned long saddr, int redo, struct inet_protocol *protocol) { struct sock *sk; struct udphdr *uh; unsigned short ulen; int addr_type = IS_MYADDR; if(!dev || dev->pa_addr!=daddr) //檢查這個資料包是不是傳送給本地的資料包 addr_type=ip_chk_addr(daddr); //該函式定義在devinet.c中,用於檢查ip地址是否是本地或多播、廣播地址 /* * Get the header. */ uh = (struct udphdr *) skb->h.uh; //獲得UDP資料報的報頭 ip_statistics.IpInDelivers++; /* * Validate the packet and the UDP length. */ ulen = ntohs(uh->len); //引數len表示ip負載長度(IP資料報的資料部分長度)= UDP資料包頭+UDP資料包的資料部分+填充部分長度 //ulen表示的是UDP資料報首部和負載部分的長度,所以正常情況下len>=ulen if (ulen > len || len < sizeof(*uh) || ulen < sizeof(*uh)) //進行UDP資料包校驗 { printk("UDP: short packet: %d/%d\n", ulen, len); udp_statistics.UdpInErrors++; kfree_skb(skb, FREE_WRITE); return(0); } if (uh->check && udp_check(uh, len, saddr, daddr)) { /* <
[email protected]> wants to know, who sent it, to go and stomp on the garbage sender... */ printk("UDP: bad checksum. From %08lX:%d to %08lX:%d ulen %d\n", ntohl(saddr),ntohs(uh->source), ntohl(daddr),ntohs(uh->dest), ulen); udp_statistics.UdpInErrors++; kfree_skb(skb, FREE_WRITE); return(0); } len=ulen; //對len賦值為實際的UDP資料報長度 #ifdef CONFIG_IP_MULTICAST if (addr_type!=IS_MYADDR) { /* * Multicasts and broadcasts go to each listener. */ struct sock *sknext=NULL; //next指標 /*get_sock_mcast 獲取在對應埠的多播套接字佇列 *下面函式的引數依次表示:sock結構指標,本地埠,遠端地址,遠端埠,本地地址 */ sk=get_sock_mcast(udp_prot.sock_array[ntohs(uh->dest)&(SOCK_ARRAY_SIZE-1)], uh->dest, saddr, uh->source, daddr); if(sk) { do { struct sk_buff *skb1; sknext=get_sock_mcast(sk->next, uh->dest, saddr, uh->source, daddr); //下一個滿足條件的套接字 if(sknext) skb1=skb_clone(skb,GFP_ATOMIC); else skb1=skb; if(skb1) udp_deliver(sk, uh, skb1, dev,saddr,daddr,len); //對滿足條件的套接字呼叫傳送函式傳送 sk=sknext; } while(sknext!=NULL); } else kfree_skb(skb, FREE_READ); return 0; } #endif sk = get_sock(&udp_prot, uh->dest, saddr, uh->source, daddr); if (sk == NULL) //沒有找到本地對應的套接字,則進行出錯處理 { udp_statistics.UdpNoPorts++; if (addr_type == IS_MYADDR) { icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0, dev); //回覆ICMP出錯報文,目的主機不可達 } /* * Hmm. We got an UDP broadcast to a port to which we * don't wanna listen. Ignore it. */ skb->sk = NULL; kfree_skb(skb, FREE_WRITE); return(0); } return udp_deliver(sk,uh,skb,dev, saddr, daddr, len); //呼叫函式傳送套接字 }
這裡首先會獲取到這個pakcet的UDP頭部資訊(582-584),同時獲取到UDP的長度(590),接著根據長度判斷這個UDP包是否壞了(592-598),如果壞了,直接把這個包對應的記憶體空間釋放(596 kfree_skb(skb, FREE_WRITE);)。如果通過了,就做UDP的checksum(600~611),如果checksum通不過,就把這個包對應的記憶體空間釋放(609).
從617行開始,根據之前取得得包頭資訊判斷這個包是否要進行mcast,如果不是(這裡暫時不討論mcast的情況),接著走下去。
從647行開始UDP資料包需要找到對應的“寄生體”,也就是struct sock *結構。sock將是這個資料包在linux核心裡的最終歸宿(它的核心之旅將在這裡終結)。
一個數據包如何找到它對應的sock結構?這需要兩個要點:1、搜尋的物件。2、搜尋的索引。
sk = get_sock(&udp_prot, uh->dest, saddr, uh->source, daddr);
這行程式碼表示了搜尋的物件是udp_prot,搜尋的索引條件包括目的埠、源地址、源埠、目的地址。該函式定義的位置在檔案af_inet.c中,具體程式碼為:
/*
* Deliver a datagram to broadcast/multicast sockets.
*/
struct sock *get_sock_mcast(struct sock *sk, //套接字指標
unsigned short num,//本地埠
unsigned long raddr,//遠端地址
unsigned short rnum,//遠端埠
unsigned long laddr)//本地地址
{
struct sock *s;
unsigned short hnum;
hnum = ntohs(num);
/*
* SOCK_ARRAY_SIZE must be a power of two. This will work better
* than a prime unless 3 or more sockets end up using the same
* array entry. This should not be a problem because most
* well known sockets don't overlap that much, and for
* the other ones, we can just be careful about picking our
* socket number when we choose an arbitrary one.
*/
s=sk;
for(; s != NULL; s = s->next)
{
if (s->num != hnum) //本地埠不符合,跳過
continue;
if(s->dead && (s->state == TCP_CLOSE))//dead=1表示該sock結構已經處於釋放狀態
continue;
if(s->daddr && s->daddr!=raddr)//sock的遠端地址不等於條件中的遠端地址
continue;
if (s->dummy_th.dest != rnum && s->dummy_th.dest != 0)
continue;
if(s->saddr && s->saddr!=laddr)//sock的本地地址不等於條件的本地地址
continue;
return(s);
}
return(NULL);
}
找到sk之後,就可以把對應的資料包掛接到sk上去了(呼叫udp_deliver(sk,uh,skb,dev, saddr, daddr, len) 664)。若沒有找到,說明出錯了,對於UDP而言,出錯了就把資料包刪除,也就是直接把資料包所有的記憶體都釋放掉。(kfree_skb(skb, FREE_WRITE); 660)
上述過程中呼叫了udp_deliver函式,下面分析udp_deliver函式:
static int udp_deliver(struct sock *sk, //sock結構指標
struct udphdr *uh, //UDP頭指標
struct sk_buff *skb, //sk_buff
struct device *dev, //接收的網路裝置
long saddr, //本地地址
long daddr, //遠端地址
int len) //資料包的長度
{
//對skb結構相應欄位賦值
skb->sk = sk;
skb->dev = dev;
skb->len = len;
/*
* These are supposed to be switched.
*/
skb->daddr = saddr;
skb->saddr = daddr;
/*
* Charge it to the socket, dropping if the queue is full.
*/
skb->len = len - sizeof(*uh);
if (sock_queue_rcv_skb(sk,skb)<0) //呼叫sock_queu_rcv_skb()函式,將skb掛到sk接構中的接收佇列中
{
udp_statistics.UdpInErrors++;
ip_statistics.IpInDiscards++;
ip_statistics.IpInDelivers--;
skb->sk = NULL;
kfree_skb(skb, FREE_WRITE);
release_sock(sk);
return(0);
}
udp_statistics.UdpInDatagrams++;
release_sock(sk);
return(0);
}
這個函式的關鍵部分是: if (sock_queue_rcv_skb(sk,skb)<0) (687)。這行程式碼呼叫了sock_queue_rcv_skb函式,這個函式是傳輸層的關鍵,其實也就是把skb與struct sock *sk關聯起來,把skb放入sk裡的skb接收隊裡。
下面分析sock_queue_rcv_skb函式:
/*
* Queue a received datagram if it will fit. Stream and sequenced protocols
* can't normally use this as they need to fit buffers in and play with them.
*/
int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
unsigned long flags;
if(sk->rmem_alloc + skb->mem_len >= sk->rcvbuf)
return -ENOMEM;
save_flags(flags);
cli();
sk->rmem_alloc+=skb->mem_len;
skb->sk=sk;
restore_flags(flags);
skb_queue_tail(&sk->receive_queue,skb);
if(!sk->dead)
sk->data_ready(sk,skb->len);
return 0;
}
到這裡,struct sk_buff *skb就與struct sock *sk關聯上了,而struct socket *sock又包含了struct sock *sk,所以,使用者可以通過socket系統呼叫操作對應的資料包了。 其實UDP簡單概括起來就是把資料從IP層掛接到sock結構上去。因為簡單,所以效能比TCP要好很多,在一些對效能要求很高,同時對質量要求不怎麼高的情況下,非常適用,比如視訊、聊天資訊等。