作者:刘磊
文中参考代码出处: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