2017-2018-1 20179202《Linux內核原理與分析》第七週做業

一 、Linux內核建立一個新進程的過程

1. 知識準備

  • 操做系統內核三大功能是進程管理,內存管理,文件系統,最核心的是進程管理
  • linux 進程的狀態和操做系統原理的描述進程狀態有所不一樣,好比就緒狀態和運行狀態都是TASK_RUNNING。(這個表示它是可運行的,可是實際上有沒有在運行取決於它是否佔有 CPU )
  • fork 被調用一次,可以返回兩次。在父進程中返回新建立子進程的 pid;在子進程中返回 0
  • 調用 fork 以後,數據、堆、棧有兩份,代碼仍然爲一份(這個代碼段成爲兩個進程的共享代碼段)。當父子進程有一個想要修改數據或者堆棧時,兩個進程真正分裂。

2. 內核代碼分析

SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
    return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
    return -EINVAL;
#endif
}
SYSCALL_DEFINE0(vfork)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
            0, NULL, NULL);
}
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int, tls_val,
         int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
         int __user *, parent_tidptr,
         int __user *, child_tidptr,
         int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
        int, stack_size,
        int __user *, parent_tidptr,
        int __user *, child_tidptr,
        int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int __user *, child_tidptr,
         int, tls_val)
#endif
{
    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif

經過上面的代碼能夠看出 fork、vfork 和 clone 三個系統調用均可以建立一個新進程,並且都是經過 do_fork 來建立進程,只不過傳遞的參數不一樣。html

(1)do_forknode

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

首先了解一下 do_fork () 的參數:linux

  • clone_flags:子進程建立相關標誌,經過此標誌能夠對父進程的資源進行有選擇的複製。

  • stack_start:子進程用戶態堆棧的地址。數組

  • regs:指向 pt_regs 結構體(當系統發生系統調用時,pt_regs 結構體保存寄存器中的值並按順序壓入內核棧)的指針。緩存

  • stack_size:用戶態棧的大小,一般是沒必要要的,總被設置爲0。安全

  • parent_tidptr 和 child_tidptr:父進程、子進程用戶態下 pid 地址。數據結構

爲方便理解,下述爲精簡關鍵代碼:dom

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))                            //若是 copy_process 執行成功
        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_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }

        wake_up_new_task(p);        //將子進程添加到調度器的隊列,使之有機會得到CPU

        /* 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 將子進程加入調度器隊列,爲之分配 CPU、經過 clone_flags 標誌作一些輔助工做。其中 copy_process()是建立一個進程內容的主要的代碼。函數

(2)copy_processui

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;
    ...
    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定時器
    ···
    retval = sched_fork(clone_flags, p);  //初始化新進程調度程序數據結構,把新進程的狀態設置爲TASK_RUNNING,並禁止內核搶佔
    ...
    // 複製全部的進程信息
    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 (likely(p->pid)) {
        ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);

        init_task_pid(p, PIDTYPE_PID, pid);
        if (thread_group_leader(p)) {
            ...
            // 將子進程加入它所在組的哈希鏈表中
            attach_pid(p, PIDTYPE_PGID);
            attach_pid(p, PIDTYPE_SID);
            __this_cpu_inc(process_counts);
        } else {
            ...
        }
        attach_pid(p, PIDTYPE_PID);
        nr_threads++;     //增長系統中的進程數目
    }
    ...
    return p;             //返回被建立的子進程描述符指針P
    ...
}

copy_process 主要完成了調用 dup_task_struct 複製當前的 task_struct、信息檢查、初始化、把進程狀態設置爲 TASK_RUNNING、複製全部進程信息、調用 copy_thread 初始化子進程內核棧、設置子進程pid。

(3)dup_task_struct

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
   
    setup_thread_stack(tsk, orig);//對子進程的thread_info結構進行初始化(複製父進程的thread_info 結構,而後將 task 指針指向子進程的進程描述符)
    ...
    return tsk;               // 返回新建立的進程描述符指針
    ...
}

(4)copy_thread

dup_task_struct 只是爲子進程建立一個內核棧,copy_thread 才真正完成賦值。

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;

    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, sizeof(struct pt_regs));
      
        p->thread.ip = (unsigned long) ret_from_kernel_thread; //若是建立的是內核線程,則從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();//複製內核堆棧(複製父進程的寄存器信息,即系統調用SAVE_ALL壓棧的那一部份內容)
    
    childregs->ax = 0;           //子進程的eax置爲0,因此fork的子進程返回值爲0
    ...
    p->thread.ip = (unsigned long) ret_from_fork;//ip指向 ret_from_fork,子進程今後處開始執行
    task_user_gs(p) = get_user_gs(current_pt_regs());
    ...
    return err;

4. gdb調試

在剛纔分析的關鍵點處分別設置斷點:

如今 sys_clone 停下,再在 do_fork 停下,繼續單步執行:

繼續在 copy_process 停下,在 copy_thread 處停下,在這個地方能夠查看 p 的值:

最後 ret_from_fork 跟蹤到 syscall_exit 後沒法繼續。

5. 遇到的問題及解決

(1) thread_info 是什麼?

經過搜索得知,它被稱爲小型的進程描述符,內存區域大小是8KB,佔據連續的兩個頁框。該結構經過 task 指針指向進程描述符。內核棧是由高地址到低地址增加,thread_info 結構由低地址到高地址增加。內核經過屏蔽 esp 的低13位有效位得到 thread_info 結構的基地址。內核棧、thread_info結構、進程描述符之間的關係以下圖所示(在較新的內核代碼中,task_struct 結構中沒有直接指向 thread_info 結構的指針,而是用一個 void 指針類型的成員表示,而後經過類型轉換來訪問 thread_info 結構)。

內核棧和 thread_info 結構是被定義在一個聯合體當中,alloc_thread_info_node 分配的實則是一個聯合體,即既分配了 thread_info 結構又分配了內核棧。

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

咱們想要得到的通常是進程描述符而不是 thread_info,能夠用 current 宏獲取進程描述符(用task指針找到進程描述符)

static inline struct task_struct * get_current(void)
{
          return current_thread_info()->task;
}

(2)do_fork 中,pid = get_task_pid(p, PIDTYPE_PID)不是就獲取了 pid值嗎?怎麼後面還有一句 nr = pid_vnr(pid) ?

參考Linux 內核進程管理之進程ID,瞭解到PID命名空間相關知識。

pid結構體:

struct pid {
    struct hlist_head tasks;        //指回 pid_link 的 node
    int nr;                       //PID
    struct hlist_node pid_chain;    //pid hash 散列表結點
};

pid_vnr:

pid_t pid_vnr(struct pid*pid)
{
     return pid_nr_ns(pid,current->nsproxy->pid_ns); //current->nsproxy->pid_ns是當前pid_namespace
}

得到 pid 實例以後,再根據 pid 中的numbers 數組中 uid 信息,得到局部PID。

pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
{
    struct upid *upid;
    pid_t nr = 0;

    if (pid && ns->level <= pid->level) {
        upid = &pid->numbers[ns->level];
        if (upid->ns == ns)
            nr = upid->nr;
    }
    return nr;
}

因爲PID命名空間的層次性,父命名空間能看到子命名空間的內容,反之則不能。所以函數中須要確保當前命名空間的level 小於等於產生局部PID的命名空間的level(全局ID:在內核自己和初始命名空間中惟一的ID,在系統啓動期間開始的 init 進程即屬於該初始命名空間。系統中每一個進程都對應了該命名空間的一個PID,叫全局ID,保證在整個系統中惟一;局部ID:對於屬於某個特定的命名空間,它在其命名空間內分配的ID爲局部ID,該ID也能夠出如今其餘的命名空間中)。

二 、課本筆記

1.定時器和時間管理

  • 內核在硬件的幫助下計算和管理時間。
  • 系統定時器以某種頻率自行觸發時鐘中斷,稱爲節拍率。
  • 連續兩次時鐘中斷的間隔時間,成爲節拍(節拍率分之一)
  • 牆上時間(實際時間)和系統運行時間(自系統啓動開始所經的時間)根據時鐘間隔來計算。
  • 全局變量jiffies用來記錄自系統啓動以來產生的節拍總數。啓動時內核將它初始化爲0,此後每次時鐘中斷處理程序增長該變量的值。每一秒鐘中斷次數HZ,jiffies一秒內增長的值就是HZ,系統運行時間(以秒爲單位) 爲 jiffie/HZ。
  • 比較時間的幾個宏:
time_after(unknown, known)      //unknown after known ? true : false;
time_before(unknown, known)     //unknown before known ? true : false;
time_after_eq(unknown, known)   //unknown after or eq known ? true : false;
time_before_eq(unknown, known)  //unknown before or eq known ? true : false;
  • 實時時鐘 RTC 是用來持久存放系統時間的設備.
  • 定時器是管理內核流逝的時間的基礎。只需執行初始化工做,設置一個超時時間,指定超時發生後執行的函數,而後激活定時器就能夠了。定時器不周期運行,它在超時後自行撤退。定時器由如下結構表示:
struct timer_list {
       struct list_head entry;//定時器鏈表的入口
       unsigned long expires;//基於jiffies的定時值
       struct tvec_base *base;//定時器內部值
       void (*function)(unsigned long);//定時器處理函數
       ...
       };

定時器處理函數的函數原型:

void my_timer_function(unsigned long data);

add_timer(&my_timer);            //激活定時器

mod_timer(&my_timer, jiffies + new_dalay);        //改變指定定時器的超時時間
                                                  //若是定時器未被激活,mod_timer會激活該定時器
                                                  //若是調用時定時器未被激活,該函數返回0;不然返回1.
                                                                                            
del_timer(&my_timer);            //在定時器超時前中止定時器
                                 //被激活或未被激活的定時器均可以使用該函數
                                 //若是調用時定時器未被激活,該函數返回0;不然返回1.
                                 //不須要爲已經超時的定時器調用該函數,由於他們會自動刪除
  • 延遲執行不該該在持有鎖時或禁止中斷時發生
  • 最簡單的延遲方法是忙等待(延遲時間是節拍的整數倍或者精確率要求不高可使用)
  • 短延遲的延遲時間精確到毫秒,微妙;短暫等待某個動做完成時,比時鐘節拍更短,須要依靠數次循環達到延遲效果。
  • schedule_timeout() 使執行的任務睡眠指定時間達到延遲.調用它的代碼必須處於進程上下文中,而且不能持有鎖。
set_current_state(state);        //將任務設置爲可中斷睡眠狀態或不可中斷睡眠狀態
schedule_timeout(s*HZ);          //S秒後喚醒,被延遲的任務並將其從新放回運行隊列。

2.內存管理

  • 虛擬地址又叫線性地址。linux沒有采用分段機制,因此邏輯地址和虛擬地址(線性地址)是一個概念。內核的虛擬地址和物理地址大部分只差一個線性偏移量。用戶空間的虛擬地址和物理地址則採用了多級頁表進行映射,但仍稱之爲線性地址。
  • 在x86結構中,Linux內核虛擬地址空間劃分0~3G爲用戶空間,3~4G爲內核空間。內核把物理頁做爲內存管理的基本單元,內核虛擬空間(3G~4G)把頁又劃分爲三種類型的區:
ZONE_DMA 3G以後起始的16MB
ZONE_NORMAL 16MB~896MB
ZONE_HIGHMEM 896MB ~1G
  • 因爲內核的虛擬和物理地址只差一個偏移量:物理地址 = 邏輯地址 – 0xC0000000。因此若是1G內核空間徹底用來線性映射,物理內存也只能訪問到1G區間。HIGHMEM能夠解決此問題,專門開闢的一塊沒必要線性映射,能夠靈活定製映射,以便訪問1G以上物理內存的區域。

  • 最核心的分配頁面函數爲:
struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)
  • 給定的頁轉爲邏輯地址:
void * page_address(struct page * page)
  • 初始化爲0的頁面:
unsigned long get_zeroed_page(unsigned int gfp_mask)
  • 釋放頁,只能釋放屬於你的頁:
void _free_pages(struct page * page, unsigned int order) //釋放page結構體指向的連續2的order次方個頁面
void free_pages(unsigned long addr, unsigned int order) //釋放從地址addr開始的,連續2的order次方個頁面
void free_page(unsigned long addr) //釋放地址addr的一個頁
  • kmalloc分配的內存虛擬、物理地址都是連續的,vmalloc分配的內存虛擬地址連續,而物理地址無需連續
  • 當代碼須要一個新的數據結構的實例時,slab分配器就從一個slab列表中分配一個這樣大小的單元出去,而當要釋放時,將其從新保存在該列表中,而不是釋放它。
  • slab分配器爲每種對象分配一個高速緩存,這個緩存能夠看作是同類型對象的一種儲備。每一個高速緩存所佔的內存區又被劃分多個slab,每一個 slab是由一個或多個連續的頁框組成。每一個頁框中包含若干個對象,既有已經分配的對象,也包含空閒的對象。

  • 每一個高速緩存經過kmem_cache結構來描述,這個結構中包含了對當前高速緩存各類屬性信息的描述。全部的高速緩存經過雙鏈表組織在一塊兒,造成 高速緩存鏈表cache_chain。每一個kmem_cache結構中並不包含對具體slab的描述,而是經過kmem_list3結構組織各個 slab。該結構的定義以下:
struct kmem_list3 {
    struct list_head slabs_partial; //包含空閒對象和已經分配對象的slab描述符
    struct list_head slabs_full;//只包含非空閒的slab描述符
    struct list_head slabs_free;//只包含空閒的slab描述符
    unsigned long free_objects;  /*高速緩存中空閒對象的個數*/
    unsigned int free_limit;   //空閒對象的上限
    unsigned int colour_next;   /* Per-node cache coloring *//*即將要着色的下一個*/
    spinlock_t list_lock;
    struct array_cache *shared; /* shared per node */
    struct array_cache **alien; /* on other nodes */
    unsigned long next_reap;    /* updated without locking *//**/
    int free_touched;       /* updated without locking */
};
  • 每一個slab處於三個狀態之一:滿、部分滿、空。非空閒對象的slab鏈表 slabs_full、部分空閒對象的slab鏈表slabs_partial、空閒對象的slab鏈表slabs_free。當內核的某一部分須要一個新的對象時,先從 slabs_partial 分配,沒有部分滿的slab,就從 slabs_free 中分配。若沒有空的 slab,就要建立一個 slab。
  • 參考:slab機制Linux內存管理中的slab分配器
相關文章
相關標籤/搜索