Linux内核分析---进程的创建,执行与切换

学号:210

“原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/ ”

一.实验要求

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

二.实验内容

1.理解task_struct数据结构

为了管理进程,内核必须对每个进程进行清晰的描述。每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。进程描述符提供了内核所需了解的进程信息:基本信息,管理信息,控制信息等。

task_struct是Linux内核的一种数据结构,每个进程都把它的信息放在 task_struct 这个数据结构体,task_struct 包含了这些内容:

(1)标示符 : 描述本进程的唯一标识符,用来区别其他进程。 

(2)状态 :任务状态,退出代码,退出信号等。 
(3)优先级 :相对于其他进程的优先级。 
(4)程序计数器:程序中即将被执行的下一条指令的地址。 
(5)内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
(6)上下文数据:进程执行时处理器的寄存器中的数据。 
(7) I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。 
(8) 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
2.分析fork函数对应的内核处理过程do_fork,理解创建一个新进程如何创建和修改task_struct数据结构

Linux系统中,除第一个进程是被捏造出来的,其他进程都是通过do_fork()复制出来的。

函数原型:int do_fork(unsigned long clone_flags, unsigned long stack_start,struct pt_regs *regs, unsigned long stack_size)

1.创建子进程task_struct结构体

首先需要申请进程最基本的单位task_struct结构,然后需要将父进程task_struct结构中的各种参数复制到子进程task_struct中。

2.获取一个空闲的pid。

3.复制各种资源:copy_files(clone_flags, p)父进程中可能打开了一系列文件,因此要复制给子进程。copy_fs(clone_flags, p),复制父进程当前目录环境,如当前文件系统。复制父进程的用户空间。copy_thread()等。

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

1.启动Menu OS

cd LinuxKernel 
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_fork.c test.c
make rootfs
  
2.进入gdb调试模式
gdb
file linux-3.18.6/vmlinux
在这几个地方设置断点
b sys_clone
b do_fork
b dup_task_struct
b copy_process
b copy_thread
b ret_from_for

首先在sys_clone处

do_fork处

copy_process处

进入copy_thread

查看p值

4.理解编译链接的过程和ELF可执行文件格式

1.链接过程

预处理:主要是做一些代码文本的替换工作。(该替换是一个递归逐层展开的过程。)

(1)将所有的#define删除,并展开所有的宏定义
(2)处理所有的条件预编译指令,如:#if  #ifdef #elif #else #endif
(3)处理#include预编译指令,将被包含的文件插进到该指令的位置,这个过程是递归的
(4)删除所有的注释//与/* */
(5)添加行号与文件名标识,以便产生调试用的行号信息以及编译错误或警告时能够显示行号
(6)保留所有的#pragma编译器指令,因为编译器需要使用它们

编译:把预处理完的文件进行一系列词法分析语法分析语义分析优化后生成汇编代码,这个过程是程序构建的核心部分。 

汇编:将汇编代码转成机器指令。

链接:此时的链接,严格说应该叫静态链接。将多个目标文件、库拼合成最终的可执行文件。


5.使用exec*库函数加载一个可执行文件

exec()族函数功能是将当前的进程替换成一个新的进程,执行到exec()函数时当前进程就会结束新进程则开始执行。

process.c代码

 

运行结果:

 

 

6.理解Linux系统中进程调度的时机

1.中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();

2.内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;

3.用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。

7.特别关注并仔细分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系

1.关键函数的调用关系:

schedule() --> context_switch() --> switch_to --> __switch_to()

2.代码分析

asm volatile("pushfl\n\t" /* 保存当前进程的标志位 */
"pushl %%ebp\n\t" /* 保存当前进程的堆栈基址EBP */
"movl %%esp,%[prev_sp]\n\t" /* 保存当前栈顶ESP */
"movl %[next_sp],%%esp\n\t" /* 把下一个进程的栈顶放到esp寄存器中,完成了内核堆栈的切换,从此往下压栈都是在next进程的内核堆栈中。 */

"movl $1f,%[prev_ip]\n\t" /* 保存当前进程的EIP */
"pushl %[next_ip]\n\t" /* 把下一个进程的起点EIP压入堆栈 */
__switch_canary
"jmp __switch_to\n" /* 因为是函数所以是jmp,通过寄存器传递参数,寄存器是prev-a,next-d,当函数执行结束ret时因为没有压栈当前eip,所以需要使用之前压栈的eip,就是pop出next_ip。 */

"1:\t" /* 认为next进程开始执行。 */
"popl %%ebp\n\t" /* restore EBP */
"popfl\n" /* restore flags */
/* output parameters 因为处于中断上下文,在内核中
prev_sp是内核堆栈栈顶
prev_ip是当前进程的eip */
: [prev_sp] "=m" (prev->thread.sp),
[prev_ip] "=m" (prev->thread.ip), //[prev_ip]是标号
"=a" (last),
/* clobbered output registers: */
"=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),

/* regparm parameters for __switch_to(): */
[prev] "a" (prev),
[next] "d" (next)

__switch_canary_iparam

: /* reloaded segment registers */
"memory");
} while (0)

内核在switch_to中执行如下操作:

1.进程切换, 即esp的切换, 由于从esp可以找到进程的描述符

2.硬件上下文切换, 设置ip寄存器的值, 并jmp到__switch_to函数

3.堆栈的切换, 即ebp的切换, ebp是栈底指针, 它确定了当前用户空间属于哪个进程

通过系统调用,用户空间的应用程序就会进入内核空间,由内核代表该进程运行于内核空间,这就涉及到上下文的切换,用户空间和内核空间具有不同的地址映射,通用或专用的寄存器组,而用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行,所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。

 

posted @ 2019-03-26 22:04  深于黑  阅读(337)  评论(0编辑  收藏  举报