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

实验目标

1.分析fork函数对应的内核处理过程do_fork,理解创建一个新进程如何创建和修改task_struct数据结构

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

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

实验环境

ubuntu系统(ubuntu-16.04.2-desktop-amd64)+ VMware Workstation Pro

一、阅读理解task_struct数据结构

代码来源:http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235
该结构部分代码:

struct task_struct {volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */void *stack;atomic_t usage;unsigned int flags;    /* per process flags, defined below */unsigned int ptrace;#ifdef CONFIG_SMPstruct llist_node wake_entry;int on_cpu;struct task_struct *last_wakee;unsigned long wakee_flips;unsigned long wakee_flip_decay_ts;int wake_cpu;
#endifint on_rq;int prio, static_prio, normal_prio;unsigned int rt_priority;const struct sched_class *sched_class;struct sched_entity se;struct sched_rt_entity rt;
#ifdef CONFIG_CGROUP_SCHEDstruct task_group *sched_task_group;
#endifstruct sched_dl_entity dl;#ifdef CONFIG_PREEMPT_NOTIFIERS/* list of struct preempt_notifier: */struct hlist_head preempt_notifiers;
#endif#ifdef CONFIG_BLK_DEV_IO_TRACEunsigned int btrace_seq;
#endifunsigned int policy;int nr_cpus_allowed;cpumask_t cpus_allowed;...
}

在阅读这个结构体之前,我们必须了解进程与程序的区别,进程是程序的一个执行的实例,为了管理进程,操作系统必须对每个进程所做的事情进行清楚的描述,为此,操作系统使用数据结构来代表处理不同的实体,这个数据结构就是通常所说的进程描述符或进程控制块(PCB),在linux操作系统下这就是task_struct结构 ,它包含了这个进程的所有信息,在任何时候操作系统都能够跟踪这个结构的信息。该结构定义位于/include/linux/sched.h

对于进程控制块PCB—task_struct:

状态信息:如就绪、执行等状态
链接信息:用来描述进程之间的家庭关系,例如指向父进程、子进程、兄弟进程等PCB的指针
各种标识符:如进程标识符、用户及组标识符等
时间和定时器信息:进程使用CPU时间的统计等
调度信息:调度策略、进程优先级、剩余时间片大小等
处理机环境信息:处理器的各种寄存器以及堆栈情况等
虚拟内存信息:描述每个进程所拥有的地址空间

文件系统信息:记录进程使用文件的情况

PCB几个重要参数

volatile long state;//表示进程的当前状态
unsigned long flags;  //进程标志
long priority;  //进程优先级。 Priority的值给出进程每次获取CPU后可使用的时间(按jiffies计)。优先级可通过系统调用sys_setpriorty改变(在kernel/sys.c中)。
long counter;  //在轮转法调度时表示进程当前还可运行多久。unsigned long policy;  //该进程的进程调度策略,可以通过系统调用sys_sched_setscheduler()更改(见kernel/sched.c)。

二、分析fork函数对应的内核处理过程do_fork

fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;

具体过程如下:fork() -> sys_clone() -> do_fork() -> dup_task_struct() -> copy_process() -> copy_thread() -> ret_from_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结构体内的pidpid = 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);}// 将子进程添加到调度器的队列,使得子进程有机会获得CPUwake_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;
}

do_fork处理了以下内容:

1.调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。
2.初始化vfork的完成处理信息(如果是vfork调用)
3.调用wake_up_new_task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。
4.如果是vfork调用,需要阻塞父进程,知道子进程执行exec。

如何创建一个新进程:
1.通过调用do_fork来实现进程的创建;
2.复制父进程PCB–task_struct来创建一个新进程,要给新进程分配一个新的内核堆栈;
3.修改复制过来的进程数据,比如pid、进程链表等等执行copy_process和copy_thread
4.成功创建新进程

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

本次实验是基于实验楼中现有的实验环境进行的。
进入menu文件夹,编辑test.c文件:

cd ~/LinuxKernel/menu/
sudo vim test.c

给qemu增加一个使用fork系统调用的菜单命令,如下所示:


在menu目录下执行如下命令:make rootfs启动MenuOS,结果如下所示:

使用GDB进行跟踪调试,设置如下断点:

在MenuOS中输入fork菜单命令以后,后面的断点依次如图所示:
首先停在sys_clone位置处:

然后进入do_fork中:

接着进入copy_process中:

接着进入copy_thread中:

最后进入ret_from_fork中:

整个fork系统调用的执行流程如下:
fork->sys_clone->do_fork->copy_process->dup_task_struct->copy_thread->ret_from_fork

Linux内核通过复制父进程来创建一个新进程,调用do_fork为每个新创建的进程动态地分配一个task_struct结构。copy_thread()函数中的代码p->thread.ip = (unsigned long) ret_from_fork;将子进程的 ip 设置为 ret_form_fork 的首地址,所以fork系统调用产生的子进程在系统调用处理过程中从ret_from_fork处开始执行。
copy_thread()函数中的代码*childregs = *current_pt_regs();将父进程的regs参数赋值到子进程的内核堆栈,里面存放了SAVE ALL中压入栈的参数,之后的RESTORE_ALL宏定义会恢复保存到堆栈中的寄存器的值。
fork系统调用发生一次,但是返回两次。父进程中返回值是子进程的进程号,子进程中返回值为0,可以通过返回值来判断当前进程是父进程还是子进程。

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

从源文件Hello.c编译链接成Hello.out,需要经历如下步骤:

ELF可执行文件格式具体分析代码:https://blog.csdn.net/wu5795175/article/details/7657580
ELF文件格式包括三种主要的类型:可执行文件、可重定向文件、共享库:
1.一个可执行(executable)文件保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来创建程序进程映象。
2.一个可重定位(relocatable)文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件。
3.一个共享库文件保存着代码和合适的数据,用来被不同的两个链接器链接。

五、编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接

第一步:先编辑一个hello.c

#include <stdio.h>
#include <stdlib.h>
int main()
{printf("Hello World!\n");return 0;
}

第二步:生成预处理文件hello.cpp(预处理负责把include的文件包含进来及宏替换等工作)
第三步:编译成汇编代码hello.s
第四步:编译成目标代码,得到二进制文件hello.o
第五步:链接成可执行文件hello,(它是二进制文件)
第六步:运行一下./hello

动态链接分为可执行程序装载时动态链接和运行时动态链接。

六、使用gdb跟踪分析一个execve系统调用内核处理函数do_execve

在实验楼提供的环境中,给qemu增加一个使用execve系统调用的菜单命令,如下所示:


在menu目录下执行如下命令:make rootfs启动MenuOS,结果如下所示:

使用GDB进行跟踪调试,设置如下断点:

在MenuOS中输入execve菜单命令以后,截图如下所示:


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 };return do_execve_common(filename, argv, envp);     // 此处调用do_execve_common
}

装载和启动一个可执行程序的大致流程如下所示:
sys_execve -> do_execve-> do_execve_common-> exec_binprm-> search_binary_handler -> load_elf_binary-> start_thread

  • 对于静态链接的可执行文件,eip指向该文件的文件头e_entry所指的入口地址;对于动态链接的可执行文件,eip指向动态链接器。执行静态链接程序时,execve系统调用修改内核堆栈中保存的eip的值作为新的进程的起点。
  • 新的可执行程序修改内核堆栈eip为新程序的起点,从new_ip开始执行,start_thread把返回到用户态的位置从int 0x80的下一条指令变成新的可执行文件的入口地址。
  • 执行execve系统调用时,调用execve的可执行程序陷入内核态,使用execve加载的可执行文件覆盖当前进程的可执行程序,当execve系统调用返回时,返回新的可执行程序的起点(main函数),故新的可执行程序能够顺利执行。

八、理解Linux系统中进程调度的时机

可以在内核代码中搜索schedule()函数,看都是哪里调用了schedule(),判断我们课程内容中的总结是否准确:

  • 中断处理过程(时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();
  • 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,内核线程作为一类的特殊的进程既可以进行主动调度,也可以进行被动调度;
  • 用户态进程无法实现主动调度,只能够通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。

九、使用gdb跟踪分析一个schedule()函数

在实验楼提供的环境中,设置断点如下所示:


schedule()函数用于实现进程调度,它的任务是从运行队列的链表中找到一个进程,并且随后将CPU分配给这个进程。
从本质上来说,每个进程切换分为两步:
1.切换页全局目录以安装一个新的地址空间;
2.切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包括CPU寄存器。

十、分析switch_to中的汇编代码

#define switch_to(prev, next, last)        //  prev指向当前进程,next指向被调度的进程
do {                                                                    unsigned long ebx, ecx, edx, esi, edi;                                                                                           asm volatile("pushfl\n\t"               /* 将标志位压栈 */     "pushl %%ebp\n\t"          /* 将当前ebp压栈 */     "movl %%esp,%[prev_sp]\n\t"        /* 保存当前进程的堆栈栈顶*/ "movl %[next_sp],%%esp\n\t"        /* 将下一个进程的堆栈栈顶保存到esp寄存器,完成内核堆栈的切换*/ "movl $1f,%[prev_ip]\n\t"  /* 保存当前进程的eip*/     "pushl %[next_ip]\n\t"     /*将下一个进程的eip压栈 */     "jmp __switch_to\n"        /*jmp通过后面的寄存器eax、edx来传递参数,__switch_to()函数通过return把next_ip弹出来 */     "1:\t"                                             "popl %%ebp\n\t"           /*恢复当前堆栈的ebp*/     "popfl\n"                  /* 恢复当前堆栈的寄存器标志位*/     /* output parameters */                            : [prev_sp] "=m" (prev->thread.sp),              // 当前内核堆栈的栈顶[prev_ip] "=m" (prev->thread.ip),             // 当前进程的eip   "=a" (last),                                     /* clobbered output registers: */                "=b" (ebx), "=c" (ecx), "=d" (edx),              "=S" (esi), "=D" (edi)                           /* input parameters: */                          : [next_sp]  "m" (next->thread.sp),                // 下一个进程的内核堆栈的栈顶[next_ip]  "m" (next->thread.ip),                // 下一个进程的eip/* regparm parameters for __switch_to(): */      [prev]     "a" (prev),                           // 寄存器的传递[next]     "d" (next));                          __switch_canary_iparam                : /* reloaded segment registers */           "memory");
} while (0)

switch_to实现了进程之间的真正切换:
1.首先在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。
2.然后将prev的内核堆栈指针ebp存入prev->thread.esp中。
3.把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中
4.将popl指令所在的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度
5.通过jmp指令(而不是call指令)转入一个函数__switch_to()
6.恢复next上次被调离时推进堆栈的内容。从现在开始,next进程就成为当前进程而真正开始执行

总结

1.Linux通过复制父进程来创建一个新进程,通过调用do_fork来实现并为每个新创建的进程动态地分配一个task_struct结构。fork()函数被调用一次,但返回两次。可以通过fork,复制一个已有的进程,进而产生一个子进程。
2.Linux的进程调度基于分时技术和进程的优先级,内核通过调用schedule()函数来实现进程调度,其中context_switch宏用于完成进程上下文切换,它通过调用switch_to宏来实现关键上下文切换。
3.进程上下文切换需要保存切换进程的相关信息(thread.sp和thread.ip);中断上下文的切换是在一个进程的用户态到一个进程的内核态,或从进程的内核态到用户态,切换进程需要在不同的进程间切换,但一般进程上下文切换是套在中断上下文切换中的。
4.Linux系统的一般执行过程可以抽象成正在运行的用户态进程X切换到运行用户态进程Y的过程。

理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换相关推荐

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

    学号后三位<168> 原创作品转载请注明出处https://github.com/mengning/linuxkernel/ 1.分析fork函数对应的内核处理过程sys_clone,理解 ...

  2. VBA 创建和使用加载项

    今天讲点新的东西--创建和使用加载项,不过内容会有点无聊,因为都是文字内容 一.什么是加载项 最有用的功能之一,增加了工作的专业度,提供了一些关键优势.+ 1.和标准工作簿的比较 所有工作簿文件都可以 ...

  3. java 类编译_Java类编译、加载、和执行机制

    Java类编译.加载.和执行机制 标签: java 类加载 类编译 类执行 机制 0.前言 个人认为,对于JVM的理解,主要是两大方面内容: Java类的编译.加载和执行. JVM的内存管理和垃圾回收 ...

  4. JS--页面加载完毕后执行

    原文网址:JS--页面加载完毕后执行_IT利刃出鞘的博客-CSDN博客 简介 说明 本文用示例介绍JavaScript如何在页面加载完毕之后执行函数. 页面加载完毕主要有两个事件: DOMConten ...

  5. 加载页面前执行js脚本,实现浏览器指纹变更

    一般浏览器先加载主页,再请求关联的js脚本文件.不管你把js代码嵌入在html中,还是独立保存为一个文件,都必须等待主页加载完成,js代码加载完成后才能执行.网上有人说把js代码放在head里面,其实 ...

  6. vue组件加载完成之后执行方法_vuejs页面加载完成后执行函数

    module.exports = { data: function(){ return { memberQrcodeState: false } }, components: {memberQrcod ...

  7. 页面载入-(dom、css、图片 等资源 加载完成) 执行

    1. // 页面载入-(dom.css.图片 等资源 加载完成) 执行 window.onload=function(){console.log("aaa"); }window.o ...

  8. js 和jQuery(自动执行函数)立即执行函数和页面加载完后执行函数写法

    js 立即执行函数的写法. js 立即执行函数只能用于匿名函数,如果声明了函数名是不可以用立即执行的,通常在函数表达式后加一对小括号()用于立即执行 如果想让函数不被调用的情况下,立即自动执行,需要在 ...

  9. 解决pjax加载页面不执行js插件的问题

    解决pjax加载页面不执行js插件的问题 参考文章: (1)解决pjax加载页面不执行js插件的问题 (2)https://www.cnblogs.com/fanwenhao/p/9643549.ht ...

  10. 网站性能分析(下)-让网站并行加载但顺序执行JS

    如果网站不支持JavaScript,复杂的功能将无法正常工作.在开发当中通常有几个脚本会写在HTML文件的头部.你嵌入的越多,网站的速度潜在的变得越慢.因此提高并行加载速度变得非常有必要,在前面的文章 ...

最新文章

  1. Blender从头到尾创建低多边形角色学习教程 Low Poly Characters – Blender Bitesize Course
  2. 小米面试:字符串解码
  3. nginx做正向代理
  4. 北斗导航 | 卫星导航基础知识(卫星轨道及卫星在轨运动)
  5. flask jinja2 mysql_flask/jinja2 SSTI注入学习
  6. 【Linux】一步一步学Linux——fc命令(224)
  7. 解决Error: undefined reference to `__android_log_print'
  8. 二进制安位处理_处理器与安​​全性之间的联系是什么?
  9. [HNOI2008 Tree]
  10. Asp.net如何生成html静态页面
  11. vue点击下载图片的实现
  12. 基于FPGA的自动售货机设计(EDA课程设计)(湖南科技大学)
  13. 排水管网计算机模拟,基于SWMM的城市合流制排水管网计算机模拟方法.ppt
  14. RadAsm模板修改
  15. 信息系统项目管理师 - 项目沟通管理
  16. mm1排队论编程c语言,完整版MM1排队系统仿真matlab实验报告
  17. python中 F “{表达式}”的用法【详细】
  18. java编写平行四边形的代码_Java代码编写四边形
  19. 小程序创业会有哪些坑
  20. ubuntu 下pip的卸载,安装,更新与使用

热门文章

  1. Apache Maven 2 简介
  2. 第四周课程总结实验报告(二)
  3. HDU 2243考研路茫茫——单词情结 (AC自动机+矩阵快速幂)
  4. Spring Boot 之发送邮件
  5. LINQ to Entities 基于方法的查询语法
  6. 2018年国内就业薪资高的7大编程语言排行
  7. centos6下jbd2进程占用大量IO处理
  8. Visual Studio 2010 第一时间体验旗舰版
  9. 47.内存连续分配管理方式有哪几种?
  10. Windows Media Player 控件的常用属性及方法