学号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");