標籤(空格分隔): 20135328陳都node
陳都 原創做品轉載請註明出處 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000linux
目前大多數擁有計算和存儲功能的設備其核心構造均爲馮諾依曼體系結構shell
中斷機制編程
堆棧是C語言程序運行時必須的一個記錄調用路徑和參數的空間數組
提供局部變量空間緩存
計算機三大法寶數據結構
中斷框架
操做系統兩把寶劍tcp
進程上下文的切換編輯器
咱們關注的部分
提供內核的各類編譯方法、生成文件的查看方法。
須要知道的一行代碼: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等
rm menu -rf 強制刪除
從新克隆一個新的Menu;
增長了兩個命令:time和time_asm,說明擴展了功能。
一直按n單步執行會進入schedule函數
Sys_time返回後進入彙編代碼處理gdb沒法繼續跟蹤
執行int 0x80以後執行system_call對應的代碼
讓系統停在system_call的位置進行調試
執行int 0x80以後執行system_call對應的代碼
(1)進程調度的時機要分析一下
一旦進入sys_exit_work:會有一個進程調度時機
(1)系統調用的工做機制一旦在start kernel初始化好以後,在代碼中一旦出現inter 0x80的指令,它就會當即跳轉到system_call這個位置
call *sys_call_table(,%eax,4)調用了系統調度處理函數,eax存的是系統調用號
(2)定義的宏SAVE_ALL和RESTORE_ALL
(3)當一個系統調用發生的時候,它進入內核處理這個系統調用,內核提供了一些服務,在這個服務結束返回到用戶態以前,它可能會發生
進程調度,就會發生進程上下文的切換和中斷上下文的切換。
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:結束
韓玉琪同窗總結得很到位,在此冒昧引用
系統調用就是特殊的一種中斷
- 保存現場 在系統調用時,咱們須要SAVE_ALL,用於保存系統調用時的上下文。 一樣,中斷處理的第一步應該也要保存中斷程序現場。 目的:在中斷處理完以後,能夠返回到原來被中斷的地方,在原有的運行環境下繼續正確的執行下去。
- 肯定中斷信息 在系統調用時,咱們須要將系統調用號經過eax傳入,經過sys_call_table查詢到調用的系統調用,而後跳轉到相應的程序進行處理。
一樣,中斷處理時系統也須要有一箇中斷號,經過檢索中斷向量表,瞭解中斷的類型和設備。- 處理中斷 跳轉到相應的中斷處理程序後,對中斷進行處理。
- 返回 系統調用時最後要restore_all恢復系統調用時的現場,並用iret返回用戶態。 一樣,執行完中斷處理程序,內核也要執行特定指令序列,恢復中斷時現場,並使得進程回到用戶態。
最核心的是進程管理
將信號、進程間通訊、內存管理和文件系統聯繫起來
爲了管理進程,內核必須對每一個進程進行清晰的描述,進程描述符提供了內核所需瞭解的進程信息。
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和進程的內核堆棧
Linux進程的狀態與操做系統原理中的描述的進程狀態有所不一樣,好比就緒狀態和運行狀態都是TASK_RUNNING
通常操做系統原理中描述的進程狀態有就緒態,運行態,阻塞態,可是在實際內核進程管理中是不同的。
struct task_struct數據結構很龐大
道生一(start_kernel....cpu_idle),一輩子二(kernel_init和kthreadd),二生三(即前面0、1和2三個進程),三生萬物(1號進程是全部用戶態進程的祖先,0號進程是全部內核線程的祖先),新內核的核心代碼已經優化的至關乾淨,都符合中國傳統文化精神了
0號進程,是代碼寫死的,1號進程複製0號進程PCB,再修改,再加載可執行程序。
系統調用進程建立過程:
iret與int 0x80指令對應,一個是彈出寄存器值,一個是壓入寄存器的值
若是將系統調用類比於fork();那麼就至關於系統調用建立了一個子進程,而後子進程返回以後將在內核態運行,而返回到父進程後仍然在用戶態運行。
進程的父子關係直觀圖:
do_fork
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以適應子進程的特色,也就是子進程的初始化
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
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; // 若是是建立的內核線程 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
fork、vfork和clone三個系統調用均可以建立一個新進程,並且都是經過調用do_fork來實現進程的建立;
$ err = arch_dup_task_struct(tsk, orig); //在這個函數複製父進程的數據結構
$ ti = alloc_thread_info_node(tsk, node); $ tsk->stack = ti; //複製內核堆棧 $ setup_thread_stack(tsk, orig); //這裏只是複製thread_info,而非複製內核堆棧
$ *childregs = *current_pt_regs(); //複製內核堆棧 $ childregs->ax = 0; //爲何子進程的fork返回0,這裏就是緣由 $ p->thread.sp = (unsigned long) childregs; //調度到子進程時的內核棧頂 $ p->thread.ip = (unsigned long) ret_from_fork; //調度到子進程時的第一條指令地址
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中壓入棧的參數。
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
左半邊是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快學完了,但感受仍是什麼都沒學到,雖然視頻內容很少,但每一個星期進程管理、內存管理、設備驅動、文件系統,從分析內核瞭解到整個系統是如何工做的,如何控制管理資源分配,進程切換並執行。內容複雜繁多,自成一體又相互聯繫,我這種學得不紮實的同窗在這幾周的學習中已經感覺到了其中的壓力。 我還有不少不足,也會在從此的學習中更加努力。