24小時學通Linux內核之調度和內核同步

  心情大好,昨晚咱們實驗室老大和咱們聊了很久,做爲已經在實驗室待了快兩年的大三工科男來講,老師讓咱們不要成爲那種技術狗,代碼工,說多了都是淚啊,,不過咱們的激情依舊不變,老師幫咱們組好了隊伍,着手參加明年的全國大賽,提及來咱們學校歷史上也就又一次拿國一的,去了一次人民大會堂領獎,能夠說老大是對咱們寄予厚望,之後我會專攻儀器儀表類的題目,激情不滅,夢想不息,不過最近一段時間仍是會繼續更新Linux內核,總之,繼續加油~html

  Linux2.6版本中的內核引入了一個全新的調度程序,稱爲O(1)調度程序,進程在被初始化並放到運行隊列後,在某個時刻應該得到對CPU的訪問,它負責把CPU的控制權傳遞到不一樣進程的兩個函數schedule()和schedule_tick()中。下圖是隨着時間推移,CPU是如何在不一樣進程之間傳遞的,至於細節這裏很少闡釋,你們看待就能夠理解的啦~linux

  下面開始介紹一下上下文切換,在操做系統中,CPU切換到另外一個進程須要保存當前進程的狀態並恢復另外一個進程的狀態:當前運行任務轉爲就緒(或者掛起、刪除)狀態,另外一個被選定的就緒任務成爲當前任務。上下文切換包括保存當前任務的運行環境,恢復將要運行任務的運行環境。安全

 如何得到上下文切換的次數?bash

  vmstat直接運行便可,在最後幾列,有CPU的context switch次數。 這個是系統層面的,加入想看特定進程的狀況,可使用pidstat。函數

1 $ vmstat 1 100
2 procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu------
3  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
4  0  0     88 233484 288756 1784744    0    0     0    23    0    0  4  1 94  0  0
5  4  0     88 233236 288756 1784752    0    0     0     0 6202 7880  4  1 96  0  0
6  2  0     88 233360 288756 1784800    0    0     0   112 6277 7612  4  1 95  0  0
7  0  0     88 232864 288756 1784804    0    0     0   644 5747 6593  6  0 92  2  0

執行pidstat,將輸出系統啓動後全部活動進程的cpu統計信息:  工具

 1 linux:~ # pidstat
 2 Linux 2.6.32.12-0.7-default (linux)             06/18/12        _x86_64_
 3  
 4 11:37:19          PID    %usr %system  %guest    %CPU   CPU  Command
 5 ……
 6 11:37:19        11452    0.00    0.00    0.00    0.00     2  bash
 7 11:37:19        11509    0.00    0.00    0.00    0.00     3  dd
 8 11:37:19: pidstat獲取信息時間點
 9 PID: 進程pid
10 %usr: 進程在用戶態運行所佔cpu時間比率
11 %system: 進程在內核態運行所佔cpu時間比率
12 %CPU: 進程運行所佔cpu時間比率
13 CPU: 指示進程在哪一個核運行
14 Command: 拉起進程對應的命令
15 備註:執行pidstat默認輸出信息爲系統啓動後到執行時間點的統計信息,於是即便當前某進程的cpu佔用率很高

上下文切換的性能消耗在哪裏呢?性能

    ​    ​context switch太高,會致使CPU像個搬運工,頻繁在寄存器和運行隊列直接奔波  ,更多的時間花在了線程切換,而不是真正工做的線程上。直接的消耗包括CPU寄存器須要保存和加載,系統調度器的代碼須要執行。間接消耗在於多核cache之間的共享數據。    ​測試

引發上下文切換的緣由有哪些?ui

對於搶佔式操做系統而言, 大致有幾種:atom

  • 當前任務的時間片用完以後,系統CPU正常調度下一個任務;
  • 當前任務碰到IO阻塞,調度線程將掛起此任務,繼續下一個任務;
  • 多個任務搶佔鎖資源,當前任務沒有搶到,被調度器掛起,繼續下一個任務;
  • 用戶代碼掛起當前任務,讓出CPU時間;
  • 硬件中斷;    ​   ​

​如何測試上下文切換的時間消耗?

  這裏我再網上查找到了一個程序,代碼不長,切換一個   差很少就是  20個微秒吧,這個程序望大神指教

 1 #include <stdio.h>  
 2 #include <unistd.h>  
 3 #include <sys/time.h>  
 4 #include<pthread.h>  
 5   
 6 int pipes[20][3];  
 7 char buffer[10];  
 8 int running = 1;  
 9   
10 void inti()  
11 {  
12     int i =20;  
13     while(i--)  
14     {  
15         if(pipe(pipes[i])<0)  
16             exit(1);  
17         pipes[i][2] = i;  
18     }  
19 }  
20   
21 void distroy()  
22 {  
23     int i =20;  
24     while(i--)  
25     {  
26         close(pipes[i][0]);  
27         close(pipes[i][1]);  
28     }  
29 }  
30   
31 double self_test()  
32 {  
33     int i =20000;  
34     struct timeval start, end;  
35     gettimeofday(&start, NULL);  
36     while(i--)  
37     {  
38         if(write(pipes[0][1],buffer,10)==-1)  
39             exit(1);  
40         read(pipes[0][0],buffer,10);  
41     }  
42     gettimeofday(&end, NULL);  
43     return (double)(1000000*(end.tv_sec-start.tv_sec)+ end.tv_usec-start.tv_usec)/20000;  
44 }  
45   
46 void *_test(void *arg)  
47 {  
48     int pos = ((int *)arg)[2];  
49     int in = pipes[pos][0];  
50     int to = pipes[(pos + 1)%20][1];  
51     while(running)  
52     {  
53         read(in,buffer,10);  
54         if(write(to,buffer,10)==-1)  
55             exit(1);  
56     }  
57 }  
58   
59 double threading_test()  
60 {  
61     int i = 20;  
62     struct timeval start, end;  
63     pthread_t tid;  
64     while(--i)  
65     {  
66         pthread_create(&tid,NULL,_test,(void *)pipes[i]);  
67     }  
68     i = 10000;  
69     gettimeofday(&start, NULL);  
70     while(i--)  
71     {  
72         if(write(pipes[1][1],buffer,10)==-1)  
73             exit(1);  
74         read(pipes[0][0],buffer,10);  
75     }  
76     gettimeofday(&end, NULL);  
77     running = 0;  
78     if(write(pipes[1][1],buffer,10)==-1)  
79         exit(1);  
80     return (double)(1000000*(end.tv_sec-start.tv_sec)+ end.tv_usec-start.tv_usec)/10000/20;  
81 }  
82   
83   
84 int main()  
85 {  
86     inti();  
87     printf("%6.6f\n",self_test());  
88     printf("%6.6f\n",threading_test());  
89     distroy();  
90     exit(0);  
91 }  

  總而言之,咱們能夠認爲,這最多隻能是依賴於底層操做系統的近似計算。 一個近似的解法是記錄一個進程結束時的時間戳,另外一個進程開始的時間戳及排除等待時間。若是全部進程總共用時爲T,那麼總的上下文切換時間爲: T – (全部進程的等待時間和執行時間)

 

  接下來來述說搶佔,搶佔是一個進程到另外一個進程的切換,那麼Linux是如何決定在什麼時候進行切換的呢?下面咱們一次來介紹這三種搶佔方式。

顯式內核搶佔:

  最容易的就是這個搶佔啦,它發生在內核代碼調用schedule(能夠直接調用或者阻塞調用)時候的內核空間中。當這種方式搶佔時,例如在wait_queue等待隊列中設備驅動程序在等候時,控制權被簡單地傳遞到調度程序,從而新的進程被選中執行。

隱式用戶搶佔:

  當內核處理完內核空間的進程並準備把控制權傳遞到用戶空間的進程時,它首先查看應該把控制權傳遞到哪個用戶空間的進程上,這個進程也行不是傳遞其控制權到內核的那個用戶空間進程。系統中中的每個進程有一個「必須從新調度」,在進程應該被從新調度的任什麼時候候設置它。代碼能夠在include/linux/sched.h中查看~

static inline void set_tsk_need_resched(struct task_struct *tsk)
{
    set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}

static inline void clear_tsk_need_resched(struct task_struct *tsk)
{
    clear_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}
//set_tsk_need_resched和clear_tsk_need_resched是兩個接口,用於設置體系結構特有的TIF_NEED_RESCHED標誌
stactic inline int need_resched(void)
{
    return unlikely(test_thread_flag(TIF_NEED_RESCHED));
}
//need_resched測試當前線程的標誌,看看TIF_NEED_RESCHED是否被設置

隱式內核搶佔:

  隱式內核搶佔有兩種可能性:內核代碼出自使搶佔禁止的代碼塊,或者處理正在從中斷返回到內核代碼時,若是控制權正在從一箇中斷返回到內核空間,該中斷調用schedule(),一個新的 進程以剛纔描述的同一種方式被選中,若是內核代碼出自禁止搶佔的代碼塊,激活搶佔 的操做可能引發當前進程被搶佔(代碼在include/linux/preempt.h中查看到):

#define preempt_enable() \
do { \
    preempt_enable_no_resched(); \
    preempt_check_resched(); \
} while(0)

  preempt_enable()調用preempt_enable_no_resched(),它把與當前進程相關的preempt_count減1,而後調用preempt_check_resched():

#define preempt_check_resched() \
do { \
    if(unlikely(test_thread_flag(TIF_NEED_RESCHED))); \
    preempt_schedule(); \
} while(0)
preempt_check_resched()判斷當前進程是否被標記爲從新調度,若是是,它調用preempt_schedule()。

  
  當兩個或者兩個以上的進程請求對共享資源獨自訪問時候,它們須要具備這樣一種條件,即它們是在給代碼段中操做的惟一進程,在Linux內核鎖的基本形式是自旋鎖。自旋鎖會由於連續循環等待或者試圖兩次得到鎖這種方式的操做而致使死鎖,因此在此以前必須初始化spin_lock_t,這個能夠經過調用spin_lock_init()來完成(代碼在include/linux/spinlock.h中查看):
#define spin_lock_init(x) \
    do { \
        (x) -> magic = SPINLOCK_MAGIC; \
        (x) -> lock = 0; \      //設置自旋鎖爲"開鎖"
        (x) -> babble = 5; \
        (x) -> module = __FILE__; \
        (x) -> owner = NULL; \
        (x) -> oline = 0; \
    } while(0)

  自旋鎖被初始化後,能夠經過調用spin_lock()或者spin_lock_irqsave()來獲取,若是你使用spin_lock()那麼進程可能在上鎖的代碼中被中斷,爲了在代碼的臨界區執行後釋放,必須調用spin_unlock()或者spin_unlock_irqrestroe(),spin_unlock_irqrestroe()把中斷寄存器的狀態恢復成調用spin_lock_irq()時寄存器所處的狀態。

  自旋鎖的缺點是它們頻繁地循環直到等待鎖的釋放,那麼對於等待時間長的代碼區,最好是使用Linux kernel的另外一個上鎖工具:信號量。它的主要優點之一是:持有信號量的進程能夠安全的阻塞,它們在SMP和中斷中是保險的(代碼在include/asm-i386/semaphore.h,include/asm-ppc/semaphore.h中能夠查看):

struct semaphore{
    atomic_t count;
    int sleepers;
    wait_queue_head_t wait;
#ifdef WAITQUEUE_DEBUG
    long __magic;
#endif
};
struct semaphore{
    atomic_t count;
    wait_queue_head_t wait;
#ifdef WAITQUEUE_DEBUG
    long __magic;
#endif
};

  l兩種體系結構的實現都提供了指向wait_queue的一個指針和一個計數,有了信號量咱們可以讓多於一個的進程同時進去代碼的臨界區,若是計數初始化爲1,則表示只有一個進程可以進去代碼的臨界區,信號量用sema_init()來初始化,分別調用down()和up()來上鎖和解鎖,down()和up()函數的使用以及一些狀況就很少說了看信號量的調用問題,很容易理解的。

 

  小結

  好吧我真的本身不懂的愈來愈多了,以爲一每天的接受不了這麼多,內核實在是塊難啃的石頭,今天這個但是查了好多一成天才寫出來的,確定有不少問題以及沒有提到的地方,各路大神路過期候多指點多批評,,對一個菜鳥來講這兒不容易了,我會好好的繼續看代碼,看到吐的~~~

 

  版權全部,轉載請註明轉載地址:http://www.cnblogs.com/lihuidashen/p/4248174.html

相關文章
相關標籤/搜索