linux/kernel/目錄下共包括 10 個 C 語言文件和 2 個匯編語言文件以及一個 kernel 下編譯文件的管理配置文件 Makefile。其中三個子目錄中代碼注釋的將放在后面的文章進行。本文主要對這 13 個代碼文件進行注釋。 首先我們對所有程序的基本功能進行概括性地總體介紹, 以便一開始就對這 12 個文件所實現的功能和它們之間的相互調用關系有個大致的了解,然后逐一對代碼進行詳細地注釋。本文地址:http://www.CUOXin.com/archimedes/p/linux011-kernel-sched.html,轉載請注明源地址。
linux/kernel/目錄
1 總體功能描述該目錄下的代碼文件從功能上可以分為三類,一類是硬件(異常)中斷處理程序文件,一類是系統調用服務處理程序文件,另一類是進程調度等通用功能文件。參見圖。我們現在根據這個分類方式,從實現的功能上進行更詳細的說明。
硬件中斷處理類程序主要包括兩個代碼文件: asm.s 和 traps.c 文件。 asm.s 用于實現大部分硬件異常所引起的中斷的匯編語言處理過程。而 traps.c 程序則實現了 asm.s 的中斷處理過程中調用的 c 函數。另外幾個硬件中斷處理程序在文件 system_call.s 和 mm/page.s 中實現。中斷信號通??梢苑譃閮深悾?硬件中斷和軟件中斷(異常)。 每個中斷是由 0-255 之間的一個數字來標識。對于中斷 int0--int31(0x00--0x1f),每個中斷的功能是由 Intel 固定設定或保留用的, 屬于軟件中斷,但 Intel 稱之為異常。因為是由 CPU 執行指令時探測到異常時引起的。通常還可分為故障(Fault)和陷阱(traps) 兩 類 。 中 斷 int32--int255 (0x20--0xff) 可以由用戶自己設定 。 在 Linux 系統中 , 則 將int32--int47(0x20--0x2f)對應于 8259A 中斷控制芯片發出的硬件中斷請求信號 IRQ0-IRQ15;并把程序編程發出的系統調用(system_call)中斷設置為 int128(0x80)。在將控制權交給中斷處理程序之前, CPU 會首先將至少 12 字節的信息壓入中斷處理程序的堆棧中。這種情況與一個長調用(段間子程序調用)比較相象。 CPU 會將代碼段選擇符和返回地址的偏移值壓入堆棧。另一個與段間調用比較相象的地方是 80386 將信息壓入到了目的代碼的堆棧上,而不是被中斷代碼的堆棧。另外, CPU 還總是將標志寄存器 EFLAGS 的內容壓入堆棧。如果優先級別發生了變化,比如從用戶級改變到內核系統級, CPU 還會將原代碼的堆棧段值和堆棧指針壓入中斷程序的堆棧中。對于具有優先級改變時堆棧的內容示意圖如圖所示:
asm.s 代碼文件主要涉及對 Intel 保留中斷 int0--int16 的處理,其余保留的中斷 int17-int31 由 Intel 公司留作今后擴充使用。對應于中斷控制器芯片各 IRQ 發出的 int32-int47 的 16 個處理程序將分別在各種硬件(如時鐘、 鍵盤、 軟盤、 數學協處理器、 硬盤等) 初始化程序中處理。 Linux 系統調用中斷 int128(0x80)的處理則將在kernel/system_call.s 中給出。各個中斷的具體定義見代碼注釋后其它信息一節中的說明。由于有些異常引起中斷時, CPU 內部會產生一個出錯代碼壓入堆棧(異常中斷 int 8 和 int10 - int 14),見圖 5.1 所示, 而其它的中斷卻并不帶有這個出錯代碼(例如被零除出錯和邊界檢查出錯等), 因此, asm.s程序中將所有中斷的處理根據是否攜帶出錯代碼而分別進行處理。但處理流程還是一樣的。對一個硬件異常所引起的中斷的處理過程見下圖所示:
功能描述:該該程序只有一個函數 mktime(),僅供內核使用。計算從1970年1月1日0時起到開機當日經過的秒數,作為開機時間。linux/kernel/mktime.c 程序:
/* * linux/kernel/mktime.c * * (C) 1991 Linus Torvalds */#include <time.h>/* * 這不是庫函數,它僅供內核使用。因此我們不關心小于 1970 年的年份等,但假定一切均很正常。 * 同樣,時間區域 TZ 問題也先忽略。我們只是盡可能簡單地處理問題。最好能找到一些公開的庫函數 * (盡管我認為 minix 的時間函數是公開的)。 * 另外,我恨那個設置 1970 年開始的人 - 難道他們就不能選擇從一個閏年開始?我恨格里高利歷、 * 羅馬教皇、主教,我什么都不在乎。我是個脾氣暴躁的人。 */#define MINUTE 60 // 1 分鐘的秒數。#define HOUR (60*MINUTE) // 1 小時的秒數。#define DAY (24*HOUR) // 1 天的秒數。#define YEAR (365*DAY) // 1 年的秒數。/* 有趣的是我們考慮進了閏年 */ // 下面以年為界限,定義了每個月開始時的秒數時間數組。static int month[12] = { 0, DAY*(31), DAY*(31+29), DAY*(31+29+31), DAY*(31+29+31+30), DAY*(31+29+31+30+31), DAY*(31+29+31+30+31+30), DAY*(31+29+31+30+31+30+31), DAY*(31+29+31+30+31+30+31+31), DAY*(31+29+31+30+31+30+31+31+30), DAY*(31+29+31+30+31+30+31+31+30+31), DAY*(31+29+31+30+31+30+31+31+30+31+30)};// 該函數計算從 1970 年 1 月 1 日 0 時起到開機當日經過的秒數,作為開機時間。long kernel_mktime(struct tm * tm){ long res; int year; year = tm->tm_year - 70;// 從 70 年到現在經過的年數(2 位表示方式),因此會有2000年問題。/* 為了獲得正確的閏年數,這里需要這樣一個魔幻偏值(y+1).*/ res = YEAR*year + DAY*((year+1)/4); // 這些年經過的秒數時間+每個閏年時多1天的秒數時間,在加上當年到當月時的秒數。 res += month[tm->tm_mon];/* 以及(y+2)。如果(y+2)不是閏年,那么我們就必須進行調整(減去一天的秒數時間)。*/ if (tm->tm_mon>1 && ((year+2)%4)) res -= DAY; res += DAY*(tm->tm_mday-1); // 再加上本月過去的天數的秒數時間。 res += HOUR*tm->tm_hour; // 再加上當天過去的小時數的秒數時間。 res += MINUTE*tm->tm_min; // 再加上 1 小時內過去的分鐘數的秒數時間。 res += tm->tm_sec; // 再加上 1 分鐘內已過的秒數。 return res; // 即等于從 1970 年以來經過的秒數時間。}PS:閏年的基本計算方法是:如果 y 能被 4 除盡且不能被 100 除盡,或者能被 400 除盡,則 y 是閏年。
sched.c 程序1 功能描述sched.c 是內核中有關任務調度函數的程序,其中包括有關調度的基本函數(sleep_on、 wakeup、schedule 等)以及一些簡單的系統調用函數(比如 getpid())。另外 Linus 為了編程的方便,考慮到軟盤驅動器程序定時的需要,也將操作軟盤的幾個函數放到了這里。這幾個基本函數的代碼雖然不長,但有些抽象,比較難以理解。這里僅對調度函數 schedule()作一些說明。schedule()函數首先對所有任務(進程)進行檢測,喚醒任何一個已經得到信號的任務。
具體方法是針對任務數組中的每個任務,檢查其報警定時值 alarm。如果任務的 alarm 時間已經過期(alarm<jiffies),則在它的信號位圖中設置 SIGALRM 信號, 然后清 alarm 值。 jiffies 是系統從開機開始算起的滴答數( 10ms/滴答)。在 sched.h 中定義。如果進程的信號位圖中除去被阻塞的信號外還有其它信號,并且任務處于可中斷睡眠狀態( TASK_INTERRUPTIBLE),則置任務為就緒狀態( TASK_RUNNING)。
隨后是調度函數的核心處理部分。這部分代碼根據進程的時間片和優先權調度機制,來選擇隨后要執行的任務。它首先循環檢查任務數組中的所有任務,根據每個就緒態任務剩余執行時間的值 counter,選取該值最大的一個任務,并利用 switch_to()函數切換到該任務。若所有就緒態任務的該值都等于零,表示此刻所有任務的時間片都已經運行完,于是就根據任務的優先權值 PRiority,重置每個任務的運行時間片值 counter,再重新執行循環檢查所有任務的執行時間片值。
另一個值得一提的函數是 sleep_on(),該函數雖然很短,卻要比schedule()函數難理解。這里用圖示的方法加以解釋。簡單地說, sleep_on()函數的主要功能是當一個進程(或任務)所請求的資源正忙或不在內存中時暫時切換出去,放在等待隊列中等待一段時間。當切換回來后再繼續運行。放入等待隊列的方式是利用了函數中的 tmp 指針作為各個正在等待任務的聯系。函數中共牽涉到對三個任務指針操作: *p、 tmp 和 current, *p 是等待隊列頭指針,如文件系統內存i 節點的 i_wait 指針、內存緩沖操作中的 buffer_wait 指針等; tmp 是臨時指針; current 是當前任務指針。對于這些指針在內存中的變化情況我們可以用下面的示意圖說明。圖中的長條表示內存字節序列。

sleep_on() 函數中指針變化示意圖
當剛進入該函數時,隊列頭指針*p 指向已經在等待隊列中等待的任務結構(進程描述符)。當然,在系統剛開始執行時,等待隊列上無等待任務。因此上圖中的原等待任務在剛開始時是不存在的,此時*p 指向 NULL。通過指針操作,在調用調度程序之前,隊列頭指針指向了當前任務結構,而函數中的臨時指針 tmp 指向了原等待任務。從而通過該臨時指針的作用,在該函數被嵌套調用時,程序就隱式地構筑出一個等待隊列。從下面的圖 5.6 中,我們可以更容易地理解 sleep_on()函數的等待隊列形成過程。圖中示出了當向隊列頭部插入第三個任務時的情況。
sleep_on() 函數的隱式任務等待隊列。還有一個函數 interruptible_sleep_on(),它的結構與 sleep_on()的基本類似,只是在進行調度之前是把當前任務置成了可中斷等待狀態,并在本任務被喚醒后還需要隊列上是否有后來的等待任務,若有,則調度它們先運行。在內核 0.12 開始,這兩個函數被合二為一,僅用任務的狀態作為參數來區分這兩種情況。linux/kernel/sched.c 程序
/* * linux/kernel/sched.c * * (C) 1991 Linus Torvalds *//* * 'sched.c' 是主要的內核文件。其中包括有關調度的基本函數(sleep_on、wakeup、schedule 等)以及 * 一些簡單的系統調用函數( 比如 getpid() ,僅從當前任務中獲取一個字段)。 */#include <linux/sched.h> // 調度程序頭文件。定義了任務結構 task_struct、第 1 個初始任務的數據。 // 還有一些以宏的形式定義的有關描述符參數設置和獲取的嵌入式匯編函數程序。#include <linux/kernel.h> // 內核頭文件。含有一些內核常用函數的原形定義。#include <linux/sys.h> // 系統調用頭文件。含有 72 個系統調用 C 函數處理程序,以'sys_'開頭。#include <linux/fdreg.h> // 軟驅頭文件。含有軟盤控制器參數的一些定義。#include <asm/system.h> // 系統頭文件。定義了設置或修改描述符/中斷門等的嵌入式匯編宏。#include <asm/io.h> // io 頭文件。定義硬件端口輸入/輸出宏匯編語句。#include <asm/segment.h> // 段操作頭文件。定義了有關段寄存器操作的嵌入式匯編函數。#include <signal.h> // 信號頭文件。定義信號符號常量, sigaction 結構,操作函數原型。#define _S(nr) (1<<((nr)-1)) // 取信號 nr 在信號位圖中對應位的二進制數值。信號編號 1-32。 // 比如信號 5 的位圖數值 = 1<<(5-1) = 16 = 00010000b。#define _BLOCKABLE (~(_S(SIGKILL) | _S(SIGSTOP))) // 除了 SIGKILL 和 SIGSTOP 信號以外其它都是 // 可阻塞的(…10111111111011111111b)。// 顯示任務號 nr 的進程號、進程狀態和內核堆棧空閑字節數(大約)。void show_task(int nr,struct task_struct * p){ int i,j = 4096-sizeof(struct task_struct); printk("%d: pid=%d, state=%d, ",nr,p->pid,p->state); i=0; while (i<j && !((char *)(p+1))[i]) // 檢測指定任務數據結構以后等于 0 的字節數。 i++; printk("%d (of %d) chars free in kernel stack/n/r",i,j);}// 顯示所有任務的任務號、進程號、進程狀態和內核堆??臻e字節數(大約)。void show_stat(void){ int i; for (i=0;i<NR_TASKS;i++) // NR_TASKS 是系統能容納的最大進程(任務)數量(64 個), if (task[i]) // 定義在 include/kernel/sched.h 第 4 行。 show_task(i,task[i]);}// 定義每個時間片的滴答數#define LATCH (1193180/HZ)extern void mem_use(void); // [??]沒有任何地方定義和引用該函數。extern int timer_interrupt(void); // 時鐘中斷處理程序(kernel/system_call.s,176)。extern int system_call(void); // 系統調用中斷處理程序(kernel/system_call.s,80)。union task_union { // 定義任務聯合(任務結構成員和 stack 字符數組程序成員)。 struct task_struct task; // 因為一個任務數據結構與其堆棧放在同一內存頁中,所以 char stack[PAGE_SIZE]; // 從堆棧段寄存器 ss 可以獲得其數據段選擇符。};static union task_union init_task = {INIT_TASK,}; // 定義初始任務的數據(sched.h 中)。long volatile jiffies=0; // 從開機開始算起的滴答數時間值(10ms/滴答)。// 前面的限定符 volatile,英文解釋是易變、不穩定的意思。這里是要求 gcc 不要對該變量進行優化// 處理,也不要挪動位置,因為也許別的程序會來修改它的值。long startup_time=0; // 開機時間。從 1970:0:0:0 開始計時的秒數。struct task_struct *current = &(init_task.task); // 當前任務指針(初始化為初始任務)。struct task_struct *last_task_used_math = NULL; // 使用過協處理器任務的指針。struct task_struct * task[NR_TASKS] = {&(init_task.task), }; // 定義任務指針數組。long user_stack [ PAGE_SIZE>>2 ] ; // 定義系統堆棧指針,4K。指針指在最后一項。struct { // 該結構用于設置堆棧 ss:esp(數據段選擇符,指針),見 head.s,第 23 行。 long * a; short b; } stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };/* * 將當前協處理器內容保存到老協處理器狀態數組中,并將當前任務的協處理器 * 內容加載進協處理器。 */ // 當任務被調度交換過以后,該函數用以保存原任務的協處理器狀態(上下文)并恢復新調度進來的 // 當前任務的協處理器執行狀態。void math_state_restore(){ if (last_task_used_math == current) // 如果任務沒變則返回(上一個任務就是當前任務)。 return; // 這里所指的"上一個任務"是剛被交換出去的任務。 __asm__("fwait"); // 在發送協處理器命令之前要先發 WAIT 指令。 if (last_task_used_math) { // 如果上個任務使用了協處理器,則保存其狀態。 __asm__("fnsave %0"::"m" (last_task_used_math->tss.i387)); } last_task_used_math=current;// 現在,last_task_used_math 指向當前任務, // 以備當前任務被交換出去時使用。 if (current->used_math) { // 如果當前任務用過協處理器,則恢復其狀態。 __asm__("frstor %0"::"m" (current->tss.i387)); } else { // 否則的話說明是第一次使用, __asm__("fninit"::); // 于是就向協處理器發初始化命令, current->used_math=1; // 并設置使用了協處理器標志。 }}/* * 'schedule()'是調度函數。這是個很好的代碼!沒有任何理由對它進行修改,因為它可以在所有的 * 環境下工作(比如能夠對 IO-邊界處理很好的響應等)。只有一件事值得留意,那就是這里的信號 * 處理代碼。 * 注意!!任務 0 是個閑置('idle') 任務,只有當沒有其它任務可以運行時才調用它。它不能被殺 * 死,也不能睡眠。任務 0 中的狀態信息'state' 是從來不用的。 */void schedule(void){ int i,next,c; struct task_struct ** p; // 任務結構指針的指針。/* 檢測 alarm(進程的報警定時值),喚醒任何已得到信號的可中斷任務 */// 從任務數組中最后一個任務開始檢測 alarm。 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) if (*p) {// 如果任務的 alarm 時間已經過期(alarm<jiffies), 則在信號位圖中置 SIGALRM 信號,然后清 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; //置為就緒(可執行)狀態。 }/* 這里是調度程序的主要部分 */ 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; } switch_to(next); // 切換到任務號為 next 的任務,并運行之。}//// pause()系統調用。轉換當前任務的狀態為可中斷的等待狀態,并重新調度。 // 該系統調用將導致進程進入睡眠狀態,直到收到一個信號。該信號用于終止進程或者使進程調用 // 一個信號捕獲函數。只有當捕獲了一個信號,并且信號捕獲處理函數返回, pause()才會返回。 // 此時 pause() 返回值應該是-1,并且 errno 被置為 EINTR。這里還沒有完全實現(直到 0.95 版)。int sys_pause(void){ current->state = TASK_INTERRUPTIBLE; schedule(); return 0;}// 把當前任務置為不可中斷的等待狀態,并讓睡眠隊列頭的指針指向當前任務。// 只有明確地喚醒時才會返回。該函數提供了進程與中斷處理程序之間的同步機制。// 函數參數*p 是放置等待任務的隊列頭指針。(參見列表后的說明)。void sleep_on(struct task_struct **p){ struct task_struct *tmp;// 若指針無效,則退出。(指針所指的對象可以是 NULL,但指針本身不會為 0)。 if (!p) return; if (current == &(init_task.task)) // 如果當前任務是任務 0,則死機(impossible!)。 panic("task[0] trying to sleep"); tmp = *p; // 讓 tmp 指向已經在等待隊列上的任務(如果有的話)。 *p = current; // 將睡眠隊列頭的等待指針指向當前任務。 current->state = TASK_UNINTERRUPTIBLE; // 將當前任務置為不可中斷的等待狀態。 schedule(); // 重新調度。 // 只有當這個等待任務被喚醒時,調度程序才又返回到這里,則表示進程已被明確地喚醒。 // 既然大家都在等待同樣的資源,那么在資源可用時,就有必要喚醒所有等待該資源的進程。該函數 // 嵌套調用,也會嵌套喚醒所有等待該資源的進程。然后系統會根據這些進程的優先條件,重新調度 // 應該由哪個進程首先使用資源。也即讓這些進程競爭上崗。 if (tmp) // 若還存在等待的任務,則也將其置為就緒狀態(喚醒)。 tmp->state=0;}// 將當前任務置為可中斷的等待狀態,并放入*p 指定的等待隊列中。參見列表后對 sleep_on()的說明。void interruptible_sleep_on(struct task_struct **p){ struct task_struct *tmp; if (!p) return; if (current == &(init_task.task)) panic("task[0] trying to sleep"); tmp=*p; *p=current;repeat: current->state = TASK_INTERRUPTIBLE; schedule();// 如果等待隊列中還有等待任務,并且隊列頭指針所指向的任務不是當前任務時,則將該等待任務置為// 可運行的就緒狀態,并重新執行調度程序。當指針*p 所指向的不是當前任務時,表示在當前任務被放// 入隊列后,又有新的任務被插入等待隊列中,因此,既然本任務是可中斷的,就應該首先執行所有// 其它的等待任務。 if (*p && *p != current) { (**p).state=0; goto repeat; }// 下面一句代碼有誤,應該是*p = tmp,讓隊列頭指針指向其余等待任務,否則在當前任務之前插入// 等待隊列的任務均被抹掉了。參見圖 4.3。 *p=NULL; if (tmp) tmp->state=0; }// 喚醒指定任務*p。void wake_up(struct task_struct **p){ if (p && *p) { (**p).state=0; // 置為就緒(可運行)狀態。 *p=NULL; }}/** 好了,從這里開始是一些有關軟盤的子程序,本不應該放在內核的主要部分中的。將它們放在這里* 是因為軟驅需要一個時鐘,而放在這里是最方便的辦法。*/static struct task_struct * wait_motor[4] = {NULL,NULL,NULL,NULL};static int mon_timer[4]={0,0,0,0};static int moff_timer[4]={0,0,0,0};unsigned char current_DOR = 0x0C; // 數字輸出寄存器(初值:允許 DMA 和請求中斷、啟動 FDC)。// 指定軟盤到正常運轉狀態所需延遲滴答數(時間)。// nr -- 軟驅號(0-3),返回值為滴答數。int ticks_to_floppy_on(unsigned int nr){ extern unsigned char selected; // 當前選中的軟盤號(kernel/blk_drv/floppy.c,122)。 unsigned char mask = 0x10 << nr; // 所選軟驅對應數字輸出寄存器中啟動馬達比特位。 if (nr>3) panic("floppy_on: nr>3"); // 最多 4 個軟驅。 moff_timer[nr]=10000; /* 100 s = very big :-) */ cli(); /* use floppy_off to turn it off */ mask |= current_DOR; // 如果不是當前軟驅,則首先復位其它軟驅的選擇位,然后置對應軟驅選擇位。 if (!selected) { mask &= 0xFC; mask |= nr; }// 如果數字輸出寄存器的當前值與要求的值不同,則向 FDC 數字輸出端口輸出新值(mask) 。并且如果// 要求啟動的馬達還沒有啟動,則置相應軟驅的馬達啟動定時器值(HZ/2 = 0.5 秒或 50 個滴答)。// 此后更新當前數字輸出寄存器值 current_DOR。 if (mask != current_DOR) { outb(mask,FD_DOR); if ((mask ^ current_DOR) & 0xf0) mon_timer[nr] = HZ/2; else if (mon_timer[nr] < 2) mon_timer[nr] = 2; current_DOR = mask; } sti(); return mon_timer[nr];}// 等待指定軟驅馬達啟動所需時間。void floppy_on(unsigned int nr){ cli(); // 關中斷。 while (ticks_to_floppy_on(nr)) // 如果馬達啟動定時還沒到,就一直把當前進程置 sleep_on(nr+wait_motor); // 為不可中斷睡眠狀態并放入等待馬達運行的隊列中。 sti(); // 開中斷。}// 置關閉相應軟驅馬達停轉定時器(3 秒)。void floppy_off(unsigned int nr){ moff_timer[nr]=3*HZ;}// 軟盤定時處理子程序。更新馬達啟動定時值和馬達關閉停轉計時值。該子程序是在時鐘定時// 中斷中被調用,因此每一個滴答(10ms)被調用一次,更新馬達開啟或停轉定時器的值。如果某// 一個馬達停轉定時到,則將數字輸出寄存器馬達啟動位復位。void do_floppy_timer(void){ int i; unsigned char mask = 0x10; for (i=0 ; i<4 ; i++,mask <<= 1) { if (!(mask & current_DOR)) // 如果不是 DOR 指定的馬達則跳過。 continue; if (mon_timer[i]) { if (!--mon_timer[i]) wake_up(i+wait_motor); // 如果馬達啟動定時到則喚醒進程。 } else if (!moff_timer[i]) { // 如果馬達停轉定時到則 current_DOR &= ~mask; // 復位相應馬達啟動位,并 outb(current_DOR,FD_DOR); // 更新數字輸出寄存器。 } else moff_timer[i]--; // 馬達停轉計時遞減。 }}#define TIME_REQUESTS 64 // 最多可有 64 個定時器鏈表(64 個任務)。// 定時器鏈表結構和定時器數組。static struct timer_list { long jiffies; // 定時滴答數。 void (*fn)(); // 定時處理程序。 struct timer_list * next; // 下一個定時器。} timer_list[TIME_REQUESTS], * next_timer = NULL;// 添加定時器。輸入參數為指定的定時值(滴答數)和相應的處理程序指針。// jiffies – 以 10 毫秒計的滴答數;*fn()- 定時時間到時執行的函數。void add_timer(long jiffies, void (*fn)(void)){ struct timer_list * p;// 如果定時處理程序指針為空,則退出。 if (!fn) return; cli(); // 如果定時值<=0,則立刻調用其處理程序。并且該定時器不加入鏈表中。 if (jiffies <= 0) (fn)(); else { // 從定時器數組中,找一個空閑項。 for (p = timer_list ; p < timer_list + TIME_REQUESTS ; p++) if (!p->fn) break; // 如果已經用完了定時器數組,則系統崩潰 if (p >= timer_list + TIME_REQUESTS) panic("No more time requests free"); // 向定時器數據結構填入相應信息。并鏈入鏈表頭 p->fn = fn; p->jiffies = jiffies; p->next = next_timer; next_timer = p;// 鏈表項按定時值從小到大排序。在排序時減去排在前面需要的滴答數,這樣在處理定時器時只要// 查看鏈表頭的第一項的定時是否到期即可。 [[?? 這段程序好象沒有考慮周全。如果新插入的定時// 器值 < 原來頭一個定時器值時,也應該將所有后面的定時值均減去新的第 1 個的定時值。]] while (p->next && p->next->jiffies < p->jiffies) { p->jiffies -= p->next->jiffies; fn = p->fn; p->fn = p->next->fn; p->next->fn = fn; jiffies = p->jiffies; p->jiffies = p->next->jiffies; p->next->jiffies = jiffies; p = p->next; } } sti();}//// 時鐘中斷 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 遞增;// 如果 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();}// 系統調用功能 - 設置報警定時時間值(秒)。 // 如果已經設置過 alarm 值,則返回舊值,否則返回 0。int sys_alarm(long seconds){ int old = current->alarm; if (old) old = (old - jiffies) / HZ; current->alarm = (seconds>0)?(jiffies+HZ*seconds):0; return (old);}// 取當前進程號 pid。int sys_getpid(void){ return current->pid;}// 取父進程號 ppid。int sys_getppid(void){ return current->father;}// 取用戶號 uid。int sys_getuid(void){ return current->uid;}// 取 euid。int sys_geteuid(void){ return current->euid;}// 取組號 gid。int sys_getgid(void){ return current->gid;}// 取 egid。int sys_getegid(void){ return current->egid;}// 系統調用功能 -- 降低對 CPU 的使用優先權(有人會用嗎?)。 // 應該限制 increment 大于 0,否則的話,可使優先權增大!!int sys_nice(long increment){ if (current->priority-increment>0) current->priority -= increment; return 0;}// 調度程序的初始化子程序。void sched_init(void){ int i; struct desc_struct * p; // 描述符表結構指針。 if (sizeof(struct sigaction) != 16) // sigaction 是存放有關信號狀態的結構。 panic("Struct sigaction MUST be 16 bytes"); // 設置初始任務(任務 0)的任務狀態段描述符和局部數據表描述符(include/asm/system.h,65)。 set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt)); // 清任務數組和描述符表項(注意 i=1 開始,所以初始任務的描述符還在)。 p = gdt+2+FIRST_TSS_ENTRY; for(i=1;i<NR_TASKS;i++) { task[i] = NULL; p->a=p->b=0; p++; p->a=p->b=0; p++; }/* 清除標志寄存器中的位 NT,這樣以后就不會有麻煩 */ // NT 標志用于控制程序的遞歸調用(Nested Task)。當 NT 置位時,那么當前中斷任務執行 // iret 指令時就會引起任務切換。NT 指出 TSS 中的 back_link 字段是否有效。 __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); // 復位 NT 標志。 ltr(0); // 將任務 0 的 TSS 加載到任務寄存器 tr。 lldt(0); // 將局部描述符表加載到局部描述符表寄存器。// 注意?。∈菍?GDT 中相應 LDT 描述符的選擇符加載到 ldtr。只明確加載這一次,以后新任務 // LDT 的加載,是 CPU 根據 TSS 中的 LDT 項自動加載。 // 下面代碼用于初始化 8253 定時器。 outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */ outb_p(LATCH & 0xff , 0x40); /* LSB */ // 定時值低字節。 outb(LATCH >> 8 , 0x40); /* MSB */ // 定時值高字節。 // 設置時鐘中斷處理程序句柄(設置時鐘中斷門)。 set_intr_gate(0x20,&timer_interrupt); // 修改中斷控制器屏蔽碼,允許時鐘中斷。 outb(inb_p(0x21)&~0x01,0x21); // 設置系統調用中斷門。 set_system_gate(0x80,&system_call);}其它信息軟盤控制器編程在編程時需要訪問 4 個端口,分別對應一個或多個寄存器。對于 1.2M 的軟盤控制器有以下一些端口。
數字輸出端口(數字控制端口)是一個 8 位寄存器,它控制驅動器馬達開啟、驅動器選擇、啟動/復位 FDC 以及允許/禁止 DMA 及中斷請求。
FDC 的主狀態寄存器也是一個 8 位寄存器,用于反映軟盤控制器 FDC 和軟盤驅動器 FDD 的基本狀態。 通常, 在 CPU 向 FDC 發送命令之前或從 FDC 獲取操作結果之前, 都要讀取主狀態寄存器的狀態位,以判別當前 FDC 數據寄存器是否就緒,以及確定數據傳送的方向。
FDC 的數據端口對應多個寄存器(只寫型命令寄存器和參數寄存器、只讀型結果寄存器),但任一時刻只能有一個寄存器出現在數據端口 0x3f5。在訪問只寫型寄存器時,主狀態控制的 DIO 方向位必須為 0( CPU Æ FDC),訪問只讀型寄存器時則反之。在讀取結果時只有在 FDC 不忙之后才算讀完結果,通常結果數據最多有 7 個字節。
軟盤控制器共可以接受 15 條命令。每個命令均經歷三個階段:命令階段、執行階段和結果階段。命令階段是 CPU 向 FDC 發送命令字節和參數字節。 每條命令的第一個字節總是命令字節(命令碼)。其后跟著 0--8 字節的參數。
執行階段是 FDC 執行命令規定的操作。在執行階段 CPU 是不加干預的,一般是通過 FDC 發出中斷請求獲知命令執行的結束。如果 CPU 發出的 FDC 命令是傳送數據,則 FDC 可以以中斷方式或 DMA 方式進行。中斷方式每次傳送 1 字節。 DMA 方式是在 DMA 控制器管理下, FDC 與內存進行數據的傳輸直至全部數據傳送完。 此時 DMA 控制器會將傳輸字節計數終止信號通知 FDC,最后由 FDC 發出中斷請求信號告知 CPU 執行階段結束。
結果階段是由 CPU 讀取 FDC 數據寄存器返回值,從而獲得 FDC 命令執行的結果。返回結果數據的長度為 0--7 字節。對于沒有返回結果數據的命令,則應向 FDC 發送檢測中斷狀態命令獲得操作的狀態。
參考資料Linux內核完全注釋(趙炯)
新聞熱點
疑難解答