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

linux 专栏收录该内容
3 篇文章 0 订阅

学号: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_SMP
	struct 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;
#endif
	int 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_SCHED
	struct task_group *sched_task_group;
#endif
	struct sched_dl_entity dl;

#ifdef CONFIG_PREEMPT_NOTIFIERS
	/* list of struct preempt_notifier: */
	struct hlist_head preempt_notifiers;
#endif

#ifdef CONFIG_BLK_DEV_IO_TRACE
	unsigned int btrace_seq;
#endif

	unsigned 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结构体内的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;
}

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的过程。

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
<p> <span style="font-size:14px;color:#337FE5;">【为什么学爬虫?】</span> </p> <p> <span style="font-size:14px;">       1爬虫入手容易但是深入较难如何写出高效率爬虫如何写出灵活性高可扩展爬虫都是一项技术活。另外在爬虫过程中经常容易遇到被反爬虫比如字体反爬IP识别验证码等如何层层攻克难点拿到想要数据这门课程你都能学到!</span> </p> <p> <span style="font-size:14px;">       2如果是作为一个其他行业开发者比如app开发web开发学习爬虫能让你加强对技术认知能够开发出更加安全软件网站</span> </p> <p> <br /> </p> <span style="font-size:14px;color:#337FE5;">【课程设计】</span> <p class="ql-long-10663260"> <span> </span> </p> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> 一个完整爬虫程序无论大小总体来说可以分成三个步骤分别是: </p> <ol> <li class="" style="font-size:11pt;color:#494949;"> 网络请求:模拟浏览器行为从网上抓取数据。 </li> <li class="" style="font-size:11pt;color:#494949;"> 数据解析:将请求下来数据进行过滤提取我们想要数据。 </li> <li class="" style="font-size:11pt;color:#494949;"> 数据存储:将提取到数据存储到硬盘或者内存中。比如用mysql数据库或者redis等。 </li> </ol> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> 那么本课程也是按照这几个步骤循序渐进进行讲解带领学生完整掌握每个步骤技术。另外因为爬虫多样性在爬取过程中可能会发生被反爬效率低下等。因此我们又增加了两个章节用来提高爬虫程序灵活性分别是: </p> <ol> <li class="" style="font-size:11pt;color:#494949;"> 爬虫进阶:包括IP代理多线程爬虫图形验证码识别JS加密解密动态网页爬虫字体反爬识别等。 </li> <li class="" style="font-size:11pt;color:#494949;"> Scrapy分布式爬虫:Scrapy框架Scrapy-redis组件分布式爬虫等。 </li> </ol> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> 通过爬虫进阶知识点我们能应付大量反爬网站而Scrapy框架作为一个专业爬虫框架使用他可以快速提高我们编写爬虫程序效率速度。另外如果一台机器不能满足你需求我们可以用分布式爬虫让多台机器帮助你快速爬取数据。 </p> <p style="font-size:11pt;color:#494949;">   </p> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> 从基础爬虫到商业化应用爬虫本套课程满足您所有需求! </p> <p class="ql-long-26664262" style="font-size:11pt;color:#494949;"> <br /> </p> <p> <br /> </p> <p> <span style="font-size:14px;background-color:#FFFFFF;color:#337FE5;">【课程服务】</span> </p> <p> <span style="font-size:14px;">专属付费社群+定期答疑</span> </p> <p> <br /> </p> <p class="ql-long-24357476"> <span style="font-size:16px;"><br /> </span> </p> <p> <br /> </p> <p class="ql-long-24357476"> <span style="font-size:16px;"></span> </p>
<div style="color:rgba(0,0,0,.75);"> <span style="color:#4d4d4d;"> </span> <div style="color:rgba(0,0,0,.75);"> <span style="color:#4d4d4d;"> </span> <div style="color:rgba(0,0,0,.75);"> <div style="color:rgba(0,0,0,.75);"> <span style="color:#4d4d4d;">当前课程中商城项目实战源码是我发布在 GitHub 上开源项目 newbee-mall (新蜂商城)目前已有 6300 多个 star</span><span style="color:#4d4d4d;">本课程是一个 Spring Boot 技术栈实战类课程课程共分为 3 大部分前面两个部分为基础环境准备相关概念介绍第三个部分是 Spring Boot 商城项目功能讲解让大家实际操作并实践上手一个大型线上商城项目并学习到一定开发经验以及其中开发技巧。<br /> 商城项目所涉及功能结构图整理如下:<br /> </span> </div> <div style="color:rgba(0,0,0,.75);">   </div> <div style="color:rgba(0,0,0,.75);"> <p style="color:#4d4d4d;"> <img alt="modules" src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9uZXdiZWUtbWFsbC5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vcG9zdGVyL3N0b3JlL25ld2JlZS1tYWxsLXMucG5n?x-oss-process=image/format,png" /> </p> </div> <p style="color:rgba(0,0,0,.75);"> <strong><span style="color:#e53333;">课程特色</span></strong> </p> <p style="color:rgba(0,0,0,.75);">   </p> <div style="color:rgba(0,0,0,.75);">   </div> <div style="color:rgba(0,0,0,.75);"> <ul> <li> 对新手开发者十分友好无需复杂操作步骤仅需 2 秒就可以启动这个完整商城项目 </li> <li> 最终实战项目是一个企业级别 Spring Boot 大型项目对于各个阶段 Java 开发者都是极佳选择 </li> <li> 实践项目页面美观且实用交互效果完美 </li> <li> 教程详细开发教程详细完整文档资源齐全 </li> <li> 代码+讲解+演示网站全方位保证向 Hello World 教程说拜拜 </li> <li> 技术栈新颖且知识点丰富学习后可以提升大家对于知识理解掌握可以进一步提升你市场竞争力 </li> </ul> </div> <p style="color:rgba(0,0,0,.75);">   </p> <p style="color:rgba(0,0,0,.75);"> <span style="color:#e53333;">课程预览</span> </p> <p style="color:rgba(0,0,0,.75);">   </p> <div style="color:rgba(0,0,0,.75);">   </div> <div style="color:rgba(0,0,0,.75);"> <p style="color:#4d4d4d;"> 以下为商城项目页面功能展示分别为: </p> </div> <div style="color:rgba(0,0,0,.75);"> <ul> <li> 商城首页 1<br /> <img alt="" src="https://img-bss.csdnimg.cn/202103050347585499.gif" /> </li> <li> 商城首页 2<br /> <img alt="" src="https://img-bss.csdn.net/202005181054413605.png" /> </li> <li>   </li> <li> 购物车<br /> <img alt="cart" src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9uZXdiZWUtbWFsbC5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vcG9zdGVyL3Byb2R1Y3QvY2FydC5wbmc?x-oss-process=image/format,png" /> </li> <li> 订单结算<br /> <img alt="settle" src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9uZXdiZWUtbWFsbC5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vcG9zdGVyL3Byb2R1Y3Qvc2V0dGxlLnBuZw?x-oss-process=image/format,png" /> </li> <li> 订单列表<br /> <img alt="orders" src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9uZXdiZWUtbWFsbC5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vcG9zdGVyL3Byb2R1Y3Qvb3JkZXJzLnBuZw?x-oss-process=image/format,png" /> </li> <li> 支付页面<br /> <img alt="" src="https://img-bss.csdn.net/201909280301493716.jpg" /> </li> <li> 后台管理系统登录页<br /> <img alt="login" src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9uZXdiZWUtbWFsbC5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vcG9zdGVyL3Byb2R1Y3QvbWFuYWdlLWxvZ2luLnBuZw?x-oss-process=image/format,png" /> </li> <li> 商品管理<br /> <img alt="goods" src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9uZXdiZWUtbWFsbC5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vcG9zdGVyL3Byb2R1Y3QvbWFuYWdlLWdvb2RzLnBuZw?x-oss-process=image/format,png" /> </li> <li> 商品编辑<br /> <img alt="" src="https://img-bss.csdnimg.cn/202103050348242799.png" /> </li> </ul> </div> </div> </div> </div>
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值