Linux進程創建、可執行文件的加載和進程執行進程切換


學號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");

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM