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

1、實驗部分:分析Linux內核建立一個新進程的過程。node

【第一部分】 根據要求完成第一部分,步驟以下:linux

1. 首先進入虛擬機,打開終端,這命令行依次敲入如下命令:

cd LinuxKernel   
rm menu -rf           //強制刪除
git clone https://github.com/mengning/menu.git   //將menu更新
cd menu    
mv test_fork.c test.c     //更新test.c 
make rootfs   //運行腳本,自動編譯和自動生成根文件系統,同時啓動,輸入fork命令,子進程和父進程都輸出

 一切正常的話,這時候咱們簡易的內核系統就啓動起來了,輸入help,就能夠看到新添加的命令fork,輸入fork:

2. gdb上述的fork命令

 關閉QEMU窗口,中命令行中輸入:

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S

 再次啓動MenuOS,並暫停等待gdb調試。而後水平分割命令行窗口,這新窗口中依次輸入如下命令,啓動調試:

gdb
file linux-3.18.6/vmlinux
target remote:1234

 而後再設置斷點:

b sys_clone
b do_fork
b dup_task_struct
b copy_process
b copy_thread
b ret_from_fork

【第二部分】分析代碼git

(1)fork()函數

 fork()在父、子進程各返回一次。在父進程中返回子進程的 pid,在子進程中返回0。fork一個子進程的代碼以下:

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) 
  {
   /* child process */
   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");
   }
}

(2)進程建立流程:

 fork 經過0x80中斷(系統調用)來陷入內核,由系統提供的相應系統調用來完成進程的建立。PCB包含了一個進程的重要運行信息,因此咱們將圍繞在建立一個新進程時,如何來創建一個新的PCB的這一個過程來進行分析,在Linux系統中,PCB主要是存儲在一個叫作task_struct這一個結構體中,建立新進程僅能經過fork,vfork,clone等系統調用的形式來進行。

//fork
#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
#endif

//vfork
#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
        0, NULL, NULL);
}
#endif

//clone
#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 來建立進程,最終都是經過 do_ fork() 方法來實現的。接下來咱們能夠追蹤到 do_ fork()的代碼:

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; //總的pid數量

 /*
  * Determine whether and which event to report to ptracer.  When
  * called from kernel_thread or CLONE_UNTRACED is explicitly
  * requested, no event is reported; otherwise, report if the event
  * for the type of forking is enabled.
 */
if (!(clone_flags & CLONE_UNTRACED)) {
    if (clone_flags & CLONE_VFORK)
        trace = PTRACE_EVENT_VFORK;
    else if ((clone_flags & CSIGNAL) != SIGCHLD)
        trace = PTRACE_EVENT_CLONE;
    else
        trace = PTRACE_EVENT_FORK;

    if (likely(!ptrace_event_enabled(current, trace)))
        trace = 0;
}

// 複製進程描述符,返回建立的task_struct的指針
p = copy_process(clone_flags, stack_start, stack_size,
         child_tidptr, NULL, trace);
/*
 * Do this prior waking up the new thread - the thread pointer
 * might get invalid after that point, if the thread exits quickly.
 */
if (!IS_ERR(p)) {
    struct completion vfork;
    struct pid *pid;

    trace_sched_process_fork(current, p);

    // 取出task結構體內的pid
    pid = get_task_pid(p, PIDTYPE_PID);
    nr = pid_vnr(pid);

    if (clone_flags & CLONE_PARENT_SETTID)
        put_user(nr, parent_tidptr);

    // 若是使用的是vfork,那麼必須採用某種完成機制,確保父進程後運行
    if (clone_flags & CLONE_VFORK) {
        p->vfork_done = &vfork;
        init_completion(&vfork);
        get_task_struct(p);
    }

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

    /* forking complete and child started to run, tell ptracer */
    if (unlikely(trace))
        ptrace_event_pid(trace, pid);

    // 若是設置了 CLONE_VFORK 則將父進程插入等待隊列,並掛起父進程直到子進程釋放本身的內存空間
    // 保證子進程優先於父進程運行
    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;
}

 能夠看出過程是經過copy_ process來複制進程描述符,返回新建立的子進程的task_ struct的指針(即PCB指針),將新建立的子進程放入調度器的隊列中,讓其有機會得到CPU,而且要確保子進程要先於父進程運行。copy_process函數以下:

/*
建立進程描述符以及子進程所須要的其餘全部數據結構
爲子進程準備運行環境
*/
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與當前進程的task,僅僅是stack地址不一樣
p = dup_task_struct(current);
if (!p)
    goto fork_out;

···

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;
}
current->flags &= ~PF_NPROC_EXCEEDED;

retval = copy_creds(p, clone_flags);
if (retval < 0)
    goto bad_fork_free;

/*
 * If multiple threads are within copy_process(), then this check
 * triggers too late. This doesn't hurt, the check is only there
 * to stop root fork bombs.
 */
retval = -EAGAIN;
// 檢查進程數量是否超過 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;

delayacct_tsk_init(p);  /* Must remain after dup_task_struct() */
p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
// 代表子進程尚未調用exec系統調用
p->flags |= PF_FORKNOEXEC;
INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);
rcu_copy_process(p);
p->vfork_done = NULL;

// 初始化自旋鎖
spin_lock_init(&p->alloc_lock);

// 初始化掛起信號
init_sigpending(&p->pending);

// 初始化定時器
p->utime = p->stime = p->gtime = 0;
p->utimescaled = p->stimescaled = 0;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
p->prev_cputime.utime = p->prev_cputime.stime = 0;
#endif
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
seqlock_init(&p->vtime_seqlock);
p->vtime_snap = 0;
p->vtime_snap_whence = VTIME_SLEEPING;
#endif

...

#ifdef CONFIG_DEBUG_MUTEXES
p->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_BCACHE
p->sequential_io    = 0;
p->sequential_io_avg    = 0;
#endif

/* Perform scheduler related setup. Assign this task to a CPU. */

// 完成對新進程調度程序數據結構的初始化,並把新進程的狀態設置爲TASK_RUNNING
// 同時將thread_info中得preempt_count置爲1,禁止內核搶佔
retval = sched_fork(clone_flags, p);
if (retval)
    goto bad_fork_cleanup_policy;

retval = perf_event_init_task(p);
if (retval)
    goto bad_fork_cleanup_policy;
retval = audit_alloc(p);
if (retval)
    goto bad_fork_cleanup_perf;
/* copy all the process information */

// 複製全部的進程信息
shm_init_task(p);
retval = copy_semundo(clone_flags, p);
if (retval)
    goto bad_fork_cleanup_audit;
retval = copy_files(clone_flags, p);
if (retval)
    goto bad_fork_cleanup_semundo;
    
...

// 初始化子進程的內核棧
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;
}

...

// 清除子進程thread_info結構的 TIF_SYSCALL_TRACE,防止 ret_from_fork將系統調用消息通知給調試進程
clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMU
clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endif
clear_all_latency_tracing(p);

/* 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 (likely(p->pid)) {
    ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);

    init_task_pid(p, PIDTYPE_PID, pid);
    if (thread_group_leader(p)) {

        ...
        
        // 將pid加入散列表
        attach_pid(p, PIDTYPE_PGID);
        attach_pid(p, PIDTYPE_SID);
        __this_cpu_inc(process_counts);
    } else {

        ...

    }
    // 將pid加入PIDTYPE_PID這個散列表
    attach_pid(p, PIDTYPE_PID);
    // 遞增 nr_threads的值
    nr_threads++;
}

total_forks++;
spin_unlock(&current->sighand->siglock);
syscall_tracepoint_update(p);
write_unlock_irq(&tasklist_lock);

...

// 返回被建立的task結構體指針
return p;

...

 能夠看出該函數主要完成如下工做:用 dup_ task_ struct 複製當前的 task_ struct,而且檢查進程數是否超過限制,初始化自旋鎖、掛起信號、CPU 定時器等,調用 sched_ fork 初始化進程數據結構,並把進程狀態設置爲 TASK_ RUNNING,複製全部進程信息:包括文件系統、信號處理函數、信號、內存管理等,最後調用 copy_ thread 初始化子進程內核棧,爲新進程分配並設置新的pid。下面是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;

    //分配一個 task_struct 節點
    tsk = alloc_task_struct_node(node);
    if (!tsk)
    return NULL;

    //分配一個 thread_info 節點,包含進程的內核棧,ti 爲棧底
    ti = alloc_thread_info_node(tsk, node);
    if (!ti)
    goto free_tsk;

   //將棧底的值賦給新節點的棧
    tsk->stack = ti;
   //……
   return tsk;

}

 能夠看出在該函數中,調用了alloc_ task_ struct_ node分配一個 task_ struct 節點,調用alloc_ thread_ info_ node分配一個 thread_ info 節點,實際上是分配了一個thread_ union聯合體,將棧底返回給 ti。最後將棧底的值 ti 賦值給新節點的棧,最終執行完dup_ task_ struct以後,子進程除了tsk->stack指針不一樣以外,所有都同樣!

總結

 能夠經過fork、vfork和clone來建立一個新進程,而他們又都是經過調用do_ fork方法來實現的。do_ fork函數主要是調用copy_ process函數來爲子進程複製父進程信息的。copy_ process函數調用 dup_task_struct爲子進程分配新的堆棧;調用sched_ fork 初始化進程數據結構,並把進程狀態設置爲TASK_ RUNNING。copy_ process函數尤其重要,咱們能夠看到爲何fork()函數返回值爲0,而且fork出的子進程是從哪裏開始執行的:將子進程的ip設置爲ret_ from_ fork的首地址,子進程從ret_ from_ fork開始執行。

2、讀書筆記github

【第一部分】 定時器和時間管理緩存

一 內核中的時間觀念

 內核在硬件的幫助下計算和管理時間。硬件爲內核提供一個系統定時器用以計算流逝的時間。系統定時器以某種頻率自行觸發,產生時鐘中斷,進入內核時鐘中斷處理程序中進行處理。牆上時間和系統運行時間根據時鐘間隔來計算。利用時間中斷週期執行的工做:

 1. 更新系統運行時間;

 2. 更新實際時間;

 3. 在smp系統上,均衡調度程序中各處理器上運行隊列;

 4. 檢查當前進程是否用盡了時間片,從新進行調度;

 5. 運行超時的動態定時器;

 6. 更新資源消耗和處理器時間的統計值;

二 節拍率

 (1)系統定時器的頻率;經過靜態預處理定義的——HZ;系統啓動按照HZ值對硬件進行設置。體系結構不一樣,HZ值也不一樣;HZ可變的。

` #define HZ 1000 //內核時間頻率

 (2)提升節拍率中斷產生更加頻繁帶來的好處:

  1. 提升時間驅動事件的解析度;

  2. 提升時間驅動事件的準確度;

  3. 內核定時器以更高的頻度和準確度;

  4. 依賴頂上執行的系統調用poll()和select()能更高的精度運行;

  5. 系統時間測量更精細;

  6. 提升進程搶佔的準確度;

 (3)提升節拍率帶來的反作用:

  1.中斷頻率增高系統負擔增長;

  2.中斷處理程序佔用處理器時間增多;

  3.頻繁打斷處理器高速緩存;

3、jiffies

 jiffies:全局變量,用來記錄自系統啓動以來產生的節拍總數。啓動時內核將該變量初始化爲0;此後每次時鐘中斷處理程序增長該變量的值。每一秒鐘中斷次數HZ,jiffies一秒內增長HZ。系統運行時間 = jiffie/HZ.jiffies用途:計算流逝時間和時間管理

4、硬時鐘和定時器

 兩種設備進行計時:系統定時器和實時時鐘。

 (1)實時時鐘(RTC):用來持久存放系統時間的設備,即使系統關閉後,靠主板上的微型電池提供電力保持系統的計時。系統啓動內核經過讀取RTC來初始化牆上時間,改時間存放在xtime變量中。

 (2)系統定時器:內核定時機制,註冊中斷處理程序,週期性觸發中斷,響應中斷處理程序,進行處理執行如下工做:

  1.得到xtime_lock鎖,訪問jiffies和更新牆上時間xtime;

  2.更新實時時鐘;

  3.更新資源統計值:當前進程耗時,系統時間等;

  4.執行已到期的動態定時器;

  5.執行scheduler_tick()

5、定時器

 定時器:管理內核時間的基礎,推後或執行時間執行某些代碼。

6、延遲執行

 使用定時器和下半部機制推遲執行任務。還有其餘延遲執行的機制:

  (1)忙等待:利用節拍,精確率不高

unsigned long delay = jiffies + 2*HZ ; //2秒 節拍整數倍才行;
   while(time_before(jiffies,delay)) ;

  (2)短延遲:延遲時間精確到毫秒,微妙;短暫等待某個動做完成時,比時鐘節拍更短;依靠數次循環達到延遲效果。

void udelay(unsigned long usecs)
   void mdelay(unsigned long msecs)

【第二部分】內存管理:虛擬內存機制數據結構

 在早期的計算機中,是沒有虛擬內存的概念的。咱們要運行一個程序,會把程序所有裝入內存,而後運行。當運行多個程序時,常常會出現如下問題:

 1)進程地址空間不隔離,沒有權限保護。因爲程序都是直接訪問物理內存,因此一個進程能夠修改其餘進程的內存數據,甚至修改內核地址空間中的數據。

 2)內存使用效率低。當內存空間不足時,要將其餘程序暫時拷貝到硬盤,而後將新的程序裝入內存運行。因爲大量的數據裝入裝出,內存使用效率會十分低下。

 3)程序運行的地址不肯定。由於內存地址是隨機分配的,因此程序運行的地址也是不肯定的。

 可看出內存管理無非就是想辦法解決三個問題:如何使進程的地址空間隔離,如何提升內存的使用效率,如何解決程序運行時的重定位問題?如今的內存管理方法就是在程序和物理內存之間引入了虛擬內存這個概念,來解決上述問題。

 Linux內核把虛擬地址空間分爲兩部分:用戶進程空間,內核進程空間。以下圖所示:

            

 記得老師上課舉過一個形象的例子:實際物理內存就像是咱們教室的70個座位,咱們進教室能夠選擇其中的任何一個座位坐下,那麼就至關於咱們每一個人都擁有70個座位這麼大的內存,這麼看來咱們就有n(人數)*70的內存,但事實上每一個人擁有的內存空間只是虛擬內存空間。

 每次訪問內存空間的某個地址,都須要把地址翻譯爲實際物理內存地址,全部進程共享同一物理內存,每一個進程只把本身目前須要的虛擬內存空間映射並存儲到物理內存上,進程要知道哪些內存地址上的數據在物理內存上,哪些不在,還有在物理內存上的哪裏,須要用頁表來記錄。頁表的每個表項分兩部分,第一部分記錄此頁是否在物理內存上,第二部分記錄物理內存頁的地址(若是在的話)當進程訪問某個虛擬地址,去看頁表,若是發現對應的數據不在物理內存中,則缺頁異常。缺頁異常的處理過程,就是把進程須要的數據從磁盤上拷貝到物理內存中,若是內存已經滿了,沒有空地方了,那就找一個頁覆蓋,固然若是被覆蓋的頁曾經被修改過,須要將此頁寫回磁盤。

具體機制能夠參考http://www.linuxidc.com/Linux/2015-02/113981.htm

相關文章
相關標籤/搜索