Linux0.11內核--進程調度分析之2.調度

来源:http://www.cnblogs.com/joey-hua/archive/2016/06/18/5596830.html
-Advertisement-
Play Games

【版權所有,轉載請註明出處。出處:http://www.cnblogs.com/joey-hua/p/5596830.html 】 上一篇說到進程調度歸根結底是調用timer_interrupt函數,在system_call.s中: 前面一堆push指令保存當前的寄存器,然後在ret_from_sy ...


【版權所有,轉載請註明出處。出處:http://www.cnblogs.com/joey-hua/p/5596830.html

上一篇說到進程調度歸根結底是調用timer_interrupt函數,在system_call.s中:

#### int32 -- (int 0x20) 時鐘中斷處理程式。中斷頻率被設置為100Hz(include/linux/sched.h,5),
# 定時晶元8253/8254 是在(kernel/sched.c,406)處初始化的。因此這裡jiffies 每10 毫秒加1。
# 這段代碼將jiffies 增1,發送結束中斷指令給8259 控制器,然後用當前特權級作為參數調用
# C 函數do_timer(long CPL)。當調用返回時轉去檢測並處理信號。
.align 2
_timer_interrupt:
push %ds 								# save ds,es and put kernel data space
push %es 								# into them. %fs is used by _system_call
push %fs
pushl %edx 							# we save %eax,%ecx,%edx as gcc doesn't
pushl %ecx 							# save those across function calls. %ebx
pushl %ebx 							# is saved as we use that in ret_sys_call
pushl %eax
movl $0x10,%eax 				# ds,es 置為指向內核數據段。
mov %ax,%ds
mov %ax,%es
movl $0x17,%eax 				# fs 置為指向局部數據段(出錯程式的數據段)。
mov %ax,%fs
incl _jiffies
# 由於初始化中斷控制晶元時沒有採用自動EOI,所以這裡需要髮指令結束該硬體中斷。
movb $0x20,%al 				# EOI to interrupt controller #1
outb %al,$0x20 					# 操作命令字OCW2 送0x20 埠。
# 下麵3 句從選擇符中取出當前特權級別(0 或3)並壓入堆棧,作為do_timer 的參數。
movl CS(%esp),%eax
andl $3,%eax 						# %eax is CPL (0 or 3, 0=supervisor)
pushl %eax
# do_timer(CPL)執行任務切換、計時等工作,在kernel/shched.c,305 行實現。
call _do_timer 						# 'do_timer(long CPL)' does everything from
addl $4,%esp 						# task switching to accounting ...
jmp ret_from_sys_call

前面一堆push指令保存當前的寄存器,然後在ret_from_sys_call中彈出。

movl $0x10,%eax把段選擇子0x10也就是內核數據段選擇子賦值給eax,然後再賦給ds、es;

然後_jiffies加1,jiffies在sched.h中定義:

extern long volatile jiffies;	// 從開機開始算起的滴答數(10ms/滴答)。

接下來三句指令比較關鍵:

movl CS(%esp),%eax
andl $3,%eax 						# %eax is CPL (0 or 3, 0=supervisor)
pushl %eax

從上面push的寄存器當中取出cs寄存器的值,也就是代碼段選擇子,根據選擇的結構,0-1位是特權級,andl $3,%eax就是取eax中0-1位的值,然後把eax壓棧當成do_timer的參數傳遞,4個位元組。

好了,現在進入do_timer函數,在sched.c中:

//// 時鐘中斷C 函數處理程式,在kernel/system_call.s 中的_timer_interrupt(176 行)被調用。
// 參數cpl 是當前特權級0 或3,0 表示內核代碼在執行。
// 對於一個進程由於執行時間片用完時,則進行任務切換。並執行一個計時更新工作。
void do_timer (long cpl)
{
  extern int beepcount;		// 揚聲器發聲時間滴答數(kernel/chr_drv/console.c,697)
  extern void sysbeepstop (void);	// 關閉揚聲器(kernel/chr_drv/console.c,691)

  // 如果發聲計數次數到,則關閉發聲。(向0x61 口發送命令,複位位0 和1。位0 控制8253
  // 計數器2 的工作,位1 控制揚聲器)。
  if (beepcount)
    if (!--beepcount)
      sysbeepstop ();

  // 如果當前特權級(cpl)為0(最高,表示是內核程式在工作),則將內核程式運行時間stime 遞增;
  // [ Linus 把內核程式統稱為超級用戶(supervisor)的程式,見system_call.s,193 行上的英文註釋]
  // 如果cpl > 0,則表示是一般用戶程式在工作,增加utime。
  if (cpl)
    current->utime++;
  else
    current->stime++;

  // 如果有用戶的定時器存在,則將鏈表第1 個定時器的值減1。如果已等於0,則調用相應的處理
  // 程式,並將該處理程式指針置為空。然後去掉該項定時器。
  if (next_timer)
    {				// next_timer 是定時器鏈表的頭指針(見270 行)。
      next_timer->jiffies--;
      while (next_timer && next_timer->jiffies <= 0)
	{
	  void (*fn) (void);	// 這裡插入了一個函數指針定義!!!??

	  fn = next_timer->fn;
	  next_timer->fn = NULL;
	  next_timer = next_timer->next;
	  (fn) ();		// 調用處理函數。
	}
    }
  // 如果當前軟盤控制器FDC 的數字輸出寄存器中馬達啟動位有置位的,則執行軟盤定時程式(245 行)。
  if (current_DOR & 0xf0)
    do_floppy_timer ();
  if ((--current->counter) > 0)
    return;			// 如果進程運行時間還沒完,則退出。
  current->counter = 0;
  if (!cpl)
    return;			// 對於超級用戶程式(內核態程式),不依賴counter 值進行調度。
  schedule ();
}

傳遞來的參數cpl的作用就是如果為0,表示是內核程式,則stime加1,否則都是普通用戶程式,則utime加1。

用戶定時器等用到再說。

接下來判斷時間片counter,在sched.h的進程描述符中:

long counter;		// long counter 任務運行時間計數(遞減)(滴答數),運行時間片。

如果還有時間片則不調用調度函數schedule(),然後時間片減1並退出此函數。

如果時間片已用完(<=0),則置時間片為0,緊接著判斷特權級,如果是內核級程式則直接退出函數。否則進入最核心的調度函數schedule:

/*
 * 'schedule()'是調度函數。這是個很好的代碼!沒有任何理由對它進行修改,因為它可以在所有的
 * 環境下工作(比如能夠對IO-邊界處理很好的響應等)。只有一件事值得留意,那就是這裡的信號
 * 處理代碼。
 * 註意!!任務0 是個閑置('idle')任務,只有當沒有其它任務可以運行時才調用它。它不能被殺
 * 死,也不能睡眠。任務0 中的狀態信息'state'是從來不用的。
 */
void schedule (void)
{
  int i, next, c;
  struct task_struct **p;	// 任務結構指針的指針。

  /* check alarm, wake up any interruptible tasks that have got a signal */
  /* 檢測alarm(進程的報警定時值),喚醒任何已得到信號的可中斷任務 */

  // 從任務數組中最後一個任務開始檢測alarm。
  for (p = &LAST_TASK; p > &FIRST_TASK; --p)
    if (*p)
      {
    	// 如果設置過任務的定時值alarm,並且已經過期(alarm<jiffies),則在信號點陣圖中置SIGALRM 信號,
    	// 即向任務發送SIGALARM 信號。然後清alarm。該信號的預設操作是終止進程。
    	// jiffies 是系統從開機開始算起的滴答數(10ms/滴答)。定義在sched.h 第139 行。
	if ((*p)->alarm && (*p)->alarm < jiffies)
	  {
	    (*p)->signal |= (1 << (SIGALRM - 1));
	    (*p)->alarm = 0;
	  }
	// 如果信號點陣圖中除被阻塞的信號外還有其它信號,並且任務處於可中斷狀態,則置任務為就緒狀態。
	// 其中'~(_BLOCKABLE & (*p)->blocked)'用於忽略被阻塞的信號,但SIGKILL 和SIGSTOP 不能被阻塞。
	if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
	    (*p)->state == TASK_INTERRUPTIBLE)
	  (*p)->state = TASK_RUNNING;	//置為就緒(可執行)狀態。
      }

  /* this is the scheduler proper: */
  /* 這裡是調度程式的主要部分 */

  while (1)
    {
      c = -1;
      next = 0;
      i = NR_TASKS;
      p = &task[NR_TASKS];
      // 這段代碼也是從任務數組的最後一個任務開始迴圈處理,並跳過不含任務的數組槽。比較每個就緒
      // 狀態任務的counter(任務運行時間的遞減滴答計數)值,哪一個值大,運行時間還不長,next 就
      // 指向哪個的任務號。
      while (--i)
	{
	  if (!*--p)
	    continue;
	  if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
	    c = (*p)->counter, next = i;
	}
      // 如果比較得出有counter 值大於0 的結果,則退出124 行開始的迴圈,執行任務切換(141 行)。
      if (c)
	break;
      // 否則就根據每個任務的優先權值,更新每一個任務的counter 值,然後回到125 行重新比較。
      // counter 值的計算方式為counter = counter /2 + priority。[右邊counter=0??]這裡計算過程不考慮進程的狀態。
      for (p = &LAST_TASK; p > &FIRST_TASK; --p)
	if (*p)
	  (*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
    }
  // 切換到任務號為next 的任務運行。在126 行next 被初始化為0。因此若系統中沒有任何其它任務
  // 可運行時,則next 始終為0。因此調度函數會在系統空閑時去執行任務0。此時任務0 僅執行
  // pause()系統調用,並又會調用本函數。
  switch_to (next);		// 切換到任務號為next 的任務,並運行之。
}

前面的比較好理解,直接分析主要部分,此部分的主要工作就是從所有的任務中找出時間片最大的任務,也就意味著運行的時間較少,next就指向這個任務並跳出迴圈去切換任務。

如果所有任務的時間片都為0,就根據每個任務的優先權值來更新每個任務的時間片counter值。然後重新找到next,最後切換任務,調用switch_to(next):

// 巨集定義,計算在全局表中第n 個任務的TSS 描述符的索引號(選擇符)。
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))

/*
* switch_to(n)將切換當前任務到任務nr,即n。首先檢測任務n 不是當前任務,
* 如果是則什麼也不做退出。如果我們切換到的任務最近(上次運行)使用過數學
* 協處理器的話,則還需複位控制寄存器cr0 中的TS 標誌。
*/
// 輸入:%0 - 新TSS 的偏移地址(*&__tmp.a); %1 - 存放新TSS 的選擇符值(*&__tmp.b);
// dx - 新任務n 的選擇符;ecx - 新任務指針task[n]。
// 其中臨時數據結構__tmp 中,a 的值是32 位偏移值,b 為新TSS 的選擇符。在任務切換時,a 值
// 沒有用(忽略)。在判斷新任務上次執行是否使用過協處理器時,是通過將新任務狀態段的地址與
// 保存在last_task_used_math 變數中的使用過協處理器的任務狀態段的地址進行比較而作出的。
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__( "cmpl %%ecx,_current\n\t" \	// 任務n 是當前任務嗎?(current ==task[n]?)
  "je 1f\n\t" \			// 是,則什麼都不做,退出。
  "movw %%dx,%1\n\t" \		// 將新任務的選擇符??*&__tmp.b。
  "xchgl %%ecx,_current\n\t" \	// current = task[n];ecx = 被切換出的任務。
  "ljmp %0\n\t" \		// 執行長跳轉至*&__tmp,造成任務切換。
// 在任務切換回來後才會繼續執行下麵的語句。
  "cmpl %%ecx,_last_task_used_math\n\t" \	// 新任務上次使用過協處理器嗎?
  "jne 1f\n\t" \		// 沒有則跳轉,退出。
  "clts\n" \			// 新任務上次使用過協處理器,則清cr0 的TS 標誌。
  "1:"::"m" (*&__tmp.a), "m" (*&__tmp.b),
  "d" (_TSS (n)), "c" ((long) task[n]));
}

分析這段代碼前先要知道,在32位保護模式下,有2種直接發起任務切換的方法:

1.call 0x0010:0x00000000

2.jmp 0x0010:0x00000000

在這兩種情況下,call和jmp指令的操作數是任務的TSS描述符選擇子或任務門。當處理器執行這兩條指令時,首先用指令中給出的描述符選擇子訪問GDT,分析它的描述符類型。如果是一般的代碼段描述符,就按普通的段間轉移規則執行;如果是調用門,按調用門的規則執行;如果是TSS描述符,或者任務門,則執行任務切換。此時,指令中給出的32位偏移量被忽略,原因是執行任務切換時,所有處理器的狀態都可以從TSS中獲得

當任務切換髮生的時候,TR寄存器的內容也會跟著指向新任務的TSS。這個過程是這樣的:首先,處理器將當前任務的現場信息保存到由TR寄存器指向的TSS;然後,再使TR寄存器指向新任務的TSS,並從新任務的TSS中恢復現場。

註意:任務門描述符可以安裝在中斷描述符表中,也可以安裝在GDT或者LDT中。

知道了理論知識,上面的代碼就不難分析了,關鍵的一句是把新任務的TSS選擇子賦值給%1也就是*&_tmp.b處,現在b的值就是TSS選擇子,註意這裡ljmp %0相當於ljmp *%0,表示是間接跳轉,相當於“ljmp *__tmp.a”,也就是跳轉到地址&__tmp.a中包含的48bit邏輯地址處。而按struct _tmp的定義,這也就意味著__tmp.a即為該邏輯地址的offset部分,__tmp.b的低16bit為seg_selector(高16bit無用)部分。

直到這行指令執行完,才算真正的任務切換!至此進程調度分析結束。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 對象,是javascript中非常重要的一個梗,是否能透徹的理解它直接關係到你對整個javascript體系的基礎理解,說白了,javascript就是一群對象在攪。。(嗶!)。 常用的幾種對象創建模式 使用new關鍵字創建 最基礎的對象創建方式,無非就是和其他多數語言一樣說的一樣:沒對象,你new ...
  • ...
  • 一:GIF(Graphics Interchange Format) 簡介GIF圖形交換格式是一種點陣圖圖形文件格式,以8位色(即256種顏色)重現真彩色的圖像。 它實際上是一種壓縮文檔,採用LZW壓縮演算法進行編碼,有效地減少了圖像文件在網路上傳輸的時間。 它是目前廣泛應用於網路傳輸的圖像格式之一。優 ...
  • ...
  • 一.概述 * 鬧鐘功能概述:添加鬧鐘,刪除鬧鐘 * 思路: * 1.給一個button添加點擊監聽,用於添加鬧鐘 * 2.提供一個視窗進行鬧鐘時間的選擇 * 3.數據保存:對鬧鐘的數據進行保存 * 4.數據讀取:打開app的時候對鬧鐘的數據進行讀取,以便保留以前設置的鬧鐘 * 5.對鬧鐘進行刪除操作 ...
  • 【版權所有,轉載請註明出處。出處:http://www.cnblogs.com/joey-hua/p/5597705.html 】 Linux內核因為使用了記憶體分頁機制,所以相對來說好理解些。因為記憶體分頁就是為了方便管理記憶體。 說到記憶體分頁,最根部的要屬頁目錄表了,head.h中: 然後再看head ...
  • “階段一”是指我第一次系統地學習Android開發。這主要是對我的學習過程作個記錄。 最近學到用AsyncTask來處理有關網路的操作。雖然代碼看上去不是很複雜,但仍有很多地方有疑惑。所以研讀了一下API文檔,在這裡把我學到的和練習的代碼展示出來。如有錯誤,歡迎指出! 一、關於AsyncTask的< ...
  • 接著上篇《Android 採用get方式提交數據到伺服器》,本文來實現採用post方式提交數據到伺服器 首先對比一下get方式和post方式: 修改佈局: 添加代碼: ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...