第一次作業:基於Linux0.01深入源碼分析進程模型
一.前言
本文主要基於Linux0.01源代碼分析進程模型。Linux 0.01雖然是Linux的第一個發行版本,但是卻基本具備了操作系統中最重要的組成部分,同時Linux 0.01只有8500行左右的代碼,對於初學者而言學習起來比較簡單一點。
Linux 0.01源代碼下載地址:
https://mirrors.edge.kernel.org/pub/linux/kernel/Historic/
二.進程的定義
進程是程序執行的基本單位。(其中,進程和程序的區別:程序指的是由若幹函數組成的可執行文件,而進程指的是特定程序的一個實例)進程是對硬件所提供的資源進行操作的基本單元,也是順序執行其實例化程序的基本單位。操作系統根據進程的需要管理和使用系統資源。在一定程度上,進程是由它要執行的一組指令、寄存器的內容和程序執行時程序計數器以及它們的狀態定義的。
三.進程的組織
從系統內核角度看,一個進程僅僅是進程控制表(process table)中的一項。進程控制表中的每一項都是一個 task_struct 結構,task_struct結構定義於 linux/sched.h 。
進程控制表既是一個數組,又是一個雙向鏈表,同時又是一棵樹。其物理實現是一個包括多個指針的靜態數組。
系統啟動後,內核通常作為某個進程的代表。一個指向task_struct的全局指針變量 current 用來記錄正在運行的進程。變量current只能由 kernel/sched.c 中的進程調度改變。
//task_struct //任務(進程)數據結構,或稱為進程描述符。 structtask_struct { /* these are hardcoded - don‘t touch */ long state; //任務的運行狀態/* -1 unrunnable, 0 runnable, >0 stopped */ long counter; //任務運行時間計數(遞減)(滴答數),運行時間片。 long priority; //運行優先數。任務開始運行時counter = priority,越大運行越長。 long signal; //信號,每個比特位代表一種信號,信號值=位偏移值+1 struct sigaction sigaction[32]; long blocked; /* bitmap of masked signals */ /* various fields */ int exit_code; //任務執行停止時的退出碼 unsigned long start_code, end_code, end_data, brk, start_stack; long pid; //進程標識號(進程號) long father; //父進程號 long pgrp; //父進程組號 long session; //會話號 long leader; unsigned short uid; //用戶標識號(用戶id) unsigned short euid; //有效用戶id unsigned short suid; //保存的用戶id unsigned short gid; //組標識號(組id) unsigned short egid; //有效組id unsigned short sgid; //保存的組id long alarm; long utime, stime, cutime, cstime, start_time; unsigned short used_math; /* file system info */ int tty; /* -1 if no tty, so it must be signed */ unsigned short umask; struct m_inode *pwd; //當前工作目錄i節點結構 struct m_inode *root; //根目錄i 節點結構 struct m_inode *executable; 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]; /* tss for this task */ struct tss_struct tss; };
四.進程的狀態及其轉換過程
1.定義進程運行可能處的狀態
在Linux 0.01內核中的include/linux/sched.h文件中,定義了進程的不同狀態,如下所示:
// include/linux/sched.h這裏定義了進程運行可能處的狀態。 #define TASK_RUNNING 0 // 進程正在運行或已準備就緒。 #define TASK_INTERRUPTIBLE 1 // 進程處於可中斷等待狀態。 #define TASK_UNINTERRUPTIBLE 2 // 進程處於不可中斷等待狀態,主要用於I/O 操作等待。 #define TASK_ZOMBIE 3 // 進程處於僵死狀態,已經停止運行,但父進程還沒發信號。 #define TASK_STOPPED 4 // 進程已停止。
2.Linux 0.01中的進程狀態及其功能
進程 | 功能 |
TASK_RUNNING | 表示進程在“Ready List”中,這個進程除了CPU以外,獲得了所有的其他資源 |
TASK_INTERRUPTIBLE | 進程在睡眠,正在等待一個信號或一個資源(Sleeping) |
TASK_UNINTERRUPTIBLE | 進程等待一個資源,當前進程在“Wait Queue” |
TASK_ZOMBIE | 僵屍進程(沒有父進程的子進程) |
TASK_STOPPED | 標識進程在被調試 |
3.進程創建,運行和消失:
Linux系統使用系統調用 fork() 來創建一個進程,使用 exit() 來結束進程。 fork() 和 exit() 的源程序保存在 kernel/fork.c 和 kernel/exit.c 中。
fork()的主要任務是初始化要創建進程的數據結構,其步驟如下:
1.申請一個空閑的頁面來保存task_struct
2.查找一個空的進程槽(find_empty_process())
3.為kernel_stack_page申請另一個空閑的內存頁作為堆棧
4.將父進程的LDT表復制給子進程
5.復制父進程的內存映射信息
6.管理文件描述符合鏈接點
fork.c
/* * ‘fork.c‘ contains the help-routines for the ‘fork‘ system call * (see also system_call.s), and some misc functions (‘verify_area‘). * Fork is rather simple, once you get the hang of it, but the memory * management can be a bitch. See ‘mm/mm.c‘: ‘copy_page_tables()‘ */ #include <errno.h> #include <linux/sched.h> #include <linux/kernel.h> #include <asm/segment.h> #include <asm/system.h> extern void write_verify(unsigned long address); long last_pid=0; void verify_area(void * addr,int size) { unsigned long start; start = (unsigned long) addr; size += start & 0xfff; start &= 0xfffff000; start += get_base(current->ldt[2]); //current->ldt[2]指向數據段 while (size>0) { size -= 4096; write_verify(start); //在memory.c中定義 start += 4096; } } 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); //0X0F=代碼段選擇器,8M data_limit=get_limit(0x17); //0x17=數據段選擇器,8m old_code_base = get_base(current->ldt[1]); old_data_base = get_base(current->ldt[2]); if (old_data_base != old_code_base) panic("We don‘t support separate I&D"); if (data_limit < code_limit) panic("Bad data_limit"); new_data_base = new_code_base = nr * 0x4000000; //0x4000000=64M set_base(p->ldt[1],new_code_base); //重新設置本任務的LDT描述符 set_base(p->ldt[2],new_data_base); if (copy_page_tables(old_data_base,new_data_base,data_limit)) { free_page_tables(new_data_base,data_limit); //如果不能copy_page_tables,那麽恢復以前的狀態 return -ENOMEM; } return 0; } /* * Ok, this is the main fork-routine. It copies the system process * information (task[nr]) and sets up the necessary registers. It * also copies the data segment in it‘s entirety. */ 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; p = (struct task_struct *) get_free_page(); if (!p) return -EAGAIN; *p = *current; /* NOTE! this doesn‘t copy the supervisor stack */ p->state = TASK_RUNNING; p->pid = last_pid; p->father = current->pid; 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; p->tss.esp0 = PAGE_SIZE + (long) p; p->tss.ss0 = 0x10; p->tss.eip = eip; p->tss.eflags = eflags; 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; p->tss.ldt = _LDT(nr); p->tss.trace_bitmap = 0x80000000; //0X80000000=2048M if (last_task_used_math == current) __asm__("fnsave %0"::"m" (p->tss.i387)); //保存浮點參數 if (copy_mem(nr,p)) { //為此任務設置LDT,並且拷貝父進程的頁表作為自己的頁表 free_page((long) p); return -EAGAIN; } for (i=0; i<NR_OPEN;i++) if (f=p->filp[i]) //系統init_task設置filp[NR_OPEN]為空 f->f_count++; if (current->pwd) //系統init_task設置PWD為空 current->pwd->i_count++; if (current->root) current->root->i_count++; //下面二行是在GDT中安裝本任務的TSS和LDT set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss)); //gdt+(nr<<1)是因為每個任務在GDT中占兩個單位 set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt)); task[nr] = p; /* do this last, just in case */ //將此任務登記入任務組 return last_pid; } int find_empty_process(void) //在初始化過的64個任務中尋找最近的一個空任務 { int i; repeat: if ((++last_pid)<0) last_pid=1; for(i=0 ; i<NR_TASKS ; i++) if (task[i] && task[i]->pid == last_pid) goto repeat; for(i=1 ; i<NR_TASKS ; i++) if (!task[i]) return i; return -EAGAIN; }
4.進程狀態切換
五.進程的調度
進程的調度( schedule() 函數)
處於TASK_RUNNING狀態的進程一道運行隊列(run queue),將schedule()函數按CPU調度算法在合適的時候被選中運行,分配給CPU。
新創建的進程都是處於TASK_RUNNING裝填,而且被掛到run queue的對手。進程調度采用變形的輪轉法(round robin)。當時間片到時(10ms的整數倍),由時鐘中斷引起新一輪調度,把當前進程掛到run queue隊尾。
調度程序的一個重要工作就是選擇系統所有可運行的進程中最適合運行的進程加以運行。一個可運行的進程是一個只等待CPU的進程。Linux使用合理而簡單的基於優先級的調度算法在系統當前的進程中進行選擇。當它選擇了準備運行的新進程,它就保存當前進程的狀態、與處理器相關的寄存器和其他需要保存的上下文信息到進程的task_struct數據結構中,然後恢復要運行的新進程的狀態(和處理器相關),把系統的控制交給這個進程。為了公平地在系統中所有可以運行(runnable)的進程之間分配CPU時間,調度程序在每一個進程的task_struct結構中保留了信息。
1 void schedule(void) 2 { 3 int i,next,c; 4 struct task_struct ** p; 5 6 /* check alarm, wake up any interruptible tasks that have got a signal */ 7 8 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) 9 if (*p) { 10 if ((*p)->alarm && (*p)->alarm < jiffies) { 11 (*p)->signal |= (1<<(SIGALRM-1)); 12 (*p)->alarm = 0; 13 } 14 if ((*p)->signal && (*p)->state==TASK_INTERRUPTIBLE) 15 (*p)->state=TASK_RUNNING; 16 } 17 18 /* this is the scheduler proper: */ 19 20 while (1) { //此循環是取得任務的counter最高的那個任務,作為要切換的任務 21 c = -1; 22 next = 0; 23 i = NR_TASKS; 24 p = &task[NR_TASKS]; 25 while (--i) { 26 if (!*--p) 27 continue; 28 if ((*p)->state == TASK_RUNNING && (*p)->counter > c) 29 c = (*p)->counter, next = i; 30 } 31 if (c) break; //如果所有的任務counter都一樣,且都為0,那麽執行下面的代碼,根據任務的先後順序和權限大小重設各任務的counter 32 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) 33 if (*p) 34 (*p)->counter = ((*p)->counter >> 1) + 35 (*p)->priority; 36 } 37 switch_to(next); //切換任務 38 } 39 40 int sys_pause(void) //系統暫停 41 { 42 current->state = TASK_INTERRUPTIBLE; 43 schedule(); 44 return 0; 45 } 46 47 void sleep_on(struct task_struct **p) //進程休眠 48 { 49 struct task_struct *tmp; 50 51 if (!p) 52 return; 53 if (current == &(init_task.task)) //初始化任務不能休眠 54 panic("task[0] trying to sleep"); 55 tmp = *p; 56 *p = current; 57 current->state = TASK_UNINTERRUPTIBLE; 58 schedule(); 59 if (tmp) 60 tmp->state=0; //讓**p的進程進入運行狀態 61 } 62 63 void interruptible_sleep_on(struct task_struct **p) 64 { 65 struct task_struct *tmp; 66 67 if (!p) 68 return; 69 if (current == &(init_task.task)) 70 panic("task[0] trying to sleep"); 71 tmp=*p; 72 *p=current; 73 repeat: current->state = TASK_INTERRUPTIBLE; 74 schedule(); 75 if (*p && *p != current) { 76 (**p).state=0; //先喚醒其他的進程 77 goto repeat; 78 } 79 *p=NULL; 80 if (tmp) 81 tmp->state=0; //喚醒指定的進程 82 } 83 84 void wake_up(struct task_struct **p) 85 { 86 if (p && *p) { 87 (**p).state=0; 88 *p=NULL; 89 } 90 }
六.對該操作系統進程模型的看法
從Linux調度器的演變過程:從0.11-2.4版本的O(n)算法 -> 2.5版本的O(1) -> 現在的CFS算法再到未來。在Linux這個集全球眾多程序員的聰明才智而不斷發展更新的超級內核,總會不斷出現新的可行性的措施,也會不斷出現新的問題(如使得系統設計越來越復雜),但隨著時代的變化,若幹年後也許在硬件越來越先進的前提上,曾經不被看好的算法也許能發揮出色(如“返璞歸真的Linux BFS調度器”)。
七.參考資料
task_struct(進程描述符)
Linux進程調度策略的發展和演變
Linux進程調度器概述
《Linux 0.01內核分析與操作系統設計--創造你自己的操作系統》(盧軍編著)
《Linux內核編程》
第一次作業:基於Linux0.01深入源碼分析進程模型