天天看點

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

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

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

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

Context Switch簡介 -

*) 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

<code>int</code>

<code>__pthread_cond_wait (cond, mutex)</code>

<code>     </code><code>pthread_cond_t *cond;</code>

<code>     </code><code>pthread_mutex_t *mutex;</code>

<code>{</code>

<code>  </code><code>struct</code> <code>_pthread_cleanup_buffer buffer;</code>

<code>  </code><code>struct</code> <code>_condvar_cleanup_buffer cbuffer;</code>

<code>  </code><code>int</code> <code>err;</code>

<code>  </code><code>int</code> <code>pshared = (cond-&gt;__data.__mutex == (</code><code>void</code> <code>*) ~0l)</code>

<code>        </code><code>? LLL_SHARED : LLL_PRIVATE;</code>

<code>  </code><code>/* yunjie: 這裡省略了部分代碼 */</code>

<code>  </code><code>do</code>

<code>    </code><code>{</code>

<code>        </code><code>/* yunjie: 這裡省略了部分代碼 */</code>

<code>      </code><code>/* Wait until woken by signal or broadcast.  */</code>

<code>      </code><code>lll_futex_wait (&amp;cond-&gt;__data.__futex, futex_val, pshared);</code>

<code>      </code><code>/* If a broadcast happened, we are done.  */</code>

<code>      </code><code>if</code> <code>(cbuffer.bc_seq != cond-&gt;__data.__broadcast_seq)</code>

<code>    </code><code>goto</code> <code>bc_out;</code>

<code>      </code><code>/* Check whether we are eligible for wakeup.  */</code>

<code>      </code><code>val = cond-&gt;__data.__wakeup_seq;</code>

<code>    </code><code>}  </code>

<code>  </code><code>while</code> <code>(val == seq || cond-&gt;__data.__woken_seq == val);</code>

<code>  </code><code>/* Another thread woken up.  */</code> 

<code>  </code><code>++cond-&gt;__data.__woken_seq;</code>

<code> </code><code>bc_out:</code>

<code>    </code><code>/* yunjie: 這裡省略了部分代碼 */</code>

<code>  </code><code>return</code> <code>__pthread_mutex_cond_lock (mutex);</code>

<code>}</code>

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

lll_futex_wait宏展開的全貌

<code>#define lll_futex_wake(futex, nr, private) \                                                                                                                                                                                                </code>

<code>  </code><code>do</code> <code>{                                        \</code>

<code>    </code><code>int</code> <code>__ignore;                                 \</code>

<code>    </code><code>register</code> <code>__typeof (nr) _nr __asm (</code><code>"edx"</code><code>) = (nr);                  \</code>

<code>    </code><code>__asm __volatile (</code><code>"syscall"</code>                           <code>\</code>

<code>              </code><code>:</code><code>"=a"</code> <code>(__ignore)                       \</code>

<code>              </code><code>:</code><code>"0"</code> <code>(SYS_futex),</code><code>"D"</code> <code>(futex),                 \</code>

<code>            </code><code>"S"</code> <code>(__lll_private_flag (FUTEX_WAKE,</code><code>private</code><code>)),       \</code>

<code>            </code><code>"d"</code> <code>(_nr)                         \</code>

<code>              </code><code>:</code><code>"memory"</code><code>,</code><code>"cc"</code><code>,</code><code>"r10"</code><code>,</code><code>"r11"</code><code>,</code><code>"cx"</code><code>);              \</code>

<code>  </code><code>}</code><code>while</code> <code>(0)</code>

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

linux/kernel/futex.c

<code>SYSCALL_DEFINE6(futex, u32 __user *, uaddr,</code><code>int</code><code>, op, u32, val,</code>

<code>        </code><code>struct</code> <code>timespec __user *, utime, u32 __user *, uaddr2,</code>

<code>        </code><code>u32, val3)</code>

<code>    </code><code>struct</code> <code>timespec ts;</code>

<code>    </code><code>ktime_t t, *tp = NULL;</code>

<code>    </code><code>u32 val2 = 0;</code>

<code>    </code><code>int</code> <code>cmd = op &amp; FUTEX_CMD_MASK;</code>

<code>    </code><code>if</code> <code>(utime &amp;&amp; (cmd == FUTEX_WAIT || cmd == FUTEX_LOCK_PI ||</code>

<code>              </code><code>cmd == FUTEX_WAIT_BITSET)) {</code>

<code>        </code><code>if</code> <code>(copy_from_user(&amp;ts, utime,</code><code>sizeof</code><code>(ts)) != 0)</code>

<code>            </code><code>return</code> <code>-EFAULT;</code>

<code>        </code><code>if</code> <code>(!timespec_valid(&amp;ts))</code>

<code>            </code><code>return</code> <code>-EINVAL;</code>

<code>        </code><code>t = timespec_to_ktime(ts);</code>

<code>        </code><code>if</code> <code>(cmd == FUTEX_WAIT)</code>

<code>            </code><code>t = ktime_add_safe(ktime_get(), t);</code>

<code>        </code><code>tp = &amp;t;</code>

<code>    </code><code>}</code>

<code>    </code><code>/*</code>

<code>     </code><code>* requeue parameter in 'utime' if cmd == FUTEX_REQUEUE.</code>

<code>     </code><code>* number of waiters to wake in 'utime' if cmd == FUTEX_WAKE_OP.</code>

<code>     </code><code>*/</code>

<code>    </code><code>if</code> <code>(cmd == FUTEX_REQUEUE || cmd == FUTEX_CMP_REQUEUE ||</code>

<code>        </code><code>cmd == FUTEX_WAKE_OP)</code>

<code>        </code><code>val2 = (u32) (unsigned</code><code>long</code><code>) utime;</code>

<code>    </code><code>return</code> <code>do_futex(uaddr, op, val, tp, uaddr2, val2, val3);</code>

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

大緻流程: pthread_cond_wait =&gt; sys_futex =&gt; do_futex =&gt; futex_wait (藍色部分為核心調用流程)

futex_wait中的部分代碼

<code>/* add_wait_queue is the barrier after __set_current_state. */</code>

<code>__set_current_state(TASK_INTERRUPTIBLE);</code>

<code>add_wait_queue(&amp;q.waiters, &amp;wait);</code>

<code>/*  </code>

<code> </code><code>* !plist_node_empty() is safe here without any lock.</code>

<code> </code><code>* q.lock_ptr != 0 is not safe, because of ordering against wakeup.</code>

<code> </code><code>*/</code>

<code>if</code> <code>(likely(!plist_node_empty(&amp;q.list))) {</code>

<code>    </code><code>if</code> <code>(!abs_time)</code>

<code>        </code><code>schedule();</code>

<code>    </code><code>else</code> <code>{</code>

<code>        </code><code>hrtimer_init_on_stack(&amp;t.timer, CLOCK_MONOTONIC,</code>

<code>                    </code><code>HRTIMER_MODE_ABS);</code>

<code>        </code><code>hrtimer_init_sleeper(&amp;t, current);</code>

<code>        </code><code>t.timer.expires = *abs_time;</code>

<code>        </code><code>hrtimer_start(&amp;t.timer, t.timer.expires,</code>

<code>        </code><code>if</code> <code>(!hrtimer_active(&amp;t.timer))</code>

<code>            </code><code>t.task = NULL;</code>

<code>        </code><code>/*  </code>

<code>         </code><code>* the timer could have already expired, in which</code>

<code>         </code><code>* case current would be flagged for rescheduling.</code>

<code>         </code><code>* Don't bother calling schedule.</code>

<code>         </code><code>*/</code>

<code>        </code><code>if</code> <code>(likely(t.task))</code>

<code>            </code><code>schedule();</code>

<code>        </code><code>hrtimer_cancel(&amp;t.timer);</code>

<code>        </code><code>/* Flag if a timeout occured */</code>

<code>        </code><code>rem = (t.task == NULL);</code>

<code>        </code><code>destroy_hrtimer_on_stack(&amp;t.timer);</code>

<code>    </code><code>}   </code>

以上是futex_wait的一部分代碼,主要邏輯是将目前程序/線程的狀态設為TASK_INTERRUPTIBLE(可被信号打斷),然後将目前程序/線程加入到核心的wait隊列(等待某種條件發生而暫時不會進行搶占的程序式列),之後會調用schedule,這是核心用于排程程序的函數,在其内部還會調用context_switch,在這裡就不展開,但有一點可以肯定就是目前程序/線程會休眠,然後核心會排程器他還有時間片的程序/線程來搶占CPU,這樣pthread_cond_wait就完成了一次CS

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

大緻流程:pthread_cond_signal =&gt; SYS_futex =&gt; do_futex =&gt; futex_wake =&gt; wake_futex =&gt; __wake_up =&gt; __wake_up_common =&gt; 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

繼續閱讀