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(比如等待內存時)。