学号:446
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
一、实验目的及原理
理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换。
二、实验平台
linux3.16+vm workstation12 pro+vs code
三、阅读task_struct源码
源码来源:http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235;
Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息,在include/linux/sched.h的1235行我们可以看到该数据结构的原型,详情见以上网址。
1235 struct task_struct {
1236 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
1237 void *stack; //函数堆栈
1238 atomic_t usage; //函数用例
1239 unsigned int flags; /* per process flags, defined below */
1240 unsigned int ptrace;
1241 ...
...
通过阅读源码,我们可以发现,为了完成进程各种复杂的工作,PCB的结构十分复杂,大致包括:
- 进程基本信息
- 调度信息
- 文件系统信息
- 内存信息
- I/O信息
- 资源信息
- 现场控制
- 环境信息
- 等
二、分析fork函数对应的内核处理过程do_fork
函数在/kernel/fork.c中可以发现
根据不同的linux内核版本,可以用find指令寻找
find -name fork.c
先看看函数原型
先介绍一下它执行时使用的参数:
/*clone_flags:与clone()参数flags相同。
stack_start:与clone()参数stack_start相同。
stack_size:未使用,总被设置为0。
parent_tidptr,child_tidptr:与clone系统调用中对应参数ptid和ctid相同。
*/
1623 long do_fork(unsigned long clone_flags,
1624 unsigned long stack_start,
1625 unsigned long stack_size,
1626 int __user *parent_tidptr,
1627 int __user *child_tidptr)
1628{
1629 struct task_struct *p;
1630 int trace = 0;
1631 long nr;
1632
1633 /*
1634 * Determine whether and which event to report to ptracer. When
1635 * called from kernel_thread or CLONE_UNTRACED is explicitly
1636 * requested, no event is reported; otherwise, report if the event
1637 * for the type of forking is enabled.
1638 */
//检查标志位
1639 if (!(clone_flags & CLONE_UNTRACED)) {
1640 if (clone_flags & CLONE_VFORK)
1641 trace = PTRACE_EVENT_VFORK;
1642 else if ((clone_flags & CSIGNAL) != SIGCHLD)
1643 trace = PTRACE_EVENT_CLONE;
1644 else
1645 trace = PTRACE_EVENT_FORK;
1646
1647 if (likely(!ptrace_event_enabled(current, trace)))
1648 trace = 0;
1649 }
//调用copy_process,复制一份返回一个进程描述符
1651 p = copy_process(clone_flags, stack_start, stack_size,
1652 child_tidptr, NULL, trace);
1653 /*
1654 * Do this prior waking up the new thread - the thread pointer
1655 * might get invalid after that point, if the thread exits quickly.
1656 */
//错误检查
1657 if (!IS_ERR(p)) {
1658 struct completion vfork;
1659 struct pid *pid;
1660
1661 trace_sched_process_fork(current, p);
1662
//获得pid
1663 pid = get_task_pid(p, PIDTYPE_PID);
1664 nr = pid_vnr(pid);
1665
1666 if (clone_flags & CLONE_PARENT_SETTID)
1667 put_user(nr, parent_tidptr);
1668
//检查调用的是否时vfork,若是vfork则用另一套方案
1669 if (clone_flags & CLONE_VFORK) {
1670 p->vfork_done = &vfork;
1671 init_completion(&vfork);
1672 get_task_struct(p);
1673 }
1674
//调用wake_up_new_task,将新任务加入调度队列
1675 wake_up_new_task(p);
1676
1677 /* forking complete and child started to run, tell ptracer */
1678 if (unlikely(trace))
1679 ptrace_event_pid(trace, pid);
1680
1681 if (clone_flags & CLONE_VFORK) {
1682 if (!wait_for_vfork_done(p, &vfork))
1683 ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
1684 }
1685
1686 put_pid(pid);
1687 } else {
1688 nr = PTR_ERR(p);
1689 }
1690 return nr;
1691}
四、使用gdb跟踪分析一个fork系统调用内核处理函数do_fork
首先是编写代码并编译,在test.c中编写了forktest()函数,如下:
编译并运行虚拟机:
cd menu
gcc linktable.c menu.c test.c -lpthread -o init -m32 -static
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
qemu-system-i386 -kernel '/home/isition/linux-5.0/arch/x86/boot/bzImage' -initrd /home/isition/linux-5.0/rootfs.img -S -s -append nokaslr
接下来开启另一终端,启用gdb调试:
gdb vmlinux
target remote:1234
b sys_clone
b dup_task_struct
b do_fork
b copy_process
b copy_thread
b ret_from_for
设置情况如下:
设置完成进行c操作观看结果:
观察实验结果得知:进程的建立经历了:sys_clone->do_fork->copy_process->copy_thread->ret_form_fork的过程。
五、理解编译链接的过程和ELF可执行文件格式
从.c文件到可执行文件的过程如下:
预处理:主要是做一些代码文本的替换工作。(该替换是一个递归逐层展开的过程。)
编译:把预处理完的文件进行一系列词法分析(lex)、语法分析(yacc)、语义分析及优化后生成汇编代码,这个过程是程序构建的核心部分。
汇编:汇编代码->机器指令。
链接:这里讲的链接,严格说应该叫静态链接。多个目标文件、库->最终的可执行文件(拼合的过程)。
五、编程使用exec*库函数加载一个可执行文件
先编写一个用于输出的hello.c
将其动态或静态编译为hello.o
编写调用程序并编译执行:
使用gdb设置断点并观差:
六、搜索并使用gdb跟踪分析一个schedule()函数
在opengrok上搜索发现与我们的猜想基本一致:
设置断点并分析:
七、总结
通过这次实验,让我更加深入的了解了linux的进程的建立等流程,也更加仔细的了解了我们的代码是怎么变成可执行文件并装入内存的,这次实验让我对linux的文件系统、进程调度等方面的知识得到了大大的强化。