1 進程結構
union task_union{
struct task_struct task ;
char stack[PAGE_SIZE] ;
}
這實際上是一個內存頁,頁的底部是進程控制塊結構。其余部分是作為進程的內核態堆棧使用。
2 task 數組
struct task_struct * task[NR_TASKS] = {&(init_task.task), };
這個數組中存儲的是task_struct 結構的指針,但是實際上數組中的每一項都指著一塊內存頁。
3 任務段數據
struct tss_struct {
long back_link; /* 16 high bits zero */
long esp0;
long ss0; /* 16 high bits zero */
long esp1;
long ss1; /* 16 high bits zero */
long esp2;
long ss2; /* 16 high bits zero */
long cr3;
long eip;
long eflags;
long eax,ecx,edx,ebx;
long esp;
long ebp;
long esi;
long edi;
long es; /* 16 high bits zero */
long cs; /* 16 high bits zero */
long ss; /* 16 high bits zero */
long ds; /* 16 high bits zero */
long fs; /* 16 high bits zero */
long gs; /* 16 high bits zero */
long ldt; /* 16 high bits zero */
long trace_bitmap; /* bits: trace 0, bitmap 16-31 */
struct i387_struct i387;
};
4 進程控制塊
struct task_struct {
/*----------------------- these are hardcoded - don't touch -----------------------*/
long state; // 進程運行狀態(-1不可運行,0可運行,>0以停止)
long counter; // 任務運行時間片,遞減到0是說明時間片用完
long priority; // 任務運行優先數,剛開始是counter=priority
long signal; // 任務的信號位圖,信號值=偏移+1
struct sigaction sigaction[32]; //信號執行屬性結構,對應信號將要執行的操作和標志信息
long blocked; // 信號屏蔽碼
/*----------------------------------- various fields--------------------------------- */
int exit_code; // 任務退出碼,當任務結束時其父進程會讀取
unsigned long start_code,end_code,end_data,brk,start_stack;
// start_code 代碼段起始的線性地址
// end_code 代碼段長度
// end_data 代碼段長度+數據段長度
// brk 代碼段長度+數據段長度+bss段長度
// start_stack 堆棧段起始線性地址
long pid,father,pgrp,session,leader;
// pid 進程號
// father 父進程號
// pgrp 父進程組號
// session 會話號
// leader 會話首領
unsigned short uid,euid,suid;
// uid 用戶標id
// euid 有效用戶id
// suid 保存的用戶id
unsigned short gid,egid,sgid;
// gid 組id
// egid 有效組id
// sgid 保存組id
long alarm; // 報警定時值
long utime,stime,cutime,cstime,start_time;
// utime 用戶態運行時間
// stime 內核態運行時間
// cutime 子進程用戶態運行時間
// cstime 子進程內核態運行時間
// start_time 進程開始運行時刻
unsigned short used_math; // 標志,是否使用了387協處理器
/* ----------------------------------file system info-------------------------------- */
int tty; // 進程使用tty的子設備號,-1表示沒有使用
unsigned short umask; //文件創建屬性屏蔽碼
struct m_inode * pwd; // 當前工作目錄的i節點
struct m_inode * root; // 根目錄的i節點
struct m_inode * executable; // 可執行文件的i節點
unsigned long close_on_exec; // 執行時關閉文件句柄位圖標志
struct file * filp[NR_OPEN]; // 進程使用的文件
/*------------------ ldt for this task 0 - zero 1 - cs 2 - ds&ss -------------------*/
struct desc_struct ldt[3]; // 本任務的ldt表,0-空,1-代碼段,2-數據和堆棧段
/* ---------------------------------tss for this task ---------------------------------*/
struct tss_struct tss; // 本任務的tss段
};
5 linux進程結構
(1) 在linux中gdt中的每一項,都有兩個表項,一個是ldt描述符,另一個是tss描述符。
(2) 在task數組中占有一項,每一項是一個物理頁面,物理內存頁面底端是進程控制塊,內存頁面的其余部分是內核態堆棧。
(3) task數組中的表項和gdt中的表項是一一對應的。 對于一個在task數組中的任務項是nr的任務來說,它的tss描述符在gdt中描述符
的位置是,gdtr + 3*8 + 16 * nr ,ldt描述符在gdt中的描述符的位置是 gdtr + 3 * 8 + 16 * nr + 8 。
(4) 對應于表項為nr的進程,它對應的頁目錄項是16 * nr --------16 * (nr + 1) 。
6 進程0
進程0是一個特殊的進程,它是所有進程的祖先進程,所有其他的進程都是復制進程0或者其后代進程產生的。 但是進程0不是。
下面主要講一下 進程0的創建順序:
(1) 進程控制塊和頁目錄頁表的手動創建
以下就是一個任務的初始過程
#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第0項是空

/**//* ldt */
{0x9f,0xc0fa00}, \ //代碼段長640K,基地0,G=1,D=1,DPL=3,P=1,TYPE=0x0a

{0x9f,0xc0f200}, \ //數據段長640K,基地0,G=1, D=1, DPL=3,P=1, TYPE=0x02
}, \

/**//*tss*/
{0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
// esp0 = PAGE_SIZE+(long)&init_task 內核態堆棧指針初始化為頁面最后
// ss0 = 0x10 內核態堆棧的段選擇符,指向系統數據段描述符,進程0的進程控制
// 塊和內核態堆棧都在system模塊中
// cr3 = (long)&pg_dir 頁目錄表,其實linux0.11所有進程共享一個頁目錄表
0,0,0,0,0,0,0,0, \
0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
_LDT(0),0x80000000, \ // ldt表選擇符指向gdt中的LDT0處

{} \
}, \
}

進程0的數據段基址為0,段限長為640KB ,代碼段基址為0,段限長為640KB。任務0的數據段和代碼段 和系統的代碼段和數據段是重合的。
進程0的內核態堆棧和進程控制塊都是位于系統模塊內。
(2)在main模塊中調用了,sched_init()函數加載了 進程0的
進程0的tss段描述符,ldt段描述符,并且加載TR寄存器,使它指向進程0的tss段,這時候
進程0才完成了啟動。

/**//*****************************************************************************/

/**//* 功能: 1. 初始化task數組和GDT(包括設置進程1的LDT和TSS) */

/**//* 2. 加載TR和IDTR寄存器 */

/**//* 3. 設置時鐘中斷門和系統調用中斷門 */

/**//* 參數: (無) */

/**//* 返回: (無) */

/**//*****************************************************************************/
void sched_init(void)


{
int i;
struct desc_struct * p;

if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
// 在gdt中設置進程0的tss段描述符
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
// 在gdt中設置進程0的ldt段描述符
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
// 下面的循環把gdt和task中其他的項清空
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++;
}

/**//* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
ltr(0); // 把進程0的tss段加載到TR寄存器
lldt(0); // 把進程0的ldt段加載到IDTR寄存器。
// 這是將gdt中進程0的ldt描述符對應的選擇符加載到TR中。CPU將
// 選擇符加載到可見部分,將tss的基地址和段長等加載到不可見部分。
// TR寄存器只在這里明確加載一次,以后新任務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); // 設置系統調用中斷門
}

(3)切換回用戶態。
// 把進程0從內核態切換到用戶態去執行,使用的方法是模擬中斷調用返回
// 利用指令iret完成特權級的轉變。
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \ // 當前堆棧指針保存到eax中
"pushl $0x17\n\t" \ // 當前堆棧段選擇符0x17入棧,它指向進程0的數據段描述符// 因為進程0的代碼段、數據段、內核代碼段、數據段4者重
// 合,所以它指向的仍然是內核模塊區域。
"pushl %%eax\n\t" \ // 把當前堆棧指針入棧。這樣模擬外層堆棧的SS:ESP。
// 由于進程0數據段選擇符0x17對應的還是內核模塊,和
// 內核數據段選擇符0x10的差別僅在與對應描述符的dpl和
// 本身rpl的不同,所以外層堆棧指針指向的還是原來的堆棧
// 即user_stack
"pushfl\n\t" \ // eflags入棧
"pushl $0x0f\n\t" \ // 進程0代碼段選擇符入棧,模擬返回的CS
"pushl $1f\n\t" \ // 下面標號1的偏移地址入棧,模擬返回的EIP
// 也是由于4段重合,所以這里返回的CS對應的段的基地址與
// 內核代碼段基地址一樣,都是0,故將返回的CS:EIP就是下
// 面標號1處。
"iret\n" \ // 中斷返回。由于當前CPL=0,將返回的CS的RPL=3,所以
// 不僅僅要改變CS,EIP,還要發生堆棧切換(但實際上堆棧
// 還是user_stack),同時CPL變成3。
"1:\tmovl $0x17,%%eax\n\t" \ // 把數據段寄存器的值設為進程0的數據段
"movw %%ax,%%ds\n\t" \
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")

6 用fork創建進程
除了進程0,所有其他的進程都是由fork()系統調用創建的,子進程是通過復制父進程的數據和代碼而產生的。
創建結束之后,子進程與父進程的代碼和數據共享,但是子進程有自己的進程控制塊,內核堆棧和頁表。
一個進程需要以下三中數據結構
(1) 進程控制塊 task__struct 。
(2) gdt中的tss 和ldt描述符。
(3)頁目錄項和頁表項。
所以fork系統調用的任務就是創建進程的上述三個部分。
sys_fork()函數分兩步實現,第一步 首先調用,find_empty_process() 函數,第二步調用 copy_process()函數,復制進程。
_sys_fork:
// 第一步,調用find_empty_process()函數,找task[]中的空閑項。
// 找到后數組下標放在eax中。如果沒找到直接跳轉到ret指令。
call _find_empty_process
testl %eax,%eax
js 1f
push %gs // 中斷時沒有入棧的寄存器入棧,
// 作為copy_process() 函數的參數
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
// 第二步,調用copy_process() 函數復制進程。
call _copy_process
addl $20,%esp
1: ret

內存復制函數
copy_mem


/**//*****************************************************************************/

/**//* 功能:設置新進程的LDT項(數據段描述符和代碼段描述符)中的基地址部分 */

/**//* 并且復制父進程(也就是當前進程)的頁目錄和頁表, */

/**//* 實現父子進程數據代碼共享 */

/**//* 參數: nr 新進程任務數組下標 */

/**//* p 新進程的進程控制塊 */

/**//* 返回: 0 (成功), -ENOMEM(出錯) */

/**//*****************************************************************************/

int copy_mem(int nr,struct task_struct * p)


{
unsigned long old_data_base,new_data_base,data_limit;
unsigned long old_code_base,new_code_base,code_limit;

code_limit=get_limit(0x0f); // 取當前進程代碼段長度
data_limit=get_limit(0x17); // 取當前進程數據段長度
old_code_base = get_base(current->ldt[1]); // 取當前進程代碼段基地址,這是線性地址
old_data_base = get_base(current->ldt[2]); // 取當前進程數據段基地址,這是線性地址
// 0.11進程代碼段和數據段基地址必須重合
if (old_data_base != old_code_base)
panic("We don't support separate I&D");
//0.11中數據段代碼段的基地址是重合的,都是nr*64M(nr是task[]數組下標),所以
//數據段的長度肯定大于代碼段長度。而且 copy_page_tables()傳入的是data_limit,這
// 把代碼和數據都包含進去了。
if (data_limit < code_limit)
panic("Bad data_limit");
// 新進程的代碼段基地址 = 數據段基地址 = 64M*nr
new_data_base = new_code_base = nr * 0x4000000;
// 設置進程的起始線性地址
p->start_code = new_code_base;
// 設置新進程的ldt項。在copy_process()中完全復制父進程的ldt,所以
// 只需重新設置ldt的基地址字段,其他字段和父進程一樣
set_base(p->ldt[1],new_code_base);
set_base(p->ldt[2],new_data_base);
// 把線性地址old_data_base處開始,一共data_limit個字節的內存對應的頁目錄、
// 頁表復制到線性地址new_data_base。這里僅僅復制相關的頁目錄和頁表,使它們
// 指向同一個物理頁面,實現父子進程數據代碼共享。

if (copy_page_tables(old_data_base,new_data_base,data_limit))
{
free_page_tables(new_data_base,data_limit);
return -ENOMEM;
}
return 0;
}

復制進程

/**//*****************************************************************************/

/**//* 功能:復制進程,把當前進程current復制到task[nr] */

/**//* 參數:當前進程(current)內核堆棧的所有內容 */

/**//* 當前進程內核堆棧保存了所有寄存器的值,在程序中要把這些寄存器的值 */

/**//* 全部復制給子進程,從而給子進程創造和父進程一樣的運行環境 */

/**//* 返回:子進程pid */

/**//*****************************************************************************/
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)


{
struct task_struct *p;
int i;
struct file *f;
// 在主內存區申請一頁新的內存,用來放置子進程的task_struct和內核堆棧
// get_free_page()返回的是物理地址
p = (struct task_struct *) get_free_page();
if (!p)
return -EAGAIN;
// 設置task數組中相關項
task[nr] = p;
// 下面的賦值語句僅僅把父基礎的task_struct部分全部復制給子進程
// 注意:僅僅復制task_struct部分,內核堆棧不復制,因此子程序的內核堆棧
// 是空的,這也是我們希望的

*p = *current; /**//* NOTE! this doesn't copy the supervisor stack */
// 下面的很多賦值語句修改子進程的task_struct中若干字段
// 這些字段跟父進程是有差別的
p->state = TASK_UNINTERRUPTIBLE; //子進程設為不可中斷狀態
p->pid = last_pid; // 設置子進程pid
p->father = current->pid; // 把當前進程pid舍為子進程的father
p->counter = p->priority; // 繼承父親的優先級
p->signal = 0;
p->alarm = 0;

p->leader = 0; /**//* process leadership doesn't inherit */
p->utime = p->stime = 0;
p->cutime = p->cstime = 0;
p->start_time = jiffies; // 子進程開始時間
p->tss.back_link = 0;
// 子進程的內核堆棧指針設置為task_struct所在頁面的最高端
p->tss.esp0 = PAGE_SIZE + (long) p;
// 子進程的內核堆棧選擇符為0x10,指向GDT中系統數據段。
// 注意 雖然子進程的內核堆棧位于內核system模塊外,在主內存區,但是因為系統數據段
// 基地址為0,限長為16M,函概了所有物理內存,故子進程內核堆棧也位于系統數
// 段內。esp0要的是段內偏移,也是因為系統數據段基地址為0,物理地址
// PAGE_SIZE + (long) p 也是段內偏移。
p->tss.ss0 = 0x10;
// 把父進程系統調用返回地址賦給子進程當前運行的eip。這樣當子進程被調度程序選中
// 后他從fork返回地址處開始執行。
p->tss.eip = eip;
p->tss.eflags = eflags;
// eax是函數返回值存放的地方,把子進程的eax設置為0,這樣fork在子進程中返回的是0。
// 注意 子進程并沒有執行fork()函數,子進程的系統堆棧沒有進行過操作,當然不會有像
// 父進程那樣的fork函數調用。但是當子進程開始運行時,就好像它從fork中返回。
p->tss.eax = 0;
p->tss.ecx = ecx;
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp; // 用戶堆棧指針和父進程一樣,子進程完全復制父進程的用戶堆棧
p->tss.ebp = ebp;
p->tss.esi = esi;
p->tss.edi = edi;
p->tss.es = es & 0xffff;
p->tss.cs = cs & 0xffff;
p->tss.ss = ss & 0xffff;
p->tss.ds = ds & 0xffff;
p->tss.fs = fs & 0xffff;
p->tss.gs = gs & 0xffff;
// 設置子進程的ldt。從這里可以看到,task下標為nr的進程在GDT中的2項一定是
// _LDT(nr)和_TSS(nr)。task[]中的項和GDT中的2項一一對應。
p->tss.ldt = _LDT(nr);
p->tss.trace_bitmap = 0x80000000;
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
// 在copy_mem函數中設置子進程的代碼段描述符,數據段描述符,并且復制父進程的
// 頁目錄、頁表。實現和父進程代碼數據的共享。

if (copy_mem(nr,p))
{
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}
// 子進程繼承父進程打開的文件,所以文件引用數目要加一
for (i=0; i<NR_OPEN;i++)
if (f=p->filp[i])
f->f_count++;
// 子進程繼承父進程的工作目錄、根目錄和可執行文件,所以引用數目加一
if (current->pwd)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
// GDT中對應位置(和nr對應)放入子進程的TSS描述符、LDT描述符
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
// 最后把子進程的狀態設置為可運行狀態,這樣子進程可以被調度

p->state = TASK_RUNNING; /**//* do this last, just in case */
// 父進程返回子進程的pid
return last_pid;
}

7 進程的結束
進程結束的時候,需要關閉的資源主要有:
(1) 釋放所有的物理頁面。(
子進程自己清除)
(2) 關閉所有打開的文件。(
子進程自己清除)
(3) 清除task[] 中的相應的項。(
父進程自己清除)
子進程通過exit()清除前面兩個選項,將自身的狀態變為TASK_ZOMBIE 。
父進程通過調用waitpid() 將task[] 數組清空。
一個進程的經過exit()之后,物理頁表被清除 , 頁表頁目錄項也被清除,但是它的進程控制塊和內核堆棧還在,,
此時進程的狀態變為TASK_ZOMBIE ,不會再被處理器處理。不被處理但是還占用著task數組中的一個表項,這
就成為了僵尸進程。
子進程調用了exit()函數之后,就通知父進程,父進程調用waitpid() 來清除 task數組中的表項。但是很有可能,
父進程沒有執行waitpid()操作,情況如下:
(1) 父進程早于子進程執行exit()函數。
(2) 子進程僵死,但是父進程沒有調用waitpid()操作。
(3) 父進程調用了waitpid(),但是因為某種愿意沒有釋放資源。
解決方法:
如果父進程無法釋放資源,那么就讓進程1來釋放資源。
當一個父進程早于子進程exit()的時候,它把所有的子進程過繼給父進程。