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