感謝前輩分享,附上鍊接:http://www.prtos.org/wind-interrupt/
接下來我們討論一下Wind內核的中斷處理模塊,中斷是操作系統內核設計中非常重要的部分。由於週期性和非週期性任務的按時執行都離不開中斷,並且大多數實時任務的調度都是由中斷引發的,中斷管理對於實時系統來說不僅重要而且要求更高。因此,實時系統要求操作系統具備迅速響應外部中斷的能力。
本篇我以x86平臺的Pentium處理器爲例,介紹Wind內核的中斷處理框架,以及中斷棧幁的設計。通過本篇的分析,我們可以看出Wind內核作爲一款優秀的實時內核,到底優秀在什麼地方,其爲什麼會有快速的中斷響應時間。
4.1 Pentium處理器概述
4.1.1 Pentium CPU的中斷類型
有兩類事件可引起Pentium處理器掛起當前的指令流,即中斷和異常。中斷是由外部事件引發的,在程序執行的任何時刻都可能出現;異常也稱異常中斷,是由內部事件引發的。中斷和異常各有兩類觸發源:
(1)中斷
可屏蔽中斷:CPU的INTR引腳收到有效信號,如果Pentium標誌寄存器IF位爲1,則允許中斷,否則信號在CPU內被屏蔽。
非屏蔽中斷:CPU的NMI引腳收到有效信號而引發的中斷,這類中斷不能被阻止。
(2)異常。
執行異常:CPU試圖執行一條指令的過程中出現錯誤、故障等不正常條件而引發的異常中斷。
執行軟件中斷指令:Pentium指令系統中包括一些如INTO,INT n這類軟件中斷指令,執行時產生異常中斷。
詳細分類的話,Pentium處理器可以識別256種中斷和異常。每種中斷給予一個編號,即0~255,稱爲中斷向量號(interrupt vector number)。其中NMI、異常以及系統保留佔用中斷向量號爲0~31,而32~255爲用戶中斷向量號,可供INTR和自定義軟件中斷(如彙編中的INT指令)使用。
我們把x86中0~31號中斷稱爲同步中斷(又稱軟中斷),由於Pentium平臺使用PIC是兩片i8259A級聯而成,因此可用於外部中斷線只有16個,因此我們把32~47號中斷稱爲異步中斷(又稱硬中斷)。
同步中斷是由指令引發的,發生在引發陷阱指令的執行過程中,由CPU內部或者指令產生的外部信號引發。若在執行第i條指令時引發同步陷阱(軟中斷),那麼同步陷阱(軟中斷)返回的仍爲第i條指令。
異步陷阱(硬中斷)是指外部事件引發的中斷,和CPU執行的指令無關,在邏輯上發生在兩條指令之間。若在執行第i條指令時發生異步陷阱,那麼異步陷阱處理的返回地址是第i+1條指令。
4.1.2 Pentium CPU的中斷響應過程
中斷處理子程序的入口地址信息存於內存中的一個表內,實模式爲中斷向量表IVT,保護模式爲中斷描述符表IDT。中斷髮生時,CPU首先通過某種方式獲得中斷向量號,再以中斷向量號檢索此表,即可獲取中斷服務子程序入口地址,詳述如下:
(1)實模式使用中斷向量表。
中斷向量表IVR的基地址由IDTR(中斷描述符寄存器)指定,大小爲1kB。中斷響應時的查表過程與8086/8088一致,在此不再贅述。
(2)保護模式使用中斷描述符表。
中斷描述符表(IDT)的基地址也由IDTR指定,大小爲2kB。中斷描述符表每一表項對應一箇中斷向量號,但表項稱爲中斷門描述符或陷阱門描述符。這些門描述符爲8字節長,對應256箇中斷向量號。以中斷向量號乘以8作爲訪問IDT的偏移,讀取相應的中斷門/陷阱門描述符表項。門描述符給出中斷服務子程序人口地址(段:偏移),其中32位偏移量裝入EIP,16位的段值被裝入CS寄存器。但此段值是選擇符,CPU會自動查GDT或LDT取得代碼段描述符並送到相應的描述符寄存器中。
4.1.3 X86架構的計算機對外部中斷的管理
在嵌入式應用中,人們感興趣的主要是指由硬件信號觸發的非屏蔽中斷與可屏蔽中斷。在單CPU的X86計算機中,採用兩片i8259A級聯來管理16個可屏蔽外部中斷,由於主i8259A的IRQ2用於級聯,所以實際可用的IRQ只有15個,其中又有一些被系統佔用,這種邏輯如今已被集成在主板芯片組的南橋中,如圖4.1所示。由於傳統的PIC提供的中斷資源較少,現代PC開始採用APIC(高級可編程中斷控制器)管理外部中斷,它的一個顯著優點是能夠擴充系統可用的IRQ資源。本章基於傳統的PIC結構(兩片i8259A級聯的結構)來分析Wind內核的中斷處理框架。
圖4.1 兩片i8259A級聯的中斷控制器
在Wind內核中與中斷相關的有以下四個概念:
外部中斷號IRQNumber:0~15,外部中斷號由兩片i8259A級聯構成的PIC控制器的輸入引腳引入,主i8259A的8條中斷線對應外部中斷0~7,從i8259A對應中斷8~15。
內部中斷號INumber:0~255,是響應的中斷描述符在中斷描述符表中的索引。
內中斷號IRQNumber(i)=i+INT_NUM_IRQ0=i+32,其中i=0,… ,15, 在Wind內核中由INT_NUM_GET(irq)宏實現外部中斷號與內部中斷號的轉換
中斷向量號iVector:相應的中斷描述符在中斷描述符表中的偏移量,由於每個中斷描述符爲8個字節,所有IVector=INumber*8
INUM_TO_IVEC(intNum):實現內部中斷號與內部向量號之間的轉換。
INT_NUM_GET(irq):實現外部中斷號與內部中斷號的轉換
中斷向量號iVector:相應的中斷描述符在中斷描述符表中的偏移量,由於每個中斷描述符爲8個字節,所有IVector=INumber*8
INUM_TO_IVEC(intNum):實現內部中斷號與內部向量號之間的轉換。
外部中斷號、內部中斷號、以及中斷向量號的對應關係如表4.1所示。
表4.1 中斷相關概念對照表
X86架構的計算機中,一些中斷資源已經固定地分配給某些外部設備,如系統時鐘固定使用IRQ0,所以在選擇中斷號時首先應參考硬件手冊,避免與已用的中斷資源衝突。選定中斷號後,需要在BIOS中加以設置。避免BIOS在初始化時,把此中斷號作爲可用資源分配給PCI設備,造成中斷衝突。
備註:VxWorks中使用intConnect()掛接中斷服務程序,但對於PCI設備,一般採用pciIntConnect()掛接中斷,它與intConnect()的主要不同在於intConnect()使用的中斷向量是獨佔的,而pciIntConnect()則可使多個外部中斷共享一箇中斷向量。它在內部使用一個鏈表管理多個ISR,發生中斷時,鏈接在一個鏈表上的各個ISR被依次調用,pciIntConnect()要求每個ISR被調用時,應該首先查詢是否爲自己的設備產生的中斷,不是則應立即返回,以繼續調用其它ISR。
4.2 Wind內核中斷處理流程
在Pentium平臺,Wind內核運行在保護模式下。在VxWorks中,可以採用intConnect()關聯中斷服務程序至某個中斷向量。然而intConnect()並不是直接將用戶設計的ISR與中斷門描述符相關聯,而是對它加了一層封裝,然後將封裝代碼的內存首地址與中斷門描述符相關聯,中斷響應過程如下圖4.2所示。
圖4.2 Wind內核中斷封裝過程
Wind內核的中斷處理模型爲“前後段”模型,如圖4.3所示,這種中斷管理的思想是把中斷處理按照重要性分成兩部分(前段和後段),前段的優先級最高,並且在關中斷的條件下執行,僅做一些必要的硬件操作,隨後設置一些相關的標誌位,所以執行的時間非常短;而把流程更復雜、可以延時操作並且影響不大的部分歸爲後段,這一部分在退出中斷處理程序後實施,後段爲用戶或者系統默認的ISR。通過這樣的中斷處理機制可以減少中斷服務時間,爲其它外部事件的中斷提供更多的時機。如在一些I/O處理部分,由I/O操作引發的中斷處理部分只作標記功能,即只設一個標誌或者發一個消息說明外部中斷來了,而具體的I/O傳輸操作放在中斷處理外部實施。
圖中“中斷前部”完成當外部事件發出中斷請求時,系統對其響應所需的必須功能,比如中斷現場保護等,“置標”部分主要通知某個任務已經有一箇中斷髮生,且中斷前部已經完成;“中斷後部”並不是在中斷處理程序中完成由接收到標記或者接收到通知的任務完成,主要完成本應該在中斷處理程序中完成的工作(其實uC/OS-III也採用了這種處理機制)。
Wind內核的“前後段”處理機制能在最短的時間內對中斷作出響應,並且在不影響接收中斷的情況下,對已經響應的中斷作出及時的處理。
例如採用intConnect()爲ISA總線設備關聯中斷服務程序IRQ_ISR至IRQ10的程序片斷如下:
#define IRQNum 10
……
if(intConneet((VOIDFUNCPTR)INUM_TO_IVEC(IRQNum+0x20),IRQ_ISR,0)==OK)
{
if(sysIntEnablePIC(IRQNum)==OK)
{
printf("Succeeded.\n");
}
}
void IRQ_ISR()
{
int intLockKey;
intLockKey=intLock(); //關中斷
……(critical section) //執行臨界區代碼
intUnlock(intLocKey); //開中斷
}
備註:上面的例子,我演示了一個簡單的中斷掛接過程,其中中斷ISR是IRQ_ISR()函數,在這個例子爲了簡化起見我假設IRQ_ISR()的所有處理代碼都是臨界區代碼,其中中斷處理ISR中並不是所有的代碼都是要關中斷的,可以把其中不需要裏面處理的部分作爲一個Job,放入內核工作隊列中延後處理,這樣可以極大的降低中斷時延。後面分析的時鐘中斷處理流程就是採用這種方式。
另外用intConneet()掛中斷後,還必須用sysIntEnablePIC()使能中斷,實際上就是針對某個IRQ,置i8259A中斷屏蔽寄存器中的相應位爲允許。
接下來我Wind內核的時鐘中斷爲例,來分析Wind內核的中斷處理過程。
4.3 VxWorks中斷處理機制
Wind內核接管中斷的處理過程主要包括兩個部分:面向應用的編程接口和麪向底層的處理。面向應用的編程接口的任務之一是支持用戶安裝ISR。面向底層的處理分成兩個部分:中斷向量表部分和中斷處理部分。中斷向量表部分主要指的是中斷向量表的定位和向量表中表項內容的形式,一般在RTOS內核中提供一箇中斷向量表,其表項的向量號和CPU中描述的向量對應;向量表採取兩種形式,一種是在向量表的位置處存儲幾條跳轉指令,轉到具體的中斷處理部分;另一種形式是中斷向量的位置存放具體的ISR,但這種方式僅僅針對向量號之間彼此具有一定的空間,足以存放ISR。
面向底層部分的中斷處理是內核中斷處理的核心,Wind的中斷處理流程如圖4.4所示。
圖4.4 Wind內核中斷處理流程
4.3.1 中斷髮生前的準備工作
VxWorks中面向應用的編程接口爲intConnect(),其原型如下:
STATUS intConnect( VOIDFUNCPTR *vector,VOIDFUNCPTR routine,int parameter)
其中
vector:要掛接的中斷向量地址;
routine:中斷髮生時調用ISR;
parameter:傳遞給IST的參數。
它允許將任務C函數作爲ISR掛接到指定的中斷向量vector上,VxWorks的Pentium平臺掛接時鐘中斷的代碼片段如下:
(void)intConnect (INUM_TO_IVEC (INT_NUM_GET (PIT0_INT_LVL)), sysClkInt, 0);
其中PIT0_INT_LVL值爲0,
INT_NUM_GET (PIT0_INT_LVL)值爲0+32=32,
INUM_TO_IVEC (INT_NUM_GET (PIT0_INT_LVL)置爲32*8=0x100=256.
即將中斷描述符表IDT的第32個表項的入口地址設置位sysClkInt()函數的地址。
intConnect()的具體實現如下:
STATUS intConnect( VOIDFUNCPTR *vector, VOIDFUNCPTR routine, int parameter)
{
FUNCPTR intDrvRtn;
VOIDFUNCPTR routineBoi;
VOIDFUNCPTR routineEoi;
int parameterBoi;
int parameterEoi;
if (intEoiGet == NULL)
{
intDrvRtn = intHandlerCreate ((FUNCPTR)routine, parameter);
}
else
{
(* intEoiGet) (vector, &routineBoi, ¶meterBoi,
&routineEoi, ¶meterEoi);
intDrvRtn = intHandlerCreateI86 ((FUNCPTR)routine, parameter,
(FUNCPTR)routineBoi, parameterBoi,
(FUNCPTR)routineEoi, parameterEoi);
}
if (intDrvRtn == NULL)
return (ERROR);
/* make vector point to synthesized code */
intVecSet ((FUNCPTR *)vector, (FUNCPTR)intDrvRtn);
return (OK);
}
在Pentium平臺,VxWorks同時通過usrInit()->sysHwInit()重新設置了intEoiGet函數指針爲:
intEoiGet = sysIntEoiGet;
因此intConnect()首先調用了sysIntEoiGet()函數來獲取在中斷ISR,即執行sysClkInt()函數調用前後的打樁例程,即在執行sysClkInt()前調用(*routineBoi)(parameterBoi),執行sysClkInt()後執行(*routineEoi)(parameterEoi)。
具體到Pentium平臺的時鐘中斷來說routineBoi=NULL,routineEoi=i8259IntEoiMaster,parameterEoi=irqNo其中VOID i8259IntEoiMaster(INT32 irqNo)向主i8259A中斷控制器發送EOI(end of interrupt)信號。
intConnect()接着調用intHandlerCreateI86()函數來獲取新的ISR的地址;這個新的ISR除了在sysClkInt()前調用(*routineBoi)(parameterBoi),在sysClkInt()後調用(*routineEoi)(parameterEoi),並且在這個新封裝的基礎上,進一步在前部封裝了intEnt(),在後部封裝了intExit()。因此新的ISR的執行流程爲:
- intEnt()
- (*routineBoi)(parameterBoi)
- sysClkInt()
- (*routineEoi)(parameterEoi()
- intExit()
相對於Pentium平臺來說就是
- intEnt()
- sysClkInt()
- i8259IntEoiMaster(irqNo)
- intExit()
從上面的分析我們可以看出intHandlerCreateI86()爲中斷指定一個C函數作爲ISR,並通過調用intVecSet()掛接到指定的中斷上。intHandlerCreateI86()在ISR的中斷“打樁”:進入該函數之前調用intEnt()和(*routineBoi)(parameterBoi),退出ISR之後調用(*routineEoi)(parameterEoi()和intExit()
“打樁”的代碼存是一個存儲在一個數組中的PIC(Position Independent Code)指令序列,以Pentium平臺爲例,其實現如下:
LOCAL UCHAR intConnectCode [] = /* intConnect stub */
{
/*
* 00 e8 kk kk kk kk call _intEnt * tell kernel
* 05 50 pushl %eax * save regs
* 06 52 pushl %edx
* 07 51 pushl %ecx
* 08 68 pp pp pp pp pushl $_parameterBoi * push BOI param
* 13 e8 rr rr rr rr call _routineBoi * call BOI routine
* 18 68 pp pp pp pp pushl $_parameter * push param
* 23 e8 rr rr rr rr call _routine * call C routine
* 28 68 pp pp pp pp pushl $_parameterEoi * push EOI param
* 33 e8 rr rr rr rr call _routineEoi * call EOI routine
* 38 83 c4 0c addl $12, %esp * pop param
* 41 59 popl %ecx * restore regs
* 42 5a popl %edx
* 43 58 popl %eax
* 44 e9 kk kk kk kk jmp _intExit * exit via kernel
*/
0xe8, 0x00, 0x00, 0x00, 0x00, /* _intEnt filled in at runtime */
0x50,
0x52,
0x51,
0x68, 0x00, 0x00, 0x00, 0x00, /* BOI parameter filled in at runtime */
0xe8, 0x00, 0x00, 0x00, 0x00, /* BOI routine filled in at runtime */
0x68, 0x00, 0x00, 0x00, 0x00, /* parameter filled in at runtime */
0xe8, 0x00, 0x00, 0x00, 0x00, /* routine filled in at runtime */
0x68, 0x00, 0x00, 0x00, 0x00, /* EOI parameter filled in at runtime */
0xe8, 0x00, 0x00, 0x00, 0x00, /* EOI routine filled in at runtime */
0x83, 0xc4, 0x0c, /* pop parameters */
0x59,
0x5a,
0x58,
0xe9, 0x00, 0x00, 0x00, 0x00, /* _intExit filled in at runtime */
};
對於Pentium平臺的中斷中斷,其打樁之後的執行序列如下:
- intEnt()
- sysClkInt()
- i8259IntEoiMaster(irqNo)
- intExit()
對於的彙編序列:
call intEnt
pushl %eax
pushl %edx
pushl %ecx
call sysClkInt
pushl $_parameterEoi
call _routineEoi
addl $4, %esp
popl %ecx
popl %edx
popl %eax
jmp intExit
intHandlerCreateI86()將時鐘中斷對應的中斷ISR(即sysClkInt()函數)的打樁序列在內存中的首地址通過intVecSet()設置到中斷描述符表IDT的第32表項(下標從0開始)的跳轉地址中。
上述工作完成好,就可以等待中斷的發生。
關於時鐘中斷,我還想多說幾句,我在本文上面提供,Wind內核通過usrRoot()->sysClkInit()->sysClkConnect()->sysHwInit2()->intConnect()->intHandlerCreateI86()完成的中斷sysClkInit()掛接。sysClkInt()實現如下:
void sysClkInt (void)
{
if (sysClkRoutine != NULL)
(* sysClkRoutine) (sysClkArg);
}
通過代碼,我們知道sysClkInt()執行的中斷處理函數是(* sysClkRoutine) (sysClkArg);
sysClkRoutine是在usrRoot()->sysClkInit()->sysClkConnect()中被初始化爲usrClock(),掛接好中斷,對於時鐘中斷而言,再設置時鐘中斷的發生頻率後,就可以啓動中斷了,其代碼usrRoot()->sysClkInit()實現如下:
void sysClkInit (void)
{
sysClkConnect ((FUNCPTR) usrClock, 0); /* 掛接中斷ISR */
sysClkRateSet (SYS_CLK_RATE); /* 設置系統時鐘頻率 */
sysClkEnable (); /* 啓動時鐘中斷 */
}
4.3.2 中斷髮生後的處理
當Wind內核響應 中斷時,根據中斷號從中斷向量表中取出對應的中斷向量,然後調用intEnt()。該函數把控制權從中斷向量傳遞給中斷ISR。intEnt()完成包括中斷響應操作、保持系統寄存器、建立一個C語言的上下文環境。由於intEnt()和具體的平臺相關,我們仍以Pentium平臺爲例,來分析intEnt()的具體實現。
intEnt()函數在中斷ISR的入口被調用,由intHandlerCreateI86()封裝在樁代碼中。
假設我們使用中斷棧,即
#define INT_STACK_USE
vxIntStackEnabled=TRUE
爲了方便我表述,我貼出了相關的代碼:
FUNC_LABEL(intEnt)
cli /* 關中段 */
pushl (%esp) /* 將esp指向的ret_addr在入棧一次 */
pushl %eax
movl FUNC(errno),%eax
movl %eax,8(%esp) /* 將errno保持到戰中返回地址所在的位置 */
incl FUNC(intCnt) /* intCnt++ */
incl FUNC(intNest) /* intNest++ */
#ifdef INT_STACK_USE
cmpl $0,FUNC(vxIntStackEnabled) /* if vxIntStackEnabled == 0 then */
je intEnt0 /* skip the interrupt stack switch */
movl ESF0_CS+12(%esp), %eax /* get CS in ESF0 */
cmpw FUNC(sysCsInt), %ax /* 中斷嵌套嗎? */
je intEnt0 /* 是的話:跳過下面的部分 */
cmpl $1,FUNC(intNest) /* 是第一層中斷嗎? */
jne intEnt0 /*如果是的話,切換到中斷棧;否的話跳過棧切換過程 */
/* copy the supervisor stack to the interrupt stack. */
intEntStackSwitch:
pushl %ecx /* save %ecx */
pushl %esi /* save %esi */
pushl %edi /* save %edi */
/* copy ESF0(12 bytes), errno, return addr, %eax */
subl $ ESF0_NBYTES+12+4, FUNC(vxIntStackPtr) /* alloc */
movl FUNC(vxIntStackPtr), %eax /* get int-stack ptr */
leal 20(%esp), %ecx /* get addr of errno */
movl %ecx, ESF0_NBYTES+12(%eax) /* save the original ESP */
leal 12(%esp), %esi /* set the source addr */
movl %eax, %edi /* set the destination addr */
movl $ ESF0_NLONGS+3, %ecx /* set number of longs to copy */
cld /* set direction ascending order */
rep /* repeat next inst */
movsl /* copy ESF0_NLONGS + 3 longs */
popl %edi /* 任務棧恢復 %edi */
popl %esi /* 任務棧恢復 %esi */
popl %ecx /* 任務棧恢復 %ecx */
movl %eax, %esp /* 棧寄存器的切換 */
/* now, we are in the interrupt stack */
#endif /* INT_STACK_USE */
intEnt0:
pushl ESF0_EFLAGS+12(%esp) /* push the saved EFLAGS */
popfl /* UNLOCK INTERRUPT */
popl %eax /* restore %eax */
ret
intEn()在邏輯上可以分成三部分:
1. 在進行中斷棧切換之前,特權態下的棧幁佈局如下。
代碼執行到語句“#ifdef INT_STACK_USE”之前的部分,其它任務棧佈局如圖4.5所示。
圖4.5 任務棧佈局
其中的返回地址是在“打樁”的代碼中調用函數intEn()壓入戰中的返回地址。其中的eflags,cs,ip寄存器的值是由硬件自動壓入棧;而errno,ret_addr,eax則是由intEn()手動壓入到任務棧中。此時仍處於任務棧中,還沒有切換到中斷棧。
2.任務棧到中斷棧的切換
由
#ifdef INT_STACK_USE
…..
#endif
中的代碼實現。
首先判斷是否開啓中斷棧;
在開啓中斷棧的情況下,判斷是否是第一次進入中斷棧,這個根據代碼段選擇子和中斷段選擇的比較來實現的。
在VxWorks的Pentium平臺,Wind內核工作在保護模式下的特權態,其全局描述符表的值如下:
FUNC_LABEL(sysGdt)
/* 0(selector=0x0000): Null descriptor */
.word 0x0000
.word 0x0000
.byte 0x00
.byte 0x00
.byte 0x00
.byte 0x00
/* 1(selector=0x0008): Code descriptor, for the supervisor mode task */
.word 0xffff /* limit: xffff */
.word 0x0000 /* base : xxxx0000 */
.byte 0x00 /* base : xx00xxxx */
.byte 0x9a /* Code e/r, Present, DPL0 */
.byte 0xcf /* limit: fxxxx, Page Gra, 32bit */
.byte 0x00 /* base : 00xxxxxx */
/* 2(selector=0x0010): Data descriptor */
.word 0xffff /* limit: xffff */
.word 0x0000 /* base : xxxx0000 */
.byte 0x00 /* base : xx00xxxx */
.byte 0x92 /* Data r/w, Present, DPL0 */
.byte 0xcf /* limit: fxxxx, Page Gra, 32bit */
.byte 0x00 /* base : 00xxxxxx */
/* 3(selector=0x0018): Code descriptor, for the exception */
.word 0xffff /* limit: xffff */
.word 0x0000 /* base : xxxx0000 */
.byte 0x00 /* base : xx00xxxx */
.byte 0x9a /* Code e/r, Present, DPL0 */
.byte 0xcf /* limit: fxxxx, Page Gra, 32bit */
.byte 0x00 /* base : 00xxxxxx */
/* 4(selector=0x0020): Code descriptor, for the interrupt */
.word 0xffff /* limit: xffff */
.word 0x0000 /* base : xxxx0000 */
.byte 0x00 /* base : xx00xxxx */
.byte 0x9a /* Code e/r, Present, DPL0 */
.byte 0xcf /* limit: fxxxx, Page Gra, 32bit */
.byte 0x00 /* base : 00xxxxxx */
從全局描述符表我們可以看出,爲了區分出任務代碼段和中斷代碼段,特權設置了代碼段描述符和中斷代碼段描述符。
並sysCsSuper指向特權任務段,sysCsInt指向特權中斷段。
FUNC_LABEL(sysCsSuper)
.long 0x00000008 /* CS for supervisor mode task */
FUNC_LABEL(sysCsExc)
.long 0x00000018 /* CS for exception */
FUNC_LABEL(sysCsInt)
.long 0x00000020 /* CS for interrupt */
如果是第一次進入中斷,並且中斷嵌套次數爲1,則需要進行中斷棧幁的切換。
VxWorks的中斷棧在usrInit()->kernelInit()函數中分配,大小爲4K,其在VxWorks內存中的佈局如圖4.6所示.
vxIntStackPtr通過usrInit()->kernelInit()->windIntStackSet()位置位執行vxIntStackBase
中斷棧分配ESF0_NBYTES+12+4=28字節的空間,然後將創建的任務棧幁拷貝到中斷棧幁中,示意圖如4.7。
圖4.7 中斷不嵌套下的中斷棧佈局
3.intEnt()返回
intEnt0:
pushl ESF0_EFLAGS+12(%esp) /* push the saved EFLAGS */
popfl /* UNLOCK INTERRUPT */
popl %eax /* restore %eax */
ret
執行intEnt0指向的代碼段,即恢復被中斷之前的EFLAGS寄存器的值,由於原來的EFLAGS寄存中IF爲沒有置位,所有恢復之後,中斷打開。
恢復eax寄存器的值;
恢復ret_addr指向的返回地址。
此時中斷棧的佈局如圖4.8所示。
圖4.8 中斷恢復時的中斷棧佈局
備註:這裏分析的都是第一場中斷,沒有考慮中斷嵌套的情況,如果考慮中斷嵌套的話,只需要在上面中斷棧的基礎上再次保持上下文即可。
從上面代碼的分區中我們指定intEnt()函數是在關中斷條件下執行,其執行的目的是構造運行中斷ISR的棧環境,當前被中斷的任務或者上一次中斷的上下文還沒有保存。
接着執行中斷ISR,對於時鐘中斷來說,最終執行的是:
void tickAnnounce (void)
{
if (kernelState) /* defer work if in kernel */
workQAdd0 ((FUNCPTR)windTickAnnounce);
else
{
kernelState = TRUE; /* KERNEL_ENT */
windTickAnnounce (); /* advance tick queue */
windExit (); /* KERNEL_EXIT */
}
}
如果當前wind內核的內核態沒有被佔用,則進入內核態,執行中斷處理函數windTickAnnounce();
如果存在代碼正在訪問內核,則將中斷處理例程放入內核處理隊列中延後執行。
VxWorks的“前後段”中斷處理框架就體現在這裏。
接着執行i8259IntEoiMaster(0),向中斷控制器寫EOI(End of Interrupt)信號。告訴中斷控制器中斷已經處理完畢。
最後執行intExit(),intExit()的執行路程如4.9。
圖4.9 中斷返回處理IntExit()處理流程
intExit()檢查調度隊列,以決定是否需要執行調度器。如果沒有更高優先級的任務就緒,並且沒有內核隊列可做,那麼intExit()返回到被中斷的任務。
如果調度是必須的,那麼此時遍歷中斷棧,將被中斷任務的上下文保存到被中斷任務的TCB中。
由於intExit()需要保持所有的寄存器,所有當使用寄存器來檢查內核隊列是否爲空時,使用的寄存器必須實現將其內容備份到棧。
另外在調用reschedule()時,edx必須保持的是taskIdCurrent。
當intExit()檢測到當前被中斷的任務已經不是優先級最高的任務,並且當前任務運行搶佔。則保存當前任務的上下文,進入內核態,並開中斷。從這一刻起,系統的運行狀態進入內核級。在任何多任務系統中,大量的應用是發生在一個或多個任務的上下文中。然而,有些CPU時間片不在任何任務的上下文。這些時間片發生在內核改變內部隊列或決定任務調度。在這些時間片中,CPU在內核級執行,而非任務級。
爲了內核安全地操作它的內部的數據結構,必須有互斥操作。內核級沒有相關的任務上下文,內核不能使用信號量保護內部鏈表。內核使用工作延期作爲實現互斥的方式。當有內核參與時,中斷服務程序調用的函數不是被直接激活,而是被放在內核的工作隊列中。內核完成這些請求的執行而清空內核工作隊列。
當內核正在執行已經被請求服務時系統將不響應到達內核的函數調用。可以簡單地認爲內核狀態類似於禁止搶佔。如前面所討論的,搶佔延時在實時系統中是不期望有的,因爲它增加了對於會引起應用任務重新調度的事件的響應時間.
儘管操作系統在內核級(此時禁止搶佔)完全避免消耗時間是不可能的,但減少這些時間是很重要的。這是減少由內核執行的函數的數量的主要原因,也是不採用統一結構的系統設計方式的原因。
言歸正傳,此時intExit()已經進入內核級執行,其進入內核態,開中斷,執行調度器schedule().
此刻雖然當前任務(taskIdCurrent指向的任務)的上下文已經保存到當前任務的TCB控制塊中。但是taskIdCurrent此時指向的仍然是這個任務。
當進入reschedule()調度器時,正常情況下它會恢復優先級最高的那個任務的上下文;如果到那個時候當前出現的優先級最高的任務由於各種原因變成非就緒態。有限性最高的任務就變爲自身的話,它將會重新恢復運行。
如果那時,自己也會阻塞了,就緒隊列將會爲空。reschedule()將會空轉執行內核隊列中的Job。
4.4 小結
VxWorks的“前後段”中斷處理機制可以有效的對併發中斷進行處理。“前半部”只完成必要的處理,在儘可能短的時間內記錄已發生的中斷,而把原來位於中斷處理程序中的某些部分移到了任務中,在儘快完成必要的中斷處理的基礎上,保證了對併發中斷的及時響應。此外,VxWorks的內核工作隊列中的Job主要來自於中斷(並非全部,因爲看門狗定時處理例程也是放在內核Job隊列中完成),這種工作推遲技術將記錄中斷中要完成的工作,並在退出中斷前或任務切換前執行所有內核Job並清空內核工作隊列,避免中斷的丟失。
VxWorks的內核中有兩個堆棧:系統棧和任務棧。系統棧是系統爲中斷上下文處理而預留的堆棧;任務棧則屬於任務本身的私有堆棧,用來存儲任務執行過程中的臨時變量等信息。當中斷髮生且非中斷嵌套時,堆棧由被中斷任務的任務棧切換到系統棧;當在中斷處理中又發生中斷即發生中斷嵌套時,堆棧不再切換,仍用系統棧;當退出最外層中斷時,堆棧又由系統棧切換到被中斷任務的任務棧。由於中斷上下文不存在於任何任務的上下文中,因此可以保證足夠快的中斷響應時間,有利於滿足空間應用實時性強的要求。
通常有幾個因素影響中斷延遲:中斷響應的硬件延遲、中斷禁止時間、中斷處理前保存
上下文的時間、中斷等待時間以及定位ISR的時間,如圖4.10所示。
圖4.10 中斷延遲組成成分
一次僅允許一個進程訪問的資源稱爲臨界資源,每個進程中訪問臨界資源的那段代碼稱臨界區。中斷禁止時間受臨界區長度的直接影響,這是由於在執行臨界區代碼的時候通常需要關中斷。Wind內核的中斷“前後段”處理模型只在上下文保存和上下文恢復時關中斷,並保存儘可能少的上下文,而在中斷等待和定位ISR時可以響應中斷,減少了中斷延遲。中斷等待時間由中斷的調度策略決定,Wind內核是完全搶佔式的內核,一個高優先級中斷到來後立即打斷低優先級中斷的處理,內核轉去處理高優先級的中斷,此時中斷等待時間爲0。
VxWorks對中斷嵌套的支持可以保證不丟失高優先級的中斷,從而保證了實時系統的實時性和可靠性。
由於ISR不是在任務上下文中運行,它沒有TCB,並且所有的中斷處理程序共享同一個堆棧,因此它必須遵循一個基本約束:不能調用可能引起阻塞的函數。例如:在ISR中不能試圖獲取一個信號量,不能通過VxWorks驅動執行I/O操作,由於在intEnter()函數中沒有保存浮點寄存器的操作,所以在ISR中也不能調用使用浮點協處理器的函數。
待續.....O(∩_∩)O~