tcp/ip 协议栈Linux内核源码分析14 udp套接字接收流程一
內核版本:3.4.39
前面兩篇文章分析了UDP套接字從應用層發送數據到內核層的處理流程,這里繼續分析相反的流程,看看數據是怎么從內核送到應用層的。
與發送類似,內核也提供了多個接收數據的系統調用接口,接口定義如下:
#include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);與send類似,recv一般也是面向連接的套接字。原因在于,對于非面向連接的套接字來說,若使用recv接收數據,通過該接口將不能獲得發送端的地址,也就是說不知道這個數據是誰發過來的。所以,如果使用者不關心發送端信息,或者該信息可以從數據中獲得,那么recv接口同樣也可以用于非面向連接的套接字。再來看看recvfrom,它會通過額外的參數src_addr和addrlen,來獲得發送方的地址,其中需要注意的是addrlen,它既是輸入值又是輸出值。最后是recvmsg,它與sendmsg一樣,把接收到的數據和地址都保存在了msg中。其中msg.msg_name和msg.msg_len用于保存接收端地址,而msg.msg_iov用于保存接收到的數據。
先看下recv系統調用的內核源碼:
asmlinkage long sys_recv(int fd, void __user *ubuf, size_t size,unsigned flags) {return sys_recvfrom(fd, ubuf, size, flags, NULL, NULL); }代碼很簡單,recv完全是通過調用sys_recvfrom來實現的,僅僅是將sys_recvfrom的最后兩個參數設置為0而已。
那么接下來就進入recvfrom的源碼:
SYSCALL_DEFINE6(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;struct sockaddr_storage address;int err, err2;int fput_needed;/* 限制讀取字節長度的最大值為整數的最大值INT_MAX */if (size > INT_MAX)size = INT_MAX;/* 從文件描述符得到套接字結構 */sock = sockfd_lookup_light(fd, &err, &fput_needed);if (!sock)goto out;/* 控制信息清零 */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 = (struct sockaddr *)&address;msg.msg_namelen = sizeof(address);/* 如果套接字設置了O_NONBLOCK標志,即非阻塞標志,則設置MSG_DONTWAIT標志,表示此次接收消息,無須等待 */if (sock->file->f_flags & O_NONBLOCK)flags |= MSG_DONTWAIT;/* 調用sock_recvmsg接收數據 */ 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 < 0)err = err2;}fput_light(sock->file, fput_needed); out:return err; }后面的調用流程則為sock_recvmsg→__sock_recvmsg→__sock_recvmsg_nosec。
下面跟蹤第三個接收數據包的系統調用recvmsg,代碼如下:
SYSCALL_DEFINE3(recvmsg, int, fd, struct msghdr __user *, msg,unsigned int, flags) {int fput_needed, err;struct msghdr msg_sys;/* 從文件描述符fd獲得套接字 */ struct socket *sock = sockfd_lookup_light(fd, &err, &fput_needed);if (!sock)goto out;/* __sys_recvmsg用于實現接收數據 */err = __sys_recvmsg(sock, msg, &msg_sys, flags, 0);/* 釋放fd引用(如果需要的話),這也是fput_light與fput的區別 */fput_light(sock->file, fput_needed); out:return err; }?下面進入__sys_recvmsg,代碼如下:
static int __sys_recvmsg(struct socket *sock, struct msghdr __user *msg,struct msghdr *msg_sys, unsigned flags, int nosec) {struct compat_msghdr __user *msg_compat =(struct compat_msghdr __user *)msg;struct iovec iovstack[UIO_FASTIOV];struct iovec *iov = iovstack;unsigned long cmsg_ptr;int err, iov_size, total_len, len;/* kernel mode address */struct sockaddr_storage addr;/* user mode address pointers */struct sockaddr __user *uaddr;int __user *uaddr_len;/* 將消息頭從用戶空間復制到內核空間 */if (MSG_CMSG_COMPAT & flags) {if (get_compat_msghdr(msg_sys, msg_compat))return -EFAULT;} else if (copy_from_user(msg_sys, msg, sizeof(struct msghdr)))return -EFAULT;err = -EMSGSIZE;/* 檢查數據段的個數 */if (msg_sys->msg_iovlen > UIO_MAXIOV)goto out;/* Check whether to allocate the iovec area */err = -ENOMEM;/*為了避免頻繁申請內存,內核在棧上申請了UIO_FASTIOV大小的iovec數組以供iov使用。 當數據段個數超過UIO_FASTIOV時,就需要動態申請內存。*/iov_size = msg_sys->msg_iovlen * sizeof(struct iovec);if (msg_sys->msg_iovlen > UIO_FASTIOV) {iov = sock_kmalloc(sock->sk, iov_size, GFP_KERNEL);if (!iov)goto out;}/** Save the user-mode address (verify_iovec will change the* kernel msghdr to use the kernel address space)*//* 驗證用戶傳遞的數據段參數和地址參數 */uaddr = (__force void __user *)msg_sys->msg_name;uaddr_len = COMPAT_NAMELEN(msg);if (MSG_CMSG_COMPAT & flags) {err = verify_compat_iovec(msg_sys, iov, &addr, VERIFY_WRITE);} elseerr = verify_iovec(msg_sys, iov, &addr, VERIFY_WRITE);if (err < 0)goto out_freeiov;total_len = err;cmsg_ptr = (unsigned long)msg_sys->msg_control;/* 確保消息標志中只有內核支持的兩個標志 */msg_sys->msg_flags = flags & (MSG_CMSG_CLOEXEC|MSG_CMSG_COMPAT);/* 如果套接字為非阻塞,則設置標志位為不等待(非阻塞) */if (sock->file->f_flags & O_NONBLOCK)flags |= MSG_DONTWAIT;/* 根據安全檢查標志,調用不同的接收函數,但最終都會調用到sock_recvmsg */err = (nosec ? sock_recvmsg_nosec : sock_recvmsg)(sock, msg_sys,total_len, flags);if (err < 0)goto out_freeiov;len = err;/* 將發送端的地址復制到用戶空間 */if (uaddr != NULL) {err = move_addr_to_user(&addr,msg_sys->msg_namelen, uaddr,uaddr_len);if (err < 0)goto out_freeiov;}err = __put_user((msg_sys->msg_flags & ~MSG_CMSG_COMPAT),COMPAT_FLAGS(msg));if (err)goto out_freeiov;if (MSG_CMSG_COMPAT & flags)err = __put_user((unsigned long)msg_sys->msg_control - cmsg_ptr,&msg_compat->msg_controllen);elseerr = __put_user((unsigned long)msg_sys->msg_control - cmsg_ptr,&msg->msg_controllen);if (err)goto out_freeiov;err = len;out_freeiov:if (iov != iovstack)sock_kfree_s(sock->sk, iov, iov_size); out:return err; }由上面的代碼可以看出,內核提供的三個接收數據包的系統調用,最終確實如我們所期望的,都會走到一個共同的函數__sock_recvmsg_nose里。下面來看一下這個函數,代碼如下:
static inline int __sock_recvmsg_nosec(struct kiocb *iocb, struct socket *sock,struct msghdr *msg, size_t size, int flags) {struct sock_iocb *si = kiocb_to_siocb(iocb);sock_update_classid(sock->sk);/* 設置套接字異步IO信息 */si->sock = sock;si->scm = NULL;si->msg = msg;si->size = size;si->flags = flags;/* 根據不同的套接字類型,調用不同的數據接收函數 */return sock->ops->recvmsg(iocb, sock, msg, size, flags); }?根據上面的代碼,后面的接收流程就要依賴于具體的協議實現了。
我們來分析一下相對簡單的UDP協議的數據包接收流程,代碼如下:
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);/* 讓sin指向msg_name,用于保存發送端地址 */struct sockaddr_in *sin = (struct sockaddr_in *)msg->msg_name;struct sk_buff *skb;unsigned int ulen, copied;int peeked, off = 0;int err;int is_udplite = IS_UDPLITE(sk);bool slow;/** Check any passed addresses*//* 若addr_len不為NULL,即用戶傳遞了地址長度參數。進入了具體的協議層,已經可以明確地址的長度信息了。 */ if (addr_len)*addr_len = sizeof(*sin);/* 用戶設置了MSG_ERRQUEUE標志,用于接收錯誤消息。因為這個應用并不廣泛,因此在此忽略這種情況,不進入該函數。 */if (flags & MSG_ERRQUEUE)return ip_recv_error(sk, msg, len);try_again:/* 接收了一個數據報文 */skb = __skb_recv_datagram(sk, flags | (noblock ? MSG_DONTWAIT : 0),&peeked, &off, &err);/* 若沒有收到報文,則直接退出 */if (!skb)goto out;/* 得到UDP的數據長度 */ulen = skb->len - sizeof(struct udphdr);/* 要復制的長度被初始化為用戶指定的長度 */copied = len;/* 若復制長度大于UDP的數據長度,則調整復制長度為數據長度。若復制長度小于數據長度,則設置標志MSG_TRUNC,表示數據發生了截斷。 */if (copied > ulen)copied = ulen;else if (copied < 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 (copied < ulen || UDP_SKB_CB(skb)->partial_cov) {/* 進行UDP校驗和校驗 */if (udp_lib_checksum_complete(skb))goto csum_copy_err;}/* 判斷是否需要進行校驗和校驗 */if (skb_csum_unnecessary(skb))/* 若不需要進行校驗,則直接復制數據包內容到msg_iov中 */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 csum_copy_err;}/* 復制錯誤檢查 */if (err)goto out_free;/* 如果不是peek動作,則增加相應的統計計數 */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. *//* 如果用戶指定了保存對端地址的參數,則從數據包中復制地址和端口信息 */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));}/* 設置了接收控制消息 */if (inet->cmsg_flags)/* 接收控制消息如TTL、TOS等 */ip_cmsg_recv(msg, skb);/* 設置了已復制的字節長度 */err = copied;if (flags & MSG_TRUNC)err = ulen;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;/* starting over for a new packet */msg->msg_flags &= ~MSG_TRUNC;goto try_again; }從上面的代碼中,我們可以得到一個大部分書中都不會涉及的信息。先想一想,在讀取一個UDP數據包時,如果傳遞給接口的緩存空間小于UDP數據包的實際大小時,結果會是什么樣的呢?對于TCP來說,這個問題比較簡單,因為其是流協議,沒有數據報文邊界,所以這次未讀取的數據,會在下一次讀取時被復制。但是UDP是基于數據包的,從上面的內核源碼可以看到,當緩存小于UDP報文的實際大小時,內核會將報文截斷,只復制緩存大小的數據,同時設置上MSG_TRUNC截斷標志。這種情況,是很難從書本上了解到的,只有通過閱讀源碼才能理解其中的奧妙。
再進入__skb_recv_datagram,來查看UDP是如何接收報文的,代碼如下:
struct sk_buff *__skb_recv_datagram(struct sock *sk, unsigned flags,int *peeked, int *off, int *err) {struct sk_buff *skb;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;/* 得到超時時間,如果設置了MSG_DONTWAIT,則超時為0。 */timeo = sock_rcvtimeo(sk, flags & MSG_DONTWAIT);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 correct in any case. 8)*/unsigned long cpu_flags;struct sk_buff_head *queue = &sk->sk_receive_queue;spin_lock_irqsave(&queue->lock, cpu_flags);/* 得到接收隊列的第一個數據包 */skb_queue_walk(queue, skb) {*peeked = skb->peeked;/* 如果只是查看動作,則要增加數據包的引用計數,并不用把數據包從隊列中移除。 */if (flags & MSG_PEEK) {if (*off >= skb->len && skb->len) {*off -= skb->len;continue;}skb->peeked = 1;atomic_inc(&skb->users);} else/* 將數據包從接收隊列中刪除 */__skb_unlink(skb, queue);/* 得到了數據包,直接返回 */spin_unlock_irqrestore(&queue->lock, cpu_flags);return skb;}spin_unlock_irqrestore(&queue->lock, cpu_flags);/* User doesn't want to wait *//* 若已經沒有了剩余的超時時間,則跳轉到no_packet并返回NULL */error = -EAGAIN;if (!timeo)goto no_packet;/* 使task在套接字上等待 */} while (!wait_for_packet(sk, err, &timeo));return NULL;no_packet:*err = error;return NULL; } EXPORT_SYMBOL(__skb_recv_datagram);如果當前的UDP套接字沒有數據包,則會進入wait_for_packet進行等待,代碼如下:
static int wait_for_packet(struct sock *sk, int *err, long *timeo_p) {int error;/* 定義等待隊列和回調的喚醒函數 */DEFINE_WAIT_FUNC(wait, receiver_wake_function);/* 初始化等待隊列,需要注意的是TASK_INTERRUPTIBLE。這表明進程在睡眠等待時,是可以被中斷的。 */prepare_to_wait_exclusive(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);/* Socket errors? *//* 檢查套接字是否出錯,如被RESET。如有錯誤,則直接退出。 */error = sock_error(sk);if (error)goto out_err;/* 若接收隊列不為空,則可以直接退出 */if (!skb_queue_empty(&sk->sk_receive_queue))goto out;/* Socket shut down? *//* 檢查套接字是否已經做了接收半關閉 */if (sk->sk_shutdown & RCV_SHUTDOWN)goto out_noerr;/* Sequenced packets can come disconnected.* If so we report the problem*//* 如果套接字是基于連接的,并且不是處于已連接狀態或監聽狀態,則報錯退出 */ error = -ENOTCONN;if (connection_based(sk) &&!(sk->sk_state == TCP_ESTABLISHED || sk->sk_state == TCP_LISTEN))goto out_err;/* 是否有未處理的信號 *//* handle signals */if (signal_pending(current))goto interrupted;/* 將當前進程調度出去,直到超時,即進程已經休眠了設定的超時時間。但是由于某些原因,進程被提前喚醒,所以需要保存返回的時間*timeo_p,表示還剩下多少時間。 */error = 0;*timeo_p = schedule_timeout(*timeo_p); out:finish_wait(sk_sleep(sk), &wait);return error; interrupted:error = sock_intr_errno(*timeo_p); out_err:*err = error;goto out; out_noerr:*err = 0;error = 1;goto out; }?至此,UDP數據包的接收流程已經跟蹤完畢,還剩下一個問題,內核接收到的數據包如何保存到套接字接收隊列里。
這個放到下一篇文章里。
?
參考文檔:
1.?《Linux環境編程:從應用到內核》
2.??淺析Linux網絡子系統(一)?
總結
以上是生活随笔為你收集整理的tcp/ip 协议栈Linux内核源码分析14 udp套接字接收流程一的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: tcp/ip 协议栈Linux内核源码分
- 下一篇: tcp/ip 协议栈Linux内核源码分