UDP協議接受資料流程
當資料達到網路層時,根據IP協議頭中的protocol資料域協議碼在全域性雜湊表inet_protos[MAX_INET_PROTOS]中尋找傳輸層的收包函式,前面介紹UDP基本資料結構時說了了UDP和網路層的介面資料結構,AF_INET協議族初始化函式inet_init中呼叫inet_add_protoc函式把UDP協議的例項struct net_protocol udp_protocol註冊到雜湊表inet_protos中。UDP接受網路層的函式是udp_rcv,udp_rcv是__udp4_lib_rcv的封裝。
static const struct net_protocol udp_protocol = { .handler = udp_rcv, //接受IP層資料包函式 .err_handler = udp_err, //icmp錯誤處理函式 .gso_send_check = udp4_ufo_send_check, .gso_segment = udp4_ufo_fragment, .no_policy = 1, .netns_ok = 1, };
註冊UDP協議到inet_protos全域性雜湊表中:
//註冊udp處理函式
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");
1、__udp4_lib_rcv函式
1..1、資料包正確性檢查
首先要檢查udp協議頭的正確性,呼叫pskb_may_pull函式,然後是檢查資料包長度,如果資料包不合法就直接扔掉。
1.2、根據資料包目的地址決定如何傳送
根據路由判斷資料包的接受地址是組地址或者是廣播地址,則呼叫__udp_lib_mcast_deliver函式完成接受,如果不是組傳送地址、廣播地址就呼叫__udp4_lib_lookup_skb根據目的埠在UDP雜湊表中尋找打卡的套接字,如果找到了就呼叫udp_queue_rcv_skb傳送給套接字的接受緩衝區,udp_queue_rcv_skb返回值大於0時需要告訴應用程式重新提交輸入資料包。
1.3、沒有開啟的UDP套接字
根據目的埠在UDP套接字雜湊表中沒有找到開啟的套接字就要對資料包在進行校驗和計算,如果校驗和檢查不正確就直接扔掉資料包,也不給對方返回錯誤資訊,因為UDP協議是不可靠的協議,如果校驗和正確則更新UDP_MIB_NOPORTS錯誤統計資訊,向資料包傳送端返回icmp錯誤資訊,告知埠不可達。
__udp4_lib_rcv程式碼如下:
int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable, int proto) { //指向udp套接字資料結構體指標 struct sock *sk; //區域性變數指向udp協議頭 struct udphdr *uh; //udp資料長度 unsigned short ulen; struct rtable *rt = skb_rtable(skb); __be32 saddr, daddr; struct net *net = dev_net(skb->dev); /* * Validate the packet. */ //udp協議頭檢查 if (!pskb_may_pull(skb, sizeof(struct udphdr))) goto drop; /* No space for header. */ //獲取udp頭 uh = udp_hdr(skb); //獲取資料包長度 ulen = ntohs(uh->len); //源地址 saddr = ip_hdr(skb)->saddr; //目的地址 daddr = ip_hdr(skb)->daddr; //檢查長度是否正確 if (ulen > skb->len) goto short_packet; //協議號是udp if (proto == IPPROTO_UDP) { /* UDP validates ulen. */ //校驗和檢查 if (ulen < sizeof(*uh) || pskb_trim_rcsum(skb, ulen)) goto short_packet; uh = udp_hdr(skb); } if (udp4_csum_init(skb, uh, proto)) goto csum_error; //如果路由標誌是廣播地址或者組播地址 if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST)) return __udp4_lib_mcast_deliver(net, skb, uh, saddr, daddr, udptable); //尋找開啟的套接字 sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable); //找到了開啟的套接字 if (sk != NULL) { //將資料包傳給套接字緩衝區 int ret = udp_queue_rcv_skb(sk, skb); //釋放套接字 sock_put(sk); /* a return value > 0 means to resubmit the input, but * it wants the return to be -protocol, or 0 */ //返回值大於0需要告訴呼叫程式重新提交輸入資料包 if (ret > 0) return -ret; return 0; } if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) goto drop; nf_reset(skb); /* No socket. Drop packet silently, if checksum is wrong */ //完成資料包校驗和檢查 //校驗錯誤直接丟包,也不給對端返回錯誤資訊 if (udp_lib_checksum_complete(skb)) goto csum_error; //更新UDP_MIB_NOPORTS統計資訊 UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE); //返回icmp錯誤資訊,告知目的埠不可達 icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0); /* * Hmm. We got an UDP packet to a port to which we * don't wanna listen. Ignore it. */ kfree_skb(skb); return 0; short_packet: LIMIT_NETDEBUG(KERN_DEBUG "UDP%s: short packet: From %pI4:%u %d/%d to %pI4:%u\n", proto == IPPROTO_UDPLITE ? "-Lite" : "", &saddr, ntohs(uh->source), ulen, skb->len, &daddr, ntohs(uh->dest)); goto drop; csum_error: /* * RFC1122: OK. Discards the bad packet silently (as far as * the network is concerned, anyway) as per 4.1.3.4 (MUST). */ LIMIT_NETDEBUG(KERN_DEBUG "UDP%s: bad checksum. From %pI4:%u to %pI4:%u ulen %d\n", proto == IPPROTO_UDPLITE ? "-Lite" : "", &saddr, ntohs(uh->source), &daddr, ntohs(uh->dest), ulen); drop: UDP_INC_STATS_BH(net, UDP_MIB_INERRORS, proto == IPPROTO_UDPLITE); kfree_skb(skb); return 0; }
2、將資料包加入套接字接受佇列處理函式
__udp4_lib_lookup_skb函式以埠在UDP雜湊表中查詢開啟的套接字,如果有就呼叫udp_queue_rcv_skb函式把資料包加入到套接字接受佇列中。udp_queue_rcv_skb函式處理分三部分,
a、首先檢視套接字佇列是否滿了,如果滿了就丟掉資料包。
b、判斷該套接字是否是IPSec協議頭封裝的套接字,如果是就呼叫up->encap_rcv處理ipsec協議資料包。
c、判斷是否有應用程式等待接受資料包,如果有就將資料包放入套接字接受緩衝區,如果沒有就把套接字加入backlog佇列。
udp_rcv_skb函式原始碼:
int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
struct udp_sock *up = udp_sk(sk);
int rc;
int is_udplite = IS_UDPLITE(sk);
/*
* Charge it to the socket, dropping if the queue is full.
*/
//檢查佇列是否滿了
if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
goto drop;
nf_reset(skb);
if (up->encap_type) {
//資料包是封裝資料包
/* if we're overly short, let UDP handle it */
if (skb->len > sizeof(struct udphdr) &&
up->encap_rcv != NULL) {
int ret;
//由ipsec協議處理函式處理封裝資料包
ret = (*up->encap_rcv)(sk, skb);
if (ret <= 0) {
UDP_INC_STATS_BH(sock_net(sk),
UDP_MIB_INDATAGRAMS,
is_udplite);
return -ret;
}
}
/* FALLTHROUGH -- it's a UDP Packet */
}
/*
* UDP-Lite specific tests, ignored on UDP sockets
*/
if ((is_udplite & UDPLITE_RECV_CC) && UDP_SKB_CB(skb)->partial_cov) {
if (up->pcrlen == 0) { /* full coverage was set */
LIMIT_NETDEBUG(KERN_WARNING "UDPLITE: partial coverage "
"%d while full coverage %d requested\n",
UDP_SKB_CB(skb)->cscov, skb->len);
goto drop;
}
if (UDP_SKB_CB(skb)->cscov < up->pcrlen) {
LIMIT_NETDEBUG(KERN_WARNING
"UDPLITE: coverage %d too small, need min %d\n",
UDP_SKB_CB(skb)->cscov, up->pcrlen);
goto drop;
}
}
if (sk->sk_filter) {
if (udp_lib_checksum_complete(skb))
goto drop;
}
if (sk_rcvqueues_full(sk, skb))
goto drop;
rc = 0;
bh_lock_sock(sk);
//套接字上有使用者程序在等待接受資料包
//就將資料包放入套接字接受緩衝區
if (!sock_owned_by_user(sk))
rc = __udp_queue_rcv_skb(sk, skb);
//套接字上沒有使用者程序等待接受資料
//就將資料包放入backlog佇列
else if (sk_add_backlog(sk, skb)) {
bh_unlock_sock(sk);
goto drop;
}
bh_unlock_sock(sk);
return rc;
drop:
UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_INERRORS, is_udplite);
atomic_inc(&sk->sk_drops);
kfree_skb(skb);
return -1;
}
3、upd協議接受廣播與組播發送資料包
如果目的地址是廣播地址或者組地址那麼一個主機上可以能有多個目的埠等待接受資料包,組接受函式是__udp4_lib_mcast_deliver,將資料包分發給所有監聽的套接字,處理流程是先找到第一個有效的套接字,如果該套接字的埠和資料包UDP協議頭中目標埠匹配,然後遍歷UDP協議的雜湊連結串列查詢每一個監聽的套接字,並把克隆好的資料包放入監聽的套接字接受緩衝區佇列中,如果udp_queue_rcv_skb函式返回值大於0,在原始碼中提示重新處理資料包而不是扔掉資料包。
__udp4_lib_mcast_deliver函式原始碼:
static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
struct udphdr *uh,
__be32 saddr, __be32 daddr,
struct udp_table *udptable)
{
struct sock *sk, *stack[256 / sizeof(struct sock *)];
struct udp_hslot *hslot = udp_hashslot(udptable, net, ntohs(uh->dest));
int dif;
unsigned int i, count = 0;
spin_lock(&hslot->lock);
//在udp雜湊表中找到第一個匹配的目的套接字
sk = sk_nulls_head(&hslot->head);
dif = skb->dev->ifindex;
sk = udp_v4_mcast_next(net, sk, uh->dest, daddr, uh->source, saddr, dif);
while (sk) {
//複製套接字到緩衝區佇列
stack[count++] = sk;
//遍歷udp雜湊連結串列
sk = udp_v4_mcast_next(net, sk_nulls_next(sk), uh->dest,
daddr, uh->source, saddr, dif);
if (unlikely(count == ARRAY_SIZE(stack))) {
if (!sk)
break;
//有開啟的套接字
flush_stack(stack, count, skb, ~0);
count = 0;
}
}
/*
* before releasing chain lock, we must take a reference on sockets
*/
for (i = 0; i < count; i++)
sock_hold(stack[i]);
spin_unlock(&hslot->lock);
/*
* do the slow work with no lock held
*/
if (count) {
flush_stack(stack, count, skb, count - 1);
for (i = 0; i < count; i++)
sock_put(stack[i]);
} else {
kfree_skb(skb);
}
return 0;
}
4、UDP的雜湊表
一個應用層程式在開啟SOCK_DGRAM套接字,這類套接字可以在各種地址和埠的組合套接字上,__udp4_lib_rcv函式的資料包可能需要傳送給多個監聽的套接字,為了提高網路效能,linux使用UDP雜湊表來快速查詢監聽中的套接字。
4.1 UDP雜湊連結串列資料結構
UDP雜湊表有兩個,一個是目的埠雜湊表,另一個是目的埠和目的IP雜湊表
struct udp_table:
/**
* struct udp_table - UDP table
*
* @hash: hash table, sockets are hashed on (local port)
* @hash2: hash table, sockets are hashed on (local port, local address)
* @mask: number of slots in hash tables, minus 1
* @log: log2(number of slots in hash table)
*/
struct udp_table {
struct udp_hslot *hash; //目的埠雜湊表
struct udp_hslot *hash2; //目的埠、目的ip雜湊表
unsigned int mask; //雜湊表槽數
unsigned int log;
};
struct udp_hslot:
/**
* struct udp_hslot - UDP hash slot
*
* @head: head of list of sockets
* @count: number of sockets in 'head' list
* @lock: spinlock protecting changes to head/count
*/
struct udp_hslot {
struct hlist_nulls_head head; //套接字連結串列
int count; //套接字數量
spinlock_t lock; //鎖
} __attribute__((aligned(2 * sizeof(long))));
雜湊表匹配函式是__udp4_lib_lookup_skb,首先根據目的埠匹配,如果匹配到了就把資料包放入套接字接受緩衝區,如果沒有匹配到再呼叫compute_core匹配目的地址、目的埠、網路裝置介面等多個條件。
__udp4_lib_lookup_skb:
static struct sock *__udp4_lib_lookup(struct net *net, __be32 saddr,
__be16 sport, __be32 daddr, __be16 dport,
int dif, struct udp_table *udptable)
{
struct sock *sk, *result;
struct hlist_nulls_node *node;
//獲取目的埠號
unsigned short hnum = ntohs(dport);
//根據目的埠查詢
unsigned int hash2, slot2, slot = udp_hashfn(net, hnum, udptable->mask);
struct udp_hslot *hslot2, *hslot = &udptable->hash[slot];
int score, badness;
rcu_read_lock();
if (hslot->count > 10) {
//目的地址雜湊值
hash2 = udp4_portaddr_hash(net, daddr, hnum);
slot2 = hash2 & udptable->mask;
hslot2 = &udptable->hash2[slot2];
if (hslot->count < hslot2->count)
goto begin;
//源地址雜湊值
result = udp4_lib_lookup2(net, saddr, sport,
daddr, hnum, dif,
hslot2, slot2);
if (!result) {
hash2 = udp4_portaddr_hash(net, htonl(INADDR_ANY), hnum);
slot2 = hash2 & udptable->mask;
hslot2 = &udptable->hash2[slot2];
if (hslot->count < hslot2->count)
goto begin;
result = udp4_lib_lookup2(net, saddr, sport,
htonl(INADDR_ANY), hnum, dif,
hslot2, slot2);
}
rcu_read_unlock();
return result;
}
begin:
result = NULL;
badness = -1;
sk_nulls_for_each_rcu(sk, node, &hslot->head) {
score = compute_score(sk, net, saddr, hnum, sport,
daddr, dport, dif);
if (score > badness) {
result = sk;
badness = score;
}
}
/*
* if the nulls value we got at the end of this lookup is
* not the expected one, we must restart lookup.
* We probably met an item that was moved to another chain.
*/
//雜湊槽中的hash值和搜尋的hash值不匹配則重新搜尋
if (get_nulls_value(node) != slot)
goto begin;
if (result) {
//找到可能的套接字引用數加1
if (unlikely(!atomic_inc_not_zero(&result->sk_refcnt)))
result = NULL;
else if (unlikely(compute_score(result, net, saddr, hnum, sport,
daddr, dport, dif) < badness)) {
sock_put(result);
goto begin;
}
}
rcu_read_unlock();
return result;
}
5、UDP協議在套接字層接受處理
當應用層程式呼叫read函式讀取資料時,套接字層呼叫的是struct proto資料結構中的rcvmsg函式指標指向的udp_rcvmsg函式,udp_rcvmsg函式完成套接字層的資料接受。
5.1、udp_rcvmsg函式輸入引數
struct kiocb *iocb:應用層I/O控制緩衝區。
struct sock *sk:執行接受資料包的套接字結構。
struct msghdr *msg:儲存資料包一些控制資訊和目的地址。
size_t len:資料包長度。
int noblock:應用層層序阻塞標誌。
int flag:套接字接受佇列中的資料包資訊標誌。
int *addr_len:應用層存放傳送方地址長度。
int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t len, int noblock, int flags, int *addr_len)
{
5.2、函式處理流程
首先設定使用者地址空間存放傳送端地址長度的引數,檢視套接字的錯誤資訊佇列中是否有錯誤資訊需要處理,如果錯誤資訊佇列中有資料就呼叫ip_rcv_error函式來處理錯誤資訊。
int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t len, int noblock, int flags, int *addr_len)
{
struct inet_sock *inet = inet_sk(sk);
struct sockaddr_in *sin = (struct sockaddr_in *)msg->msg_name;
struct sk_buff *skb;
unsigned int ulen;
int peeked;
int err;
int is_udplite = IS_UDPLITE(sk);
bool slow;
/*
* Check any passed addresses
*/
//獲取傳送端源地址長度
if (addr_len)
*addr_len = sizeof(*sin);
//套接字接受佇列是否有錯誤資訊要處理
if (flags & MSG_ERRQUEUE)
return ip_recv_error(sk, msg, len);
...
}
然後呼叫接受函式__skb_rcv_datagram函式從套接字緩衝區佇列中讀取下一個資料包的緩衝區,如果該緩衝區的首地址存放在區域性變數sk中,如果skb指標為空說明套接字接受緩衝區沒有等待讀入的資料,函式結束處理。
...
skb = __skb_recv_datagram(sk, flags | (noblock ? MSG_DONTWAIT : 0),
&peeked, &err);
if (!skb)
goto out;
//從資料包中扔掉UDP協議頭
ulen = skb->len - sizeof(struct udphdr);
if (len > ulen)
len = ulen;
else if (len < ulen)
msg->msg_flags |= MSG_TRUNC;
...
檢視使用者程式是否需要讀取比當前資料包負載中更多的資料,在網路資料包的處理過程中一個重要原則是避免對資料包進行多次處理,提高網路資料吞吐量。在接受網路資料包過程中如果要對資料包全校驗和檢驗,則應在複製資料的同時完成校驗和處理。
如果只是對資料包進行部分校驗和檢驗,則應該在資料複製前完成校驗和檢驗。
處理過程:
(1)、對資料包進行部分校驗和檢驗,如果部分校驗和出錯,則應該扔掉資料包。
(2)、如果不需要對資料包進行校驗和,則直接呼叫skb_copy_datagram_iovec函式把skb中的資料從核心地址空間複製到使用者地址空間
(3)、如果需要對資料包進行校驗和檢驗,則呼叫skb_copy_and_csum_datagram_iovec函式將資料包從核心地址空間複製到
使用者地址空間同時完成檢驗和驗證計數。
...
if (len < ulen || UDP_SKB_CB(skb)->partial_cov) {
//只做部分校驗和
if (udp_lib_checksum_complete(skb))
goto csum_copy_err;
}
//不做校驗和直接拷貝資料包
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;
}
...
標記資料包接受的時間戳,如果應用程式提供有效的緩衝區sin來接受資料包傳送端源IP和埠號,就要把資料包資料包的源地址和埠複製到sin指定的使用者緩衝區。
...
//標記資料包的時間戳
sock_recv_ts_and_drops(msg, sk, skb);
/* Copy the address. */
//如果有緩衝區sin複製傳送端IP和埠
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));
}
...
接下來檢視控制資訊標誌cmsg_flags檢視是否設定了IP套接字選項,如果設定了則呼叫ip_cmsg_rev函式完成對IP選項值的提取。
...
//有ip選項則提取ip選項
if (inet->cmsg_flags)
ip_cmsg_recv(msg, skb);
err = len;
if (flags & MSG_TRUNC)
err = ulen;
...
錯誤處理有三個:
(1)、如果從核心地址空間複製資料包到使用者地址空間不成功,則釋放socket buffer和使用者程序持有的套接字。
(2)、如果套接字接受佇列為空直接返回。
(3)、當校驗和出錯扔掉資料包,更新錯誤統計資訊,釋放使用者程序持有的套接字。
udp_msgrcv函式原始碼:
int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t len, int noblock, int flags, int *addr_len)
{
struct inet_sock *inet = inet_sk(sk);
struct sockaddr_in *sin = (struct sockaddr_in *)msg->msg_name;
struct sk_buff *skb;
unsigned int ulen;
int peeked;
int err;
int is_udplite = IS_UDPLITE(sk);
bool slow;
/*
* Check any passed addresses
*/
//獲取傳送端源地址長度
if (addr_len)
*addr_len = sizeof(*sin);
//套接字接受佇列是否有錯誤資訊要處理
if (flags & MSG_ERRQUEUE)
return ip_recv_error(sk, msg, len);
try_again:
skb = __skb_recv_datagram(sk, flags | (noblock ? MSG_DONTWAIT : 0),
&peeked, &err);
if (!skb)
goto out;
//從資料包中扔掉UDP協議頭
ulen = skb->len - sizeof(struct udphdr);
if (len > ulen)
len = ulen;
else if (len < ulen)
msg->msg_flags |= MSG_TRUNC;
/*
* If checksum is needed at all, try to do it while copying the
* data. If the data is truncated, or if we only want a partial
* coverage checksum (UDP-Lite), do it before the copy.
*/
if (len < ulen || UDP_SKB_CB(skb)->partial_cov) {
//只做部分校驗和
if (udp_lib_checksum_complete(skb))
goto csum_copy_err;
}
//不做校驗和直接拷貝資料包
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;
}
if (err)
goto out_free;
if (!peeked)
UDP_INC_STATS_USER(sock_net(sk),
UDP_MIB_INDATAGRAMS, is_udplite);
//標記資料包的時間戳
sock_recv_ts_and_drops(msg, sk, skb);
/* Copy the address. */
//如果有緩衝區sin複製傳送端IP和埠
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));
}
//有ip選項則提取ip選項
if (inet->cmsg_flags)
ip_cmsg_recv(msg, skb);
err = len;
if (flags & MSG_TRUNC)
err = ulen;
//從核心空間複製資料包到應用層空間不成功就是否socket buffer
out_free:
skb_free_datagram_locked(sk, skb);
//佇列為空直接返回
out:
return err;
//校驗和出錯扔掉資料包,更新錯誤統計資訊
csum_copy_err:
slow = lock_sock_fast(sk);
if (!skb_kill_datagram(sk, skb, flags))
UDP_INC_STATS_USER(sock_net(sk), UDP_MIB_INERRORS, is_udplite);
unlock_sock_fast(sk, slow);
if (noblock)
return -EAGAIN;
goto try_again;
}