理解进程创建、可执行文件的加载和进程执行进程切换
学号:076
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
本实验是基于实验楼的。
源码来源:http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235;
由源码可知,pcb包含以下内容:
进程基本信息、调度信息、文件系统信息、内存信息、I/O信息等。
一.分析fork函数对应的内核处理过程do_fork
do_fork代码如下:
long do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { struct task_struct *p; int trace = 0; long nr; // ... // 复制进程描述符,返回创建的task_struct的指针 p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace); if (!IS_ERR(p)) { struct completion vfork; struct pid *pid; trace_sched_process_fork(current, p); // 取出task结构体内的pid pid = get_task_pid(p, PIDTYPE_PID); nr = pid_vnr(pid); if (clone_flags & CLONE_PARENT_SETTID) put_user(nr, parent_tidptr); // 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行 if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); } // 将子进程添加到调度器的队列,使得子进程有机会获得CPU wake_up_new_task(p); // ... // 如果设置了 CLONE_VFORK 则将父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间 // 保证子进程优先于父进程运行 if (clone_flags & CLONE_VFORK) { if (!wait_for_vfork_done(p, &vfork)) ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); } put_pid(pid); } else { nr = PTR_ERR(p); } return nr; }
1.do_fork处理了以下内容:
(1)调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。
(2)初始化vfork的完成处理信息(如果是vfork调用)
(3)调用wake_up_new_task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。
如果是vfork调用,需要阻塞父进程,知道子进程执行exec。
2.do_fork()流程
(1)首先调用copy_process()为子进程复制出一份进程信息,如果是vfork()则初始化完成处理信息;
(2)然后调用wake_up_new_task将子进程加入调度器,为之分配CPU,如果是vfork(),则父进程等待子进程完成exec替换自己的地址空间。
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
在qemu界面输入fork
(2)gdb调试
gdb
file linux-3.18.6/vmlinux
target remote:1234
b sys_clone
b do_fork
b dup_task_struct
b copy_process
b copy_thread
b ret_from_fork
运行后首先停在sys_clone处
然后到do_fork
再到copy_process
接着进入copy_thread
最后进入ret_form_fork。
ENTRY(ret_from_fork) CFI_STARTPROC pushl_cfi %eax call schedule_tail GET_THREAD_INFO(%ebp) popl_cfi %eax pushl_cfi $0x0202 # Reset kernel eflags popfl_cfi jmp syscall_exit CFI_ENDPROC END(ret_from_fork)
观察实验结果得知:进程的建立经历了:sys_clone->do_fork->copy_process->copy_thread->ret_form_fork的过程。
二.编程使用exec*库函数加载一个可执行文件
1.编写一个用于输出的hello.c
#include<stdio.h> int main(){ printf("Hello World!\n"); return 0;
2.动态编译
(1)生成预处理文件hello.cpp
(2)编译成汇编代码hello.s
(3)编译成目标代码,得到二进制文件hello.o
(4)链接成可执行文件hello
(5)运行一下./hello
3.静态编译
分析hello.static比hello大的多
4.跟踪do_execve
(1)设置断点
结果如下:
do_execve代码如下:
int do_execve(struct filename *filename, const char __user *const __user *__argv, const char __user *const __user *__envp) { struct user_arg_ptr argv = { .ptr.native = __argv }; struct user_arg_ptr envp = { .ptr.native = __envp }; //调用do_execve_common return do_execve_common(filename, argv, envp); }
三.理解进程调度时机并用gdb跟踪分析一个schedule函数
1.调度时机
(1)中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()。
(2)内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度。
(3)用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
2.gdb跟踪分析
四.实验总结
linux执行过程
1.运行用户态进程U
2.发生中断:save cs:eip/ss:eip/eflags,加载当前进程内核堆栈,跳转至中断处理程序
3.SAVE_ALL,保存现场,完成中断上下文的切换。
4.中断处理过程若调用了schedule函数,其中switch_to做进程上下文的切换。(假设由进程U到进程M)
5.$1f之后,运行用户态进程
6.restore_all,恢复现场
7.iret 从U进程内核堆栈弹出硬件完成的压栈内容,完成中断上下文的切换,即U的内核态到用户态。
8.继续运行U。