1. 程式人生 > >程序上下文切換 – 殘酷的效能殺手(上)

程序上下文切換 – 殘酷的效能殺手(上)

對於伺服器的優化,很多人都有自己的經驗和見解,但就我觀察,有兩點常常會被人忽視 – 上下文切換 和 Cache Line同步 問題,人們往往都會習慣性地把視線集中在盡力減少記憶體拷貝,減少IO次數這樣的問題上,不可否認它們一樣重要,但一個高效能伺服器需要更細緻地去考察這些問題,這個問題我將分成兩篇文章來寫:

1)從一些我們常用的使用者空間函式,到linux核心程式碼的跟蹤,來看一個上下文切換是如何產生的

2)從實際資料來看它對我們程式的影響

Context Switch簡介 -

上下文切換(以下簡稱CS)的定義,https://blog.csdn.net/zhangmingcai/article/details/84832799

,此文中已做了詳細的說明,這裡我又偷懶不詳細解釋了:)  只提煉以下幾個關鍵要點:

*) context(這裡我覺得叫process context更合適)是指CPU暫存器和程式計數器在任何時間點的內容

*)CS可以描述為kernel執行下面的操作

      1. 掛起一個程序,並儲存該程序當時在記憶體中所反映出的狀態

      2. 從記憶體中恢復下一個要執行的程序,恢復該程序原來的狀態到暫存器,返回到其上次暫停的執行程式碼然後繼續執行

*)CS只能發生在核心態(kernel mode)

*)system call會陷入核心態,是user mode => kernel mode的過程,我們稱之為mode switch,但不表明會發生CS(其實mode switch同樣也會做很多和CS一樣的流程,例如通過暫存器傳遞user mode 和 kernel mode之間的一些引數)

*)一個硬體中斷的產生,也可能導致kernel收到signal後進行CS

 

 

什麼樣的操作可能會引起CS -

首先我們一定是希望減少CS,那什麼樣的操作會發生CS呢?也許看了上面的介紹你還雲裡霧裡?

首先,linux中一個程序的時間片到期,或是有更高優先順序的程序搶佔時,是會發生CS的,但這些都是我們應用開發者不可控的。那麼我們不妨更多地從應用開發者(user space)的角度來看這個問題,我們的程序可以主動地向核心申請進行CS,而使用者空間通常有兩種手段能達到這一“目的”:

1)休眠當前程序/執行緒

2)喚醒其他程序/執行緒

pthread庫中的pthread_cond_wait 和 pthread_cond_signal就是很好的例子(雖然是針對執行緒,但linux核心並不區分程序和執行緒,執行緒只是共享了address space和其他資源罷了),pthread_cond_wait負責將當前執行緒掛起並進入休眠,直到條件成立的那一刻,而pthread_cond_signal則是喚醒守候條件的執行緒。我們直接來看它們的程式碼吧

pthread_cond_wait.c

複製程式碼

 1 int
 2 __pthread_cond_wait (cond, mutex)
 3      pthread_cond_t *cond;
 4      pthread_mutex_t *mutex;
 5 {
 6   struct _pthread_cleanup_buffer buffer;
 7   struct _condvar_cleanup_buffer cbuffer;
 8   int err;
 9   int pshared = (cond->__data.__mutex == (void *) ~0l)
10         ? LLL_SHARED : LLL_PRIVATE;
11 
12   /* yunjie: 這裡省略了部分程式碼 */
13 
14   do
15     {
16         /* yunjie: 這裡省略了部分程式碼 */
17 
18       /* Wait until woken by signal or broadcast.  */
19       lll_futex_wait (&cond->__data.__futex, futex_val, pshared);
20 
21         /* yunjie: 這裡省略了部分程式碼 */
22       
23       /* If a broadcast happened, we are done.  */
24       if (cbuffer.bc_seq != cond->__data.__broadcast_seq)
25     goto bc_out;
26 
27       /* Check whether we are eligible for wakeup.  */
28       val = cond->__data.__wakeup_seq;
29     }   
30   while (val == seq || cond->__data.__woken_seq == val);
31 
32   /* Another thread woken up.  */  
33   ++cond->__data.__woken_seq;
34 
35  bc_out:
36     /* yunjie: 這裡省略了部分程式碼 */
37   return __pthread_mutex_cond_lock (mutex);
38 }

複製程式碼

程式碼已經經過精簡,但我們仍然直接把目光放到19行,lll_futex_wait,這是一個pthread內部巨集,用處是呼叫系統呼叫sys_futex(futex是一種user mode和kernel mode混合mutex,這裡不展開講了),這個操作會將當前執行緒掛起休眠(馬上我們將會到核心中一探究竟)

lll_futex_wait巨集展開的全貌

複製程式碼

 1 #define lll_futex_wake(futex, nr, private) \
 2   do {                                        \
 3     int __ignore;                                 \
 4     register __typeof (nr) _nr __asm ("edx") = (nr);                  \
 5     __asm __volatile ("syscall"                           \
 6               : "=a" (__ignore)                       \
 7               : "0" (SYS_futex), "D" (futex),                 \
 8             "S" (__lll_private_flag (FUTEX_WAKE, private)),       \
 9             "d" (_nr)                         \
10               : "memory", "cc", "r10", "r11", "cx");              \
11   } while (0)

複製程式碼

可以看到,該巨集的行為很簡單,就是通過內嵌彙編的方式,快速呼叫syscall:SYS_futex,所以我們也不用再多費口舌,直接看kernel的實現吧

linux/kernel/futex.c

複製程式碼

 1 SYSCALL_DEFINE6(futex, u32 __user *, uaddr, int, op, u32, val, 
 2         struct timespec __user *, utime, u32 __user *, uaddr2,
 3         u32, val3)
 4 {
 5     struct timespec ts;
 6     ktime_t t, *tp = NULL;
 7     u32 val2 = 0; 
 8     int cmd = op & FUTEX_CMD_MASK;
 9 
10     if (utime && (cmd == FUTEX_WAIT || cmd == FUTEX_LOCK_PI ||
11               cmd == FUTEX_WAIT_BITSET)) {
12         if (copy_from_user(&ts, utime, sizeof(ts)) != 0)
13             return -EFAULT;
14         if (!timespec_valid(&ts))
15             return -EINVAL;
16 
17         t = timespec_to_ktime(ts);
18         if (cmd == FUTEX_WAIT)
19             t = ktime_add_safe(ktime_get(), t);
20         tp = &t;
21     }    
22     /*   
23      * requeue parameter in 'utime' if cmd == FUTEX_REQUEUE.
24      * number of waiters to wake in 'utime' if cmd == FUTEX_WAKE_OP.
25      */
26     if (cmd == FUTEX_REQUEUE || cmd == FUTEX_CMP_REQUEUE ||
27         cmd == FUTEX_WAKE_OP)
28         val2 = (u32) (unsigned long) utime;
29 
30     return do_futex(uaddr, op, val, tp, uaddr2, val2, val3);
31 }

複製程式碼

linux 2.5核心以後都使用這種SYSCALL_DEFINE的方式來實現核心對應的syscall(我這裡閱讀的是inux-2.6.27.62核心), 略過一些條件檢測和引數拷貝的程式碼,我們可以看到在函式最後呼叫了do_futex,由於這裡核心會進行多個函式地跳轉,我這裡就不一一貼程式碼汙染大家了

大致流程: pthread_cond_wait => sys_futex => do_futex => futex_wait (藍色部分為核心呼叫流程)

futex_wait中的部分程式碼

複製程式碼

 1 /* add_wait_queue is the barrier after __set_current_state. */                                
 2     __set_current_state(TASK_INTERRUPTIBLE);                                                      
 3     add_wait_queue(&q.waiters, &wait);                                                            
 4     /*                                                                                            
 5      * !plist_node_empty() is safe here without any lock.                                         
 6      * q.lock_ptr != 0 is not safe, because of ordering against wakeup.                           
 7      */                                                                                           
 8     if (likely(!plist_node_empty(&q.list))) {                                                     
 9         if (!abs_time)                                                                            
10             schedule();                                                                           
11         else {                                                                                    
12             hrtimer_init_on_stack(&t.timer, CLOCK_MONOTONIC,                                      
13                         HRTIMER_MODE_ABS);                                                        
14             hrtimer_init_sleeper(&t, current);                                                    
15             t.timer.expires = *abs_time;                                                          
16                                                                                                   
17             hrtimer_start(&t.timer, t.timer.expires,                                              
18                         HRTIMER_MODE_ABS);                                                        
19             if (!hrtimer_active(&t.timer))                                                        
20                 t.task = NULL;                                                                    
21                                                                                                                                                                                                      
22             /*                                                                                    
23              * the timer could have already expired, in which                                     
24              * case current would be flagged for rescheduling.                                    
25              * Don't bother calling schedule.                                                     
26              */                                                                                   
27             if (likely(t.task))                                                                   
28                 schedule();                                                                       
29                                                                                                   
30             hrtimer_cancel(&t.timer);                                                             
31                                                                                                   
32             /* Flag if a timeout occured */                                                       
33             rem = (t.task == NULL);                                                               
34                                                                                                   
35             destroy_hrtimer_on_stack(&t.timer);                                                   
36         }                                                                                         
37     }

複製程式碼

以上是futex_wait的一部分程式碼,主要邏輯是將當前程序/執行緒的狀態設為TASK_INTERRUPTIBLE(可被訊號打斷),然後將當前程序/執行緒加入到核心的wait佇列(等待某種條件發生而暫時不會進行搶佔的程序序列),之後會呼叫schedule,這是核心用於排程程序的函式,在其內部還會呼叫context_switch,在這裡就不展開,但有一點可以肯定就是當前程序/執行緒會休眠,然後核心會排程器他還有時間片的程序/執行緒來搶佔CPU,這樣pthread_cond_wait就完成了一次CS

pthread_cond_signal的流程基本和pthread_cond_wait一致,這裡都不再貼程式碼耽誤時間

大致流程:pthread_cond_signal => SYS_futex => do_futex => futex_wake => wake_futex => __wake_up => __wake_up_common => try_to_wake_up (藍色部分為核心呼叫流程)

try_to_wake_up()會設定一個need_resched標誌,該標誌標明核心是否需要重新執行一次排程,當syscall返回到user space或是中斷返回時,核心會檢查它,如果已被設定,核心會在繼續執行之前呼叫排程程式,之後我們萬能的schedule函式就會在wait_queue(還記得嗎,我們呼叫pthread_cond_wait的執行緒還在裡面呢)中去拿出程序並挑選一個讓其搶佔CPU,所以,根據我們跟蹤的核心程式碼,pthread_cond_signal也會發生一次CS

 

本篇結束 -

會造成CS的函式遠遠不止這些,例如我們平時遇到mutex競爭,或是我們呼叫sleep時,都會發生,我們總是忽略了它的存在,但它卻默默地扼殺著我們的程式效能(相信我,它比你想象中要更嚴重),在下一篇中我將以chaos庫(我編寫的一個開源網路庫)中的一個多執行緒元件為例,給大家演示CS所帶來的效能下降

本文轉自https://www.cnblogs.com/zhiranok/archive/2012/08/13/context_switch_1.html