學號023作品
原創作品轉載請注明出處:https://github.com/mengning/linuxkernel/
實驗環境
Parallels Desktop
Ubuntu 16.04
進程創建
進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。
1、描述進程的數據結構
在操作系統中,進程也需要一個數據結構來保存內核對進程狀態等信息,此數據結構我們一般將其稱作進程控制塊(Process Control Block)。PCB在linux內核中定義為task_struct結構體,並在http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235%EF%BC%9B源文件中實現。
由於源代碼較多,在此處只給出部分常用參數的代碼:
volatile long state; //表示進程狀態 void *stack; //進程所屬堆棧指針 unsigned int rt_priority;//進程優先級 int exit_state;//退出時狀態 pid_t pid;//進程號,作為進程的全局標識符 pid_t tgid;//進程組號 struct task_struct __rcu *real_parent;//父進程 struct list_head children;//子進程 struct list_head sibling;//兄弟進程 struct task_struct *group_leader;//所屬進程組的主進程
2、fork函數對應的內核處理過程do_fork
傳統的UNIX中用於復制進程的系統調用是fork。但它並不是Liunx為此實現的唯一的調用,實際上Linux實現了3個:
(1)fork是重量級調用,它建立了父進程的一個完整副本,然后為子進程執行。
(2)vfork類似於fork,但並不創建父進程數據的副本。相反,父子進程之間共享數據。
(3)clone產生線程,可以對父子進程之間的共享、復制進行精確控制。
fork、vfork和close系統調用的入口分別是sys_fork、sys_vfork和sys_clone函數。以上函數從寄存器中取出由用戶定義的信息,並調用與體系結構無關的do_fork函數進行進程的復制。do_fork函數的原型如下:
long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr)
所有3個fork機制最終都調用了http://codelab.shiyanlou.com/xref/linux-3.18.6/kernel/fork.c中的do_fork。
3、使用gdb跟蹤分析一個fork系統調用內核處理函數do_fork
(1)添加源代碼創建rootfs
在此節我們使用qemu和gdb跟蹤分析do_fork的調用過程,qemu及gdb環境的搭建,請參考此處https://www.cnblogs.com/LucasChang/p/10562279.html
我們首先在test.c文件中加入以下代碼用於測試:
int Fork(int argc, char *argv[]) { int child = fork(); if(child < 0){ printf("fail to create a new process\n"); } else { if(child == 0){ printf("successfully create a new process, and I am the parent\n"); } else { printf("successfully create a new process, and I am the child\n"); } } return 0; }
並在main函數中加入以下代碼完成登記:
MenuConfig("fork","Create a new process",Fork);
對menu進行重新編譯,並創建rootfs:
# make rootfs # cd ../rootfs # cp ../menu/init ./ # find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
(2)跟蹤分析
打開兩個終端,在其中一個中輸入以下命令,打開qemu終端:
# qemu-system-i386 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -s -S -append nokaslr
輸入后依舊會彈出一個處於stopped狀態的qemu終端:
然后在另外一個終端中輸入以下命令:
# gdb (gdb) file linux-3.18.6/vmlinux (gdb) target remote:1234 (gdb) b sys_clone (gdb) b do_fork (gdb) b dup_task_struct (gdb) b copy_process (gdb) b copy_thread
以上命令分別在sys_clone、do_fork、dup_task_struct、copy_process和copy_thread函數調用處加上斷點:
根據上述調試方法可以得到如下的結果:
可執行文件的加載
“ELF”的全稱是:Executable and Linking Format. 大意為可執行,可關聯的文件格式,擴展名為elf. 因此把這一類型的文件簡稱為“ELF”。
此節對一個簡單的C程序的編譯鏈接執行過程進行分析。
(1)編譯測試代碼
首先我們編寫一個簡單的test.c源代碼文件:
#include<stdio.h> void main(){ printf("I'm the testing program!"); }
運行以下命令對源文件進行編譯鏈接生成可執行文件:
# gcc -o test test.c
(2)gdb跟蹤內核處理函數do_execve
同樣還是利用gdb和qemu工具來跟蹤分析,首先給do_execve函數打上斷點,進行跟蹤可以得到以下結果:
由跟蹤結果可知,當調用新的可執行程序時,會先進入內核態調用do_execve處理函數,並使用堆棧對原來的現場進行保護。然后,根據返回的可執行文件的地址,對當前可執行文件進行覆蓋。由於返回地址為調用可執行文件的main函數入口,所以可以繼續執行該文件。
進程執行與切換
(1)跟蹤分析schedule函數
我們先對schedule,pick_next_task,context_switch和__switch_to設置斷點,觀察程序運行的情況。
由以上跟蹤結果可以得知,在進行進程間的切換時,各處理函數的調用順序如下:pick_next_task -> context_switch -> __switch_to 。由此可以得出,當進程間切換時,首先需要調用pick_next_task函數挑選出下一個將要被執行的程序;然后再進行進程上下文的切換,此環節涉及到“保護現場”及“現場恢復”;在執行完以上兩個步驟后,調用__switch_to進行進程間的切換。
(2)switch_to中的匯編代碼
匯編代碼及分析如下:
asm volatile("pushfl\n\t" //保存當前進程的標志寄存器PSW內容 "pushl %%ebp\n\t" //保存堆棧基址寄存器內容 "movl %%esp,%[prev_sp]\n\t" // 保存棧頂指針 "movl %[next_sp],%%esp\n\t" // 將下一個進程的棧頂指針mov到esp寄存器中,完成了內核堆棧的切換 "movl $1f,%[prev_ip]\n\t" // 保存當前進程的EIP "pushl %[next_ip]\n\t" //將下一個進程的EIP壓棧 __switch_canary "jmp __switch_to\n" "1:\t" //next進程開始執行 "popl %%ebp\n\t" //恢復堆棧基址 "popfl\n" //恢復PSW /* output parameters 因為處於中斷上下文,在內核中 prev_sp是內核堆棧棧頂 prev_ip是當前進程的eip */ : [prev_sp] "=m" (prev->thread.sp), [prev_ip] "=m" (prev->thread.ip), //[prev_ip]是標號 "=a" (last), "=b" (ebx), "=c" (ecx), "=d" (edx), "=S" (esi), "=D" (edi) __switch_canary_oparam /* input parameters: next_sp下一個進程的內核堆棧的棧頂 next_ip下一個進程執行的起點,一般是$1f,對於新創建的子進程是ret_from_fork*/ : [next_sp] "m" (next->thread.sp), [next_ip] "m" (next->thread.ip), [prev] "a" (prev), [next] "d" (next) __switch_canary_iparam : /* reloaded segment registers */ "memory");