进程创建、可执行文件的加载和进程执行进程切换。

操作系统是如何工作的

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

实验环境

使用实验楼的虚拟机打开shell
实验楼链接: https://www.shiyanlou.com/courses/195

进程描述的数据结构task_struct

  1. 进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

  2. 进程控制块PCB——task_struct:
    PCB在linux内核中定义为task_struct结构体,
    并在 http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235; 源文件中实现。

  3. 关键参数如下:

volatile long state; //表示进程状态
void *stack; //进程所属堆栈指针
unsigned int rt_priority;//进程优先级
int exit_state;//退出时状态
pid_t pid;//进程号,作为进程的全局标识符
pid_t tgid;//进程组号
struct task_struct __rcu *real_parent;//父进程
struct list_head children;//子进程
struct list_head sibling;//兄弟进程
struct task_struct *group_leader;//所属进程组的主进程

fork函数对应的内核处理过程do_fork

  1. Linux提供三个创建进程的系统调用,do_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;

    // ...

    // 复制进程描述符,返回创建的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. 得到它的实际处理内容如下:
    a: 调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。
    b:初始化vfork的完成处理信息(如果是vfork调用)
    c:调用wake_up_new_task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。
    d:如果是vfork调用,需要阻塞父进程,知道子进程执行exec。

  2. 进程的建立过程顺序大致为:fork() ,sys_clone() ,do_fork() , dup_task_struct() , copy_process() , copy_thread(), ret_from_fork()

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

  1. 输入命令如下:

在这里插入图片描述
这样就启动了MenuOS:
在这里插入图片描述
2. 使用gdb进行调试

qemu-system-i386 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -s -S -append nokaslr
gdb
 (gdb)  file linux-3.18.6/vmlinux
 (gdb)  target remote:1234
 (gdb)  b sys_clone 
 (gdb)  b do_fork
 (gdb)  b dup_task_struct
 (gdb)  b copy_process
 (gdb)  b copy_thread

分别在sys_clone、do_fork、dup_task_struct、copy_process和copy_thread函数调用处加上断点:
可以得到如下的结果:

可执行文件的加载

  1. ELF即:Executable and Linking Format. 意为可执行可关联的文件。
    加载流程:execve –> do_execve –> search_binary_handle –> load_binary。
  2. 编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接.
  3. 编写一个helloworld.c程序,再对源文件进行编译链接生成可执行文件。
  4. 利用gdb和qemu工具来跟踪分析,首先给do_execve函数打上断点,
    在这里插入图片描述
    进行跟踪可以得到以下结果:
    在这里插入图片描述
    显然,当调用新的可执行程序时,优先进入内核态调用do_execve处理函数,并使用堆栈对原来的现场进行保护。然后,根据返回的可执行文件的地址,对当前可执行文件进行覆盖。由于返回地址为调用可执行文件的main函数入口,所以可以继续执行该文件。

跟踪分析schedule函数

在这里插入图片描述
各处理函数的调用顺序如下:pick_next_task -> context_switch -> __switch_to 。由此可以得出,当进程间切换时,首先需要调用pick_next_task函数挑选出下一个将要被执行的程序;然后再进行进程上下文的切换,此环节涉及到“保护现场”及“现场恢复”;在执行完以上两个步骤后,调用__switch_to进行进程间的切换。

分析switch_to中的汇编代码

asm volatile("pushfl\n\t"     //保存当前进程的标志寄存器内容   
         "pushl %%ebp\n\t"    //保存堆栈基址寄存器内容
         "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压栈
         __switch_canary                   
         "jmp __switch_to\n" 


         "1:\t"              //next进程开始执行        
         "popl %%ebp\n\t"     //恢复堆栈基址   
         "popfl\n"         //恢复PSW
                   //  output parameters               
         /* prev_sp是内核堆栈栈顶,prev_ip是当前进程的eip */                
         : [prev_sp] "=m" (prev->thread.sp),     
         [prev_ip] "=m" (prev->thread.ip),  //[prev_ip]是标号        
         "=a" (last),                 
                                    
        
         "=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),       
                                        
         [prev]     "a" (prev),              
         [next]     "d" (next)               
                                    
         __switch_canary_iparam                
                                    
         : /* reloaded segment registers */           
         "memory");

switch_to的主要内容包括:在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。将prev的内核堆栈指针ebp存入prev->thread.esp中。把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中。将popl指令所在的地址保存在prev->thread.eip中。

总结

总的来说,此次实验加深了对进程创建,文件加载,进程执行和切换的理解。内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度。schedule()函数实现进程调度,context_ switch完成进程上下文切换,switch_ to完成寄存器的切换。用户态进程不能进行主动调度,只在中断处理过程中进行调度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值