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

2021-06-22 15:36:32 字數 3812 閱讀 7561

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

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

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

context switch簡介 -

上下文切換(以下簡稱cs)的定義, 此文中已做了詳細的說明,這裡我又偷懶不詳細解釋了:)  只提煉以下幾個關鍵要點:

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

int__pthread_cond_wait (cond, mutex)

pthread_cond_t *cond;

pthread_mutex_t *mutex;

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巨集展開的全貌

#define lll_futex_wake(futex, nr, private) 

do  while (0)

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

linux/kernel/futex.c

syscall_define6(futex, u32 __user *, uaddr, int, op, u32, val,

struct timespec __user *, utime, u32 __user *, uaddr2,

u32, val3)

/** 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中的部分**

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

以上是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

本篇結束 -

希望對大家有幫助 :)

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

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

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

幾個月一直懶得沒動筆寫寫部落格,最近開始動筆寫點什麼,今天就趁著加班出版本,橫下心決定把上次爛尾的文章給收了 上篇 接上篇,我們已經通過分析核心 看到pthread cond signal和pthread cond wait會發生cs context switch 本篇我將從實際測試資料出發,來看c...

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

幾個月一直懶得沒動筆寫寫部落格,最近開始動筆寫點什麼,今天就趁著加班出版本,橫下心決定把上次爛尾的文章給收了 上篇 接上篇,我們已經通過分析核心 看到pthread cond signal和pthread cond wait會發生cs context switch 本篇我將從實際測試資料出發,來看c...