- int tcp_v4_rcv(struct sk_buff *skb)
- {
- ... ...
- //是否有進程正在使用這個套接字,將會對處理流程產生影響
- //或者從代碼層面上,只要在tcp_recvmsg裏,執行lock_sock後只能進入else,而release_sock後會進入if
- if (!sock_owned_by_user(sk)) {
- {
- //當 tcp_prequeue 返回0時,表示這個函數沒有處理該報文
- if (!tcp_prequeue(sk, skb))//如果報文放在prequeue隊列,即表示延後處理,不佔用軟中斷過長時間
- ret = tcp_v4_do_rcv(sk, skb);//不使用prequeue或者沒有用戶進程讀socket時(圖3進入此分支),立刻開始處理這個報文
- }
- } else
- sk_add_backlog(sk, skb);//如果進程正在操作套接字,就把skb指向的TCP報文插入到backlog隊列(圖3涉及此分支)
- ... ...
- }
圖1第1步裏,我們從網絡上收到了序號爲S1-S2的包。此時,沒有用戶進程在讀取套接字,因此,sock_owned_by_user(sk)會返回0。所以,tcp_prequeue方法將得到執行。簡單看看它:
- static inline int tcp_prequeue(struct sock *sk, struct sk_buff *skb)
- {
- struct tcp_sock *tp = tcp_sk(sk);
- //檢查tcp_low_latency,默認其爲0,表示使用prequeue隊列。tp->ucopy.task不爲0,表示有進程啓動了拷貝TCP消息的流程
- if (!sysctl_tcp_low_latency && tp->ucopy.task) {
- //到這裏,通常是用戶進程讀數據時沒讀到指定大小的數據,休眠了。直接將報文插入prequeue隊列的末尾,延後處理
- __skb_queue_tail(&tp->ucopy.prequeue, skb);
- tp->ucopy.memory += skb->truesize;
- //當然,雖然通常是延後處理,但如果TCP的接收緩衝區不夠用了,就會立刻處理prequeue隊列裏的所有報文
- if (tp->ucopy.memory > sk->sk_rcvbuf) {
- while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {
- //sk_backlog_rcv就是下文將要介紹的tcp_v4_do_rcv方法
- sk->sk_backlog_rcv(sk, skb1);
- }
- } else if (skb_queue_len(&tp->ucopy.prequeue) == 1) {
- &nbsnbsp; //prequeue裏有報文了,喚醒正在休眠等待數據的進程,讓進程在它的上下文中處理這個prequeue隊列的報文
- wake_up_interruptible(sk->sk_sleep);
- }
- return 1;
- }
- //prequeue沒有處理
- return 0;
- }
由於tp->ucopy.task此時是NULL,所以我們收到的第1個報文在tcp_prequeue函數裏直接返回了0,因此,將由 tcp_v4_do_rcv方法處理。
- int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
- {
- if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
- //當TCP連接已經建立好時,是由tcp_rcv_established方法處理接收報文的
- if (tcp_rcv_established(sk, skb, skb->h.th, skb->len))
- goto reset;
- return 0;
- }
- ... ...
- }
tcp_rcv_established方法在圖1裏,主要調用tcp_data_queue方法將報文放入隊列中,繼續看看它又幹了些什麼事:
- static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
- {
- struct tcp_sock *tp = tcp_sk(sk);
- //如果這個報文是待接收的報文(看seq),它有兩個出路:進入receive隊列,正如圖1;直接拷貝到用戶內存中,如圖3
- if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
- //滑動窗口外的包暫不考慮,篇幅有限,下次再細談
- if (tcp_receive_window(tp) == 0)
- goto out_of_window;
- //如果有一個進程正在讀取socket,且正準備要拷貝的序號就是當前報文的seq序號
- if (tp->ucopy.task == current &&
- tp->copied_seq == tp->rcv_nxt && tp->ucopy.len &&
- sock_owned_by_user(sk) && !tp->urg_data) {
- //直接將報文內容拷貝到用戶態內存中,參見圖3
- if (!skb_copy_datagram_iovec(skb, 0, tp->ucopy.iov, chunk)) {
- tp->ucopy.len -= chunk;
- tp->copied_seq += chunk;
- }
- }
- if (eaten <= 0) {
- queue_and_out:
- //如果沒有能夠直接拷貝到用戶內存中,那麼,插入receive隊列吧,正如圖1中的第1、3步
- __skb_queue_tail(&sk->sk_receive_queue, skb);
- }
- //更新待接收的序號,例如圖1第1步中,更新爲S2
- tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
- //正如圖1第4步,這時會檢查out_of_order隊列,若它不爲空,需要處理它
- if (!skb_queue_empty(&tp->out_of_order_queue)) {
- //tcp_ofo_queue方法會檢查out_of_order隊列中的所有報文
- tcp_ofo_queue(sk);
- }
- }
- ... ...
- //這個包是無序的,又在接收滑動窗口內,那麼就如圖1第2步,把報文插入到out_of_order隊列吧
- if (!skb_peek(&tp->out_of_order_queue)) {
- nbsp;__skb_queue_head(&tp->out_of_order_queue,skb);
- } else {
- ... ...
- __skb_append(skb1, skb, &tp->out_of_order_queue);
- }
- }
圖1第4步時,正是通過tcp_ofo_queue方法把之前亂序的S3-S4報文插入receive隊列的。
- static void tcp_ofo_queue(struct sock *sk)
- {
- struct tcp_sock *tp = tcp_sk(sk);
- __u32 dsack_high = tp->rcv_nxt;
- struct sk_buff *skb;
- //遍歷out_of_order隊列
- while ((skb = skb_peek(&tp->out_of_order_queue)) != NULL) {
- ... ...
- //若這個報文可以按seq插入有序的receive隊列中,則將其移出out_of_order隊列
- __skb_unlink(skb, &tp->out_of_order_queue);
- //插入receive隊列
- __skb_queue_tail(&sk->sk_receive_queue, skb);
- //更新socket上待接收的下一個有序seq
- tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
- }
- }
- //參數裏的len就是read、recv方法裏的內存長度,flags正是方法的flags參數,nonblock則是阻塞、非阻塞標誌位
- int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
- size_t len, int nonblock, int flags, int *addr_len)
- {
- //鎖住socket,防止多進程併發訪問TCP連接,告知軟中斷目前socket在進程上下文中
- lock_sock(sk);
- //初始化errno這個錯誤碼
- err = -ENOTCONN;
- //如果socket是阻塞套接字,則取出SO_RCVTIMEO作爲讀超時時間;若爲非阻塞,則timeo爲0。下面會看到timeo是如何生效的
- timeo = sock_rcvtimeo(sk, nonblock);
- //獲取下一個要拷貝的字節序號
- //注意:seq的定義爲u32 *seq;,它是32位指針。爲何?因爲下面每向用戶態內存拷貝後,會更新seq的值,這時就會直接更改套接字上的copied_seq
- seq = &tp->copied_seq;
- //當flags參數有MSG_PEEK標誌位時,意味着這次拷貝的內容,當再次讀取socket時(比如另一個進程)還能再次讀到
- if (flags & MSG_PEEK) {
- //所以不會更新copied_seq,當然,下面會看到也不會刪除報文,不會從receive隊列中移除報文
- peek_seq = tp->copied_seq;
- seq = &peek_seq;
- }
- //獲取SO_RCVLOWAT最低接收閥值,當然,target實際上是用戶態內存大小len和SO_RCVLOWAT的最小值
- //注意:flags參數中若攜帶MSG_WAITALL標誌位,則意味着必須等到讀取到len長度的消息才能返回,此時target只能是len &nbnbsp;
- target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);
- //以下開始讀取消息
- do {
- //從receive隊列取出1個報文
- skb = skb_peek(&sk->sk_receive_queue);
- do {
- //沒取到退出當前循環
- if (!skb)
- break;
- //offset是待拷貝序號在當前這個報文中的偏移量,在圖1、2、3中它都是0,只有因爲用戶內存不足以接收完1個報文時才爲非0
- offset = *seq - TCP_SKB_CB(skb)->seq;
- //有些時候,三次握手的SYN包也會攜帶消息內容的,此時seq是多出1的(SYN佔1個序號),所以offset減1
- if (skb->h.th->syn)
- offset--;
- //若偏移量還有這個報文之內,則認爲它需要處理
- if (offset < skb->len)
- goto found_ok_skb;
- skb = skb->next;
- } while (skb != (struct sk_buff *)&sk->sk_receive_queue);
- //如果receive隊列爲空,則檢查已經拷貝的字節數,是否達到了SO_RCVLOWAT或者長度len。滿足了,且backlog隊列也爲空,則可以返回用戶態了,正如圖1的第11步
- if (copied >= target && !sk->sk_backlog.tail)
- break;
- //在tcp_recvmsg裏,copied就是已經拷貝的字節數
- if (copied) {
- ... ...
- } else {
- //一個字節都沒拷貝到,但如果shutdown關閉了socket,一樣直接返回。當然,本文不涉及關閉連接
- if (sk->sk_shutdown & RCV_SHUTDOWN)
- break;
- //如果使用了非阻塞套接字,此時timeo爲0
- if (!timeo) {
- //非阻塞套接字讀取不到數據時也會返回,錯誤碼正是EAGAIN
- copied = -EAGAIN;
- break;
- }
- ... ...
- }
- //tcp_low_latency默認是關閉的,圖1、圖2都是如此,圖3則例外,即圖3不會走進這個if
- if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) {
- //prequeue隊列就是爲了提高系統整體效率的,即prequeue隊列有可能不爲空,這是因爲進程休眠等待時可能有新報文到達prequeue隊列
- if (!skb_queue_empty(&tp->ucopy.prequeue))
- goto do_prequeue;
- }
- //如果已經拷貝了的字節數超過了最低閥值
- if (copied >= target) {
- //release_sock這個方法會遍歷、處理backlog隊列中的報文
- release_sock(sk);
- lock_sock(sk);
- } else
- sk_wait_data(sk, &timeo);//沒有讀取到足夠長度的消息,因此會進程休眠,如果沒有被喚醒,最長睡眠timeo時間
- if (user_recv) {
- if (tp->rcv_nxt == tp->copied_seq &&
- !skb_queue_empty(&tp->ucopy.prequeue)) {
- do_prequeue:
- //接上面代碼段,開始處理prequeue隊列裏的報文
- tcp_prequeue_process(sk);
- }
- }
- //繼續處理receive隊列的下一個報文
- continue;
- found_ok_skb:
- /* Ok so how much can we use? */
- //receive隊列的這個報文從其可以使用的偏移量offset,到總長度len之間,可以拷貝的長度爲used
- used = skb->len - offset;
- //len是用戶態空閒內存,len更小時,當然只能拷貝len長度消息,總不能導致內存溢出吧
- if (len < used)
- used = len;
- //MSG_TRUNC標誌位表示不要管len這個用戶態內存有多大,只管拷貝數據吧
- if (!(flags & MSG_TRUNC)) {
- {
- //向用戶態拷貝數據
- err = skb_copy_datagram_iovec(skb, offset,
- msg->msg_iov, used);
- }
- }
- //因爲是指針,所以同時更新copied_seq--下一個待接收的序號
- *seq += used;
- //更新已經拷貝的長度
- copied += used;
- //更新用戶態內存的剩餘空閒空間長度
- len -= used;
- ... ...
- } while (len > 0);
- //已經裝載了接收器
- if (user_recv) {
- //prequeue隊列不爲空則處理之
- if (!skb_queue_empty(&tp->ucopy.prequeue)) {
- tcp_prequeue_process(sk);
- }
- //準備返回用戶態,socket上不再裝載接收任務
- tp->ucopy.task = NULL;
- tp->ucopy.len = 0;
- }
- //釋放socket時,還會檢查、處理backlog隊列中的報文
- release_sock(sk);
- //向用戶返回已經拷貝的字節數
- return copied;
- }
- int sk_wait_data(struct sock *sk, long *timeo)
- {
- //注意,它的自動喚醒條件有兩個,要麼timeo時間到達,要麼receive隊列不爲空
- rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue));
- }
sk_wait_event也值得我們簡單看下:
- #define sk_wait_event(__sk, __timeo, __condition) \
- ({ int rc; \
- release_sock(__sk); \
- rc = __condition; \
- if (!rc) { \
- *(__timeo) = schedule_timeout(*(__timeo)); \
- } \
- lock_sock(__sk); \
- rc = __condition; \
- rc; \
- })
注意,它在睡眠前會調用release_sock,這個方法會釋放socket鎖,使得下面的第5步中,新到的報文不再只能進入backlog隊列。
- void fastcall release_sock(struct sock *sk)
- {
- mutex_release(&sk->sk_lock.dep_map, 1, _RET_IP_);
- spin_lock_bh(&sk->sk_lock.slock);
- //這裏會遍歷backlog隊列中的每一個報文
- if (sk->sk_backlog.tail)
- __release_sock(sk);
- //這裏是網絡中斷執行時,告訴內核,現在socket並不在進程上下文中
- sk->sk_lock.owner = NULL;
- if (waitqueue_active(&sk->sk_lock.wq))
- wake_up(&sk->sk_lock.wq);
- spin_unlock_bh(&sk->sk_lock.slock);
- }
再看看__release_sock方法是如何遍歷backlog隊列的:
- static void __release_sock(struct sock *sk)
- {
- struct sk_buff *skb = sk->sk_backlog.head;
- //遍歷backlog隊列
- do {
- sk->sk_backlog.head = sk->sk_backlog.tail = NULL;
- bh_unlock_sock(sk);
- do {
- struct sk_buff *next = skb->next;
- skb->next = NULL;
- //處理報文,其實就是tcp_v4_do_rcv方法,上文介紹過,不再贅述
- sk->sk_backlog_rcv(sk, skb);
- cond_resched_softirq();
- skb = next;
- } while (skb != NULL);
- bh_lock_sock(sk);
- } while((skb = sk->sk_backlog.head) != NULL);
- }
此時遍歷到S3-S4報文,但因爲它是失序的,所以從backlog隊列中移入out_of_order隊列中(參見上文說過的tcp_ofo_queue方法)。
http://blog.csdn.net/yusiguyuan/article/details/24671351