linux 內核tcp接收數據的實現

相比於發送數據,接收數據更復雜一些。接收數據這裏和3層的接口是tcp_v4_rcv(我前面的blog有介紹3層和4層的接口的實現).而4層和用戶空間,也就是系統調用是socket_recvmsg(其他的讀取函數也都會調用這個函數).而這個系統調用會調用__sock_recvmsg.下面我們就先來看下這個函數。 

它的主要功能是初始化sock_iocb,以便與將來數據從內核空間拷貝到用戶空間。然後調用 
recvmsg這個虛函數(tcp協議的話也就是tcp_recvmsg). 

Java代碼  收藏代碼
  1. static inline int __sock_recvmsg(struct kiocb *iocb, struct socket *sock,  
  2.                  struct msghdr *msg, size_t size, int flags)  
  3. {  
  4.     int err;  
  5.     struct sock_iocb *si = kiocb_to_siocb(iocb);  
  6. ///初始化si。  
  7.     si->sock = sock;  
  8.     si->scm = NULL;  
  9.     si->msg = msg;  
  10.     si->size = size;  
  11.     si->flags = flags;  
  12.   
  13.     err = security_socket_recvmsg(sock, msg, size, flags);  
  14.     if (err)  
  15.         return err;  
  16. //調用tcp_recvmsg  
  17.     return sock->ops->recvmsg(iocb, sock, msg, size, flags);  
  18. }  


內核對待數據的接收分爲2部分,一部分是當用戶是阻塞的讀取數據時,這時如果有數據則是直接拷貝到用戶空間。而另一方面,如果是非阻塞,則會先把數據拷貝到接收隊列。 

而在內核中這個隊列分爲3種形式。分別是: 

1 sock域結構的 sk_backlog隊列。 

2 tcp_sock的ucopy.prequeue隊列。 

3 sock結構的 receive_queue隊列。 

我們先來看兩個主要的結構體,然後再來解釋這3各隊列的區別,首先是ucopy結構. 

這個結構表示將要直接複製到用戶空間的數據。 

Java代碼  收藏代碼
  1. /* Data for direct copy to user */  
  2.     struct {  
  3. ///prequeue隊列。  
  4.         struct sk_buff_head prequeue;  
  5. ///表示當前所處的進程,其實也就是skb的接受者。  
  6.         struct task_struct  *task;  
  7. ///數據區  
  8.         struct iovec        *iov;  
  9. ///prequeue隊列總的所佔用的內存大小  
  10.         int         memory;  
  11. ///這個域表示用戶所請求的長度(要注意這個值是可變的,隨着拷貝給用戶的數據而減少)  
  12.         int         len;  
  13. ........................  
  14.     } ucopy;  


接下來是sock的sock_lock結構. 

內核的註釋很詳細,這個鎖主要是用來對軟中斷和進程上下文之間提供一個同步。 

Java代碼  收藏代碼
  1. /* This is the per-socket lock.  The spinlock provides a synchronization 
  2.  * between user contexts and software interrupt processing, whereas the 
  3.  * mini-semaphore synchronizes multiple users amongst themselves. 
  4.  */  
  5. typedef struct {  
  6. ///自選鎖  
  7.     spinlock_t      slock;  
  8. ///如果有用戶進程在使用這個sock 則owned爲1,否則爲0  
  9.     int         owned;  
  10. ///等待隊列,也就是當sock被鎖住後,等待使用這個sock對象。  
  11.     wait_queue_head_t   wq;  
  12.     /* 
  13.      * We express the mutex-alike socket_lock semantics 
  14.      * to the lock validator by explicitly managing 
  15.      * the slock as a lock variant (in addition to 
  16.      * the slock itself): 
  17.      */  
  18. #ifdef CONFIG_DEBUG_LOCK_ALLOC  
  19.     struct lockdep_map dep_map;  
  20. #endif  
  21. } socket_lock_t;  


然後來看3個隊列的區別。 

首先sk_backlog隊列是噹噹前的sock在進程上下文中被使用時,如果這個時候有數據到來,則將數據拷貝到sk_backlog. 

prequeue則是數據buffer第一站一般都是這裏,如果prequeue已滿,則會拷貝數據到receive_queue隊列種。 

最後一個receive_queue也就是進程上下文第一個取buffer的隊列。(後面介紹tcp_recvmsg時會再介紹這3個隊列). 

這裏爲什麼要有prequeue呢,直接放到receive_queue不就好了.這裏我是認receive_queue的處理比較繁瑣(看tcp_rcv_established的實現就知道了,分爲slow path和fast path),而軟中斷每次只能處理一個數據包(在一個cpu上),因此爲了軟中斷能儘快完成,我們就可以先將數據放到prequeue中(tcp_prequeue),然後軟中斷就直接返回.而處理prequeue就放到進程上下文去處理了. 

最後在分析tcp_v4_rcv和tcp_recvmsg之前,我們要知道tcp_v4_rcv還是處於軟中斷上下文,而tcp_recvmsg是處於進程上下文,因此比如socket_lock_t纔會提供一個owned來鎖住對應的sock。而我們也就是需要這3個隊列來進行軟中斷上下文和進程上下文之間的通信。最終當數據拷貝到對應隊列,則軟中斷調用返回。這裏要注意的是相同的函數在軟中斷上下文和進程上下文種調用是不同的,我們下面就會看到(比如tcp_rcv_established函數) 

ok,現在來看tcp_v4_rcv的源碼。這個函數是在軟中斷上下文中被調用的,我們這裏來看下她的代碼片斷: 


Java代碼  收藏代碼
  1.     int tcp_v4_rcv(struct sk_buff *skb)  
  2. {  
  3. ///一些用到的變量  
  4.     const struct iphdr *iph;  
  5.     struct tcphdr *th;  
  6.     struct sock *sk;  
  7.     int ret;  
  8.     struct net *net = dev_net(skb->dev);  
  9. ............................  
  10.   
  11. //通過四元組得到對應的sock。  
  12.     sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);  
  13.     if (!sk)  
  14.         goto no_tcp_socket;  
  15.   
  16. process:  
  17.   
  18. ///如果是time_wait狀態,則進入相關處理(這次不會分析time_wait狀態,以後分析tcp的斷開狀態變遷時,會詳細分析這個).  
  19.     if (sk->sk_state == TCP_TIME_WAIT)  
  20.         goto do_time_wait;  
  21. .................................  
  22. ///加下半部的鎖  
  23.     bh_lock_sock_nested(sk);  
  24.     ret = 0;  
  25. ///這個宏很簡單就是判斷(sk)->sk_lock.owned.也就是當進程上下文在使用這個sock時爲1.  
  26.     if (!sock_owned_by_user(sk)) {  
  27. 。........................  
  28.         {  
  29. ///先將buffer放到prequeue隊列中。如果成功則返回1.  
  30.             if (!tcp_prequeue(sk, skb))  
  31. ///假設失敗,則直接調用tcp_v4_do_rcv處理這個skb(其實也就是直接放到receive_queue中).  
  32.                 ret = tcp_v4_do_rcv(sk, skb);  
  33.         }  
  34.     } else  
  35. ///當有進程在使用這個sock則放buf到sk_backlog中。  
  36.         sk_add_backlog(sk, skb);  
  37. //解鎖。  
  38.     bh_unlock_sock(sk);  
  39.   
  40.     sock_put(sk);  
  41.   
  42.     return ret;  
  43. ...................................................  


上面的流程很簡單,我們接下來來看幾個跳過的函數,第一個是tcp_prequeue。 

這裏我們可以看到sysctl_tcp_low_latency可以決定我們是否使用prequeue隊列. 

Java代碼  收藏代碼
  1. static inline int tcp_prequeue(struct sock *sk, struct sk_buff *skb)  
  2. {  
  3.     struct tcp_sock *tp = tcp_sk(sk);  
  4.   
  5. ///如果啓用tcp_low_latency或者ucopy.task爲空則返回0.ucopy.task爲空一般是表示進程空間有進程在等待sock的數據的到來,因此我們需要直接複製數據到receive隊列。並喚醒它。  
  6.     if (sysctl_tcp_low_latency || !tp->ucopy.task)  
  7.         return 0;  
  8. ///加數據包到prequeue隊列。  
  9.     __skb_queue_tail(&tp->ucopy.prequeue, skb);  
  10. ///update內存大小。  
  11.     tp->ucopy.memory += skb->truesize;  
  12. ///如果prequeue已滿,則將處理prequeue隊列。  
  13.     if (tp->ucopy.memory > sk->sk_rcvbuf) {  
  14.         struct sk_buff *skb1;  
  15.   
  16.         BUG_ON(sock_owned_by_user(sk));  
  17. ///遍歷prequeue隊列。  
  18.         while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {  
  19. ///這個函數最終也會調用tcp_v4_do_rcv(也就是加入到receive隊列中).  
  20.             sk_backlog_rcv(sk, skb1);  
  21.             NET_INC_STATS_BH(sock_net(sk),  
  22.                      LINUX_MIB_TCPPREQUEUEDROPPED);  
  23.         }  
  24. ///清空內存。  
  25.         tp->ucopy.memory = 0;  
  26.     } else if (skb_queue_len(&tp->ucopy.prequeue) == 1) {  
  27. ///這裏表示這個數據包是prequeue的第一個包。然後喚醒等待隊列。  
  28.         wake_up_interruptible_poll(sk->sk_sleep,  
  29.                        POLLIN | POLLRDNORM | POLLRDBAND);  
  30.   
  31. ///這裏的定時器以後會詳細介紹。  
  32.         if (!inet_csk_ack_scheduled(sk))  
  33.             inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK,  
  34.                           (3 * tcp_rto_min(sk)) / 4,  
  35.                           TCP_RTO_MAX);  
  36.     }  
  37.     return 1;  
  38. }  



我們這裏只關注TCP_ESTABLISHED狀態,來看tcp_v4_do_rcv:它主要是通過判斷相應的tcp狀態來進入相關的處理函數。 


Java代碼  收藏代碼
  1. int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)  
  2. {  
  3.     struct sock *rsk;  
  4. ...................................  
  5.     if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */  
  6.         TCP_CHECK_TIMER(sk);  
  7. ///處理數據包。  
  8.         if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {  
  9.             rsk = sk;  
  10.             goto reset;  
  11.         }  
  12.         TCP_CHECK_TIMER(sk);  
  13.         return 0;  
  14.     }  
  15.   
  16.     ........................................  
  17. }  



因此我們這裏重點要看的函數就是tcp_rcv_established,當它在軟中斷上下文中被調用時,主要的目的是將skb加入到receive_queue隊列中。因此這裏我們只看這一部分,等下面分析tcp_recvmsg時,我們再來看進程上下文才會處理的一部分。 

Java代碼  收藏代碼
  1. ///程序如何到達這裏,我們在分析tcp_recvmsg時會再次分析tcp_rcv_established,那個時候會介紹這個。  
  2. if (!eaten) {  
  3. ///進行checksum  
  4.     if (tcp_checksum_complete_user(sk, skb))  
  5.       
  6.                 goto csum_error;  
  7. ..................................................  
  8.   
  9.                 __skb_pull(skb, tcp_header_len);  
  10.   
  11. ///最重要的在這裏,我們可以看到直接將skb加入到sk_receive隊列中。  
  12.             __skb_queue_tail(&sk->sk_receive_queue, skb);  
  13.                 skb_set_owner_r(skb, sk);  
  14. ///更新rcv_nxt,也就是表示下一個接收序列起始號。  
  15.                 tp->rcv_nxt =             TCP_SKB_CB(skb)->end_seq;  
  16.             }  
  17. ............................................  



接下來來看tcp_rcvmsg函數。 

通過上面我們知道有找個隊列可供我們取得skbuf,那麼具體的次序是什麼呢,我這裏摘抄內核的註釋,它講的非常清楚: 

引用
Look: we have the following (pseudo)queues: 
1. packets in flight 
2. backlog 
3. prequeue 
4. receive_queue 
Each queue can be processed only if the next ones are empty. At this point we have empty receive_queue.But prequeue _can_ be not empty after 2nd iteration, when we jumped to start of loop because backlog 
processing added something to receive_queue. We cannot release_sock(), because backlog containd packets arrived _after_ prequeued ones. 

Shortly, algorithm is clear --- to process all the queues in order. We could make it more directly,requeueing packets from backlog to prequeue, if is not empty. It is more elegant, but eats cycles,


由於這個函數比較複雜,因此我們分段來分析這個函數。 
首先是處理包之前的一些合法性判斷,以及取得一些有用的值。 

Java代碼  收藏代碼
  1. int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,  
  2.         size_t len, int nonblock, int flags, int *addr_len)  
  3. {  
  4. ...................................  
  5.   
  6. ///鎖住當前的socket。  
  7.     lock_sock(sk);  
  8.   
  9.     TCP_CHECK_TIMER(sk);  
  10.   
  11.     err = -ENOTCONN;  
  12.     if (sk->sk_state == TCP_LISTEN)  
  13.         goto out;  
  14.   
  15.   
  16. ///得到超時時間(前面已經介紹過了).如果非阻塞則爲0.  
  17.     timeo = sock_rcvtimeo(sk, nonblock);  
  18.   
  19.     /* Urgent data needs to be handled specially. */  
  20.     if (flags & MSG_OOB)  
  21.         goto recv_urg;  
  22.   
  23. ///取得當前tcp字節流中的未讀數據的起始序列號。  
  24.     seq = &tp->copied_seq;  
  25.     if (flags & MSG_PEEK) {  
  26.         peek_seq = tp->copied_seq;  
  27.         seq = &peek_seq;  
  28.     }  
  29.   
  30. ///主要是用來處理MSG_WAITALL套接字選項。這個選項是用來標記是否等待所有的數據到達才返回的。  
  31.     target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);  
  32. }  


在上面我們看到了lock_sock,這個函數是用來鎖住當前的sock, 我們來看它的詳細實現,它最終會調用lock_sock_nested: 

Java代碼  收藏代碼
  1. void lock_sock_nested(struct sock *sk, int subclass)  
  2. {  
  3.     might_sleep();  
  4. ///首先加鎖。  
  5.     spin_lock_bh(&sk->sk_lock.slock);  
  6. ///如果owned爲1,也就是有其他進程在使用這個sock。此時調用__lock_sock(這個函數用來休眠進程,進入等待隊列)。  
  7.     if (sk->sk_lock.owned)  
  8.         __lock_sock(sk);  
  9. ///當sock可以使用了,則設置owned爲1,標記被當前進程所使用。  
  10.     sk->sk_lock.owned = 1;  
  11. ///解鎖。  
  12.     spin_unlock(&sk->sk_lock.slock);  
  13.     /* 
  14.      * The sk_lock has mutex_lock() semantics here: 
  15.      */  
  16.     mutex_acquire(&sk->sk_lock.dep_map, subclass, 0, _RET_IP_);  
  17.     local_bh_enable();  
  18. }  


我們再來看__lock_sock如何來處理的。 

Java代碼  收藏代碼
  1. static void __lock_sock(struct sock *sk)  
  2. {  
  3.     DEFINE_WAIT(wait);  
  4.   
  5.     for (;;) {  
  6. ///加入等待隊列,可以看到加入的等待隊列是sl_lock.wq,也就是我們上面介紹過得。而這個等待隊列的喚醒我們下面會介紹。  
  7.         prepare_to_wait_exclusive(&sk->sk_lock.wq, &wait,  
  8.                     TASK_UNINTERRUPTIBLE);  
  9. ///解鎖。  
  10.         spin_unlock_bh(&sk->sk_lock.slock);  
  11. ///讓出cpu,進入休眠。  
  12.         schedule();  
  13.         spin_lock_bh(&sk->sk_lock.slock);  
  14. ///如果輪到我們處理這個sock,則跳出循環。  
  15.         if (!sock_owned_by_user(sk))  
  16.             break;  
  17.     }  
  18.     finish_wait(&sk->sk_lock.wq, &wait);  
  19. }  



ok,再回到tcp_recvmsg.接下來我們來看如何處理數據包。 

下面這一段主要是用來從receive隊列中讀取數據。 

Java代碼  收藏代碼
  1. do {  
  2.         u32 offset;  
  3.   
  4. ///是否有urgent數據,如果已經讀取了一些數據或者有個未決的sigurg信號,則直接退出循環。  
  5.         if (tp->urg_data && tp->urg_seq == *seq) {  
  6.             if (copied)  
  7.                 break;  
  8.             if (signal_pending(current)) {  
  9.                 copied = timeo ? sock_intr_errno(timeo) : -EAGAIN;  
  10.                 break;  
  11.             }  
  12.         }  
  13.   
  14.   
  15. ///開始處理buf,首先是從receive隊列中讀取buf。  
  16.         skb_queue_walk(&sk->sk_receive_queue, skb) {  
  17. ///開始遍歷receive_queue.  
  18.             if (before(*seq, TCP_SKB_CB(skb)->seq)) {  
  19.                 printk(KERN_INFO "recvmsg bug: copied %X "  
  20.                        "seq %X\n", *seq, TCP_SKB_CB(skb)->seq);  
  21.                 break;  
  22.             }  
  23. ///由於tcp是字節流,因此我們拷貝給用戶空間,需要正序的拷貝給用戶,這裏的第一個seq前面已經描述了,表示當前的總的sock連接中的未讀數據的起始序列號,而後一個seq表示當前skb的起始序列號。因此這個差值如果小於skb->len,就表示,當前的skb就是我們需要讀取的那個skb(因爲它的序列號最小).  
  24.             offset = *seq - TCP_SKB_CB(skb)->seq;  
  25. ///跳過syn。  
  26.             if (tcp_hdr(skb)->syn)  
  27.                 offset--;  
  28. ///找到skb。  
  29.             if (offset < skb->len)  
  30.                 goto found_ok_skb;  
  31.             if (tcp_hdr(skb)->fin)  
  32.                 goto found_fin_ok;  
  33.             WARN_ON(!(flags & MSG_PEEK));  
  34.         }  
  35. ....................................  
  36. }while(len > 0)  



接下來是對tcp狀態做一些校驗。這裏要注意,copied表示的是已經複製到用戶空間的skb的大小。而len表示還需要拷貝多少數據。 

Java代碼  收藏代碼
  1. ///如果複製的值大於等於所需要複製的,並且sk_backlog爲空,則跳出循環。這是因爲我們每次複製完畢之後,都需要將sk_backlog中的數據複製到receive隊列中。  
  2. if (copied >= target && !sk->sk_backlog.tail)  
  3.             break;  
  4.   
  5.         if (copied) {  
  6.             if (sk->sk_err ||  
  7.                 sk->sk_state == TCP_CLOSE ||  
  8.                 (sk->sk_shutdown & RCV_SHUTDOWN) ||  
  9.                 !timeo ||  
  10.                 signal_pending(current))  
  11.                 break;  
  12.         } else {  
  13.   
  14. ///如沒有複製到數據(也就是receive爲空),則判斷是否有錯誤發生。這裏主要是狀態的判斷和超時的判斷。  
  15.             if (sock_flag(sk, SOCK_DONE))  
  16.                 break;  
  17.   
  18.             if (sk->sk_err) {  
  19.                 copied = sock_error(sk);  
  20.                 break;  
  21.             }  
  22.   
  23.             if (sk->sk_shutdown & RCV_SHUTDOWN)  
  24.                 break;  
  25.   
  26.             if (sk->sk_state == TCP_CLOSE) {  
  27.                 if (!sock_flag(sk, SOCK_DONE)) {  
  28.                     copied = -ENOTCONN;  
  29.                     break;  
  30.                 }  
  31.                 break;  
  32.             }  
  33.   
  34.             if (!timeo) {  
  35.                 copied = -EAGAIN;  
  36.                 break;  
  37.             }  
  38.   
  39.             if (signal_pending(current)) {  
  40.                 copied = sock_intr_errno(timeo);  
  41.                 break;  
  42.             }  
  43.         }  



然後就是根據已經複製的數據大小來清理receive隊列中的數據,並且發送ACK給對端。然後就是給tcp_socket的ucopy域賦值,主要是iov域和task域。一個是數據區,一個是當前從屬的進程。 
Java代碼  收藏代碼
  1. tcp_cleanup_rbuf(sk, copied);  
  2.   
  3.         if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) {  
  4.   
  5. ///循環的第一次的話user_recv爲空,因此給ucopy得想關域賦值。  
  6.             if (!user_recv && !(flags & (MSG_TRUNC | MSG_PEEK))) {  
  7. //進程爲當前進程。  
  8.                 user_recv = current;  
  9.                 tp->ucopy.task = user_recv;  
  10.                 tp->ucopy.iov = msg->msg_iov;  
  11.             }  
  12. ///長度爲還需拷貝的數據的長度。  
  13.             tp->ucopy.len = len;  
  14.   
  15.             WARN_ON(tp->copied_seq != tp->rcv_nxt &&  
  16.                 !(flags & (MSG_PEEK | MSG_TRUNC)));  
  17. ///如果prequeue不爲空則跳到 do_prequeue,處理backlog隊列。  
  18.             if (!skb_queue_empty(&tp->ucopy.prequeue))  
  19.                 goto do_prequeue;  
  20.   
  21.             /* __ Set realtime policy in scheduler __ */  
  22.         }  
  23. ///已經複製完畢,則開始拷貝back_log隊列到receive隊列。  
  24.         if (copied >= target) {  
  25.             /* Do not sleep, just process backlog. */  
  26.             release_sock(sk);  
  27.             lock_sock(sk);  
  28.         } else  
  29. ///否則進入休眠,等待數據的到來。  
  30.             sk_wait_data(sk, &timeo);  



上面的分析中有release_sock函數,這個函數用來release這個sock,也就是對這個sock解除鎖定。然後喚醒等待隊列。 

這裏要注意,sock一共有兩個等待隊列,一個是sock的sk_sleep等待隊列,這個等待隊列用來等待數據的到來。一個是ucopy域的等待隊列wq,這個表示等待使用這個sock。 

Java代碼  收藏代碼
  1. void release_sock(struct sock *sk)  
  2. {  
  3.   
  4.     mutex_release(&sk->sk_lock.dep_map, 1, _RET_IP_);  
  5.   
  6.     spin_lock_bh(&sk->sk_lock.slock);  
  7. ///如果backlog隊列不爲空,則調用__release_sock處理  
  8.     if (sk->sk_backlog.tail)  
  9.         __release_sock(sk);  
  10. ///處理完畢則給owened賦值爲0.釋放對這個sock的控制。  
  11.     sk->sk_lock.owned = 0;  
  12. ///喚醒wq上的所有元素。  
  13.     if (waitqueue_active(&sk->sk_lock.wq))  
  14.         wake_up(&sk->sk_lock.wq);  
  15.     spin_unlock_bh(&sk->sk_lock.slock);  
  16. }  


然後來看主要的處理函數__release_sock,它主要是遍歷backlog隊列,然後處理skb。這裏它有兩個循環,外部循環是遍歷backlog,而內部循環是遍歷skb(也就是數據)。 


Java代碼  收藏代碼
  1. static void __release_sock(struct sock *sk)  
  2. {  
  3.     struct sk_buff *skb = sk->sk_backlog.head;  
  4.   
  5. ///遍歷backlog隊列。  
  6.     do {  
  7.         sk->sk_backlog.head = sk->sk_backlog.tail = NULL;  
  8.         bh_unlock_sock(sk);  
  9.   
  10.         do {  
  11.             struct sk_buff *next = skb->next;  
  12.   
  13.             skb->next = NULL;  
  14.   
  15. ///這個函數我們知道最終會調tcp_v4_do_rcv.而在tcp_v4_do_rcv中,會把數據複製到receive_queue隊列中。  
  16.             sk_backlog_rcv(sk, skb);  
  17.             cond_resched_softirq();  
  18.   
  19.             skb = next;  
  20.         } while (skb != NULL);  
  21.   
  22.         bh_lock_sock(sk);  
  23.     } while ((skb = sk->sk_backlog.head) != NULL);  
  24. }  



而當數據tp->ucopy.prequeue爲空,並且所複製的數據不能達到所期望的值,此時我們進入sk_wait_data等待數據的到來。 


Java代碼  收藏代碼
  1. #define sk_wait_event(__sk, __timeo, condition)         \  
  2.     ({int  __rc;                        \  
  3. ///這個我們通過上面知道,會將數據複製到receive-queue隊列。  
  4.     release_sock(__sk);                 \  
  5.     __rc = condition;     
  6. ///當sk_wait_data調用時,rc是用來判斷receive_queue是否爲空的,          \  
  7.     if (!__rc) {      
  8. ///如果爲空則會休眠等待,sk_sleep等待隊列的喚醒。                  \  
  9.         *(__timeo) = schedule_timeout(*(__timeo));  \  
  10.         }                           \  
  11.         lock_sock(__sk);                    \  
  12. __rc = condition;               \  
  13.         __rc;                           \  
  14.     })  
  15.   
  16.   
  17.   
  18. int sk_wait_data(struct sock *sk, long *timeo)  
  19. {  
  20.     int rc;  
  21.     DEFINE_WAIT(wait);  
  22. ///加入sk_sleep的等待隊列  
  23.     prepare_to_wait(sk->sk_sleep, &wait, TASK_INTERRUPTIBLE);  
  24.     set_bit(SOCK_ASYNC_WAITDATA, &sk->sk_socket->flags);  
  25. ///處理事件  
  26.     rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue));  
  27.     clear_bit(SOCK_ASYNC_WAITDATA, &sk->sk_socket->flags);  
  28.     finish_wait(sk->sk_sleep, &wait);  
  29.     return rc;  
  30. }  


接下來就是一些域的更新,以及處理prequeue隊列: 


Java代碼  收藏代碼
  1.           
  2.   
  3. if (user_recv) {  
  4.             int chunk;  
  5.   
  6.             /* __ Restore normal policy in scheduler __ */  
  7. ///這個判斷主要是由於在release_sock中,有可能會將數據直接複製到用戶空間了。此時我們需要更新len以及copied域。  
  8.             if ((chunk = len - tp->ucopy.len) != 0) {  
  9.                 NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMBACKLOG, chunk);  
  10.                 len -= chunk;  
  11.                 copied += chunk;  
  12.             }  
  13. ///tp->rcv_nxt == tp->copied_seq主要用來判斷是否receive隊列中還需要數據要執行嗎(下面會說明爲什麼)。  
  14.             if (tp->rcv_nxt == tp->copied_seq &&  
  15.                 !skb_queue_empty(&tp->ucopy.prequeue)) {  
  16. do_prequeue:  
  17. ///執行prequeue  
  18.                 tcp_prequeue_process(sk);  
  19.   
  20. ///和上面一樣,更新len和cpoied域。  
  21.                 if ((chunk = len - tp->ucopy.len) != 0) {  
  22.                     NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);  
  23.                     len -= chunk;  
  24.                     copied += chunk;  
  25.                 }  
  26.             }  
  27.         }  
  28. ...................................  
  29.         continue;  


在分析tcp_prequeue_process之前,我們先來看下什麼情況下release_sock會直接複製數據到用戶空間。我們知道它最終會調用tcp_rcv_established函數,因此來看tcp_rcv_established的代碼片斷 
內核接收到的數據包有可能不是正序的,可是內核傳遞給用戶空間的數據必須是正序的,只有這樣才能拷貝給用戶空間。 



Java代碼  收藏代碼
  1. else {  
  2.             int eaten = 0;  
  3.             int copied_early = 0;  
  4. ///判斷從這裏開始。copied_seq表示未讀的skb的序列號。而rcv_nxt爲我們所期望接收的下一個數據的序列號。這裏我們是要保證字節流的正序。而第二個條件len - tcp_header_len <= tp->ucopy.len這個說明用戶請求的數據還沒有複製夠。如果已經複製夠了,則會複製數據到receive_queue隊列。  
  5.         if (tp->copied_seq == tp->rcv_nxt &&  
  6.                len - tcp_header_len <= tp->ucopy.len) {  
  7. .........................  
  8.   
  9. ///然後判斷從屬的進程必須等於當前調用進程。並且必須爲進程上下文。  
  10.     if (tp->ucopy.task == current &&  
  11.         sock_owned_by_user(sk) && !copied_early) {  
  12.                     __set_current_state(TASK_RUNNING);  
  13. ///開始複製數據到用戶空間。  
  14.         if (!tcp_copy_to_iovec(sk, skb, tcp_header_len))  
  15.                         eaten = 1;  
  16.                 }  
  17.       
  18. ...............................  


通過上面的判斷條件我們很容易看出前面調用release_sock,爲何有時將數據拷貝到用戶空間,有時拷貝到receive隊列。 


ok,最後我們來看下tcp_prequeue_process的實現。它的實現很簡單,就是遍歷prequeue,然後處理buf。這裏要注意,它會處理完所有的prequeue,也就是會清空prequeue. 

Java代碼  收藏代碼
  1. static void tcp_prequeue_process(struct sock *sk)  
  2. {  
  3.     struct sk_buff *skb;  
  4.     struct tcp_sock *tp = tcp_sk(sk);  
  5.   
  6.     NET_INC_STATS_USER(sock_net(sk), LINUX_MIB_TCPPREQUEUED);  
  7.   
  8.     /* RX process wants to run with disabled BHs, though it is not 
  9.      * necessary */  
  10.     local_bh_disable();  
  11. ///遍歷並處理skb。  
  12.     while ((skb = __skb_dequeue(&tp->ucopy.prequeue)) != NULL)  
  13. ///最終會調用tcp_rcv_established.  
  14.         sk_backlog_rcv(sk, skb);  
  15.     local_bh_enable();  
  16. ///內存清空爲0.  
  17.     tp->ucopy.memory = 0;  
  18. }  


最後簡要的分析下數據如何複製到用戶空間。這裏的主要函數是skb_copy_datagram_iovec。最終都是通過這個函數複製到用戶空間的。 


我們知道內核存儲數據有兩種形式如果支持S/G IO的網卡,它會保存數據到skb_shinfo(skb)->frags(詳見前面的blog),否則則會保存在skb的data區中。 

因此這裏也是分爲兩部分處理。 

還有一個就是這裏遍歷frags也是遍歷兩次,第一次遍歷是查找剛好 

Java代碼  收藏代碼
  1. int skb_copy_datagram_iovec(const struct sk_buff *skb, int offset,  
  2.                 struct iovec *to, int len)  
  3. {  
  4.     int start = skb_headlen(skb);  
  5.     int i, copy = start - offset;  
  6.     struct sk_buff *frag_iter;  
  7. ///支持S/G IO的網卡,第一個數據包也是保存在data域中的。  
  8.     if (copy > 0) {  
  9.         if (copy > len)  
  10.             copy = len;  
  11. ///複製data域。  
  12.         if (memcpy_toiovec(to, skb->data + offset, copy))  
  13.             goto fault;  
  14.         if ((len -= copy) == 0)  
  15.             return 0;  
  16.         offset += copy;  
  17.     }  
  18.   
  19. ///遍歷frags,開始複製數據。  
  20.     for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {  
  21.         int end;  
  22.   
  23.         WARN_ON(start > offset + len);  
  24. ///計算複製的字節數  
  25.         end = start + skb_shinfo(skb)->frags[i].size;  
  26. ///判斷將要複製的字節數是否足夠  
  27.         if ((copy = end - offset) > 0) {  
  28.             int err;  
  29.             u8  *vaddr;  
  30.             skb_frag_t *frag = &skb_shinfo(skb)->frags[i];  
  31.             struct page *page = frag->page;  
  32. ///如果將要複製的數據太大,則縮小它爲請求的長度。  
  33.             if (copy > len)  
  34.                 copy = len;  
  35. ///轉換物理地址到虛擬地址。  
  36.             vaddr = kmap(page);  
  37. ///複製數據。  
  38.             err = memcpy_toiovec(to, vaddr + frag->page_offset +  
  39.                          offset - start, copy);  
  40.             kunmap(page);  
  41.             if (err)  
  42.                 goto fault;  
  43. ///如果複製完畢則返回0  
  44.             if (!(len -= copy))  
  45.                 return 0;  
  46. ///更新offset域。  
  47.             offset += copy;  
  48.         }  
  49. //更新start域。  
  50.         start = end;  
  51.     }  
  52.   
  53. ///到達這裏說明數據還沒有寶貝完畢,也就是請求的數據還沒拷貝完成。此時我們就需要變化offset域。  
  54.     skb_walk_frags(skb, frag_iter) {  
  55.         int end;  
  56.   
  57.         WARN_ON(start > offset + len);  
  58.   
  59.         end = start + frag_iter->len;  
  60.         if ((copy = end - offset) > 0) {  
  61.             if (copy > len)  
  62.                 copy = len;  
  63. ///改變offset域爲offset-start遞歸重新開始。  
  64.             if (skb_copy_datagram_iovec(frag_iter,  
  65.                             offset - start,  
  66.                             to, copy))  
  67.                 goto fault;  
  68.             if ((len -= copy) == 0)  
  69.                 return 0;  
  70.             offset += copy;  
  71.         }  
  72.         start = end;  
  73.     }  
  74.     if (!len)  
  75.         return 0;  
  76.   
  77. fault:  
  78.     return -EFAULT;  
  79. }  
發佈了7 篇原創文章 · 獲贊 4 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章