前面了解過 sk 有一個接收隊列,用于存儲接 收到的 skb,對于 socket 層面上來講,資料接收,就
是要把資料從這個隊列中取出來,交給上層使用者态。這裡涉及到出隊操作,但是,要了解如何出隊,
就 得了解傳輸層協定如何入隊。前面一直用 tcp協定來分析,現在還沒有把整個 tcp棧分析出來,
要再繼續用 tcp 協定來分析,就有點問題了,是以,資料的接 收和發送,都将以 udp 協定來分析。
雖然它很簡單,但同樣也反應了 socket 層資料與接收的全部核心内容與思路。我以希望,下一步拿
下 tcp 協定後,再 把這部份的 tcp 實作補上來。
<b>udp 層的資料接收</b>
udp 層的資料接收,對于 socket 而言,就是接收隊列的入隊操作。在 ip 層中,如果是本地資料,則
會交由 ip_local_deliver_finish()函數處理,它會根據傳輸層協定的類型,交由相應的處理函數,對于udp 協定而言,
就是 udp_rcv():
/*
* All we need to do is get the socket, and then do a checksum.
*/
int udp_rcv(struct sk_buff *skb)
{
struct sock *sk;
struct udphdr *uh;
unsigned short ulen;
struct rtable *rt = (struct rtable*)skb->dst;
u32 saddr = skb->nh.iph->saddr;
u32 daddr = skb->nh.iph->daddr;
int len = skb->len;
/*
* 資料包至少應有 UDP 首部長度.
*/
if (!pskb_may_pull(skb, sizeof(struct udphdr)))
goto no_header;
/*擷取 udp 首部指針*/
uh = skb->h.uh;
/* 資料長度,含首部長度 */
ulen = ntohs(uh->len);
/* 資料包長度不夠:UDP 長度比 skb 長度小,意味着資料的丢失,而 udp 長度比 udp 首部
* 還要小,好像這個不太可能,除非封包出錯 ^o^*/
if (ulen > len || ulen goto short_packet;
/* 截去 udp 封包後面的多餘封包 */
if (pskb_trim(skb, ulen))
goto short_packet;
/* 開始 udp 校驗和計算,主要查依賴于 skb 的 ip_summumed 字段的設定來決定是否需要行校驗和計算 */
if (udp_checksum_init(skb, uh, ulen, saddr, daddr) goto csum_error;
/* 轉換多點傳播或廣播處理例程 */
if(rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))
return udp_v4_mcast_deliver(skb, uh, saddr, daddr);
/* 查找資料段對應的 socket結構的 sk */
sk = udp_v4_lookup(saddr, uh->source, daddr, uh->dest, skb->dev->ifindex);
if (sk != NULL) {
/* 找到了,資料包進入 UDP 的 socket 的接收隊列*/
int ret = udp_queue_rcv_skb(sk, skb);
sock_put(sk);
/* a return value > 0 means to resubmit the input,
but * it it wants the return to be -protocol, or 0
*/
if (ret > 0)
return -ret;
return 0;
}
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
goto drop;
/* 沒有對應的 socket. 如果校驗和錯誤,則丢棄它 */
if (udp_checksum_complete(skb))
goto csum_error;
/* 發送一個目的不可達封包 */
UDP_INC_STATS_BH(UDP_MIB_NOPORTS);
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:
NETDEBUG(if (net_ratelimit())
printk(KERN_DEBUG "UDP: short packet: From %u.%u.%u.%u:%u %d/%d to
%u.%u.%u.%u:%u/n",
NIPQUAD(saddr),
ntohs(uh->source),
ulen,
len,
NIPQUAD(daddr),
ntohs(uh->dest)));
no_header:
UDP_INC_STATS_BH(UDP_MIB_INERRORS);
csum_error:
/*
* RFC1122: OK. Discards the bad packet silently (as far as
* the network is concerned, anyway) as per 4.1.3.4 (MUST). */
printk(KERN_DEBUG "UDP: bad checksum. From %d.%d.%d.%d:%d to
%d.%d.%d.%d:%d ulen %d/n",
ntohs(uh->dest),
ulen));
drop:
}
函數的核心思想,是根據 skb,查找到與之對應的 sk,調用 udp_v4_lookup()函數實作——udp 與 tcp
一樣,socket有一個 hash表,這個查找,就是查找 hash 表的過程。
如果找到了對應的 sk,則進入 udp_queue_rcv_skb()函數:
/* returns:
* -1: error
* 0: success
* >0: "udp encap" protocol resubmission
*
* Note that in the success and error cases, the skb is assumed to
* have either been requeued or freed.
static int udp_queue_rcv_skb(struct sock * sk, struct sk_buff *skb)
/* 擷取 sk 對應的udp_sock 結構指針 */
struct udp_sock *up = udp_sk(sk);
* Charge it to the socket, dropping if the queue is full.
if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb)) {
kfree_skb(skb);
return -1;
if (up->encap_type) {
/*
* This is an encapsulation socket, so let's see if this is
* an encapsulated packet.
* If it's a keepalive packet, then just eat it.
* If it's an encapsulateed packet, then pass it to the
* IPsec xfrm input and return the response
* appropriately. Otherwise, just fall through and
* pass this up the UDP socket.
int ret;
ret = udp_encap_rcv(sk, skb);
if (ret == 0) {
/* Eat the packet .. */
kfree_skb(skb);
return 0;
}
if (ret /* process the ESP packet */
ret = xfrm4_rcv_encap(skb, up->encap_type);
UDP_INC_STATS_BH(UDP_MIB_INDATAGRAMS);
/* FALLTHROUGH -- it's a UDP Packet */
/* 如果需要校驗 */
if (sk->sk_filter && skb->ip_summed != CHECKSUM_UNNECESSARY) {
/* 那就校驗它吧 */
if (__udp_checksum_complete(skb)) {
/* 結果校驗出錯,那就算了吧 */
UDP_INC_STATS_BH(UDP_MIB_INERRORS);
return -1;
/* 已經校驗過了,就設定不用再校驗了 */
skb->ip_summed = CHECKSUM_UNNECESSARY;
/* 設用 sock_queue_rcv_skb 入隊 */
if (sock_queue_rcv_skb(sk,skb) UDP_INC_STATS_BH(UDP_MIB_INERRORS);
UDP_INC_STATS_BH(UDP_MIB_INDATAGRAMS);
return 0; }[/code]
encap_type字段用于判斷 udp 包,是否是一個 IPSEC 協定的封裝封包,這裡不關心 ipsec,是以接下
來的工作,就是校驗和計算,然後緊跟着調用 sock_queue_rcv_skb():
static inline int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
int err = 0;
int skb_len;
/* Cast skb->rcvbuf to unsigned... It's pointless, but reduces
number of warnings when compiling with -W --ANK
if (atomic_read(&sk->sk_rmem_alloc) + skb->truesize >=
(unsigned)sk->sk_rcvbuf) {
err = -ENOMEM;
goto out;
/* 進入 socket 層的包過濾 */
err = sk_filter(sk, skb, 1);
if (err)
/* 設定 skb 的一些必要的指針和計數器變量
* dev:關連備設指針;
* sk:所對應的 sk 結構;
* destructor:函數指針可以初始化成一個在緩沖區釋放時完成某些動作的函數。
* 如果 skb 不屬于一個 socket,則其常為 NULL。反之,這個函數會被指派為 sock_rfree 或sock_wfree.
* 用于更新 socket的隊列中的記憶體容量。*/
skb->dev = NULL;
skb_set_owner_r(skb, sk);
/* 在skb 入隊之前,儲存其長度至臨時變量 skb_len,這是因為一旦 skb 入隊後,它将被
* 其它線程處理,skb 命運未知。。。。。。,而 len 值後面還會用到。
skb_len = skb->len;
/* 将skb 加入 sk的接收隊列 */
skb_queue_tail(&sk->sk_receive_queue, skb);
/* 喚醒 socket 上的接收線程? */
if (!sock_flag(sk, SOCK_DEAD))
sk->sk_data_ready(sk, skb_len); out:
return err;
對于 udp 而言,直接調用 skb_queue_tail(),将 skb 加入至 sk的 sk_receive_queue 隊列即可。
<b>udp 層的資料出隊操作</b>
了解了資料包如何被加入隊列,上層 socket 的資料接收,就是從這個隊列中來取資料。udp 對應的
取資料的函數 udp_recvmsg(),後面再來看它是如何被調用的,現在先來分析它的實作:
* This should be easy, if there is something there we
* return it, otherwise we block.
static 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); /* 取得 sk對應的 inet_sock 指針 */
struct sockaddr_in *sin = (struct sockaddr_in *)msg->msg_name;
struct sk_buff *skb;
int copied, err;
* 校驗位址長度
if (addr_len)
*addr_len=sizeof(*sin);
/* 如果 sk 隊列中有錯誤的資訊,則設用 ip_recv_error函數 */
if (flags & MSG_ERRQUEUE)
return ip_recv_error(sk, msg, len);
try_again:
/* 出隊 */
skb = skb_recv_datagram(sk, flags, noblock, &err);
if (!skb)
/* 需要拷貝的資料不包含 UDP 首部 */
copied = skb->len - sizeof(struct udphdr);
/* 如果使用者提供的緩存不夠,則設定為隻拷貝使用者需要的長度,并設定截短資料标志 */
if (copied > len) {
copied = len;
msg->msg_flags |= MSG_TRUNC; }
if (skb->ip_summed==CHECKSUM_UNNECESSARY) {
/* 拷貝資料至使用者空間 */
err = skb_copy_datagram_iovec(skb, sizeof(struct udphdr), msg->msg_iov, copied);
} else if (msg->msg_flags&MSG_TRUNC) {
if (__udp_checksum_complete(skb))
goto csum_copy_err;
err = skb_copy_datagram_iovec(skb, sizeof(struct udphdr), msg->msg_iov,
copied);
} else {
err = skb_copy_and_csum_datagram_iovec(skb, sizeof(struct udphdr), msg->msg_iov);
if (err == -EINVAL)
goto out_free;
/* 更新 sk 的接收時間戳,根據 SOCK_RCVTSTAMP标志的設定來決定:
* 選擇目前時間還是skb buffer的接收時間 */
sock_recv_timestamp(msg, sk, skb);
/* 拷貝位址等資訊. */
if (sin)
{
sin->sin_family = AF_INET;
sin->sin_port = skb->h.uh->source;
sin->sin_addr.s_addr = skb->nh.iph->saddr;
memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
}
/* 檢視是否設定了一些控制标志,檢視某些IP socket選項是否被設定,例如,設定了IP_TOS socket 選項,
* 把 IP首部中的tos字段拷貝至使用者空間等等,這個工作是由 ip_cmsg_recv 函數完成的 */
if (inet->cmsg_flags)
ip_cmsg_recv(msg, skb);
/* 設定拷貝的位元組數,如果資料段已經被截短,則傳回原始實際的長度 */
err = copied;
if (flags & MSG_TRUNC)
err = skb->len - sizeof(struct udphdr);
out_free:
skb_free_datagram(sk, skb);
out:
csum_copy_err:
/* Clear queue. */
if (flags&MSG_PEEK) {
int clear = 0;
spin_lock_bh(&sk->sk_receive_queue.lock);
if (skb == skb_peek(&sk->sk_receive_queue)) {
__skb_unlink(skb, &sk->sk_receive_queue);
clear = 1;
spin_unlock_bh(&sk->sk_receive_queue.lock);
if (clear)
if (noblock)
return -EAGAIN;
goto try_again;
}[/code]
skb 的出隊,是通過調用 skb_recv_datagram()函數,出隊操作時,隊列中可能沒有資料,如果是阻塞,
則需要一直睡眠等待,直到逾時或隊列中有資料而被喚醒。
另外出隊操作,還分為兩種情況,一種是将資料包從隊列中取中來,将其從原有隊列中删除,還有一種可能是設定了
MSG_PEEK 标志,它意味着:“檢視目前資料,資料将被複制到緩沖區中,但并不從輸入隊列中删除”。
struct sk_buff *skb_recv_datagram(struct sock *sk, unsigned flags,
int noblock, int *err)
long timeo;
* Caller is allowed not to check sk->sk_err before skb_recv_datagram()
int error = sock_error(sk);
if (error)
goto no_packet;
/* 擷取逾時時間*/
timeo = sock_rcvtimeo(sk, noblock);
/* 這個循環将一直等到隊列中有資料包,直到逾時 */
do {
/* Again only user level code calls this function, so nothing
* interrupt level will suddenly eat the receive_queue.
*
* Look at current nfs client by the way...
* However, this function was corrent in any case. 8)
if (flags & MSG_PEEK) {
unsigned long cpu_flags;
/* 如果設定了 MSG_PEEK,則設用 skb_peek,這裡要對接收隊列加鎖的原因在于*/
spin_lock_irqsave(&sk->sk_receive_queue.lock,
cpu_flags);
skb = skb_peek(&sk->sk_receive_queue);
if (skb)
atomic_inc(&skb->users);
spin_unlock_irqrestore(&sk->sk_receive_queue.lock,
cpu_flags);
} else
skb = skb_dequeue(&sk->sk_receive_queue); /* 直接出隊 */
/* 找到要收獲的葫蘆了,摘下來 */
if (skb)
return skb;
/* 如果是非阻塞,就不等了,直接跳出循環,傳回 */
error = -EAGAIN;
if (!timeo)
goto no_packet;
} while (!wait_for_packet(sk, err, &timeo));
return NULL;
no_packet:
*err = error;
繼續回到 udp_recvmsg 函數中來,當取出 skb 後,需要将它拷貝至使用者空間。使用者空間的資料緩存,
用了一個很重要的資料結構 struct msghdr來表示:
[code]struct msghdr {
void *msg_name; /* Socket name */
int msg_namelen; /* Length of name */
struct iovec *msg_iov; /* Data blocks */
__kernel_size_t msg_iovlen; /* Number of blocks */
void *msg_control; /* Per protocol magic (eg BSD file descriptor passing) */
__kernel_size_t msg_controllen; /* Length of cmsg list */
unsigned msg_flags;
};
結構中的 msg_name、 msg_namelen 以及 msg_flags分别對應于 sys_sendto()[其它接收/發送函數類似]
的參數 addr、addr_len 以及 flags。指針 msg_control 可以指向一個附加的資料結構,用來提供一些
附加的控制資訊。後面可以看到 ip_cmsg_recv()使用了它。最重要的是,結構中的指針 msg_iov,
指向一個 iovec 結構資料,而 msg_iovlen 則指明了這個數組 的大小:
struct iovec
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
數組中的每一個元互素,都是 struct iovec 結構, 稱之為"io 向量",由指向資料緩沖區的指針 iov_base
和表示緩沖區中資料長度的 iov_len構成。這樣,一個 msghdr中的資料,就由控制資訊 msg_control
和若幹個"io向量"組成。
一個疑問是,為什麼不設定為一個緩沖區,而要分為若幹個緩沖區呢?一個很重要的原因就是,每
個資料封包的大小一緻,對于 ip 包而言。46-1500 都是為 可能的,那麼如果是一個緩沖區的話,
就得定義為一個“最大值”,但是如果絕大部份的包,都小于這個最大值,例如為 256 或 512,那麼
記憶體空間就浪費得太 多了……。是以,一個好的辦法是,用一個個小的緩沖區将資料切分,要浪
費,也浪費最後一個緩沖區的空間。——這種設計跟 Unix上著名的 mbuf好像是一 緻的。
OK,了解了 msghdr 的結構後,再來看資料的拷貝工作,拷貝的核心函數是 memcpy_toiovec:
* Copy kernel to iovec. Returns -EFAULT on error.
* Note: this modifies the original iovec.
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;
因為每個 io向量的緩沖區可能很小,例如 100位元組,而要拷貝的資料很長,例如 1000 位元組。這樣,需要在一個循環裡,
将資料拷貝至若幹個 io 向量數組元素當中去。
回到 udp_recvmsg 中來,它通過設用 skb_copy_datagram_iovec()函數完成資料的拷貝工作。來看看
它是如何調用 memcpy_toiovec 的,當然,類似于這樣:
int skb_copy_datagram_iovec(const struct sk_buff *skb, int offset,
struct iovec *to, int len)
return memcpy_toiovec(to, skb->data+offset, len);
這樣的調用,該是多麼美好呀,這樣的日子曾經是有過,不過已經一去不複返了。
考慮到 skb 結構的複雜性,例如分片,拷貝工作要複雜得多:
/**
* skb_copy_datagram_iovec - Copy a datagram to an iovec.
* @skb: buffer to copy
* @offset: offset in the buffer to start copying from
* @to: io vector to copy to
* @len: amount of data to copy from buffer to iovec
* Note: the iovec is modified during the copy.
int start = skb_headlen(skb); /* start 表示要從 skb
的哪裡開始拷貝資料 */
int i, copy = start - offset; /*
offset 表示調用者指出的緩存開始拷貝的偏移位元組,一般情況下,
* 它 與 start 是重疊的,表示不需要拷貝資料包的首部 */
/* 需要拷貝首部. */
if (copy > 0) {
if (copy > len)
copy = len;
if (memcpy_toiovec(to, skb->data + offset, copy))
goto fault;
if ((len -= copy) == 0)
offset += copy;
/* 周遊每一個分片 */
for (i = 0; i nr_frags; i++) {
int end;
BUG_TRAP(start
end = start + skb_shinfo(skb)->frags[i].size;
if ((copy = end - offset) > 0) {
int err;
u8 *vaddr;
skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
struct page *page = frag->page;
if (copy > len)
copy = len;
vaddr = kmap(page);
err = memcpy_toiovec(to, vaddr + frag->page_offset +
offset - start, copy);
kunmap(page);
if (err)
goto fault;
if (!(len -= copy))
return 0;
offset += copy;
start = end;
if (skb_shinfo(skb)->frag_list) {
struct sk_buff *list = skb_shinfo(skb)->frag_list;
for (; list; list = list->next) {
int end;
BUG_TRAP(start end = start + list->len;
if ((copy = end - offset) > 0) {
if (copy > len)
copy = len;
if (skb_copy_datagram_iovec(list, offset - start, to, copy))
goto fault;
if ((len -= copy) == 0)
return 0;
offset += copy;
}
start = end;
if (!len)
fault:
return -EFAULT;
現在還沒有分析 ip 的分片,是以,要完全了解這個函數的代碼有點難度,等到 ip 分片分析完了,再來補充完整它。
<b>socket 層的資料接收</b>
recv(2)、recvfrom(2)和 recvmsg(3),在 sys_socketcall()中,最終都會歸于一個統一的系統調用sock_recvmsg。
例如:
asmlinkage long sys_recv(int fd, void __user * ubuf, size_t size, unsigned flags)
return sys_recvfrom(fd, ubuf, size, flags, NULL, NULL);
sys_recv 轉向了 sys_recvfrom():
* Receive a frame from the socket and optionally record the address of the
* sender. We verify the buffers are writable and if needed move the * sender address from kernel to user space.
asmlinkage long sys_recvfrom(int fd, void __user * ubuf, size_t size, unsigned flags,
struct sockaddr __user *addr, int __user *addr_len)
struct socket *sock;
struct iovec iov;
struct msghdr msg;
char address[MAX_SOCK_ADDR];
int err,err2;
sock = sockfd_lookup(fd, &err);
if (!sock)
msg.msg_control=NULL;
msg.msg_controllen=0;
msg.msg_iovlen=1;
msg.msg_iov=&iov;
iov.iov_len=size;
iov.iov_base=ubuf;
msg.msg_name=address;
msg.msg_namelen=MAX_SOCK_ADDR;
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
err=sock_recvmsg(sock, &msg, size, flags);
if(err >= 0 && addr != NULL)
err2=move_addr_to_user(address, msg.msg_namelen, addr, addr_len);
if(err2 err=err2;
sockfd_put(sock);
函數先調用 sockfd_lookup,根據 socket 描述符查找以相應的 sock 結構,然後封裝了一個 msghdr結
構後,接着調用 sock_recvmsg()。
int sock_recvmsg(struct socket *sock, struct msghdr *msg,
size_t size, int flags)
{ struct kiocb iocb;
struct sock_iocb siocb;
int ret;
init_sync_kiocb(&iocb, NULL);
iocb.private = &siocb;
ret = __sock_recvmsg(&iocb, sock, msg, size, flags);
if (-EIOCBQUEUED == ret)
ret = wait_on_sync_kiocb(&iocb);
return ret;
iocb 和 siocb 用于核心和 socket 的 io 控制,函數中主要初始化了 iocb,siocb 的初始化,在
__sock_recvmsg 中完成。 進一步的按收動作,是通過__sock_recvmsg 函數設用完成的:
[code]static inline int __sock_recvmsg(struct kiocb *iocb, struct socket *sock,
struct msghdr *msg, size_t size, int flags)
int err;
struct sock_iocb *si = kiocb_to_siocb(iocb);
si->sock = sock;
si->scm = NULL;
si->msg = msg;
si->size = size;
si->flags = flags;
err = security_socket_recvmsg(sock, msg, size, flags);
return err;
return sock->ops->recvmsg(iocb, sock, msg, size, flags);
初始化完 siocb,也就是 si 後,調用協定簇的 recvmsg 函數,對于 UDP 而言,是
sock_common_recvmsg():
int sock_common_recvmsg(struct kiocb *iocb, struct socket *sock,
struct msghdr *msg, size_t size, int flags)
struct sock *sk = sock->sk; /*取得 sock 結構對應的 sk指針*/
int addr_len = 0;
err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags &
MSG_DONTWAIT,
flags &
~MSG_DONTWAIT, &addr_len);
if (err >= 0)
msg->msg_namelen = addr_len;
于是,udp_recvmsg 就被調用了。整個過程也就結束了。
最後再回到 sys_recvfrom中來,如果使用者調用時,需要傳回對方的位址資訊:
就需要調用 move_addr_to_user() 函數來完成,前面分析 msghdr 結構時已經提到其 msg_name 成員
變量,它包含了位址的相應資訊,msg.msg_namelen 成員變量,決定了位址的長度資訊:
int move_addr_to_user(void *kaddr, int klen, void __user *uaddr, int __user *ulen)
int len;
if((err=get_user(len, ulen))) //緩存長度校驗
if(len>klen)
len=klen;
if(len MAX_SOCK_ADDR)
return -EINVAL;
if(len)
if(copy_to_user(uaddr,kaddr,len)) //拷貝位址資訊
return -EFAULT;
* "fromlen shall refer to the value before truncation.."
* 1003.1g
return __put_user(klen, ulen);