進程上下文切換 – 殘酷的性能殺手(上)


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

1)從一些我們常用的用戶空間函數,到linux內核代碼的跟蹤,來看一個上下文切換是如何產生的

2)從實際數據來看它對我們程序的影響

另外,關於Cache Line 的測試大家可移步 http://www.cppthinker.com/cpp/9/cpu_cache/

 

Context Switch簡介 -

上下文切換(以下簡稱CS)的定義,http://www.linfo.org/context_switch.html 此文中已做了詳細的說明,這里我又偷懶不詳細解釋了:)  只提煉以下幾個關鍵要點:

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

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

lll_futex_wait宏展開的全貌

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

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

linux/kernel/futex.c

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

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

以上是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所帶來的性能下降

希望對大家有幫助 :)

我的個人博客地址:www.cppthinker.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM