Linux核心建立一個程式的過程分析

xingjiarong的專欄發表於2016-03-18

不管在什麼系統中,所有的任務都是以程式為載體的,所以理解程式的建立對於理解作業系統的原理是非常重要的,本文是我在學習linux核心中所做的筆記,如有錯誤還請大家批評指正。注:我所閱讀的核心版本是0.11。

一、關於PCB

對於一個程式來說,PCB就好像是他的記賬先生,當一個程式被建立時PCB就被分配,然後有關程式的所有資訊就全都儲存在PCB中,例如,開啟的檔案,頁表基址暫存器,程式號等等。在linux中PCB是用結構task_struct來表示的,我們首先來看一下task_struct的組成。

程式碼位於linux/include/linux/Sched.h

struct task_struct {

    long state; //表示程式的狀態,-1表示不可執行,0表示可執行,>0表示停止
    long counter;/* 執行時間片,以jiffs遞減計數 */
    long priority; /* 執行優先數,開始時,counter = priority,值越大,表示優先數越高,等待時間越長. */
    long signal;/* 訊號.是一組點陣圖,每一個bit代表一種訊號. */
    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程式碼長度(byte),
end_data程式碼長度+資料長度(byte),brk總長度(byte),start_stack堆疊段地址 */
    long pid,father,pgrp,session,leader;/* 程式號,父程式號 ,父程式組號,會話號,會話頭(發起者)*/
    unsigned short uid,euid,suid;/* 使用者id 號,有效使用者 id 號,儲存使用者 id 號*/
    unsigned short gid,egid,sgid;/* 組標記號 (組id),有效組 id,儲存的組id */
    long alarm;/* 報警定時值 (jiffs數) */
    long utime,stime,cutime,cstime,start_time;/* 使用者態執行時間 (jiffs數),
系統態執行時間 (jiffs數),子程式使用者態執行時間,子程式系統態執行時間,程式開始執行時刻 */
    unsigned short used_math;/* 是否使用了協處理器 */
/* 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];
/* 檔案結構指標表,最多32項. 表項號即是檔案描述符的值 */
    struct desc_struct ldt[3];
/* 任務區域性描述符表.0-空,1-cs段,2-Ds和Ss段 */
    struct tss_struct tss; /* 程式的任務狀態段資訊結構 */
};

二、程式的建立

系統中的程式是由父程式呼叫fork()函式來建立的,那麼呼叫fork()函式的時候究竟會發生什麼呢?

1、引發0×80中斷

程式1是由程式0通過fork()建立的,其中的fork程式碼如下:

init/main.c

#define _syscall0(type,name) /   
type name(void) /  
{ /  
long __res; /  
__asm__ volatile ( "int $0x80" /    // 呼叫系統中斷0x80。   
:"=a" (__res) /     // 返回值??eax(__res)。   
:"0" (__NR_##name)); /           // 輸入為系統中斷呼叫號__NR_name。   
      if (__res >= 0) /      // 如果返回值>=0,則直接返回該值。   
      return (type) __res; errno = -__res; /    // 否則置出錯號,並返回-1。   
      return -1;}

這樣使用int 0×80中斷,呼叫sys_fork系統呼叫來建立程式。

2、sys_fork()

_sys_fork:  
call _find_empty_process # 呼叫find_empty_process()(kernel/fork.c,135)。  
testl %eax,%eax  
js 1f  
push %gs  
pushl %esi  
pushl %edi  
pushl %ebp  
pushl %eax  
call _copy_process # 呼叫C 函式copy_process()(kernel/fork.c,68)。  
addl $20,%esp # 丟棄這裡所有壓棧內容。  
1: ret

雖然是一段彙編程式碼,但是我們可以很清楚的看到首先呼叫的是find_empty_process(),然後又呼叫了copy_process(),而這兩個函式就是fork.c中的函式。下面我們來看一下這兩個函式。

3、find_empty_process()

// 為新程式取得不重複的程式號last_pid,並返回在任務陣列中的任務號(陣列index)。  
int  find_empty_process (void)  
{  
  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++) // 任務0 排除在外。  
    if (!task[i])  
      return i;  
  return -EAGAIN;  
}

find_empty_process的作用就是為所要建立的程式分配一個程式號。在核心中用全域性變數last_pid來存放系統自開機以來累計的程式數,也將此變數用作新建程式的程式號。核心第一次遍歷task[64],如果&&條件成立說明last_pid已經被別的程式使用了,所以++last_pid,直到獲取到新的程式號。第二次遍歷task[64],獲得第一個空閒的i,也就是任務號。因為在linux0.11中,最多允許同時執行64個程式,所以如果當前的程式已滿,就會返回-EAGAIN。

4、copy_process()

獲得程式號並且將一些暫存器的值壓棧後,開始執行copy_process(),該函式主要負責以下的內容。

  • 為子程式建立task_struct,將父程式的task_struct複製給子程式。
  • 為子程式的task_struct,tss做個性化設定。
  • 為子程式建立第一個頁表,也將父程式的頁表內容賦給這個頁表。
  • 子程式共享父程式的檔案。
  • 設定子程式的GDT項。
  • 最後將子程式設定為就緒狀態,使其可以參與程式間的輪轉。
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;  
  task[nr] = p;         // 將新任務結構指標放入任務陣列中。  
// 其中nr 為任務號,由前面find_empty_process()返回。  
  *p = *current;        /* NOTE! this doesn't copy the supervisor stack */  
/* 注意!這樣做不會複製超級使用者的堆疊 */ (只複製當前程式內容)。  
    p->state = TASK_UNINTERRUPTIBLE; // 將新程式的狀態先置為不可中斷等待狀態。  
  p->pid = last_pid;     // 新程式號。由前面呼叫find_empty_process()得到。  
  p->father = current->pid;   // 設定父程式號。  
  p->counter = p->priority;  
  p->signal = 0;     // 訊號點陣圖置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;   // 當前滴答數時間。  
// 以下設定任務狀態段TSS 所需的資料(參見列表後說明)。  
  p->tss.back_link = 0;  
  p->tss.esp0 = PAGE_SIZE + (long) p;    // 堆疊指標(由於是給任務結構p 分配了1 頁  
// 新記憶體,所以此時esp0 正好指向該頁頂端)。  
  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;   // 段暫存器僅16 位有效。  
  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);    // 該新任務nr 的區域性描述符表選擇符(LDT 的描述符在GDT 中)。  
  p->tss.trace_bitmap = 0x80000000;   
// 如果當前任務使用了協處理器,就儲存其上下文。  
    if (last_task_used_math == current)  
    __asm__ ("clts ; fnsave %0"::"m" (p->tss.i387));  
// 設定新任務的程式碼和資料段基址、限長並複製頁表。如果出錯(返回值不是0),則復位任務陣列中  
// 相應項並釋放為該新任務分配的記憶體頁。  
  if (copy_mem (nr, p))  
    {               // 返回不為0 表示出錯。  
      task[nr] = NULL;  
      free_page ((long) p);  
      return -EAGAIN;  
    }  
// 如果父程式中有檔案是開啟的,則將對應檔案的開啟次數增1。  
  for (i = 0; i < NR_OPEN; i++)  
    if (f = p->filp[i])  
      f->f_count++;  
// 將當前程式(父程式)的pwd, root 和executable 引用次數均增1。  
  if (current->pwd)  
    current->pwd->i_count++;  
  if (current->root)  
    current->root->i_count++;  
  if (current->executable)  
    current->executable->i_count++;  
// 在GDT 中設定新任務的TSS 和LDT 描述符項,資料從task 結構中取。  
// 在任務切換時,任務暫存器tr 由CPU 自動載入。  
  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 */  
/* 最後再將新任務設定成可執行狀態,以防萬一 */  
  return last_pid;      // 返回新程式號(與任務號是不同的)。  
}

進入copy_prossess函式後,呼叫get_free_page()函式,在主記憶體申請一個空閒頁面,並將申請到的頁面清0。將這個頁面的指標強制型別轉化成task_struct型別的指標,並掛接在task[nr]上,nr就是在find_empty_process中返回的任務號。

接下來的*p=*current將當前程式的指標賦給了子程式的,也就是說子程式繼承了父程式一些重要的屬性,當然這是不夠的,所以接下來的一大堆程式碼都是為子程式做個性化設定的。

一般來講,每個程式都要載入屬於自己的程式碼、資料,所以copy_process設定子程式的記憶體地址。通過copy_mem來設定新任務的程式碼和資料段基址、限長並複製頁表。

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]);    // 取原資料段基址。  
  if (old_data_base != old_code_base)   // 0.11 版不支援程式碼和資料段分立的情況。  
    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;   // 新基址=任務號*64Mb(任務大小)。  
  p->start_code = new_code_base;  
  set_base (p->ldt[1], new_code_base);   // 設定程式碼段描述符中基址域。  
  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); // 如果出錯則釋放申請的記憶體。  
      return -ENOMEM;  
    }  
  return 0;  
}

然後是對檔案,pwd等資源的修改,接著要設定子程式在GDT中的表項,最後將程式設定為就緒狀態,並返回程式號。

三、建立過程總結

可以將上面繁瑣的建立過程總結為一下的幾步:

1、呼叫fork()函式引發0×80中斷
2、呼叫sys_fork
3、通過find_empty_process為新程式分配一個程式號
4、通過copy_process函式使子程式複製父程式的資源,並進行一些個性化設定後,返回程式號。

相關文章