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

学号404
原创作品转载请注明出处 https

一. 实验要求

在这里插入图片描述

二. 实验内容

1. 阅读理解task_struct数据结构

为了方便管理进程,操作系统需要清楚地描述每一个进程。所以,操作系统定义了一个数据结构来描述不同的进程,这个数据结构就是进程控制块(PCB),也就是task_struct,这里面包含了系统执行过程中需要了解的进程的信息。
以下列举了task_stuct中定义的重要参数及作用。

volatile long state;	//表示进程当前的状态是就绪,阻塞还是运行三个状态 /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;   // 进程的内核堆栈
unsigned int flags;	//  进程的标志 /* per process flags, defined below */
pid_t pid; // 进程的pid
struct list_head tasks; // 进程的链表
struct task_struct __rcu; //描述进程的父子进程关系。
unsigned int rt_priority;     //描述调度优先级
unsigned int policy;          //描述调度策略

2.分析fork函数对应的内核处理过程do_fork

do_fork函数主要处理的是进程的创建,在linux系统中主要以下有三个系统调用(fork, vfork, clone)可以创建一个新进程,它们的共同点是都是通过调用do_fork函数实现进程创建的,它们的不同点如下:

  • fork:创建子进程。
  • vfork,创建子进程,并且父子进程共享地址空间,子进程要先于父进程运行。
  • clone,主要用于创建线程。

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;

	/*
 	 * Determine whether and which event to report to ptracer.  When
	 * called from kernel_thread or CLONE_UNTRACED is explicitly
	 * requested, no event is reported; otherwise, report if the event
	 * for the type of forking is enabled.
	 */
	// 检查标志位,选择通过哪一个系统调用实现进程创建
	if (!(clone_flags & CLONE_UNTRACED)) {
		if (clone_flags & CLONE_VFORK)
			trace = PTRACE_EVENT_VFORK;
		else if ((clone_flags & CSIGNAL) != SIGCHLD)
			trace = PTRACE_EVENT_CLONE;
		else
			trace = PTRACE_EVENT_FORK;

		if (likely(!ptrace_event_enabled(current, trace)))
			trace = 0;
	}



    // 复制一份进程描述符
    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);

        // 获取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);
        }

        // 调用wake_up_new_task,将新进程加入调度队列
         wake_up_new_task(p);


        // 上面检查是vfork,保证子进程先于父进程执行的解决方案:将父进程加入等待队列,直到子进程处理完
          if (clone_flags & CLONE_VFORK) {
            if (!wait_for_vfork_done(p, &vfork))
                ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
        }

        put_pid(pid);s
    } else {
        nr = PTR_ERR(p);
    }
    return nr;
}

do_fork主要完成了:

  • 调用copy_process,复制一份进程标识符作为子进程,同时进行错误检查。
  • 检查是否是vfork调用,保证子进程先于父进程执行的解决方案:将父进程加入等待队列,直到子进程处理完。

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

使用gdb跟踪分析一个fork系统调用内核处理函数do_fork ,验证对Linux系统创建一个新进程的理解,特别关注以下问题:

  • 新进程是从哪里开始执行的?
  • 为什么从那里能顺利执行下去?即执行起点与内核堆栈如何保证一致。

(1)启动MenuOS 并打开gdb调试
这部分内容在实验二中已经介绍。不同的是,本次实验是在实验楼中完成的。
启动MenuOS代码如下:

rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_fork.c test.c
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
 -S freeze CPU at startup (use ’c’ to start execution)
 -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项

(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

在这里插入图片描述

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

(1) 编译链接
从源文件编译链接成可执行文件需要经历以下步骤:
源文件 -> 预处理 -> 编译 -> 链接 -> 可执行文件

编译: 把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成汇编代码。
链接: 就是把多个目标文件和库文件拼合成一个最终的可执行文件的过程。

(2) ELF可执行文件格式
ELF全称Executable and Linkable Format,可执行连接格式,ELF格式的文件用于存储Linux程序。ELF文件(目标文件)格式主要三种:

  • 可重定向文件:文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件。(目标文件或者静态库文件,即linux通常后缀为.a和.o的文件)
  • 可执行文件:文件保存着一个用来执行的程序。(例如bash,gcc等)
  • 共享目标文件:共享库。文件保存着代码和合适的数据,用来被下连接编辑器和动态链接器链接。(linux下后缀为.so的文件。)目标文件既要参与程序链接又要参与程序执行:

一般的 ELF 文件包括三个索引表:ELF header,Program header table,Section header table。

  • ELF header:在文件的开始,保存了路线图,描述了该文件的组织情况。
  • Program header table:告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。
  • Section header table:包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。

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

先编写一个hello.c

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

接下来采取两种方式,静态链接和动态链接分别执行hello.c
(1) 动态编译

gcc -E -o hello.cpp hello.c -m32  // 
gcc -x cpp-ouput -S -o hello.s hello.cpp -m32
gcc -x assembler -c hello.s -o hello.o -m32
gcc -o hello hello.o -m32
./hello

执行结果如下:
在这里插入图片描述
(2)静态编译

gcc -o hello.static hell.o -m32 -static
./hello.static

执行结果:
在这里插入图片描述
可以发现hello.static 733254 比hello 7292要 大很多,所以静态编译要更加占空间,因为它需要在程序运行之前就完成所有的拼合,生成一个可执行的目标文件,而且每一个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,同一个目标文件都在内存存在多个副本;而动态编译时程序是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,多个程序在执行时共享同一份副本。

6.理解Linux系统中进程调度的时机

Linux进程调度时机主要有

  • 进程状态转换的时刻:进程终止、进程睡眠;
  • 当前进程的时间片用完时(current->counter=0);
  • 设备驱动程序
  • 进程从中断、异常及系统调用返回到用户态时;

schedule():进程调度函数,由它来完成进程的选择(调度)。
schedule()调用的地方:

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

7.使用gdb跟踪分析一个schedule()函数 ,验证对Linux系统进程调度与进程切换过程的理解

添加断点:
在这里插入图片描述
执行结果:
在这里插入图片描述

  • __schedule完成了真正的调度工作
  • pick_next_task选择抢占的进程;pick_next_task函数会从按照优先级遍历所有调度器类的pick_next_task函数, 去查找最优的那个进程,。
  • context_switch完成了进程上下文切换

8. 特别关注并仔细分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系

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)

switch_to实现了进程之间的真正切换:

  • 首先在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。
  • 然后将prev的内核堆栈指针ebp存入prev->thread.esp中。
  • 把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中
  • 将popl指令所在的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度
  • 通过jmp指令(而不是call指令)转入一个函数__switch_to()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值