Linux进程创建可执行文件的加载和进程执行进程切换

Posted liulei-ustc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux进程创建可执行文件的加载和进程执行进程切换相关的知识,希望对你有一定的参考价值。

 

作者:刘磊

文中参考代码出处:https://github.com/mengning/linuxkernel/ 

本文主要针对进程创建、可执行文件的加载和进程间切换三大部分进行实验并分析。

实验环境:Ubuntu 16虚拟机、VMware 14

1 进程创建

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

1.1 描述进程的数据结构

在操作系统中,进程也需要一个数据结构来保存内核对进程状态等信息,此数据结构我们一般将其称作进程控制块(Process Control Block)。PCB在linux内核中定义为task_struct结构体,并在/include/linux/sched.h源文件中实现。

由于源代码较多,在此处只给出部分常用参数的代码及其注释:

 1 volatile long state; //表示进程状态
 2 void *stack; //进程所属堆栈指针
 3 unsigned int rt_priority;//进程优先级
 4 int exit_state;//退出时状态
 5 pid_t pid;//进程号,作为进程的全局标识符
 6 pid_t tgid;//进程组号
 7 struct task_struct __rcu *real_parent;//父进程
 8 struct list_head children;//子进程
 9 struct list_head sibling;//兄弟进程
10 struct task_struct *group_leader;//所属进程组的主进程

1.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函数的原型如下:

1 long do_fork(unsigned long clone_flags,
2                     unsigned long stack_start,
3                     struct pt_regs *regs,
4                     unsigned long stack_size,
5                     int __user *parent_tidptr,
6                     int __user *child_tidptr)

所有3个fork机制最终都调用了kernel/fork.c中的do_fork,其代码流程图如下(图片出处为《深入Linux内核架构》人民邮电出版社):

 技术图片

 

在do_fork中大多数工作是由copy_process函数完成的,其代码流程如下图所示(图片出处为《深入Linux内核架构》人民邮电出版社):

技术图片

1.3 使用gdb跟踪分析一个fork系统调用内核处理函数do_fork

1.3.1 添加源代码创建rootfs

在此节我们使用qemu和gdb跟踪分析do_fork的调用过程,qemu及gdb环境的搭建,请参考本此处

我们首先在test.c文件中加入以下代码用于测试:

 1 int Fork(int argc, char *argv[])
 2 {
 3     int child = fork();
 4     
 5     if(child < 0){
 6         printf("fail to create a new process
");
 7     } else {
 8         if(child == 0){
 9             printf("successfully create a new process, and I am the parent
");
10         } else {
11             printf("successfully create a new process, and I am the child
");
12         }
13     }
14     return 0;
15 }

并在main函数中加入以下代码完成登记:

1 MenuConfig("fork","Create a new process",Fork);

对menu进行重新编译,并创建rootfs:

1  # make rootfs
2  # cd ../rootfs
3  # cp ../menu/init ./
4  # find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img

1.3.2 跟踪分析

打开两个终端,在其中一个中输入以下命令,打开qemu终端:

1 # qemu-system-i386 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -s -S -append nokaslr

输入后依旧会弹出一个处于stopped状态的qemu终端:

技术图片

然后在另外一个终端中输入以下命令:

1  # gdb
2  (gdb)  file linux-3.18.6/vmlinux
3  (gdb)  target remote:1234
4  (gdb)  b sys_clone 
5  (gdb)  b do_fork
6  (gdb)  b dup_task_struct
7  (gdb)  b copy_process
8  (gdb)  b copy_thread

以上命令分别在sys_clone、do_fork、dup_task_struct、copy_process和copy_thread函数调用处加上断点:

技术图片

根据上述调试方法可以得到如下的结果:

技术图片

技术图片

2 可执行文件的加载

“ELF”的全称是:Executable and Linking Format. 大意为可执行,可关联的文件格式,扩展名为elf .因此把这一类型的文件简称为“ELF”。

此节对一个简单的c程序的编译链接执行过程进行分析。

2.1 编译测试代码

首先我们编写一个简单的test.c源代码文件:

1 #include<stdio.h>
2 
3 void main(){
4         printf("I‘m the testing program!");
5 }

运行以下命令对源文件进行编译链接生成可执行文件:

1 # gcc -o test test.c

2.2 gdb跟踪内核处理函数do_execve

同样还是利用gdb和qemu工具来跟踪分析,首先给do_execve函数打上断点,进行跟踪可以得到以下结果:

技术图片

由跟踪结果可知,当调用新的可执行程序时,会先进入内核态调用do_execve处理函数,并使用堆栈对原来的现场进行保护。然后,根据返回的可执行文件的地址,对当前可执行文件进行覆盖。由于返回地址为调用可执行文件的main函数入口,所以可以继续执行该文件。

3 进程执行与切换

3.1 跟踪分析schedule函数

我们先对schedule,pick_next_task,context_switch和__switch_to设置断点,观察程序运行的情况。

技术图片

由以上跟踪结果可以得知,在进行进程间的切换时,各处理函数的调用顺序如下:pick_next_task -> context_switch -> __switch_to 。由此可以得出,当进程间切换时,首先需要调用pick_next_task函数挑选出下一个将要被执行的程序;然后再进行进程上下文的切换,此环节涉及到“保护现场”及“现场恢复”;在执行完以上两个步骤后,调用__switch_to进行进程间的切换。

3.2 switch_to中的汇编代码

汇编代码及分析如下:

 1 asm volatile("pushfl
	"     //保存当前进程的标志寄存器PSW内容   
 2          "pushl %%ebp
	"    //保存堆栈基址寄存器内容
 3          "movl %%esp,%[prev_sp]
	"  // 保存栈顶指针
 4          "movl %[next_sp],%%esp
	"  // 将下一个进程的栈顶指针mov到esp寄存器中,完成了内核堆栈的切换
 5     
 6 
 7          "movl $1f,%[prev_ip]
	"    // 保存当前进程的EIP    
 8          "pushl %[next_ip]
	"   //将下一个进程的EIP压栈
 9          __switch_canary                   
10          "jmp __switch_to
" 
11 
12 
13          "1:	"              //next进程开始执行        
14          "popl %%ebp
	"     //恢复堆栈基址   
15          "popfl
"         //恢复PSW
16                                     
17          /* output parameters 因为处于中断上下文,在内核中
18          prev_sp是内核堆栈栈顶
19          prev_ip是当前进程的eip */                
20          : [prev_sp] "=m" (prev->thread.sp),     
21          [prev_ip] "=m" (prev->thread.ip),  //[prev_ip]是标号        
22          "=a" (last),                 
23                                     
24         
25          "=b" (ebx), "=c" (ecx), "=d" (edx),      
26          "=S" (esi), "=D" (edi)             
27                                        
28          __switch_canary_oparam                
29                                     
30          /* input parameters: 
31          next_sp下一个进程的内核堆栈的栈顶
32          next_ip下一个进程执行的起点,一般是$1f,对于新创建的子进程是ret_from_fork*/                
33          : [next_sp]  "m" (next->thread.sp),        
34          [next_ip]  "m" (next->thread.ip),       
35                                         
36          [prev]     "a" (prev),              
37          [next]     "d" (next)               
38                                     
39          __switch_canary_iparam                
40                                     
41          : /* reloaded segment registers */           
42          "memory");  

 

 

 


 

以上是关于Linux进程创建可执行文件的加载和进程执行进程切换的主要内容,如果未能解决你的问题,请参考以下文章

从整理上理解进程创建可执行文件的加载和进程执行进程切换,重点理解分析forkexecve和进程切换

Linux 可执行文件与进程内存结构, Linux 进程内存加载

[linux] 进程五状态模型

Linux进程启动过程分析do_execve(可执行程序的加载和运行)---Linux进程的管理与调度

Linux进程启动过程分析do_execve(可执行程序的加载和运行)

ubuntu12.04下创建了一个守护进程,生成了一个可执行文件,如何让这个可执行文件开机自动运行?