11.3 TCP內核同步

11.3.1 鎖的結構

        由2.1 Socket系統調用我們知道,一個TCP socket在內核有一個數據結構,這個數據結構是不能被兩個及其以上的使用者同時訪問的,否則就會由於數據不一致導致嚴重的問題。在Linux中,TCP socket的使用者有兩種:進程(線程)和軟中斷。同一時間可能會有兩個進程(線程),或位於不同CPU的兩個軟中斷,或進程(線程)與軟中斷訪問同一個socket。既然socket在同一時刻只能被一個使用者訪問,那麼互斥機制是如何實現的呢?是使用鎖完成的。進程(線程)在訪問socket之前會申請鎖,訪問結束時釋放鎖。軟中斷也是一樣,但軟中斷所申請的鎖與進程(線程)不同。TCP的內核同步就是靠鎖實現的。由於TCP並不區分進程與線程,所以下面進程和線程一律用進程指代。
        進程用lock_sock申請鎖:
1459 static inline void lock_sock(struct sock *sk)
1460 {   
1461     lock_sock_nested(sk, 0);
1462 }
        lock_sock_nested:
2284 void lock_sock_nested(struct sock *sk, int subclass)
2285 {
2286     might_sleep();    //說明調用本函數可能導致睡眠
2287     spin_lock_bh(&sk->sk_lock.slock);    //申請自旋鎖並關閉本地軟中斷
2288     if (sk->sk_lock.owned)        //已有進程正在持有鎖
2289         __lock_sock(sk);
2290     sk->sk_lock.owned = 1;    //標記鎖正在被進程、持有
2291     spin_unlock(&sk->sk_lock.slock);    //釋放自旋鎖(注意軟中斷沒有恢復)
2292     /*
2293      * The sk_lock has mutex_lock() semantics here:
2294      */
2295     mutex_acquire(&sk->sk_lock.dep_map, subclass, 0, _RET_IP_);
2296     local_bh_enable();    //開啓軟中斷,允許軟中斷運行
2297 }
        釋放鎖時使用release_sock:
2300 void release_sock(struct sock *sk)
2301 {   
2302     /*
2303      * The sk_lock has mutex_unlock() semantics:
2304      */
2305     mutex_release(&sk->sk_lock.dep_map, 1, _RET_IP_);
2306 
2307     spin_lock_bh(&sk->sk_lock.slock);
2308     if (sk->sk_backlog.tail)    //backlog隊列有skb
2309         __release_sock(sk);    //處理backlog隊列中的skb
2310 
2311     if (sk->sk_prot->release_cb)    
2312         sk->sk_prot->release_cb(sk);    //執行因進程鎖定socket而被延遲的軟中斷任務
2313 
2314     sk->sk_lock.owned = 0;  //標識進程釋放鎖
2315     if (waitqueue_active(&sk->sk_lock.wq))    //有進程在等待隊列中
2316         wake_up(&sk->sk_lock.wq);    //喚醒進程
2317     spin_unlock_bh(&sk->sk_lock.slock);
2318 }
        軟中斷使用bh_lock_sock_nested申請自旋鎖,使用bh_unlock_sock釋放自旋鎖。

        下面來分析一下TCP是如何在不同類型的使用者之間實現數據同步的。

11.3.2 進程之間

        進程T1先調用lock_sock_nested函數獲取鎖,設置sk->sk_lock.owned = 1後訪問socket;進程T2調用lock_sock_nested函數時會調用__lock_sock函數:

1832 static void __lock_sock(struct sock *sk)
1833     __releases(&sk->sk_lock.slock)
1834     __acquires(&sk->sk_lock.slock)
1835 {
1836     DEFINE_WAIT(wait);
1837 
1838     for (;;) {
1839         prepare_to_wait_exclusive(&sk->sk_lock.wq, &wait,
1840                     TASK_UNINTERRUPTIBLE);  //設置進程狀態爲TASK_UNINTERRUPTIBLE,一旦放棄CPU進程就會無法被調度,除非狀態被改變
1841         spin_unlock_bh(&sk->sk_lock.slock);
1842         schedule();    //放棄CPU
1843         spin_lock_bh(&sk->sk_lock.slock);
1844         if (!sock_owned_by_user(sk))
1845             break;
1846     }
1847     finish_wait(&sk->sk_lock.wq, &wait);
1848 }
        DEFINE_WAIT定義了一個睡眠事件:

889 #define DEFINE_WAIT_FUNC(name, function)                \
890     wait_queue_t name = {                       \
891         .private    = current,              \
892         .func       = function,             \
893         .task_list  = LIST_HEAD_INIT((name).task_list), \
894     }
895 
896 #define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)

        T2在執行1842行的schedule後會進入睡眠狀態,因爲在prepare_to_wait_exclusive函數中設置了進程狀態:

 81 void     
 82 prepare_to_wait_exclusive(wait_queue_head_t *q, wait_queue_t *wait, int state)
 83 {        
 84     unsigned long flags;
 85 
 86     wait->flags |= WQ_FLAG_EXCLUSIVE;
 87     spin_lock_irqsave(&q->lock, flags);
 88     if (list_empty(&wait->task_list))
 89         __add_wait_queue_tail(q, wait);     //將進程所屬的wait加入到sk->sk_lock.wq.task_list中
 90     set_current_state(state);    //設置進程狀態
 91     spin_unlock_irqrestore(&q->lock, flags);
 92 }
        T1執行release_sock釋放鎖時,會執行wake_up喚醒T2,wake_up是封裝了__wake_up函數的宏,__wake_up來執行喚醒動作:
3159 static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
3160             int nr_exclusive, int wake_flags, void *key)
3161 {
3162     wait_queue_t *curr, *next;
3163 
3164     list_for_each_entry_safe(curr, next, &q->task_list, task_list) {    //從第一個開始喚醒
3165         unsigned flags = curr->flags;
3166 
3167         if (curr->func(curr, mode, wake_flags, key) &&    //curr->func指向DEFINE_WAIT函數所安裝的函數autoremove_wake_function
3168                 (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
3169             break;
3170     }
3171 }
...
3183 void __wake_up(wait_queue_head_t *q, unsigned int mode,
3184             int nr_exclusive, void *key)
3185 {
3186     unsigned long flags;
3187 
3188     spin_lock_irqsave(&q->lock, flags);
3189     __wake_up_common(q, mode, nr_exclusive, 0, key);
3190     spin_unlock_irqrestore(&q->lock, flags);
3191 }
        autoremove_wake_function調用default_wake_function函數,default_wake_function函數調用try_to_wake_up喚醒T2:
1484 static int
1485 try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
1486 {
...
1514     p->state = TASK_WAKING;    //進程可以重新被CPU調度
...
        也就是說,兩個進程先後訪問同一個socket,後訪問的會睡眠,等待先訪問的釋放了鎖後纔會被喚醒從而有機會進行訪問。喚醒的順序就是排隊的順序。

11.3.3 軟中斷之間

        一個CPU在同一時刻只能運行一個軟中斷,故軟中斷之間的併發訪問只能在不同CPU之間進行。軟中斷使用的鎖是自旋鎖,第二個軟中斷申請這種鎖時會執行緊緻的循環直到鎖的擁有者釋放鎖。由於CPU在軟中斷上下文不能停留太長時間(否則CPU的其它任務無法執行),使用這種鎖會以最快的速度得到鎖。在得到鎖後的訪問也不能時間過長,尤其是不能睡眠。兩個及其以上軟中斷同時訪問一個socket的情況有:收包軟中斷與定時器超時同時發生、開啓irqloadbalance時由同一網卡收到的包由不同的CPU同時處理、由不同的網卡抵達的請求訪問同一個listen scoket等。

11.3.4 進程與軟中斷之間

        軟中斷的運行優先級很高,進程在運行的任意時刻都有可能被軟中斷打斷(除非關閉軟中斷)。按照訪問的先後順序有兩種情況:

(1)軟中斷先訪問進程後訪問

        這時軟中斷已經獲取了自旋鎖,進程在獲取自旋鎖時會等待,軟中斷釋放鎖時進程才能成功獲取鎖。

(2)進程先訪問軟中斷後訪問

        進程獲取自旋鎖(關軟中斷,防止被軟中斷打斷)時會將sk->sk_lock.owned設置爲1後釋放自旋鎖並開啓軟中斷,然後執行對socket的訪問。這時如果軟中斷髮生,則進程的執行被中止。軟中斷執行到TCP入口函數tcp_v4_rcv時:

1961 int tcp_v4_rcv(struct sk_buff *skb)
1962 {
...
2024     bh_lock_sock_nested(sk);    //獲取自旋鎖
2025     ret = 0;
2026     if (!sock_owned_by_user(sk)) {    sk->sk_lock.owned爲1時判斷爲假
...
2039     } else if (unlikely(sk_add_backlog(sk, skb,
2040                        sk->sk_rcvbuf + sk->sk_sndbuf))) {
2041         bh_unlock_sock(sk);    //釋放自旋鎖
2042         NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);
2043         goto discard_and_relse;
2044     }
2045     bh_unlock_sock(sk);     //釋放自旋鎖
        在進程鎖定socket的情況下skb會由sk_add_backlog函數處理:
 777 static inline __must_check int sk_add_backlog(struct sock *sk, struct sk_buff *skb,
 778                           unsigned int limit)
 779 {
 780     if (sk_rcvqueues_full(sk, skb, limit))
 781         return -ENOBUFS;
 782 
 783     __sk_add_backlog(sk, skb);
 784     sk->sk_backlog.len += skb->truesize;
 785     return 0;
 786 }
        由__sk_add_backlog函數將skb放入backlog隊列中:
 749 static inline void __sk_add_backlog(struct sock *sk, struct sk_buff *skb)
 750 {
 751     /* dont let skb dst not refcounted, we are going to leave rcu lock */
 752     skb_dst_force(skb);
 753 
 754     if (!sk->sk_backlog.tail)
 755         sk->sk_backlog.head = skb;
 756     else
 757         sk->sk_backlog.tail->next = skb;
 758 
 759     sk->sk_backlog.tail = skb;
 760     skb->next = NULL;
 761 }
       將skb放入backlog隊列後,軟中斷返回,進程得到機會運行。在進程釋放鎖之前所有軟中斷都會將skb放入到backlog隊列中。當進程調用release_sock釋放鎖時,如果backlog隊列非空則會執行__release_sock:
1850 static void __release_sock(struct sock *sk)
1851     __releases(&sk->sk_lock.slock)
1852     __acquires(&sk->sk_lock.slock)
1853 {
1854     struct sk_buff *skb = sk->sk_backlog.head;
1855 
1856     do {
1857         sk->sk_backlog.head = sk->sk_backlog.tail = NULL;
1858         bh_unlock_sock(sk);    //釋放自旋鎖,但不開啓軟中斷
1859 
1860         do {
1861             struct sk_buff *next = skb->next;
1862 
1863             prefetch(next);
1864             WARN_ON_ONCE(skb_dst_is_noref(skb));
1865             skb->next = NULL;
1866             sk_backlog_rcv(sk, skb);  //處理一個skb
1867 
1868             /*
1869              * We are in process context here with softirqs
1870              * disabled, use cond_resched_softirq() to preempt.
1871              * This is safe to do because we've taken the backlog
1872              * queue private:
1873              */
1874             cond_resched_softirq();    //開啓軟中斷並放棄CPU,等待下次被調度到;被調度到時重新禁用軟中斷
1875 
1876             skb = next;
1877         } while (skb != NULL);
1878 
1879         bh_lock_sock(sk);
1880     } while ((skb = sk->sk_backlog.head) != NULL);
1881 
1882     /*
1883      * Doing the zeroing here guarantee we can not loop forever
1884      * while a wild producer attempts to flood us.
1885      */
1886     sk->sk_backlog.len = 0;
1887 }
        __release_sock處理backlog隊列的方法是:首先將backlog隊列中的所有skb轉移到私有隊列(保證處理時的安全),然後釋放自旋鎖,並在關閉軟中斷的條件下調用sk_backlog_rcv函數處理skb。每處理一個skb就放棄CPU一次,以防止隊列中skb過多導致軟中斷關閉時間過長。在處理期間如果發生了軟中斷則skb被放入到原理的backlog隊列中,與當前處理的隊列沒有關係。sk_backlog_rcv將skb放入TCP中進行處理:

 790 static inline int sk_backlog_rcv(struct sock *sk, struct sk_buff *skb)
 791 {
 792     if (sk_memalloc_socks() && skb_pfmemalloc(skb))
 793         return __sk_backlog_rcv(sk, skb);  //調用sk->sk_backlog_rcv
 794 
 795     return sk->sk_backlog_rcv(sk, skb); //指向tcp_v4_do_rcv函數
 796 }   
        最終,backlog隊列中的skb會由tcp_v4_do_rcv函數進行處理。

        總之,當進程先鎖定socket時,軟中斷就只能把skb放入backlog隊列然後就返回,不能訪問socket。當進程釋放socket時會處理backlog隊列中的skb。進程持有socket的時間越長則backlog隊列越大,過大時會導致丟包(實際上很少發生)。使用這種併發方式既實現了socket在進程和軟中斷之間的併發保護,又不影響軟中斷的運行。進程在訪問socket時睡眠一小段時間(比如在用戶態與內核之間傳遞數據時)也不會引起嚴重的後果,但進行長時間睡眠時必須釋放socket(比如等待內存時)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章