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

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

原文  http://www.cppthinker.com/linux/224/context_switch_1/    原網頁已經掛了,轉載留存。

【上篇】

對於伺服器的優化,很多人都有自己的經驗和見解,但就我觀察,有兩點常常會被人忽視 – 上下文切換 和 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

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

#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

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中的部分程式碼

/* 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所帶來的效能下降

希望對大家有幫助 :)

【下篇】

接上篇,我們已經通過分析核心程式碼看到pthread_cond_signal和pthread_cond_wait會發生CS(Context Switch),本篇我將從實際測試資料出發,來看CS究竟會對我們的應用程式產生怎樣的影響。

一般我們可以通過工具vmstat, dstat, pidstat來觀察CS的切換情況。

vmstat, dstat只能觀察整個系統的切換情況,而pidstat可以更精確地觀察某個程序的上下文切換情況。

這兩個檔案中,在post非同步訊息給task_service_t時,會根據標頭檔案中定義的巨集在編譯期控制呼叫pthread_cond_signal還是write(fd),這是典型的生產者消費者模型

注意,通過系統呼叫write來通知task_service_t內部的執行緒會有以下幾種可能:

*) pipe

*) socketpair

*) eventfd

它們都是linux中的多程序/多執行緒通訊的常用手段

我們直接跑一下chaos/test/task_service 下的用例來分別看下不同機制的結果吧

CS/s post cost exec cost
pthread_cond_wat/pthread_cond_signal 600k 32,235,597 us 32,235,078 us
sleep 300 3,987,928 us 3,996,383 us
pipe 500 11,928,024 us 11,928,174 us
socket_pair 4000 16,532,314 us 16,532,461 us
eventfd 200 5,136,712 us 5,303,645 us
boost::io_service 750k 26,355,836 us 26,355,708 us

好,讓我們一個一個來解讀

首先,使用了pthread的條件變數的chaos::task_service引起的CS非常之大,效率也是最慢,原因其實上篇已經講述,不管是pthread_cond_wait還是pthread_cond_signal,都會發生一次CS。

使用了sleep的chaos::task_service,效率是最高的,主要原因是在於生產者每次投遞時不需要系統呼叫進行notify,且CS也是很小的,但是這種模型在理論上沒有其他 wait/notify的模型要來的好,而且CS和整體的效率還和sleep的引數有關

pipe, socket_pair 和 eventfd 都是基於 write系統呼叫來notify消費者,eventfd是最新核心提供的機制,幾乎感受不到的CS讓其效率也遙遙領先其他的通訊機制

值得注意的是boost::io_service,我這裡的測試系統是linux,windows上的boost::io_service實現沒有測試,但其CS切換如此之高,卻整體效率比chaos::task_service使用pthread 條件變數的模型來得快一些,我想應該是由於其內部的佇列實現,畢竟目前chaos::task_service的佇列只是簡單的lock deque。

基於以上統計,我們可以看出基本是呈現CS越少,整體執行效率越高的趨勢

我們可以得出一個比較淺顯的結論是,CS起碼會是影響我們程式效能的主要因素之一

當然,任何時候我都覺得測試資料只是眼前的測試資料,它只能告訴我們什麼東西值得我們去注意,而不是什麼東西一定是怎樣的,至少對於後臺服務,CS應該是我們常常需要去考量效能的一個因素

好了,就到這,希望對大家有幫助

PS. 加班寫文章思緒有些亂,前言不搭後語望包含,趕緊撤了~


相關推薦

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

幾個月一直懶得沒動筆寫寫部落格,最近開始動筆寫點什麼,今天就趁著加班出版本,橫下心決定把上次爛尾的文章給收了(上篇:https://blog.csdn.net/zhangmingcai/article/details/84823156)。 接上篇,我們已經通過分析核心程式碼看到pthread_c

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

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

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

原文  http://www.cppthinker.com/linux/224/context_switch_1/    原網頁已經掛了,轉載留存。 【上篇】 對於伺服器的優化,很多人都有自己的經驗和見解,但就我觀察,有兩點常常會被人忽視 – 上下文切換 和 Ca

Linux程序上下文切換過程context_switch詳解--Linux程序的管理與排程(二十一)

1 前景回顧 1.1 Linux的排程器組成 2個排程器 可以用兩種方法來啟用排程 一種是直接的, 比如程序打算睡眠或出於其他原因放棄CPU 另一種是通過週期性的機制, 以固定的頻率執行, 不時的檢測是否有必要 因此當前linux的排程程式由兩個排程器組成:主排程器,週期性排程器(兩者

執行緒上下文切換效能損耗測試

執行緒上下文切換的效能損耗到底有多少,一直沒有直觀的理解,今天寫個程式測試一下。先看看下面的程式(點選下載): ThreadTester是所有Tester的基類。所有的Tester都乾的是同樣一件事情,把counter增加到100000000,每次只能加1。 1: public abstr

計算機的異常控制:中斷、陷阱、故障、終止、程序上下文切換、訊號

總結《深入理解計算機系統》:異常控制流 1,計算機中的異常處理機制:處理器設計人員(如被零除、缺頁,儲存器訪問違例等)以及作業系統開發人員(如系統呼叫以及來自外部的IO裝置訊號等)為每種型別的異常分配了一個唯一的非負整數異常號。在系統啟動時,作業系統分配和初始化一張成為異常表

執行緒上下文切換程序上下文切換的區別

程序切換分兩步1.切換頁目錄以使用新的地址空間2.切換核心棧和硬體上下文。對於linux來說,執行緒和程序的最大區別就在於地址空間。對於執行緒切換,第1步是不需要做的,第2是程序和執行緒切換都要做的。所以明顯是程序切換代價大 執行緒上下文切換和程序上下問切換一個最主要的區別是執行緒的切換虛擬記憶體空間依

結合中斷上下文切換程序上下文切換分析Linux核心的一般執行過程

# 結合中斷上下文切換和程序上下文切換分析Linux核心的一般執行過程 [toc] ## 一. 實驗準備 1. 詳細要求 > 結合中斷上下文切換和程序上下文切換分析Linux核心一般執行過程 > > - 以fork和execve系統呼叫為例分析中斷上下文的切換 > - 分析ex

程序上下文頻繁切換導致load average過高(轉)

一、問題現象 現網有兩臺虛擬機器主機95%的cpu處於idle狀態,記憶體使用率也不是特別高,而主機的load average達到了40多。 二、問題分析 先在主機上通過top、free、ps、iostat 等常用工具分析了下主機的CPU、記憶體、IO使用情況,發現三者都不高。

[轉載] [嵌入式開發]Linux效能分析——上下文切換

標籤 PostgreSQL , Linux , 上下文切換 背景 原文 一、從一個問題說起 相信很多人在玩手機還是PC時,都曾碰到過這樣一種情況,安裝的軟體多了系統性能就變慢了,但是去檢視CPU利用率一直都低於10%,記憶體也很充足。我在近期的開發工作中就碰到了

Linux效能優化-上下文切換

目錄 CPU上下文 程序上下文切換 執行緒上下文切換 中斷上下文切換 上下文切換的兩個指標 簡單案例   CPU上下文 CPU的平均負載升高,上下文切換是罪歸禍首 Linux支援遠大於CPU數量的任務同時執行,通過將時間分片造成同時執行的錯覺 每

Java併發程式設計基礎//程序:每個程序都有獨立的程式碼和資料空間(程序上下文),程序間的切換開銷比較大,一個程序包含1-n個執行緒 //執行緒:同一類執行緒共享程式碼和資料空間,每個執行緒擁有獨立的執行棧和程式計

1.實現多執行緒的兩種方式: (1)繼承Thread類; (2)實現Runnable介面 //程序:每個程序都有獨立的程式碼和資料空間(程序上下文),程序間的切換開銷比較大,一個程序包含1-n個執行緒 //執行緒:同一類執行緒共享程式碼和資料空間,每個執行緒擁有獨立的執行

03-解析多執行緒與多程序的聯絡以及上下文切換所導致資源浪費問題

什麼是程序?簡單來講就是執行中的程式,那麼,何為執行中的程式呢?我們如何來看看程序呢?作業系統都是多程序的。我們通過工作管理員就可以看到作業系統中當前執行的很多的程序 我們寫過的任何的一個程式,我們寫一個程式,那麼,這個程式執行起來,它就稱之為程序。 程序和執行緒之間又有什麼關係呢?這

Context Switches上下文切換效能詳解

Context Switches 上下文切換,有時也被稱為程序切換(process switch)或任務切換。是一個重要的效能指標。 CPU從一個執行緒切換到另外一個執行緒,需要儲存當前任務的執行環境,恢復將要執行任務的執行環境,必然帶來效能消耗。 Context Switches 上下文切換簡介 作業系統

Linux下的程序1——程序概念,程序切換上下文切換,虛擬地址空間

程序概述   當一個可執行程式在現代系統上執行時,作業系統會提供一種假象——好像系統上只有這個程式在執行,看上去只有這個程式在使用處理器,主存和IO裝置。   處理器看上去就像在不間斷的一條接一條的執行程式中的指令,即改程式的程式碼和資料是系統儲存器中唯一的

linux程序切換程序上下文,thread_union資料結構(task_union V0.11)

程序是作業系統中很重要的一個抽象,是對程式的一次執行例項的抽象。每個程序執行在自己的上下中,其上下文包括被程序正文所定義的程序狀態、程序的全域性使用者變數和資料結構、它使用的暫存器的值、儲存在它的程序表項的值以及它的使用者棧和核心棧的內容。當出現執行中程序發生中斷或異常核心

Linux效能分析——上下文切換

一、從一個問題說起 相信很多人在玩手機還是PC時,都曾碰到過這樣一種情況,安裝的軟體多了系統性能就變慢了,但是去檢視CPU利用率一直都低於10%,記憶體也很充足。我在近期的開發工作中就碰到了類似的情況,不同的是,系統此時只有一個測試程式和幾個睡眠的後臺程序,說明是系統,

效能測試必備知識(5)- 深入理解“CPU 上下文切換

做效能測試的必備知識系列,可以看下面連結的文章哦 https://www.cnblogs.com/poloyy/category/1806772.html   前言 上一篇文章中,舉例了大量程序等待 CPU 排程的場景   靈魂拷問 既然程序是在等待,並沒有執行,為什麼系統的平均負載還是會

效能測試必備知識(6)- 如何檢視“CPU 上下文切換

做效能測試的必備知識系列,可以看下面連結的文章哦 https://www.cnblogs.com/poloyy/category/1806772.html   課前準備,安裝 sysbench 下載 sysbench git clone https://github.com/akopytov/sy

Spring Cloud應用程序上下文服務

spring springboot springcloud 配置 bootstrap Spring Boot對於如何使用Spring構建應用程序有一個看法:例如它具有常規配置文件的常規位置,以及用於常見管理和監視任務的端點。Spring Cloud建立在此之上,並添加了一些可能系統中所有組