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

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


实验环境: 实验楼
https://www.shiyanlou.com/courses/195

实验要求
  1. 阅读理解task_struct数据结构;
  2. 分析fork函数对应的内核处理过程do_fork,使用gdb跟踪分析一个fork系统调用内核处理,函数do_fork;
  3. 理解编译链接的过程和ELF可执行文件格式;
  4. 使用gdb跟踪分析一个execve系统调用内核处理函数do_execve;
  5. 使用gdb跟踪分析一个schedule()函数;
  6. 分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系;

1. 阅读理解task_struct数据结构

  • 为了管理进程,操作系统必须对每个进程所做的事情进行清楚的描述,为此,操作系统使用数据结构来代表处理不同的实体,这个数据结构就是通常所说的进程描述符或进程控制块(PCB)。
  • 进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
  • 关键参数
 	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;//所属进程组的主进程
  • 在linux操作系统下这就是task_struct结构 ,所属的头文件#include <sched.h>每个进程都会被分配一个task_struct结构,它包含了这个进程的所有信息,在任何时候操作系统都能够跟踪这个结构的信息。保存进程信息的数据结构叫tast_struct,进程的信息可以通过/proc系统文件夹查看。

2. 分析fork函数对应的内核处理过程do_fork,使用gdb跟踪分析一个fork系统调用内核处理,函数do_fork

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

Linux提供三个创建进程的系统调用:do_fork(); vfork(); clone()

  1. fork是重量级调用,它建立了父进程的一个完整副本,然后为子进程执行。
  2. vfork类似于fork,但并不创建父进程数据的副本。相反,父子进程之间共享数据。
  3. clone产生线程,可以对父子进程之间的共享、复制进行精确控制。
  • fork、vfork和close系统调用的入口分别是sys_fork、sys_vfork和sys_clone函数。以上函数从寄存器中取出由用户定义的信息,并调用与体系结构无关的do_fork函数进行进程的复制
  • 实际处理

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

 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;
}
  • 使用gdb跟踪分析一个fork系统调用内核处理函数do_fork

在这里插入图片描述

在这里插入图片描述

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

    //main.c
    int add(int a,int b);
    static int si;//.bss
    extern int buf[];
    int *copy = &buf[0];//.rel.data
    int main()
    {
        int a = 3;
        int b = 5;
        int c = add(a,b);//.rel.text
        char *s = "hello c";//.rodata
        static int si;//.bss
        return 0;
    }

    //add.c
    int buf[2];
    int add(int a,int b)
    {
        return (a+b);
    }

    //makefile(为了简化讨论,makefile文件中没有添加-g选项)
    all:main
    main:main.o add.o
        gcc -o main main.o add.o -m32
    main.o:main.c
        gcc -c main.c -m32
    add.o:add.c
        gcc -c add.c -m32
    clean:
        rm -rf *.o main

源代码(.c .cpp .h)经过c预处理器(cpp)后生成.i文件,编译器(cc1、cc1plus)编译.i文件后生成.s文件,汇编器(as)汇编.s文件后生成.o文件,链接器(ld)链接.o文件生成可执行文件。
gcc是对cpp、cc1(cc1plus)、as、ld这些后台程序的包装,它会根据不同的参数要求去调用后台程序。

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

  • 利用gdb和qemu工具来跟踪分析,首先给do_execve函数打上断点,当调用新的可执行程序时,会先进入内核态调用do_execve处理函数,并使用堆栈对原来的现场进行保护。
  • 然后,根据返回的可执行文件的地址,对当前可执行文件进行覆盖。由于返回地址为调用可执行文件的main函数入口,所以可以继续执行该文件。

5. 使用gdb跟踪分析一个schedule()函数

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

6. 分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系

//schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换
next = pick_next_task(rq, prev);//进程调度算法都封装这个函数内部
context_switch(rq, prev, next);//进程上下文切换
//switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程
#define switch_to(prev, next, last)                    \
do {                                 \
 /*                              \
  * Context-switching clobbers all registers, so we clobber  \
  * them explicitly, via unused output variables.     \
  * (EAX and EBP is not listed because EBP is saved/restored  \
  * explicitly for wchan access and EAX is the return value of   \
  * __switch_to())                     \
  */                                \
 unsigned long ebx, ecx, edx, esi, edi;                \
                                 \
 asm volatile("pushfl\n\t"      /* save    flags */   \ 
          "pushl %%ebp\n\t"        /* save    EBP   */ \ 当前进程堆栈基址压栈
          "movl %%esp,%[prev_sp]\n\t"  /* save    ESP   */ \ 将当前进程栈顶保存prev->thread.sp
          "movl %[next_sp],%%esp\n\t"  /* restore ESP   */ \ 讲下一个进程栈顶保存到esp中
          "movl $1f,%[prev_ip]\n\t"    /* save    EIP   */ \ 保存当前进程的eip
          "pushl %[next_ip]\n\t"   /* restore EIP   */    \ 将下一个进程的eip压栈,next进程的栈顶就是他的的起点
          __switch_canary                   \
          "jmp __switch_to\n"  /* regparm call  */ \ 
          "1:\t"                        \
          "popl %%ebp\n\t"     /* restore EBP   */    \ 
          "popfl\n"         /* restore flags */  \ 开始执行下一个进程的第一条命令
                                 \
          /* output parameters */                \
          : [prev_sp] "=m" (prev->thread.sp),     \
            [prev_ip] "=m" (prev->thread.ip),        \
            "=a" (last),                 \
                                 \
            /* clobbered output registers: */     \
            "=b" (ebx), "=c" (ecx), "=d" (edx),      \
            "=S" (esi), "=D" (edi)             \
                                      \
            __switch_canary_oparam                \
                                 \
            /* input parameters: */                \
          : [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)

通过系统调用,用户空间的应用程序就会进入内核空间,由内核代表该进程运行于内核空间,这就涉及到上下文的切换,用户空间和内核空间具有不同的地址映射,通用或专用的寄存器组,而用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行.
所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值