学号165,原创作品转载请注明出处。
实验来源:https://github.com/mengning/linuxkernel/
一、进程创建
1.阅读理解task_struct数据结构
在 http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235 中可以找到task_struct结构,即为进程控制块(PCB)主要包含了进程状态、堆栈、标志、优先级等信息。
2.分析fork函数对应的内核处理过程do_fork
创建进程可以通过fork()、vfork()、clone()实现,系统调用均为do_fork。在内核启动时,除手动创建0号进程外,其他进程均由复制0号进程内容而得来。
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
do_fork函数通过调用copy_process(),复制一份当前进程作为子进程,并设置上下文信息;调用wake_up_new_task(),将子进程放入调度器队列,此时该子进程可以被调度程序选中并运行;如果是vfork()调用,将初始化vfork的完成处理信息,并阻塞父进程,直到子进程执行。
3.使用gdb跟踪分析一个fork系统调用内核处理函数do_fork
使用的内核是Linux-5.0.1,执行以下命令
git clone https://github.com/mengning/menu.git
cd menu (记得修改Makefile信息)
mv test_fork.c test.c
make rootfs
打开gdb,并设置断点
观察执行
在do_fork中,以ret_from_fork函数为执行起点,复制父进程的内存堆栈和数据,并修改某些参数实现子进程的定义和初始化,创建子进程的工作完成后,通过sys_call_exit函数退出并pop父进程的内存堆栈,实现新进程的创建工作。
二、可执行文件的加载
1.理解编译链接的过程和ELF可执行文件格式
- “ELF"全称为"Executable Linking Format”,从源文件(如.c/.cpp文件)编译链接成可执行文件(如.out/.exe文件)需要经历预处理、编译、汇编、连接等步骤。
- ELF可执行文件格式包括三种主要的类型:可执行文件、可重定向文件、共享库。
- 可执行文件保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来创建程序进程映象。
- 可重定位文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件。
- 共享文件也称动态库,保存着代码和合适的数据,用来被不同的两个链接器链接。
2.使用exec*库函数加载一个可执行文件
编辑hello.c文件
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
编译链接得到可执行文件hello
也可以静态编译得到hello.static
查看大小可以发现hello.static比hello大得多。
3.使用gdb跟踪分析一个execve系统调用内核处理函数do_execve
设置断点
观察情况
新的可执行程序通过修改内核堆栈eip作为新程序的起点;当execve系统调用返回时,返回新的可执行程序的执行起点(即main函数),能顺利执行;静态链接时,返回可执行程序的头部,动态链接时返回动态链接器的起点。
进程执行与切换
1.理解Linux系统中进程调度的时机
进程调度的时机:
- 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()
- 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
- 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
2.使用gdb跟踪分析一个schedule()函数
在进程间切换时,首先调用pick_next_task函数挑选下一个执行的程序;然后进行上下文的切换,包括保护现场和现场回复,最后调用__switch_to进行进程间切换。
3.分析switch_to的汇编代码
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)
四、总结
通过系统调用do_fork()、copy_process()、dup_task_struct()、copy_thread()等实现进程的创建;通过execve()及相关调用实现可执行文件的加载;通过schedule()、context_switch()、switch_to()实现进程的上下文切换。