作者:楊沙洲 本文將Linux內核中用於同步的幾種機制集中起來分析,強調了它們之間在實現和使用上的不同。 同步通常是為了達到多線程協同的目的而設計的一種機制,通常包含異步信號機制和互斥機制作為其實現的底層。在Linux 2.4內核中也有相應的技術實現,包括信號量、自旋鎖、原子操作和等待隊列,其中原子操作和等待隊列又是實現信號量的底層。 一. 等待隊列和異步信號 wait queue很早就作為一個基本的功能單位出現在Linux內核裡了,它以隊列為基礎數據結構,與進程調度機制緊密結合,能夠用於實現核心的異步事件通知機制。我們從它的使用范例著手,看看等待隊列是如何實現異步信號功能的。 在核心運行過程中,經常會因為某些條件不滿足而需要掛起當前線程,直至條件滿足了才繼續執行。在2.4內核中提供了一組新接口來實現這樣的功能,下面的代碼節選自kernel/printk.c: unsigned long log_size; 1: DECLARE_WAIT_QUEUE_HEAD(log_wait);... 4: spinlock_t console_lock = SPIN_LOCK_UNLOCKED;... int do_syslog(int type,char *buf,int len){ ... 2: error=wait_event_interruptible(log_wait,log_size); if(error) goto out; ... 5: spin_lock_irq(&console_lock); ... log_size--; ... 6: spin_unlock_irq(&console_lock); ... } asmlinkage int printk(const char *fmt,...){ ... 7: spin_lock_irqsave(↦console_lock,flags); ... log_size++;... 8: spin_unlock_irqrestore(&console_lock,flags); 3: wake_up_interruptible(↦log_wait); ... } 這段代碼實現了printk調用和syslog之間的同步,syslog需要等待printk送數據到緩沖區,因此,在2:處等待log_size非0;而printk一邊傳送數據,一邊增加log_size的值,完成後喚醒在log_wait上等待的所有線程(這個線程不是用戶空間的線程概念,而是核內的一個執行序列)。執行了3:的wake_up_interruptible()後,2:處的wait_event_interruptible()返回0,從而進入syslog的實際動作。 1:是定義log_wait全局變量的宏調用。 在實際操作log_size全局變量的時候,還使用了spin_lock自旋鎖來實現互斥,關於自旋鎖,這裡暫不作解釋,但從這段代碼中已經可以清楚的知道它的使用方法了。 所有wait queue使用上的技巧體現在wait_event_interruptible()的實現上,代碼位於include/linux/sched.h中,前置數字表示行號: 779 #define __wait_event_interruptible(wq, condition, ret) 780 do { 781 wait_queue_t __wait; 782 init_waitqueue_entry(&__wait, current); 783 784 add_wait_queue(&wq, &__wait); 785 for (;;) { 786 set_current_state(TASK_INTERRUPTIBLE); 787 if (condition) 788 break; 789 if (!signal_pending(current)) { 790 schedule(); 791 continue; 792 } 793 ret = -ERESTARTSYS; 794 break; 795 } 796 current->state = TASK_RUNNING; 797 remove_wait_queue(&wq, &__wait); 798 } while (0) 799 800 #define wait_event_interruptible(wq, condition) 801 ({ 802 int __ret = 0; 803 if (!(condition)) 804 __wait_event_interruptible(wq, condition, __ret); 805 __ret; 806 }) 在wait_event_interruptible()中首先判斷condition是不是已經滿足,如果是則直接返回0,否則調用__wait_event_interruptible(),並用__ret來存放返回值。__wait_event_interruptible()首先定義並初始化一個wait_queue_t變量__wait,其中數據為當前進程結構current(strUCt task_struct),並把__wait入隊。在無限循環中,__wait_event_interruptible()將本進程置為可中斷的掛起狀態,反復檢查condition是否成立,如果成立則退出,如果不成立則繼續休眠;條件滿足後,即把本進程運行狀態置為運行態,並將__wait從等待隊列中清除掉,從而進程能夠調度運行。如果進程當前有異步信號(POSIX的),則返回-ERESTARTSYS。 掛起的進程並不會自動轉入運行的,因此,還需要一個喚醒動作,這個動作由wake_up_interruptible()完成,它將遍歷作為參數傳入的log_wait等待隊列,將其中所有的元素(通常都是task_struct)置為運行態,從而可被調度到,執行__wait_event_interruptible()中的代碼。 DECLARE_WAIT_QUEUE_HEAD(log_wait)經過宏展開後就是定義了一個log_wait等待隊列頭變量: struct __wait_queue_head log_wait = { lock: SPIN_LOCK_UNLOCKED, task_list: { ↦log_wait.task_list, &log_wait.task_list } } 其中task_list是struct list_head變量,包括兩個list_head指針,一個next、一個prev,這裡把它們初始化為自身,屬於隊列實現上的技巧,其細節可以參閱關於內核list數據結構的討論,add_wait_queue()和remove_wait_queue()就等同於list_add()和list_del()。 wait_queue_t結構在include/linux/wait.h中定義,關鍵元素即為一個struct task_struct變量表征當前進程。 除了wait_event_interruptible()/wake_up_interruptible()以外,與此相對應的還有wait_event()和wake_up()接口,interruptible是更安全、更常用的選擇,因為可中斷的等待可以接收信號,從而掛起的進程允許被外界kill。 wait_event*()接口是2.4內核引入並推薦使用的,在此之前,最常用的等待操作是interruptible_sleep_on(wait_queue_head_t *wq),當然,與此配套的還有不可中斷版本sleep_on(),另外,還有帶有超時控制的*sleep_on_timeout()。sleep_on系列函數的語義比wait_event簡單,沒有條件判斷功能,其余動作與wait_event完全相同,也就是說,我們可以用interruptible_sleep_on()來實現wait_event_interruptible()(僅作示意〉: do{ interruptible_sleep_on(&log_wait); if(condition) break; }while(1); 相對而言,這種操作序列有反復的入隊、出隊動作,更加耗時,而很大一部分等待操作的確是需要判斷一個條件是否滿足的,因此2.4才推薦使用wait_event接口。 在wake_up系列接口中,還有一類wake_up_sync()和wake_up_interruptible_sync()接口,保證調度在wake_up返回之後進行。 二. 原子操作和信號量 POSIX有信號量,SysV IPC有信號量,核內也有信號量,接口很簡單,一個down(),一個up(),分別對應P操作和V操作,down()調用可能引起線程掛起,因此和sleep_on類似,也有interruptible系列接口。down意味著信號量減1,up意味著信號量加1,這兩個操作顯然需要互斥。在Linux 2.4中,並沒有如想象中的用鎖實現,而是使用了原子操作。 在include/asm/atomic.h中定義了一系列原子操作,包括原子讀、原子寫、原子加等等,大多是直接用匯編語句來實現的,這裡就不詳細解釋。 我們從信號量數據結構開始,它定義在include/asm/semaphore.h中: struct semaphore { atomic_t count; int sleepers; wait_queue_head_t wait; } down()操作可以理解為申請資源,up()操作可以理解為釋放資源,因此,信號量實際表示的是資源的數量以及是否有進程正在等待。在semaphore結構中,count相當於資源計數,為正數或0時表示可用資源數,-1則表示沒有空閒資源且有等待進程。而等待進程的數量並不關心。這種設計主要是考慮與信號量的原語相一致,當某個進程執行up()函數釋放資源,點亮信號燈時,如果count恢復到0,則表示尚有進程在等待該資源,因此執行喚醒操作。一個典型的down()-up()流程是這樣