關於idle進程
也就是pid=0的進程。它是核心完成初始化後所建立的第一個進程,在系統空閑時執行。它的代碼很簡單:
for(;;) pause();
強調一下,idle進程是使用者態進程。那麼問題來了,核心從啟動到初始化過程總都處在核心態,那麼核心是怎麼創 建idle並且切換到使用者態呢。
一種很直接簡單的想法是,核心直接調用使用者空間的代碼實現核心態到使用者態的轉換,但是這是不可能的,因為規
不能這麼做。那怎麼辦呢。這就是這篇文章要講的問題。
進程相關的結構
進程是一個動態概念,要管理進程首先我們需要將這個動態概念用一些待用資料結構抽象化。這個結構就是經 常所說的task_struct。裡面存放了保護模式下進程的資訊(比如ldt和tss等)。關於保護模式相關知識請參考 《linux0.11核心完全注釋》前幾章。所以我們要建立idle,首先就要先準備好相應的task_struct。在linux0.11中 是直接初始化的:
static union task_union init_task = {INIT_TASK,};
#define INIT_TASK \
/* state etc */ { 0,15,15, \
/* signals */ 0,{{},},0, \
/* ec,brk... */ 0,0,0,0,0,0, \
/* pid etc.. */ 0,-1,0,0,0, \
/* uid etc */ 0,0,0,0,0,0, \
/* alarm */ 0,0,0,0,0,0, \
/* math */ 0, \
/* fs info */ -1,0022,NULL,NULL,NULL,0, \
/* filp */ {NULL,}, \
{ \
{0,0}, \
/* ldt */ {0x9f,0xc0fa00}, \
{0x9f,0xc0f200}, \
}, \
/*tss*/ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
0,0,0,0,0,0,0,0, \
0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
_LDT(0),0x80000000, \
{} \
}, \
}
然後把idle任務的LDT和TSS放在通用描述元表GDT中:
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
核心態--->使用者態
進程相關的資訊準備好了,接下來就是狀態的切換了。
直接調用顯然是不能改變特權級的,但是我們知道,中斷處理是可以在不同的特權級之間切換的。所以核心採用 了一種“類比中斷返回”的方式。
先看看CPU處理中斷的時候是怎麼做的:
其中上半部分著色的“原ss,原esp,原flags,原cs,原eip”,是指被中斷的程式ss、esp、flags、cs、eip, 這些寄存器的入棧和出棧(由iret指令完成)都是由CPU自動完成,而且其他的寄存器出入棧全部由程式員自己處 理。提醒一下,這裡的cs和eip是用在保護模式下的,所以cs是在GDT中定址相應的程式碼片段(idle的程式碼片段和資料區段已經在前一節裡面準備好放在GDT中了。)
接下來看linux0.11是怎麼模仿中斷返回的:
#define move_to_user_mode() \//切換到使用者態
__asm__ ("movl %%esp,%%eax\n\t" \
"pushl $0x17\n\t" \//壓入原ss(指向idle的程式碼片段,低兩位代表cpl=3,代表使用者 態)
"pushl %%eax\n\t" \//壓入原esp
"pushfl\n\t" \//壓入原flags
"pushl $0x0f\n\t" \ //壓入原cs(指向idle的程式碼片段,低兩位代表cpl=3,代表使用者態)
"pushl $1f\n\t" \//壓入原eip,指向iret指令後面的代碼
/*以上的入棧操作本來都應該在程式被中斷是由CPU自動完成,這裡是手動壓入,製造被中斷的假象。以便馬 上調用iret,由CPU自動完成這些寄存器的出棧操作,完成核心態到使用者態的切換*/
"iret\n" \ //中斷返回,由CPU恢複先前壓入的寄存器。
"1:\tmovl $0x17,%%eax\n\t" \
"movw %%ax,%%ds\n\t" \
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")