<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          操作系統(tǒng)就是一個“死循環(huán)”!

          共 8588字,需瀏覽 18分鐘

           ·

          2021-04-09 17:26


          操作系統(tǒng)就是一個“死循環(huán)”? 

          在回答之前,我們先從進程調(diào)度的角度來看看。

          進程調(diào)度想必大家都有所了解,又都不太了解。

          有所了解是因為這個概念被提到太多次,不太了解是因為總覺得不直觀,浮于概念層。
          今天我們從三個視角來看看進程調(diào)度究竟是怎么回事,啟車了請扶好。
          小貼士:本文講述的是 linux-0.11 版本的進程調(diào)度機制,學(xué)習(xí)其骨干和框架,不要鉆入細(xì)節(jié)。

          1
          滴答視角

          計算機中有一個設(shè)備,叫定時器,準(zhǔn)確說叫可編程定時/計數(shù)器。
          這個定時器每隔一段時間就會向 CPU 發(fā)起一個中斷信號。
          在 linux-0.11 中,這個間隔時間被設(shè)置為 10 ms,也就是 100 Hz。
          shedule.c
          #define HZ 100
          發(fā)起的中斷叫時鐘中斷,其中斷向量號被設(shè)置為了 0x20。

          時鐘中斷

          一切的源頭,就源于這個每 10ms 產(chǎn)生的一次時鐘中斷。
          當(dāng)然如果沒有操作系統(tǒng)的存在,這個 10ms 一次的時鐘中斷,就打了水漂,CPU 會收到這個時鐘中斷信號,但不會做出任何反應(yīng)。
          但很不幸,linux 提前設(shè)置好了中斷向量表。
          schedule.c
          set_intr_gate(0x20, &timer_interrupt);
          這樣,當(dāng)時鐘中斷,也就是 0x20 號中斷來臨時,CPU 會查找中斷向量表中 0x20 處的函數(shù)地址,這個函數(shù)地址即中斷處理函數(shù),并跳轉(zhuǎn)過去執(zhí)行。
          這個中斷處理函數(shù)就是 timer_interrupt,是用匯編語言寫的。
          system_call.s
          _timer_interrupt:
          ...
          // 增加系統(tǒng)滴答數(shù)
          incl _jiffies
          ...
          // 調(diào)用函數(shù) do_timer
          call _do_timer
          ...
          這個函數(shù)做了兩件事,一個是將系統(tǒng)滴答數(shù)這個變量 jiffies 加一,一個是調(diào)用了另一個函數(shù) do_timer。
          sched.c
          void do_timer(long cpl) {
              ...
              // 當(dāng)前線程還有剩余時間片,直接返回
              if ((--current->counter)>0return;
              // 若沒有剩余時間片,調(diào)度
              schedule();
          }
          do_timer 最重要的部分就是上面這段代碼,非常簡單。
          首先將當(dāng)前進程的時間片 -1,然后判斷:
          如果時間片仍然大于零,則什么都不做直接返回。
          如果時間片已經(jīng)為零,則調(diào)用 schedule(),用腳去想也知道,這就是進行進程調(diào)度的主干。

          進程的調(diào)度

          void schedule(void) {
              int i, next, c;
              struct task_struct ** p;
              ...
              while (1) {
                  c = -1;
                  next = 0;
                  i = NR_TASKS;
                  p = &task[NR_TASKS];
                  while (--i) {
                      if (!*--p)
                          continue;
                      if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
                          c = (*p)->counter, next = i;
                  }
                  if (c) break;
                  for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
                      if (*p)
                          (*p)->counter = ((*p)->counter >> 1) +
                                  (*p)->priority;
              }
              switch_to(next);
          }
          別看一大坨,我做個不嚴(yán)謹(jǐn)?shù)暮喕?,你就懂了?/span>
          void schedule(void) {
              int next = get_max_counter_from_runnable();
              refresh_all_thread_counter();
              switch_to(next);
          }
          很簡答,這個函數(shù)就做了三件事:

          1. 拿到剩余時間片(counter的值)最大且在 runnable 狀態(tài)(state = 0)的進程號 next。

          2. 如果所有 runnable 進程時間片都為 0,則將所有進程(注意不僅僅是 runnable 的進程)的 counter 重新賦值(counter = counter/2 + priority),然后再次執(zhí)行步驟 1。

          3. 最后拿到了一個進程號 next,調(diào)用了 switch_to(next) 這個方法,就切換到了這個進程去執(zhí)行了。

          切換進程

          看 switch_to 方法,是用內(nèi)聯(lián)匯編語句寫的。
          sched.h
          #define switch_to(n) {\
          struct {long a,b;} __tmp; \
          __asm__("cmpl %%ecx,_current\n\t" \
              "je 1f\n\t" \
              "movw %%dx,%1\n\t" \
              "xchgl %%ecx,_current\n\t" \
              "ljmp %0\n\t" \
              "cmpl %%ecx,_last_task_used_math\n\t" \
              "jne 1f\n\t" \
              "clts\n" \
              "1:" \
              ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
              "d" (_TSS(n)),"c" ((long) task[n])); \
          }
          這段話就是進程切換的最最最最底層的代碼了,看不懂沒關(guān)系,其實主要就干了兩件事。

          1.通過 ljmp 跳轉(zhuǎn)指令跳轉(zhuǎn)到新進程的偏移地址處。

          2.將當(dāng)前各個寄存器的值保存在當(dāng)前進程的 TSS 中,并將新進程的 TSS 信息加載到各個寄存器。(這部分是執(zhí)行 ljmp 指令的副作用,并且是由硬件實現(xiàn)的)

          簡單說,保存當(dāng)前進程上下文,恢復(fù)下一個進程的上下文,跳過去!啥是上下文,就是他喵的一堆寄存器的值而已。

          上圖來源于《Linux內(nèi)核完全注釋V5.0》

          至此,我們梳理完了一個進程切換的整條鏈路,先來回顧一下。
          1.罪魁禍?zhǔn)椎?,就是那個每 10ms 觸發(fā)一次的定時器滴答。
          2.而這個滴答將會給 CPU 產(chǎn)生一個時鐘中斷信號。
          3.而這個中斷信號會使 CPU 查找中斷向量表,找到操作系統(tǒng)寫好的一個時鐘中斷處理函數(shù) do_timer。
          4.do_timer 會首先將當(dāng)前進程的 counter 變量 -1,如果 counter 此時仍然大于 0,則就此結(jié)束。
          5.但如果 counter = 0 了,就開始進行進程的調(diào)度。
          6.進程調(diào)度就是找到所有處于 RUNNABLE 狀態(tài)的進程,并找到一個 counter 值最大的進程,把它丟進 switch_to 函數(shù)的入?yún)⒗铩?/span>
          7.switch_to 這個終極函數(shù),會保存當(dāng)前進程上下文,恢復(fù)要跳轉(zhuǎn)到的這個進程的上下文,同時使得 CPU 跳轉(zhuǎn)到這個進程的偏移地址處。
          8.接著,這個進程就舒舒服服地運行了起來,等待著下一次滴答的來臨。
          行行行,給你畫個圖,瞧把你懶的。。

          這就是滴答視角。
           
          2

          數(shù)據(jù)結(jié)構(gòu)視角

           
          上面我們從一次滴答開始,掀起了一陣波浪,走完了一個滴答的整個流程。
          下面我們換個靜態(tài)視角,看看數(shù)據(jù)結(jié)構(gòu)。
          一切承載進程相關(guān)的數(shù)據(jù),其罪魁禍?zhǔn)讈碜杂谶@個數(shù)據(jù)結(jié)構(gòu)。
          struct task_struct * task[64] = {};
          沒錯,一個容量只有 64 大小的數(shù)組,數(shù)組中的元素是 task_struct 結(jié)構(gòu)。
          struct task_struct {
              long state; 
              long counter;
              long priority;
              struct tss_struct tss;
          };
          這里只取了我們需要關(guān)心的關(guān)鍵字段。
          state就是進程的狀態(tài),取值 linux 中有明確定義。
          #define TASK_RUNNING  0
          #define TASK_INTERRUPTIBLE  1
          #define TASK_UNINTERRUPTIBLE  2
          #define TASK_ZOMBIE  3
          #define TASK_STOPPED  4
          比如 state 取值不是 RUNNING 狀態(tài)的,它就不會被進程調(diào)度。這在上面滴答視角的講述中講得很明白。
          counterpriority就是記錄進程時間片的,counter 記錄了剩余時間片,priority 表示優(yōu)先級的意思,其實也就是為進程初始時間片分配一個值而已。這部分同樣在上面的滴答視角的代碼中,講的很明白。
          最后一個重要的結(jié)構(gòu)就是tss,它是個結(jié)構(gòu)體,記錄了進程上下文信息。
          struct tss_struct {
              ...
              long    eip;
              long    eflags;
              long    eax,ecx,edx,ebx;
              long    esp;
              long    ebp;
              ...
          };
          在講滴答視角時我們也說了,我們老是說上下文上下文,究竟什么是上下文,其實就是這個結(jié)構(gòu)體里的值,就是一堆寄存器的值而已。
          同樣在滴答視角的講解中也提到了,進程切換的最核心一步,就是一個 ljmp 指令,該指令的副作用會將當(dāng)前各個寄存器的值保存在當(dāng)前進程的 TSS 中,并將新進程的 TSS 信息加載到各個寄存器,這就是上下文切換的本質(zhì)。
          所以我們看到,數(shù)據(jù)結(jié)構(gòu)視角中所提到的數(shù)據(jù),在滴答視角下都被用到了。
           
          3

          操作系統(tǒng)啟動流程視角

           
          當(dāng)你按下了開機鍵,引導(dǎo)程序把內(nèi)核從硬盤加載到內(nèi)存,經(jīng)過一番折騰后,開始執(zhí)行系統(tǒng)初始化程序 init/main.c。
          這部分的細(xì)節(jié)如果你很好奇,可以閱讀我的自制操作系統(tǒng)系列文章的開頭幾篇。
          好了,我們就從這 main.c 開啟我們的旅程,當(dāng)然,我們只關(guān)注進程相關(guān)的部分。
          void main(void) {
              ...
              // 第一步:進程調(diào)度初始化
              sched_init();
              ...
              // 第二步:創(chuàng)建一個新進程并做些事
              if (!fork()) {
                  init();
              }
              // 第三步:死循環(huán),操作系統(tǒng)正式啟動完畢
              for(;;) pause();
          }
          第一步是 sched_init 進程調(diào)度初始化,初始化些啥呢?很簡單,我挑主要的講。
          void sched_init(void) {
              // 初始化第一個進程的 tss
              set_tss_desc(...);
              // 將進程數(shù)組清零
              for(i=1;i<64;i++) {
                  task[i] = NULL;
                  ...
              }
              // 設(shè)置始終中斷(滴答)
              set_intr_gate(0x20,&timer_interrupt);
              ...
          }
          其實就是為進程管理需要的數(shù)據(jù)結(jié)構(gòu)做一些初始化工作,并設(shè)置好時鐘中斷,以便可以走滴答視角那個流程。
          第二步與進程調(diào)度關(guān)系不大,與操作系統(tǒng)原理的關(guān)系很大,主要是最終執(zhí)行到 shell 程序等待用戶輸入,暫時不講。
          第三步,for(;;) pause(),反映了操作系統(tǒng)的本質(zhì),操作系統(tǒng)就是一個中斷驅(qū)動的死循環(huán)代碼
          這段代碼就是個死循環(huán),將操作系統(tǒng)怠速在這里。而通過各種中斷,比如本講所說的時鐘中斷完成進程調(diào)度,再比如鍵盤中斷完成用戶輸入,并還可能通過 shell 進程解釋命令而執(zhí)行一個新的程序。
          當(dāng)沒有任何進程需要運行時,也即 CPU 空閑時,操作系統(tǒng)會調(diào)度到這段代碼來運行,承載這段代碼的進程我們通常叫它 0 號進程,這部分的原理可以看碼農(nóng)的荒島求生的一篇文章《CPU 空閑時在干嘛?》,講的很明白,且形象。
          這就是操作系統(tǒng)啟動流程的視角,我們可以看到,其實就是做各種各樣的準(zhǔn)備工作,然后啟動一個 shell 進程,并進入死循環(huán)的等待狀態(tài),這期間不斷由時鐘中斷觸發(fā)進程調(diào)度機制。


          后記





          以上,分別從滴答視角、數(shù)據(jù)結(jié)構(gòu)視角、操作系統(tǒng)啟動流程視角,來講解來進程調(diào)度的細(xì)節(jié)。

          所謂滴答視角,可以理解為常說的進程調(diào)度視角。所謂數(shù)據(jù)結(jié)構(gòu)視角,可以理解為常說的進程管理視角。

          但我更喜歡我起的這兩個名字,尤其是滴答視角,好可愛有木有!

          不過本文是以 linux 最早的版本 linux-0.11 為例,在后來的操作系統(tǒng)演進過程中,進程調(diào)度的細(xì)節(jié)也在不斷添枝加葉,比如選出下一個要調(diào)度的進程不再是簡單地比較時間片大小,比如進程實際發(fā)生切換的時機改到了系統(tǒng)調(diào)用返回前,再比如對頁表切換的變化等等。

          但整個骨架和流程都是一樣的,也即你再去研究更為復(fù)雜的現(xiàn)代操作系統(tǒng)進程調(diào)度原理時,只要按照這三個視角去分析,總是可以把握主干。

          1、網(wǎng)曝IDEA2020.3.2,自動注釋類和方法注釋模板配置

          2、牛逼!IntelliJ IDEA居然支持視頻聊天了~速來嘗鮮!快來沖一波

          3、微信這些表情包,我可能再也不敢用了!你還用嗎?

          4、知名國產(chǎn)網(wǎng)盤翻車?清空免費用戶文件后,又開始清理付費用戶資源

          5、Chrome新功能曝光:你訪問的敏感網(wǎng)站可以自動隱藏起來

          6、萬萬沒想到,“紅孩兒”竟然做了程序員,還是CTO!

          7、徒手?jǐn)]一個Spring Boot中的starter,解密自動化配置,超級棒!

          點分享

          點收藏

          點點贊

          點在看

          瀏覽 53
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  四虎黄色按摩 | 男人手机天堂 | 午夜网久久久成人 | 九热精品视频 | 成人自拍网址 |