程序描述和建立

sgqmax發表於2024-12-05

程序描述

作業系統透過程序控制塊PCB來描述程序,對應Linux核心資料結構struct task_struct
在Linux3.18.6核心中,定義於include/linux/sched.h#1235

pidtgid標識程序
state程序狀態
stack程序堆疊
CONFIG_SMP在多處理器時使用
fs檔案系統描述
tty控制檯
files程序開啟檔案的檔案描述符
mm記憶體管理描述
signal程序間通訊的訊號描述
struct list_head tasks管理程序資料結構的雙向連結串列

資料結構list_head,定義於include/linux/types.h#186

struct list_head{
  struct list_head *next, *prev;
};

Linux程序狀態
就緒態,執行態,阻塞態

程序狀態轉換如下:
程序呼叫do_fork()建立子程序,轉為就緒態TASK_RUNNING

  • 就緒態
    當排程器從就緒態中選擇一個程序時,轉為執行態TASK_RUNNING
  • 執行態
    當高優先順序程序搶佔或時間片用盡時,轉為就緒態TASK_RUNNING
    當程序睡眠等待待定事件或特定資源時,轉為阻塞態TASK_RUNNING
    呼叫do_exit(),程序終止
  • 阻塞態
    當事件發生或資源可用,程序被喚醒,進入就緒佇列

作業系統原理中,就緒態和執行態是兩個狀態,在Linux核心中都是TASK_RUNNING
當程序為TASK_RUNNING狀態時,是可執行的,即就緒態,是否執行取決於有沒有獲得CPU控制權

正在執行的程序,呼叫使用者態庫函式exit(),會陷入核心執行do_exit()終止程序,轉為TASK_ZOMBIE
TASK_ZOMBIE狀態的程序一般稱為殭屍程序,核心會在適當的時候處理殭屍程序,釋放程序描述符

阻塞態有兩種
TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE
前者是可以被訊號或wake_up()喚醒,而後者只能被wake_up()喚醒

Linux核心中定義的程序狀態,位於include/linux/sched.h#203

程序建立

0號程序初始化

雙向連結串列的第一個節點為init_task
0號程序描述符結構體變數init_task的初始化是透過硬編碼方式固定下來
除此之外,其他程序都是透過do_fork()複製父程序的方式進行初始化

init_task變數初始化程式碼如下,位於init/init_task.c#18

// initial task structure
struct task_struct init_task= INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);

其中INIT_TASK宏定義位於include/linux/init_task.h#173

記憶體管理相關程式碼

程序描述符結構struct task_struct中管理記憶體的結構struct mm_struct *mm, *active_mm
mm描述程序地址空間
active_mm描述記憶體管理

每個程序都有若干個資料段、程式碼段、堆疊段等,都是由這個資料結構管理

邏輯地址空間分段分頁記憶體管理單元MMU
用來管理實體地址和邏輯地址轉換

32位x86體系結構中,程序是4GB的邏輯地址空間

程序之間的父子、兄弟關係

程序描述符struct task_struct中除了雙向連結串列struct list_head tasks管理程序外
記錄當前程序的父程序
struct task_struct __rcu* real_parent
struct task_struct __rcu* parent

雙向連結串列記錄當前程序的子程序
struct list_head children

雙向連結串列記錄當前程序的兄弟程序
struct list_head sibling

保持程序上下文中CPU相關的一些狀態資訊的資料結構

核心在程序描述符中使用struct thread_struct thread記錄程序上下文中CPU狀態資訊
定義於arch/x86/include/asm/processor.h#468
spip用來儲存程序上下文中的ESP暫存器和EIP暫存器狀態

程序的建立過程分析

start_kernel()中透過kernel_thread方式(fork方式)建立了兩個核心執行緒:kernel_init和kthreadd核心執行緒

kernel_init
啟動使用者態程序init

kthreadd
是所有核心執行緒的祖先,負責管理所有核心執行緒

系統啟動時,處理0號程序的初始化是透過手動編碼建立外
1號init程序的建立實際上是複製0號程序,並修改pid等,然後再載入一個init可執行程式
2號程序kthreadd核心執行緒也是透過複製0號執行緒建立

1.使用者態建立程序的方法

int pid= fork()
if(pid<0){        // fork error
}else if(pid==0){ // 子程序
}else{}

fork()系統呼叫複製當前程序,建立了一個子程序,兩個程序執行相同的程式碼

2.fork系統呼叫
在觸發系統呼叫時,使用者態有個int $0x80指令觸發中斷機制

將當前程序使用者態的堆疊SS:ESPCS:EIPEFLAGS壓棧到當前程序的核心堆疊,由CPU自動完成
從使用者態堆疊轉換到核心態堆疊

接下來執行到彙編程式碼system_call,用於儲存現場、執行系統呼叫核心處理函式、處理完返回、恢復現場

最後iret將CPU關鍵現場SS:ESPCS:EIPEFLAGS恢復到對應暫存器中
並回到使用者態int $0x80的下一條指令

fork()也是一個系統呼叫
建立一個子程序,複製了父程序所有的程序資訊,如核心堆疊、程序描述符等

問題是:當子程序被排程時,是從哪裡開始執行的呢?

建立程序相關的幾個系統呼叫核心處理函式,位於kernel/fork.c#1693
可以看出fork()vfork()clone()這三個系統呼叫和kernel_thread()核心函式都可以建立程序
而且都是透過do_fork()函式建立程序,只不過傳遞的引數不同

3.fork()核心處理過程
透過fork()建立的父子程序,大部分資訊都是一樣的,但是如pid值,核心堆疊等資訊需要修改

fork一個子程序時,在複製父程序資源過程中,採用了寫時複製copy on write技術,不需要修改程序資源,父子程序共享記憶體儲存空間

do_fork()跟蹤分析程式碼,位於kernel/fork.c#1617

long do_fork(unsigned long clone_flags,
            unsigned long stack_start,
            unsigned long stack_size,
            int __user* parent_tidptr,
            int __user* child_tidptr);

引數說明

  • clone_flags
    子程序建立標誌,控制對父程序資源有選擇的複製
    標誌定義於include/uapi/linux/sched.h#4
  • stack_start
    子程序使用者態堆疊的地址
  • regs
    指向pt_regs結構體的指標
    當發生系統呼叫時,int指令和SAVE_ALL儲存現場等操作會將CPU暫存器值按順序壓棧
  • stack_size
    使用者態棧大小,通常不需要,總是被設定為0
  • parent_tidptrchild_tidptr
    父程序、子程序使用者態下的pid地址

為了便於理解,下述為do_fork()函式體關鍵程式碼

struct task_struct* p; // 建立程序描述符指標
int trace= 0;
long nr;               // 子程序pid
...
// 建立子程序的描述符,和執行時所需的其他資料結構
p= copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace);
if(!IS_ERR(p)){
  struct completion vfork; // 定義完成量,一個執行單元等待另一個執行單元完成任務
  struct pid* pid;
  ...
  pid= get_task_pid(p, PIDTYPE_PID); // 獲取 task 結構體中的 pid
  nr= pid_vnr(pid);                  // 根據 pid 結構體獲取程序 pid
  ...
  // 若 clone_flags 包含 CLONE_VFORK 標誌,就將完成量 vfork 賦值給程序描述符中的 vfork_done 欄位
  // 此處只是對完成量進行初始化
  if(clone_flags & CLONE_FLORK){
    p->vfork_done= &vfork;
    init_completion(&vfork);
    get_task_struct(p);
  }
  wake_up_new_task(p); // 將子程序新增到排程器佇列,使之有機會執行

  // forking complete and child started to run, tell ptracer
  ...
  // 若 clone_flags 包含 CLONE_VFORK 標誌
  // 就將父程序插入等待佇列直到子程序呼叫 exec()或退出,此處是具體的阻塞
  if(clone_flags & CLONE_VFORK){
    if(!wait_for_vfork_done(p, &vfork)){
      ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
    }
  }
  put_pid(pid);
}else{
  nr= PTR_ERR(p); // 錯誤處理
}
return nr; // 返回子程序 pid (父程序的fork()返回值為子程序pid的原因)

do_fork()呼叫copy_process()完成複製父程序資訊、獲取pid,呼叫wake_up_new_task將子程序加入排程器佇列,透過clone_flags標誌做一些輔助工作
copy_process()說明如下,位於kernel/fork.c#1174

static struct task_struct* copy_process(){
  int retval;
  struct task_struct* p;
  ...
  retval= security_task_create(clone_flags); // 安全性檢查
  ...
  p= dup_task_struct(current); // 複製PCB,為子程序建立核心棧,程序描述符
  ftrace_graph_init_task(p);
  ...

  retval= -EAGAIN;
  // 檢查該使用者的程序數是否超過限制
  if(atomic_read(&p->real_cred->user->processes)>=task_rlimit(p,RLIMIT_NPROC)){
    // 檢查使用者是否具有相關許可權,不一定是root
    if(p->real_cred->user!=INIT_USER && !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN)){
      goto bad_fork_free;
    }
    ...
    // 檢查程序數量是否超過 max_threads,後者取決於記憶體大小
    if(nr_threads>=max_threads){
      goto bad_fork_cleanup_count;
    }
    if(!try_module_get(task_thread_info(p)->exec_domain->module)){
      goto bad_fork_cleanup_count;
    }
    ...
    spin_lock_init(&p->alloc_lock); // 初始化自旋鎖
    init_sigpending(&p->pending);   // 初始化掛起程序
    posix_cpu_timers_init(p);       // 初始化cpu定時器
    ...
    // 初始化新程序排程程式資料結構,將新程序的狀態設定為TASK_RUNNING,並禁止核心搶佔
    retval= sched_fork(clone_flags, p);
    ...
    // 複製所有程序資訊
    shm_init_task(p);
    retval= copy_semundo(clone_flags, p);
    ...
    retval= copy_files(clone_flags, p);
    ...
    retval= copy_fs(clone_flags, p);
    ...
    retval= copy_sighand(clone_flags, p);
    ...
    retval= copy_signal(clone_flags, p);
    ...
    retval= copy_mm(clone_flags, p);
    ...
    retval= copy_namespaces(clone_flags, p);
    ...
    retval= copy_io(clone_flags, p);
    ...
    retval= copy_thread(clone_flags, stack_start, stack_size, p); // 初始化子程序核心棧
    ...
    // 若傳入的 pid 指標和全域性結構體變數 init_struct_pid 的地址不同
    // 則為子程序分配新的 pid
    if(pid!=&init_struct_pid){
      retval= -ENOMEM;
      pid= alloc_pid(p->nsproxy->pid_ns_for_children);
      if(!pid){
        goto bad_fork_cleanup_io;
      }
    }
    ...
    p->pid= pid_nr(pid); // 根據 pid 結構體獲取程序 pid
    // 若 clone_flags 包含 CLONE_THREAD 標誌,說明子程序和父程序在同一個執行緒組
    if(clone_flags & CLONE_THREAD){
      p->exit_signal= -1;
      p->group_leader= current->group_leader; // 將執行緒組的 leader 設為子程序的組 leader
      p->tgid= current->tgid;                 // 子程序繼承父程序的 tgid
    }else{
      if(clone_flags & CLONE_PARENT){
        p->exit_signal= current->group_leader->exit_signal;
      }else{
        p->exit_signal= (clone_flags & CSIGNAL);
      }
      p->group_leader= p; // 子程序的組 leader 是其自己

      p->tgid= p->pid; // 組號 tgid 是其自己的 pid
    }
    ...
    if(){
      ptrace_init_task();
      init_task_pid();
      if(){
      }else{
      }
      attach_pid(p, PIDTYPE_PID);
      nr_threads++; // 增加系統的程序數
    }
    ...
    return p; // 返回被建立的子程序描述符指標
    ...
  }
}

copy_process()呼叫dup_task_struct()複製當前程序描述符task_struct、資訊檢查、初始化、設定程序狀態為TASK_RUNNING(此時子程序為就緒態)、採用寫時複製技術複製程序資源,呼叫copy_thread()初始化子程序核心棧、設定子程序pid等

dup_task_struct()說明如下,位於kernel/fork.c#305

static struct task_struct* dup_task_struct(struct task_struct* orig){
  struct task_struct* tsk;
  struct thread_info* ti;
  int node= tsk_fork_get_node(orig);
  int err;
  tsk= alloc_task_struct_node(node);     // 為子程序建立程序描述符分配儲存空間
  ...
  ti= alloc_thread_info_node(tsk, node); // 實際上建立了兩個頁,一部分用來存放 thread_info,另一部分是核心棧
  ...
  err= arch_dup_task_struct(tsk, orig);  // 複製父程序的 task_struct 資訊
  ...
  tsk->stack= ti; // 將棧底的值賦給新節點的 stack
  // 對子程序的 thread_info 結構進行初始化,複製父程序的 thread_info 結構,然後將 task 指標指向子程序的程序描述符
  setup_thread_stack(tsk, orig);
  ...
  return tsk; // 返回新建立的程序描述符指標
  ...
}

thread_info結構,被稱為小型的程序描述符,記憶體區域大小為8KB,佔據連續兩個頁框
透過task指標指向程序描述符,該結構體位於arch/x86/include/asm/thread_info.h#26

核心棧由高地址向低地址增長,C語言中thread_info由低地址向高地址增長
核心透過遮蔽ESP暫存器的低13有效位獲取thread_info結構的基地址

在較新的核心程式碼中,task_struct沒有直接指向thread_info結構的指標,而是用一個void指標表示,然後透過型別轉換來訪問該結構

核心棧和thread_info結構被定義在一個聯合體中,alloc_thread_info_node分配一個聯合體
thread_union定義於include/linux/sched.h#2241

union thread_union{
  struct thread_info thread_info;
  unsigned long stack[THREAD_SIZE/sizeof(long)];
};

4.核心堆疊關鍵資訊的初始化
dup_task_struct()為子程序分配了核心棧,copy_thread()才能真正完成核心棧關鍵資訊的初始化
copy_thread()程式碼說明如下,位於arch/x86/kernel/process_32.c#132

int copy_thread(){
  struct pt_regs* childregs= task_pt_regs(p);
  struct task_struct* tsk;
  int err;

  p->thread.sp= (unsigned long)childregs;
  p->thread.sp0= (unsigned long)(childregs+1);
  memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));

  if(unlikely(p->flags & PF_KTHREAD)){
    // kernel thread
    memset(childregs, 0, );
    // 若建立
    p->thread.ip= (unsigned long)ret_from_kernel_thread;
    task_user_gs(p)= __KERNEL_STACK_CANARY;
    childregs->ds= __USER_DS;
    childregs->es= __USER_DS;
    childregs->fs= __KERNEL_PERCPU;
    childregs->bx= sp; // function
    childregs->bp= arg;
    childregs->orig_ax= -1;
    childregs->cs= __KERNEL_CS | get_kernel_rpl();
    childregs->flags= X86_EFLAGS_IF | X86_EFLAGS_FIXED;
    p->thread.io_bitmap_ptr= NULL;
    return 0;
  }
  // 複製核心堆疊
  *childregs= * current_pt_regs();
  childregs->ax= 0; // 將子程序的 eax 置0,所以 fork 的子程序返回值為 0
  ...
  // ip 指向 ret_from_fork,子程序從此處開始執行
  p->thread.ip= (unsigned long)ret_from_fork;
  task_user_gs(p)= get_user_gs(current_pt_regs());
  ...
  return err;
}

對子程序開始執行的起點ret_from_kernel_thread核心執行緒或ret_from_fork使用者態程序
以及在子程序中fork()系統呼叫的返回值,都進行了註釋

相關文章