Linux內核分析期中總結

linux內核分析期中總結

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


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

1.1馮諾依曼體系結構:即具備存儲程序的計算機體系結構

目前大多數擁有計算和存儲功能的設備其核心構造均爲馮諾依曼體系結構shell

1.函數調用堆棧

1.1小結

三把寶劍:

  • 存儲程序計算機
  • 函數調用堆棧
  • 中斷機制編程

    1.2堆棧

    堆棧是C語言程序運行時必須的一個記錄調用路徑和參數的空間數組

  • 函數條用框架
  • 傳遞參數
  • 保存返回地址
  • 提供局部變量空間緩存

C代碼中嵌入彙編代碼的寫法

0. 內嵌彙編語法

asm(彙編語句模板: 輸出部分: 輸入部分: 破壞描述部分)

  • 各部分使用「:」格開

1. 彙編語句模板

2. 輸出部分

3. 輸入部分

4. 破壞描述部分

5. 限制字符

## 構造一個簡單的Linux系統MenuOS

上週回顧:

  • 計算機三大法寶數據結構

  • 存儲程序計算機
  • 函數調用堆棧
  • 中斷框架

  • 操做系統兩把寶劍tcp

  • 中斷上下文的切換
  • 進程上下文的切換編輯器


Linux內核源碼簡介

咱們關注的部分

  • arch/x86目錄下的代碼
  • init/main.c中start_kernel函數就至關於普通C程序的main函數
  • kernel目錄:存放linux內核最核心的代碼,用於實現系統的核心模塊,包括進程管理、進程調度器、中斷處理、系統時鐘管理、同步機制等

README

提供內核的各類編譯方法、生成文件的查看方法。

  • installing 如何安裝內核源代碼
  • make mrproper 清理安裝時生成的中間代碼

啓動Linux內核的三個參數:

  • kernel
  • initrd
  • root所在分區、目錄

須要知道的一行代碼:qemu -kernel (文件名) -initrd (rootfs.img)

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


使用gdb跟蹤調試內核

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s
-S # 關於-s和-S選項的說明:
-S freeze CPU at startup (use ’c’ to start execution)
-s shorthand for -gdb tcp::1234 若不想使用1234端口,則可使用-gdb
tcp:xxxx來取代-s選項

另開一個shell窗口

gdb (gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote以前加載符號表
(gdb)target remote:1234 # 創建gdb和gdbserver之間的鏈接,按c 讓qemu上的Linux繼續運行
(gdb)break start_kernel # 斷點的設置能夠在target remote以前,也能夠在以後

經過查詢瞭解和使用man查看open的說明我瞭解到
open系統調用的服務例程是sys_open()函數,它接受三個參數:要打開文件的路徑名filename, 訪問模式的表示flags和文件權限掩碼mode。在內核中,sys_open實際調用do_sys_open函數來完成全部操做。

do_sys_open主要執行以下操做:

1,經過getname()從進程地址空間獲取該文件的路徑名

2,調用get_unused_fd_flags(flags)函數從current->files結構中分配一個空閒的fd。

3,調用do_filep_open(dfd, pathname, flags, mode, 0)找到文件對象的指針struct file* f。

4,調用fd_install(fd, f),將f與fd關聯起來。實際是將f保存在current->files->fdtab->fd數組的第fd位置處。

do_filep_open函數的主要功能,就是經過路徑名來分配並填充這個文件對應的文件對象。

do_filep_open(dfd, pathname, flags, mode, acc_mode)函數主要執行以下操做:

1,設置一堆訪問模式標誌

2,調用get_empty_filep()函數從名爲filp_cachep的slab緩存中分配一個struct file*的文件對象。

3,若是flags中有O_CREATE標誌,跳到,不然到4

4,調用do_path_lookup(dfd, pathname, flags, &nd)作目錄查找,將查找結果填充到struct nameidata *nd中。還記得目錄查找麼?見這裏

5,調用finish_open(nd, flags, mode)作一些合法性驗驗證並從nd->intent.open.file中獲取到struct file* filep

6,調用release_open_intent(nd)作一些清理工做。主要是減小nd->intent.open.file中的一些引用計數。

7,返回filep

8,到這一步說明flags中有O_CREATE標誌,須要在目錄查找過程當中逐級建立對應的目錄和文件,這一步依次調用path_init_rcu(), path_walk_rcu()和path_finish_rcu()完成建立文件的目錄查找工做,最終依然是將查找結果填充到struct nameidata *nd中。(在標準的目錄查找do_path_lookup()的實現中,主幹流程也是依次調用着三個函數作查找工做)

9,調用do_last(&nd, &path, flags, acc_mode, mode, pathname)函數獲取最終的struct file filep結構。在這個函數中,內核會根據nd->last_type作不一樣的處理,對於普通文件,會調用finish_open(nd, flags, mode)作一些合法性驗驗證並從nd->intent.open.file中獲取到struct file filep

10,調用release_open_intent(nd)作一些清理工做。主要是減小nd->intent.open.file中的一些引用計數。

11,返回filep

內核源代碼中涉及到sys_open實現的文件主要有fs/open.c fs/namei.c fs/compat.c fs/file.c fs/file_table.c等

1、給MenuOS增長time和time-asm命令

  • 把menu刪除;

rm menu -rf 強制刪除

  • 從新克隆一個新的Menu;

  • 進入Menu,用makerootfs自動編譯生成根文件系統,同時還自動啓動MenuOS
  • 增長了兩個命令:time和time_asm,說明擴展了功能。

2、使用gdb跟蹤系統調用內核函數sys_time

  • 一直按n單步執行會進入schedule函數

  • Sys_time返回後進入彙編代碼處理gdb沒法繼續跟蹤

  • 執行int 0x80以後執行system_call對應的代碼

  • 讓系統停在system_call的位置進行調試

  • 執行int 0x80以後執行system_call對應的代碼

3、系統調用在內核代碼中的處理過程

1.系統調用在內核代碼中的工做機制和初始化

(1)進程調度的時機要分析一下

  • sys_call_table是系統調用分派表
  • syscall_after_all,須要先保存返回值
  • sys_exit_work
  • 沒有這個就restore_all,返回用戶態。
  • 一旦進入sys_exit_work:會有一個進程調度時機

    2.簡化後便於理解的system_call僞代碼:

(1)系統調用的工做機制一旦在start kernel初始化好以後,在代碼中一旦出現inter 0x80的指令,它就會當即跳轉到system_call這個位置
call *sys_call_table(,%eax,4)調用了系統調度處理函數,eax存的是系統調用號

(2)定義的宏SAVE_ALL和RESTORE_ALL

(3)當一個系統調用發生的時候,它進入內核處理這個系統調用,內核提供了一些服務,在這個服務結束返回到用戶態以前,它可能會發生

進程調度,就會發生進程上下文的切換和中斷上下文的切換。

3.system_call到iret之間的主要代碼分析:

SAVE_ALL:保存現場;

syscall_call:調用了系統調用處理函數;

restore all:恢復現場(由於系統調用處理函數也算是一種特殊的「中斷」);syscallexitwork:同上一條i;

INTERRUPT RETURN:也就是iret,系統調用到此結束;

\arch\x86\kernel\traps.c中有一個函數,將SYSCALL_WECTOR(系統調用中斷向量)和system_call彙編代碼的入口綁定。完成初始化

簡化彙編僞代碼

Save_all保存現場

Sys_call_table:綁定系統調用函數

Interrupt_return:結束

韓玉琪同窗總結得很到位,在此冒昧引用

系統調用就是特殊的一種中斷

  1. 保存現場 在系統調用時,咱們須要SAVE_ALL,用於保存系統調用時的上下文。 一樣,中斷處理的第一步應該也要保存中斷程序現場。 目的:在中斷處理完以後,能夠返回到原來被中斷的地方,在原有的運行環境下繼續正確的執行下去。
  2. 肯定中斷信息 在系統調用時,咱們須要將系統調用號經過eax傳入,經過sys_call_table查詢到調用的系統調用,而後跳轉到相應的程序進行處理。
    一樣,中斷處理時系統也須要有一箇中斷號,經過檢索中斷向量表,瞭解中斷的類型和設備。
  3. 處理中斷 跳轉到相應的中斷處理程序後,對中斷進行處理。
  4. 返回 系統調用時最後要restore_all恢復系統調用時的現場,並用iret返回用戶態。 一樣,執行完中斷處理程序,內核也要執行特定指令序列,恢復中斷時現場,並使得進程回到用戶態。

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,再修改,再加載可執行程序。

系統調用進程建立過程:

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; //調度到子進程時的第一條指令地址

  • 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中壓入棧的參數。

可執行程序的裝載

1.可執行程序時如何產生的

  • 編譯器預處理
gcc -E -o XX.cpp XX.c (-m32)//.cpp是預處理文件
  • 彙編器編譯成彙編代碼
gcc -x cpp-output -S -o hello.s hello.cpp (-m32)//.s是彙編代碼
  • 彙編代碼編譯成二進制目標文件(不可讀,含有部分機器代碼但不可執行)
gcc -x assembler -c hello.s -o hello.o (-m32)
  • 連接成可執行文件
gcc -o hello.static hello.c (-m32) -static

2.目標文件格式ELF

目標文件三種形式

  1. 可重定位文件(用來和其餘object文件一塊兒建立下面兩種文件)——.o文件
  2. 可執行文件(指出了應該從哪裏開始執行)
  3. 共享文件(主要是.so文件,用來被連接編輯器和動態連接器鏈)

ELF格式:


左半邊是ELF格式,右半邊是執行時的格式
其中,ELF頭描述了該文件的組織狀況,程序投標告訴系統如何建立一個進程的內存映像,section頭表包含了描述文件sections的信息。
當系統要執行一個文件的時候,理論上講,他會把程序段拷貝到虛擬內存中某個段

裝載可執行程序以前的工做

可執行程序的執行環境

通常咱們執行一個程序的Shell環境,咱們的實驗直接使用execve系統調用。
Shell自己不限制命令行參數的個數,命令行參數的個數受限於命令自身
例如,int main(int argc, char *argv[])
又如, int main(int argc, char argv[], char envp[])//envp是shell的執行環境
Shell會調用execve將命令行參數和環境參數傳遞給可執行程序的main函數

int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

裝載時動態連接和運行時動態連接應用

動態連接分爲可執行程序裝載時動態連接和運行時動態連接(通常使用前者)

半期總結:

mooc快學完了,但感受仍是什麼都沒學到,雖然視頻內容很少,但每一個星期進程管理、內存管理、設備驅動、文件系統,從分析內核瞭解到整個系統是如何工做的,如何控制管理資源分配,進程切換並執行。內容複雜繁多,自成一體又相互聯繫,我這種學得不紮實的同窗在這幾周的學習中已經感覺到了其中的壓力。 我還有不少不足,也會在從此的學習中更加努力。

相關文章
相關標籤/搜索