linux內核分析第六週學習筆記

LINUX內核分析第六週學習總結

標籤(空格分隔): 20135328陳都node


陳都 原創做品轉載請註明出處 《Linux內核分析》MOOC課程 http://mooc.study.163.com/course/USTC-1000029000linux


1、進程的描述

1.操做系統三大功能

  • 進程管理
  • 內存管理
  • 文件系統

最核心的是進程管理編程

二、進程的做用

將信號、進程間通訊、內存管理和文件系統聯繫起來數組

3.進程控制塊PCB——task_struct

爲了管理進程,內核必須對每一個進程進行清晰的描述,進程描述符提供了內核所需瞭解的進程信息。
struct task_struct數據結構很龐大
Linux進程的狀態與操做系統原理中的描述的進程狀態彷佛有所不一樣,好比就緒狀態和運行狀態都是TASK_RUNNING,爲何呢?
進程的標示pid
全部進程鏈表struct list_head tasks;
內核的雙向循環鏈表的實現方法 - 一個更簡略的雙向循環鏈表
程序建立的進程具備父子關係,在編程時每每須要引用這樣的父子關係。進程描述符中有幾個域用來表示這樣的關係
Linux爲每一個進程分配一個8KB大小的內存區域,用於存放該進程兩個不一樣的數據結構:Thread_info和進程的內核堆棧
進程處於內核態時使用,不一樣於用戶態堆棧,即PCB中指定了內核棧,那爲何PCB中沒有用戶態堆棧?用戶態堆棧是怎麼設定的?
內核控制路徑所用的堆棧不多,所以對棧和Thread_info來講,8KB足夠了
struct thread_struct thread; //CPU-specific state of this task
文件系統和文件描述符
內存管理——進程的地址空間數據結構

分析:框架

pid_t pid又叫進程標識符,惟一地標識進程
list_head tasks即進程鏈表
    ——雙向循環鏈表連接起了全部的進程,也表示了父子、兄弟等進程關係
struct mm_struct 指的是進程地址空間,涉及到內存管理(對於X86而言,一共有4G的地址空間)
thread_struct thread 與CPU相關的狀態結構體 
struct *file表示打開的文件鏈表
Linux爲每一個進程分配一個8KB大小的內存區域,用於存放該進程兩個不一樣的數據結構:Thread_info和進程的內核堆棧

4.進程狀態轉換圖

Linux進程的狀態與操做系統原理中的描述的進程狀態有所不一樣,好比就緒狀態和運行狀態都是TASK_RUNNING
通常操做系統原理中描述的進程狀態有就緒態,運行態,阻塞態,可是在實際內核進程管理中是不同的。函數

struct task_struct數據結構很龐大學習

2、進程的建立

1.進程的建立概覽及fork一個進程的用戶態代碼

道生一(start_kernel....cpu_idle),一輩子二(kernel_init和kthreadd),二生三(即前面0、1和2三個進程),三生萬物(1號進程是全部用戶態進程的祖先,0號進程是全部內核線程的祖先),新內核的核心代碼已經優化的至關乾淨,都符合中國傳統文化精神了優化

0號進程,是代碼寫死的,1號進程複製0號進程PCB,再修改,再加載可執行程序。this

系統調用進程建立過程:

iret與int 0x80指令對應,一個是彈出寄存器值,一個是壓入寄存器的值
若是將系統調用類比於fork();那麼就至關於系統調用建立了一個子進程,而後子進程返回以後將在內核態運行,而返回到父進程後仍然在用戶態運行。

進程的父子關係直觀圖:

2.分析內核處理過程

do_fork

  • 調用copy_process,將當前進程複製一份出來給子進程,而且爲子進程設置相應地上下文信息。
  • 調用wake_up_new_task,將子進程放入調度器的隊列中,此時的子進程就能夠被調度進程選中運行。

fork代碼:fork、vfork和clone這三個函數最終都是經過do_fork函數實現的

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < 0) 
{ 
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-1);
} 
else if (pid == 0) //pid == 0和下面的else都會被執行到(一個是在父進程中即pid ==0的狀況,一個是在子進程中,即pid不等於0)
{
/* child process */pid=0時 if和else都會執行  fork系統調用在父進程和子進程各返回一次
printf("This is Child Process!\n");
} 
else 
{  
/* parent process  */
printf("This is Parent Process!\n");
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
}

建立新進程的框架do_fork:dup_thread複製父進程的PCB

long do_fork(unsigned long clone_flags,
      unsigned long stack_start,
      unsigned long stack_size,
      int __user *parent_tidptr,
      int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;
p = copy_process(clone_flags, stack_start, stack_size,
         child_tidptr, NULL, trace);
}

copy_process:進程建立的關鍵,修改複製的PCB以適應子進程的特色,也就是子進程的初始化

  • 建立進程描述符以及子進程所須要的其餘全部數據結構,爲子進程準備運行環境
  • 調用dup_task_struct複製一份task_struct結構體,做爲子進程的進程描述符。
  • 複製全部的進程信息
  • 調用copy_thread,設置子進程的堆棧信息,爲子進程分配一個pid。
static struct task_struct *copy_process(unsigned long clone_flags,
                unsigned long stack_start,
                unsigned long stack_size,
                int __user *child_tidptr,
                struct pid *pid,
                int trace)
{
int retval;
struct task_struct *p;

// 分配一個新的task_struct
p = dup_task_struct(current);

// 檢查該用戶的進程數是否超過限制
if (atomic_read(&p->real_cred->user->processes) >=
        task_rlimit(p, RLIMIT_NPROC)) {
    // 檢查該用戶是否具備相關權限
    if (p->real_cred->user != INIT_USER &&
        !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
        goto bad_fork_free;
}

retval = -EAGAIN;
// 檢查進程數量是否超過 max_threads
if (nr_threads >= max_threads)
    goto bad_fork_cleanup_count;
// 初始化自旋鎖,掛起信號,定時器
retval = sched_fork(clone_flags, p);
 // 初始化子進程的內核棧
retval = copy_thread(clone_flags, stack_start, stack_size, p);
if (retval)
    goto bad_fork_cleanup_io;

if (pid != &init_struct_pid) {
    retval = -ENOMEM;
    // 這裏爲子進程分配了新的pid號
    pid = alloc_pid(p->nsproxy->pid_ns_for_children);
    if (!pid)
        goto bad_fork_cleanup_io;
}

/* ok, now we should be set up.. */
// 設置子進程的pid
p->pid = pid_nr(pid);
// 若是是建立線程
if (clone_flags & CLONE_THREAD) {
    p->exit_signal = -1;
    // 線程組的leader設置爲當前線程的leader
    p->group_leader = current->group_leader;
    // tgid是當前線程組的id,也就是main進程的pid
    p->tgid = current->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;
    // tgid和pid相同
    p->tgid = p->pid;
}

if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
    //同一線程組內的全部線程、進程共享父進程
    p->real_parent = current->real_parent;
    p->parent_exec_id = current->parent_exec_id;
} else {
    // 若是是建立進程,當前進程就是子進程的父進程
    p->real_parent = current;
    p->parent_exec_id = current->self_exec_id;
}

dup_ task_ struct

  • 先調用alloc_task_struct_node分配一個task_struct結構體。
  • 調用alloc_thread_info_node,分配了一個union。這裏分配了一個thread_info結構體,還分配了一個stack數組。返回值爲ti,實際上就是棧底。
  • tsk->stack = ti將棧底的地址賦給task的stack變量。
  • 最後爲子進程分配了內核棧空間。
  • 執行完dup_task_struct以後,子進程和父進程的task結構體,除了stack指針以外,徹底相同。

copy_thread:

  • 獲取子進程寄存器信息的存放位置
  • 對子進程的thread.sp賦值,未來子進程運行,這就是子進程的esp寄存器的值。
  • 若是是建立內核線程,那麼它的運行位置是ret_from_kernel_thread,將這段代碼的地址賦給thread.ip,以後準備其餘寄存器信息,退出
  • 將父進程的寄存器信息複製給子進程。
  • 將子進程的eax寄存器值設置爲0,因此fork調用在子進程中的返回值爲0.
  • 子進程從ret_from_fork開始執行,因此它的地址賦給thread.ip,也就是未來的eip寄存器。
int copy_thread(unsigned long clone_flags, unsigned long sp,
unsigned long arg, struct task_struct *p)
{
struct pt_regs *childregs = task_pt_regs(p);
struct task_struct *tsk;
int err;
// 若是是建立的內核線程
if (unlikely(p->flags & PF_KTHREAD)) {
    /* kernel thread */
    memset(childregs, 0, sizeof(struct pt_regs));
    // 內核線程開始執行的位置
    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;
}

// 複製內核堆棧,並非所有,只是regs結構體(內核堆棧棧底的程序)
*childregs = *current_pt_regs();
childregs->ax = 0;
if (sp)
    childregs->sp = sp;

// 子進程從ret_from_fork開始執行
p->thread.ip = (unsigned long) ret_from_fork;//調度到子進程時的第一條指令地址,也就是說返回的就是子進程的空間了
task_user_gs(p) = get_user_gs(current_pt_regs());

return err;
}
#ifdef CONFIG_SMP //條件編譯,多處理器會用到
   struct llist_node wake_entry;
   int on_cpu;
   struct task_struct *last_wakee;
   unsigned long wakee_flips;
   unsigned long wakee_flip_decay_ts;
    int wake_cpu;
#endif
   int on_rq;
    int prio, static_prio, normal_prio;
   unsigned int rt_priority; //與優先級相關
   const struct sched_class *sched_class;
   struct sched_entity se;
   struct sched_rt_entity rt;

……
   struct list_head tasks; //進程鏈表
#ifdef CONFIG_SMP
   struct plist_node pushable_tasks;
   struct rb_node pushable_dl_tasks;
#endif

3.建立一個新進程在內核中的執行過程

fork、vfork和clone三個系統調用均可以建立一個新進程,並且都是經過調用do_fork來實現進程的建立;

  • Linux經過複製父進程來建立一個新進程,那麼這就給咱們理解這一個過程提供一個想象的框架:
    • 複製一個PCB——task_struct
      $ err = arch_dup_task_struct(tsk, orig);  //在這個函數複製父進程的數據結構
    • 要給新進程分配一個新的內核堆棧
    $ ti = alloc_thread_info_node(tsk, node);
       $ tsk->stack = ti;  //複製內核堆棧
      $ setup_thread_stack(tsk, orig); //這裏只是複製thread_info,而非複製內核堆棧
    • 要修改複製過來的進程數據,好比pid、進程鏈表等等都要改
    • 從用戶態的代碼看fork();函數返回了兩次,即在父子進程中各返回一次,父進程從系統調用中返回比較容易理解,子進程從系統調用中返回。那它在系統調用處理過程當中的哪裏開始執行的呢?這就涉及子進程的內核堆棧數據狀態和task_struct中thread記錄的sp和ip的一致性問題,這是在哪裏設定的?copy_thread in copy_process
    $ *childregs = *current_pt_regs(); //複製內核堆棧
    $ childregs->ax = 0; //爲何子進程的fork返回0,這裏就是緣由
    
    $ p->thread.sp = (unsigned long) childregs; //調度到子進程時的內核棧頂
    $ p->thread.ip = (unsigned long) ret_from_fork; //調度到子進程時的第一條指令地址

4、小結

  • Linux經過複製父進程來建立一個新進程,經過調用do_ fork來實現併爲每一個新建立的進程動態地分配一個task_ struct結構。不管是使用 fork 仍是 vfork 來建立進程,最終都是經過 do_fork() 方法來實現的。PS:當子進程得到CPU控制權的時候,它的ret_ from_ fork能夠把後面堆棧從iret返回到用戶態,這裏的用戶態是子進程的用戶態
  • fork建立的新的子進程是從ret_from_fork開始執行的,而後跳轉到syscall_exit,從系統調用中返回。
  • Linux中的線程,又是一種特殊的進程。
  • 爲了把內核中的全部進程組織起來,Linux提供了幾種組織方式,其中哈希表和雙向循環鏈表方式是針對系統中的全部進程(包括內核線程),而運行隊列和等待隊列是把處於同一狀態的進程組織起來
  • fork()函數被調用一次,但返回兩次

  • 新進程如何開始的關鍵:

copy_thread()中:
p->thread.ip = (unsigned long) ret_from_fork; //調度到子進程時的第一條指令地址

將子進程的ip設置爲ret_ form _ fork的首地址,所以子進程是從ret_ from_ fork開始執行的。

在設置子進程的ip以前:
p->thread.sp = (unsigned long) childregs; //調度到子進程時的內核棧頂
*childregs = *current_ pt_ regs();

將父進程的regs參數賦值到子進程的內核堆棧,*childregs的類型爲pt_regs,其中存放了SAVE ALL中壓入棧的參數。

相關文章
相關標籤/搜索