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\n");
 7     } else {
 8         if(child == 0){
 9             printf("successfully create a new process, and I am the parent\n");
10         } else {
11             printf("successfully create a new process, and I am the child\n");
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\n\t"     //保存当前进程的标志寄存器PSW内容
 2          "pushl %%ebp\n\t"    //保存堆栈基址寄存器内容
 3          "movl %%esp,%[prev_sp]\n\t"  // 保存栈顶指针
 4          "movl %[next_sp],%%esp\n\t"  // 将下一个进程的栈顶指针mov到esp寄存器中,完成了内核堆栈的切换
 5
 6
 7          "movl $1f,%[prev_ip]\n\t"    // 保存当前进程的EIP
 8          "pushl %[next_ip]\n\t"   //将下一个进程的EIP压栈
 9          __switch_canary
10          "jmp __switch_to\n"
11
12
13          "1:\t"              //next进程开始执行
14          "popl %%ebp\n\t"     //恢复堆栈基址
15          "popfl\n"         //恢复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");  


原文地址:https://www.cnblogs.com/liulei-ustc/p/10588529.html

时间: 2024-11-08 12:27:07

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

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

一.首先我们来看看进程控制块PCB也就是task_struct,(源码) 选出task_struct中几个关键的参数进行分析 struct task_struct {volatile long state; //进程状态 /* -1 unrunnable, 0 runnable, >0 stopped */ void *stack; //进程内核堆栈 atomic_t usage; unsigned int flags; //进程标识符 /* per process flags, defined

Linux内核启动及文件系统加载过程

上接博文<u-boot之u-boot-2009.11启动过程分析> 当u-boot开始执行bootcmd命令,就进入Linux内核启动阶段,与u-boot类似,普通Linux内核的启动过程也可以分为两个阶段,但针对压缩了的内核如uImage就要包括内核自解压过程了.本文以项目中使用的linux-2.6.37版源码为例分三个阶段来描述内核启动全过程.第一阶段为内核自解压过程,第二阶段主要工作是设置ARM处理器工作模式.使能MMU.设置一级页表等,而第三阶段则主要为C代码,包括内核初始化的全部工作

Linux共享库两种加载方式简述

动态库技术通常能减少程序的大小,节省空间,提高效率,具有很高的灵活性,对于升级软件版本也更加容易.与静态库不同,动态库里面的函数不是执行程序本身 的一部分,而是在程序执行时按需载入,其执行代码可以同时在多个程序中共享.由于在编译过程中无法知道动态库函数的地址,所以需要在运行期间查找,这对程 序的性能会有影响. 共享库 对于共享库来讲,它只包括2个段:只读的代码段 和可修改的数据段.堆和栈段,只有进程才有.如果你在共享库的函数里,分配了一块内存,这段内存将被算在调用该函数的进程的堆中.代码段由于其

Linux驱动的两种加载方式过程分析

一.概念简述 在Linux下可以通过两种方式加载驱动程序:静态加载和动态加载. 静态加载就是把驱动程序直接编译进内核,系统启动后可以直接调用.静态加载的缺点是调试起来比较麻烦,每次修改一个地方都要重新编译和下载内核,效率较低.若采用静态加载的驱动较多,会导致内核容量很大,浪费存储空间. 动态加载利用了Linux的module特性,可以在系统启动后用insmod命令添加模块(.ko),在不需要的时候用rmmod命令卸载模块,采用这种动态加载的方式便于驱动程序的调试,同时可以针对产品的功能需求,进行

linux内核设计的艺术--加载内核代码

在BIOS触发0x19中断将磁盘的第一个扇区(512B)加载到内存中后,计算机才真正开始执行磁盘上的程序.而这512B的程序bootsect.s中的第一批代码,此时处理器还处于实模式内存寻址的最大范围是1M(0x0000-0xFFFF),接下来我们看看在bootsect.s的第一批代码中做了些什么? SYSSIZE = 0x3000 //内核程序的大小 SETUPLEN = 4 //要加载的setup程序长度单位为扇区数 BOOTSEG = 0x07c0 //启动扇区被BIOS加载的位置,也就是

linux和windows动态库加载路径区别

# linux和windows动态库加载路径区别 ### 简介------------------------------ linux加载动态库的路径是系统目录/lib和/usr/lib.- windows加载动态库的路径是本地目录下,然后再搜索windows/system和windows/system32目录 ### 备注------------------------------ linux加载动态库的路径方式,对于习惯windows开发的开发者是不太方便的.- 其实linux下可以设置从当

js的并行加载与顺序执行

javaScript文件(下面简称脚本文件)需要被HTML文件引用才能在浏览器中运行.在HTML文件中可以通过不同的方式来引用脚本文件,我们需要关注的是,这些方式的具体实现和这些方式可能会带来的性能问题. 当浏览器遇到(内嵌)<script>标签时,当前浏览器无从获知javaScript是否会修改页面内容.因此,这时浏览器会停止处理页面,先执行javaScript代码,然后再继续解析和渲染页面.同样的情况也发生在使用 src 属性加在javaScript的过程中(即外链 javaScript)

并行加载与顺序执行

Javascript文件(下面简称脚本文件)需要被HTML文件引用才能在浏览器中运行.在HTML文件中可以通过不同的方式来引用脚本文件,我们需要关注的是,引用的具体实现方式和这些方式可能会带来的性能问题. 首先,引用脚本必须用到<script>标签,所以需要了解<script>标签的特性,引述书中作者原话: 当浏览器遇到(即内嵌)<script>标签时,当前浏览器无从获知Javascript是否会修改页面内容.因此,这时浏览器会停止处理页面,先执行Javascript代

js的并行加载以及顺序执行

重新温习了下这段内容,发现各个浏览器的兼容性真的是搞大了头,处理起来很是麻烦. 现在现总结下并行加载多个js的方法: 1,对于动态createElement('script')的方式,对所有浏览器都是异步并行加载的.这里所说的并行不仅仅指的是 js并行加载,也包括js和其他资源比如图片,iframe的加载.但是此种方式在Firefox的2.0 3.0 3.1版本和opera 9.63 下是可以顺序执行的.但是由于Kyle的提议,现代浏览器都可以通过对动态创建的script元素设置属性async=