C++面試寶典:操作系統(三)

● 請你來說一說協程

參考回答:

1、概念:

協程,又稱微線程,纖程,英文名Coroutine。協程看上去也是子程序,但執行過程中,在子程序內部可中斷,然後轉而執行別的子程序,在適當的時候再返回來接着執行。

例如:

def A() :
print '1'
print '2'
print '3'
def B() :
print 'x'
print 'y'
print 'z'

由協程運行結果可能是12x3yz。在執行A的過程中,可以隨時中斷,去執行B,B也可能在執行過程中中斷再去執行A。但協程的特點在於是一個線程執行。

2)協程和線程區別

那和多線程比,協程最大的優勢就是協程極高的執行效率。因爲子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。

第二大優勢就是不需要多線程的鎖機制,因爲只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。

3)其他

在協程上利用多核CPU呢——多進程+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的性能。

Python對協程的支持還非常有限,用在generator中的yield可以一定程度上實現協程。雖然支持不完全,但已經可以發揮相當大的威力了。

● 系統調用是什麼,你用過哪些系統調用

參考回答:

1)概念:

在計算機中,系統調用(英語:system call),又稱爲系統呼叫,指運行在使用者空間的程序向操作系統內核請求需要更高權限運行的服務。系統調用提供了用戶程序與操作系統之間的接口(即系統調用是用戶程序和內核交互的接口)。

操作系統中的狀態分爲管態(核心態)和目態(用戶態)。大多數系統交互式操作需求在內核態執行。如設備IO操作或者進程間通信。特權指令:一類只能在核心態下運行而不能在用戶態下運行的特殊指令。不同的操作系統特權指令會有所差異,但是一般來說主要是和硬件相關的一些指令。用戶程序只在用戶態下運行,有時需要訪問系統核心功能,這時通過系統調用接口使用系統調用。

應用程序有時會需要一些危險的、權限很高的指令,如果把這些權限放心地交給用戶程序是很危險的(比如一個進程可能修改另一個進程的內存區,導致其不能運行),但是又不能完全不給這些權限。於是有了系統調用,危險的指令被包裝成系統調用,用戶程序只能調用而無權自己運行那些危險的指令。另外,計算機硬件的資源是有限的,爲了更好的管理這些資源,所有的資源都由操作系統控制,進程只能向操作系統請求這些資源。操作系統是這些資源的唯一入口,這個入口就是系統調用。

2)系統調用舉例:

對文件進行寫操作,程序向打開的文件寫入字符串“hello world”,open和write都是系統調用。如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<errno.h>

#include<unistd.h>

#include<sys/types.h>

#include<sys/stat.h>

#include<fcntl.h>

int main(int argc, char *argv[])

{

    if (argc<2)

        return 0;

    //用讀寫追加方式打開一個已經存在的文件

    int fd = open(argv[1], O_RDWR | O_APPEND);

    if (fd == -1)

    {

        printf("error is %s\n", strerror(errno));

    }

    else

    {

        //打印文件描述符號

        printf("success fd = %d\n", fd);

        char buf[100];

        memset(buf, 0, sizeof(buf));

        strcpy(buf, "hello world\n");

        write(fd, buf, strlen(buf));

        close(fd);

    }

    return 0;

}

還有寫數據write,創建進程fork,vfork等都是系統調用。

● 請你來手寫一下fork調用示例

參考回答:

1、概念:

Fork:創建一個和當前進程映像一樣的進程可以通過fork( )系統調用:

成功調用fork( )會創建一個新的進程,它幾乎與調用fork( )的進程一模一樣,這兩個進程都會繼續運行。在子進程中,成功的fork( )調用會返回0。在父進程中fork( )返回子進程的pid。如果出現錯誤,fork( )返回一個負值。

最常見的fork( )用法是創建一個新的進程,然後使用exec( )載入二進制映像,替換當前進程的映像。這種情況下,派生(fork)了新的進程,而這個子進程會執行一個新的二進制可執行文件的映像。這種“派生加執行”的方式是很常見的。

在早期的Unix系統中,創建進程比較原始。當調用fork時,內核會把所有的內部數據結構複製一份,複製進程的頁表項,然後把父進程的地址空間中的內容逐頁的複製到子進程的地址空間中。但從內核角度來說,逐頁的複製方式是十分耗時的。現代的Unix系統採取了更多的優化,例如Linux,採用了寫時複製的方法,而不是對父進程空間進程整體複製。

2、fork實例

int main(void)
{
pid_t pid;
signal(SIGCHLD, SIG_IGN);
printf("before fork pid:%d\n", getpid());
int abc = 10;
pid = fork();

if (pid == -1) {           //錯誤返回

perror("tile");
return -1;
}

if (pid > 0) {              //父進程空間

abc++;
printf("parent:pid:%d \n", getpid());
printf("abc:%d \n", abc);
sleep(20);
}

else if (pid == 0) {       //子進程空間

1

2

3

4

5

6

abc++;

printf("child:%d,parent: %d\n", getpid(), getppid());

printf("abc:%d", abc);

}

printf("fork after...\n");

 }

 

● 請你來說一說用戶態到內核態的轉化原理

參考回答:

1)用戶態切換到內核態的3種方式

1、系統調用

這是用戶進程主動要求切換到內核態的一種方式,用戶進程通過系統調用申請操作系統提供的服務程序完成工作。而系統調用的機制其核心還是使用了操作系統爲用戶特別開放的一箇中斷來實現,例如Linux的ine 80h中斷。

2、異常

當CPU在執行運行在用戶態的程序時,發現了某些事件不可知的異常,這是會觸發由當前運行進程切換到處理此。異常的內核相關程序中,也就到了內核態,比如缺頁異常。

3、外圍設備的中斷

當外圍設備完成用戶請求的操作之後,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條將要執行的指令,轉而去執行中斷信號的處理程序,如果先執行的指令是用戶態下的程序,那麼這個轉換的過程自然也就發生了有用戶態到內核態的切換。比如硬盤讀寫操作完成,系統會切換到硬盤讀寫的中斷處理程序中執行後續操作等。

2)切換操作

從出發方式看,可以在認爲存在前述3種不同的類型,但是從最終實際完成由用戶態到內核態的切換操作上來說,涉及的關鍵步驟是完全一樣的,沒有任何區別,都相當於執行了一箇中斷響應的過程,因爲系統調用實際上最終是中斷機制實現的,而異常和中斷處理機制基本上是一樣的,用戶態切換到內核態的步驟主要包括:

1、從當前進程的描述符中提取其內核棧的ss0及esp0信息。

2、使用ss0和esp0指向的內核棧將當前進程的cs,eip,eflags,ss,esp信息保存起來,這個過程也完成了由用戶棧找到內核棧的切換過程,同時保存了被暫停執行的程序的下一條指令。

3、將先前由中斷向量檢索得到的中斷處理程序的cs,eip信息裝入相應的寄存器,開始執行中斷處理程序,這時就轉到了內核態的程序執行了。

● 請你說一下源碼到可執行文件的過程

參考回答:

1)預編譯

主要處理源代碼文件中的以“#”開頭的預編譯指令。處理規則見下

1、刪除所有的#define,展開所有的宏定義。

2、處理所有的條件預編譯指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。

3、處理“#include”預編譯指令,將文件內容替換到它的位置,這個過程是遞歸進行的,文件中包含其他文件。

4、刪除所有的註釋,“//”和“/**/”。

5、保留所有的#pragma 編譯器指令,編譯器需要用到他們,如:#pragma once 是爲了防止有文件被重複引用。

6、添加行號和文件標識,便於編譯時編譯器產生調試用的行號信息,和編譯時產生編譯錯誤或警告是能夠顯示行號。

2)編譯

把預編譯之後生成的xxx.i或xxx.ii文件,進行一系列詞法分析、語法分析、語義分析及優化後,生成相應的彙編代碼文件。

1、詞法分析:利用類似於“有限狀態機”的算法,將源代碼程序輸入到掃描機中,將其中的字符序列分割成一系列的記號。

2、語法分析:語法分析器對由掃描器產生的記號,進行語法分析,產生語法樹。由語法分析器輸出的語法樹是一種以表達式爲節點的樹。

3、語義分析:語法分析器只是完成了對錶達式語法層面的分析,語義分析器則對錶達式是否有意義進行判斷,其分析的語義是靜態語義——在編譯期能分期的語義,相對應的動態語義是在運行期才能確定的語義。

4、優化:源代碼級別的一個優化過程。

5、目標代碼生成:由代碼生成器將中間代碼轉換成目標機器代碼,生成一系列的代碼序列——彙編語言表示。

6、目標代碼優化:目標代碼優化器對上述的目標機器代碼進行優化:尋找合適的尋址方式、使用位移來替代乘法運算、刪除多餘的指令等。

3)彙編

將彙編代碼轉變成機器可以執行的指令(機器碼文件)。 彙編器的彙編過程相對於編譯器來說更簡單,沒有複雜的語法,也沒有語義,更不需要做指令優化,只是根據彙編指令和機器指令的對照表一一翻譯過來,彙編過程有彙編器as完成。經彙編之後,產生目標文件(與可執行文件格式幾乎一樣)xxx.o(Windows下)、xxx.obj(Linux下)。

4)鏈接

將不同的源文件產生的目標文件進行鏈接,從而形成一個可以執行的程序。鏈接分爲靜態鏈接和動態鏈接:

1、靜態鏈接:

函數和數據被編譯進一個二進制文件。在使用靜態庫的情況下,在編譯鏈接可執行文件時,鏈接器從庫中複製這些函數和數據並把它們和應用程序的其它模塊組合起來創建最終的可執行文件。

空間浪費:因爲每個可執行程序中對所有需要的目標文件都要有一份副本,所以如果多個程序對同一個目標文件都有依賴,會出現同一個目標文件都在內存存在多個副本;

更新困難:每當庫函數的代碼修改了,這個時候就需要重新進行編譯鏈接形成可執行程序。

運行速度快:但是靜態鏈接的優點就是,在可執行程序中已經具備了所有執行程序所需要的任何東西,在執行的時候運行速度快。

2、動態鏈接:

動態鏈接的基本思想是把程序按照模塊拆分成各個相對獨立部分,在程序運行時纔將它們鏈接在一起形成一個完整的程序,而不是像靜態鏈接一樣把所有程序模塊都鏈接成一個單獨的可執行文件。

共享庫:就是即使需要每個程序都依賴同一個庫,但是該庫不會像靜態鏈接那樣在內存中存在多分,副本,而是這多個程序在執行時共享同一份副本;

更新方便:更新時只需要替換原來的目標文件,而無需將所有的程序再重新鏈接一遍。當程序下一次運行時,新版本的目標文件會被自動加載到內存並且鏈接起來,程序就完成了升級的目標。

性能損耗:因爲把鏈接推遲到了程序運行時,所以每次執行程序都需要進行鏈接,所以性能會有一定損失。

● 請你來說一下微內核與宏內核

參考回答:

宏內核:除了最基本的進程、線程管理、內存管理外,將文件系統,驅動,網絡協議等等都集成在內核裏面,例如linux內核。

優點:效率高。

缺點:穩定性差,開發過程中的bug經常會導致整個系統掛掉。

微內核:內核中只有最基本的調度、內存管理。驅動、文件系統等都是用戶態的守護進程去實現的。

優點:穩定,驅動等的錯誤只會導致相應進程死掉,不會導致整個系統都崩潰

缺點:效率低。典型代表QNX,QNX的文件系統是跑在用戶態的進程,稱爲resmgr的東西,是訂閱發佈機制,文件系統的錯誤只會導致這個守護進程掛掉。不過數據吞吐量就比較不樂觀了。

● 請你說一下殭屍進程

參考回答:

1)正常進程

正常情況下,子進程是通過父進程創建的,子進程再創建新的進程。子進程的結束和父進程的運行是一個異步過程,即父進程永遠無法預測子進程到底什麼時候結束。 當一個進程完成它的工作終止之後,它的父進程需要調用wait()或者waitpid()系統調用取得子進程的終止狀態。

unix提供了一種機制可以保證只要父進程想知道子進程結束時的狀態信息, 就可以得到:在每個進程退出的時候,內核釋放該進程所有的資源,包括打開的文件,佔用的內存等。 但是仍然爲其保留一定的信息,直到父進程通過wait / waitpid來取時才釋放。保存信息包括:

1進程號the process ID

2退出狀態the termination status of the process

3運行時間the amount of CPU time taken by the process等

2)孤兒進程

一個父進程退出,而它的一個或多個子進程還在運行,那麼那些子進程將成爲孤兒進程。孤兒進程將被init進程(進程號爲1)所收養,並由init進程對它們完成狀態收集工作。

3)殭屍進程

一個進程使用fork創建子進程,如果子進程退出,而父進程並沒有調用wait或waitpid獲取子進程的狀態信息,那麼子進程的進程描述符仍然保存在系統中。這種進程稱之爲殭屍進程。

殭屍進程是一個進程必然會經過的過程:這是每個子進程在結束時都要經過的階段。

如果子進程在exit()之後,父進程沒有來得及處理,這時用ps命令就能看到子進程的狀態是“Z”。如果父進程能及時 處理,可能用ps命令就來不及看到子進程的殭屍狀態,但這並不等於子進程不經過殭屍狀態。

如果父進程在子進程結束之前退出,則子進程將由init接管。init將會以父進程的身份對殭屍狀態的子進程進行處理。

危害:

如果進程不調用wait / waitpid的話, 那麼保留的那段信息就不會釋放,其進程號就會一直被佔用,但是系統所能使用的進程號是有限的,如果大量的產生僵死進程,將因爲沒有可用的進程號而導致系統不能產生新的進程。

外部消滅:

通過kill發送SIGTERM或者SIGKILL信號消滅產生殭屍進程的進程,它產生的僵死進程就變成了孤兒進程,這些孤兒進程會被init進程接管,init進程會wait()這些孤兒進程,釋放它們佔用的系統進程表中的資源

內部解決:

1、子進程退出時向父進程發送SIGCHILD信號,父進程處理SIGCHILD信號。在信號處理函數中調用wait進行處理殭屍進程。

2、fork兩次,原理是將子進程成爲孤兒進程,從而其的父進程變爲init進程,通過init進程可以處理殭屍進程。

● 請問GDB調試用過嗎,什麼是條件斷點

參考回答:

1、GDB調試

GDB 是自由軟件基金會(Free Software Foundation)的軟件工具之一。它的作用是協助程序員找到代碼中的錯誤。如果沒有GDB的幫助,程序員要想跟蹤代碼的執行流程,唯一的辦法就是添加大量的語句來產生特定的輸出。但這一手段本身就可能會引入新的錯誤,從而也就無法對那些導致程序崩潰的錯誤代碼進行分析。

GDB的出現減輕了開發人員的負擔,他們可以在程序運行的時候單步跟蹤自己的代碼,或者通過斷點暫時中止程序的執行。此外,他們還能夠隨時察看變量和內存的當前狀態,並監視關鍵的數據結構是如何影響代碼運行的。

2、條件斷點

條件斷點是當滿足條件就中斷程序運行,命令:break line-or-function if expr。

例如:(gdb)break 666 if testsize==100

● 請你來介紹一下5種IO模型

參考回答:

1.阻塞IO:調用者調用了某個函數,等待這個函數返回,期間什麼也不做,不停的去檢查這個函數有沒有返回,必須等這個函數返回才能進行下一步動作
2.非阻塞IO:非阻塞等待,每隔一段時間就去檢測IO事件是否就緒。沒有就緒就可以做其他事。
3.信號驅動IO:信號驅動IO:linux用套接口進行信號驅動IO,安裝一個信號處理函數,進程繼續運行並不阻塞,當IO時間就緒,進程收到SIGIO信號。然後處理IO事件。
4.IO複用/多路轉接IO:linux用select/poll函數實現IO複用模型,這兩個函數也會使進程阻塞,但是和阻塞IO所不同的是這兩個函數可以同時阻塞多個IO操作。而且可以同時對多個讀操作、寫操作的IO函數進行檢測。知道有數據可讀或可寫時,才真正調用IO操作函數
5.異步IO:linux中,可以調用aio_read函數告訴內核描述字緩衝區指針和緩衝區的大小、文件偏移及通知的方式,然後立即返回,當內核將數據拷貝到緩衝區後,再通知應用程序。

● 請你說一說異步編程的事件循環

參考回答:

事件循環就是不停循環等待時間的發生,然後將這個事件的所有處理器,以及他們訂閱這個事件的時間順序依次依次執行。當這個事件的所有處理器都被執行完畢之後,事件循環就會開始繼續等待下一個事件的觸發,不斷往復。當同時併發地處理多個請求時,以上的概念也是正確的,可以這樣理解:在單個的線程中,事件處理器是一個一個按順序執行的。即如果某個事件綁定了兩個處理器,那麼第二個處理器會在第一個處理器執行完畢後,纔開始執行。在這個事件的所有處理器都執行完畢之前,事件循環不會去檢查是否有新的事件觸發。在單個線程中,一切都是有順序地一個一個地執行的!

● 請你回答一下操作系統爲什麼要分內核態和用戶態

參考回答:

爲了安全性。在cpu的一些指令中,有的指令如果用錯,將會導致整個系統崩潰。分了內核態和用戶態後,當用戶需要操作這些指令時候,內核爲其提供了API,可以通過系統調用陷入內核,讓內核去執行這些操作。

● 請你回答一下爲什麼要有page cache,操作系統怎麼設計的page cache

參考回答:

加快從磁盤讀取文件的速率。page cache中有一部分磁盤文件的緩存,因爲從磁盤中讀取文件比較慢,所以讀取文件先去page cache中去查找,如果命中,則不需要去磁盤中讀取,大大加快讀取速度。在 Linux 內核中,文件的每個數據塊最多隻能對應一個 Page Cache 項,它通過兩個數據結構來管理這些 Cache
項,一個是radix tree,另一個是雙向鏈表。Radix tree 是一種搜索樹,Linux
內核利用這個數據結構來通過文件內偏移快速定位Cache 項

● server端監聽端口,但還沒有客戶端連接進來,此時進程處於什麼狀態?

參考回答:

這個需要看服務端的編程模型,如果如上一個問題的回答描述的這樣,則處於阻塞狀態,如果使用了epoll,select等這樣的io複用情況下,處於運行狀態

● 請問如何設計server,使得能夠接收多個客戶端的請求

參考回答:

多線程,線程池,io複用

● 就緒狀態的進程在等待什麼?

參考回答:

被調度使用cpu的運行權

● 死循環+來連接時新建線程的方法效率有點低,怎麼改進?

參考回答:

提前創建好一個線程池,用生產者消費者模型,創建一個任務隊列,隊列作爲臨界資源,有了新連接,就掛在到任務隊列上,隊列爲空所有線程睡眠。改進死循環:使用select epoll這樣的技術

● 請你說一下多線程的同步,鎖的機制

參考回答:

同步的時候用一個互斥量,在訪問共享資源前對互斥量進行加鎖,在訪問完成後釋放互斥量上的鎖。對互斥量進行加鎖以後,任何其他試圖再次對互斥量加鎖的線程將會被阻塞直到當前線程釋放該互斥鎖。如果釋放互斥鎖時有多個線程阻塞,所有在該互斥鎖上的阻塞線程都會變成可運行狀態,第一個變爲運行狀態的線程可以對互斥量加鎖,其他線程將會看到互斥鎖依然被鎖住,只能回去再次等待它重新變爲可用。在這種方式下,每次只有一個線程可以向前執行。

● 兩個進程訪問臨界區資源,會不會出現都獲得自旋鎖的情況?

參考回答:

單核cpu,並且開了搶佔可以造成這種情況。

● 請問怎麼實現線程池

參考回答:

1.設置一個生產者消費者隊列,作爲臨界資源
2.初始化n個線程,並讓其運行起來,加鎖去隊列取任務運行
3.當任務隊列爲空的時候,所有線程阻塞
4.當生產者隊列來了一個任務後,先對隊列加鎖,把任務掛在到隊列上,然後使用條件變量去通知阻塞中的一個線程

● Linux下怎麼得到一個文件的100到200行

參考回答:

sed -n '100,200p' inputfile

awk 'NR>=100&&NR<=200{print}' inputfile

head -200 inputfile|tail -100

● 請你來說一下awk的使用

參考回答:

1)作用:

樣式掃描和處理語言。它允許創建簡短的程序,這些程序讀取輸入文件、爲數據排序、處理數據、對輸入執行計算以及生成報表,還有無數其他的功能。

2)用法:

awk [-F  field-separator]  'commands'  input-file(s)

3)內置變量

ARGC

命令行參數個數

ARGV

命令行參數排列

ENVIRON

支持隊列中系統環境變量的使用

FILENAME

awk瀏覽的文件名

FNR

瀏覽文件的記錄數

FS

設置輸入域分隔符,等價於命令行 -F選項

NF

瀏覽記錄的域的個數

NR

已讀的記錄數

OFS

輸出域分隔符

ORS

輸出記錄分隔符

RS

控制記錄分隔符

4)實例:

1、找到當前文件夾下所有的文件和子文件夾,並顯示文件大小

> ls -l | awk '{print $5 "\t" $9}'

讀入有'\n'換行符分割的一條記錄,然後將記錄按指定的域分隔符劃分域,填充域。$0則表示所有域,$1表示第一個域,$n表示第n個域。默認域分隔符是"空白鍵" 或 "[tab]鍵"。

2、找到當前文件夾下所有的文件和子文件夾,並顯示文件大小,並顯示排序

> ls -l | awk 'BEGIN {COUNT = -1; print "BEGIN COUNT"}
{COUNT = COUNT + 1; print COUNT"\t"$5"\t"$9}
END {print "END, COUNT = "COUNT}'

先處理BEGIN, 然後進行文本分析,進行第二個{}的操作,分析完進行END操作。

3、找到當前文件夾下所有的子文件夾,並顯示排序

> ls -l | awk 'BEGIN {print "BEGIN COUNT"} /4096/{print NR"\t"$5"\t"$9}

END {print "END"}'

* /4096/ 正則匹配式子

* 使用print $NF可以打印出一行中的最後一個字段,使用$(NF-1)則是打印倒數第二個字段,其他以此類推。

 

● 請你來說一下linux內核中的Timer 定時器機制

參考回答:

1)低精度時鐘

Linux 2.6.16之前,內核只支持低精度時鐘,內核定時器的工作方式:

1、系統啓動後,會讀取時鐘源設備(RTC, HPET,PIT…),初始化當前系統時間。

2、內核會根據HZ(系統定時器頻率,節拍率)參數值,設置時鐘事件設備,啓動tick(節拍)中斷。HZ表示1秒種產生多少個時鐘硬件中斷,tick就表示連續兩個中斷的間隔時間。

3、設置時鐘事件設備後,時鐘事件設備會定時產生一個tick中斷,觸發時鐘中斷處理函數,更新系統時鐘,並檢測timer wheel,進行超時事件的處理。

在上面工作方式下,Linux 2.6.16 之前,內核軟件定時器採用timer wheel多級時間輪的實現機制,維護操作系統的所有定時事件。timer wheel的觸發是基於系統tick週期性中斷。

所以說這之前,linux只能支持ms級別的時鐘,隨着時鐘源硬件設備的精度提高和軟件高精度計時的需求,有了高精度時鐘的內核設計。

2)高精度時鐘

Linux 2.6.16 ,內核支持了高精度的時鐘,內核採用新的定時器hrtimer,其實現邏輯和Linux 2.6.16 之前定時器邏輯區別:

hrtimer採用紅黑樹進行高精度定時器的管理,而不是時間輪;

高精度時鐘定時器不在依賴系統的tick中斷,而是基於事件觸發。

舊內核的定時器實現依賴於系統定時器硬件定期的tick,基於該tick,內核會掃描timer wheel處理超時事件,會更新jiffies,wall time(牆上時間,現實時間),process的使用時間等等工作。

新的內核不再會直接支持週期性的tick,新內核定時器框架採用了基於事件觸發,而不是以前的週期性觸發。新內核實現了hrtimer(high resolution timer):於事件觸發。

hrtimer的工作原理:

通過將高精度時鐘硬件的下次中斷觸發時間設置爲紅黑樹中最早到期的Timer 的時間,時鐘到期後從紅黑樹中得到下一個 Timer 的到期時間,並設置硬件,如此循環反覆。

在高精度時鐘模式下,操作系統內核仍然需要週期性的tick中斷,以便刷新內核的一些任務。hrtimer是基於事件的,不會週期性出發tick中斷,所以爲了實現週期性的tick中斷(dynamic tick):系統創建了一個模擬 tick 時鐘的特殊 hrtimer,將其超時時間設置爲一個tick時長,在超時回來後,完成對應的工作,然後再次設置下一個tick的超時時間,以此達到週期性tick中斷的需求。

引入了dynamic tick,是爲了能夠在使用高精度時鐘的同時節約能源,這樣會產生tickless 情況下,會跳過一些 tick。

新內核對相關的時間硬件設備進行了統一的封裝,定義了主要有下面兩個結構:

時鐘源設備(closk source device):抽象那些能夠提供計時功能的系統硬件,比如 RTC(Real Time Clock)、TSC(Time Stamp Counter),HPET,ACPI PM-Timer,PIT等。不同時鐘源提供的精度不一樣,現在pc大都是支持高精度模式(high-resolution mode)也支持低精度模式(low-resolution mode)。

時鐘事件設備(clock event device):系統中可以觸發 one-shot(單次)或者週期性中斷的設備都可以作爲時鐘事件設備。

當前內核同時存在新舊timer wheel 和 hrtimer兩套timer的實現,內核啓動後會進行從低精度模式到高精度時鐘模式的切換,hrtimer模擬的tick中斷將驅動傳統的低精度定時器系統(基於時間輪)和內核進程調度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章