Linux进程和程序

学号最后三位编号:403
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/

实验要求

实验:
1.从整理上理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换
2.阅读理解task_struct数据结构http://codelab.shiyanlou.com/xref/linux3.18.6/include/linux/sched.h#1235;
3.分析fork函数对应的内核处理过程do_fork,理解创建一个新进程如何创建和修改task_struct数据结构;
4.使用gdb跟踪分析一个fork系统调用内核处理函数do_fork ,验证您对Linux系统创建一个新进程的理解,特别关注新进程是从哪里开始执行的?为什么从那里能顺利执行下去?即执行起点与内核堆栈如何保证一致。
5.理解编译链接的过程和ELF可执行文件格式;
6.编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接;
7.使用gdb跟踪分析一个execve系统调用内核处理函数do_execve ,验证您对Linux系统加载可执行程序所需处理过程的理解;
8.特别关注新的可执行程序是从哪里开始执行的?为什么execve系统调用返回后新的可执行程序能顺利执行?对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同?
9.理解Linux系统中进程调度的时机,可以在内核代码中搜索schedule()函数,看都是哪里调用了schedule(),判断我们课程内容中的总结是否准确;
使用gdb跟踪分析一个schedule()函数 ,验证您对Linux系统进程调度与进程切换过程的理解;
10.特别关注并仔细分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系;

实验环境

VMware Workstation 14 Player虚拟机 Ubuntu 64位系统

实验过程

阅读理解task_struct数据结构

代码链接.
task_struct实际上就是进程PCB以下是pcb的重要参数:
volatile long state;//表示进程的当前状态:
unsigned long flags; //进程标志:
long priority; //进程优先级。
long counter; //在轮转法调度时表示进程当前还可运行多久。
unsigned long policy; //进程调度策略

分析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以调用copy_process开始, 后者执行生成新的进程的实际工作, 并根据指定的标志复制父进程的数据。在子进程生成后, 内核必须执行下列操作:
调用 copy_process 为子进程复制出一份进程信息,如果是 vfork(设置了CLONE_VFORK和ptrace标志)初始化完成处理信息,调用 wake_up_new_task 将子进程加入调度器,为之分配 CPU,如果是 vfork,父进程等待子进程完成 exec 替换自己的地址空间
copy_process流程
调用 dup_task_struct 复制当前的 task_struct,检查进程数是否超过限制
初始化自旋锁、挂起信号、CPU 定时器等,调用 sched_fork 初始化进程数据结构,并把进程状态设置为 TASK_RUNNING,复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等,调用 copy_thread_tls 初始化子进程内核栈,为新进程分配并设置新的 pid
copy_thread()
获取子进程寄存器信息的存放位置,对子进程的thread.sp赋值,将来子进程运行,这就是子进程的esp寄存器的值。如果是创建内核线程,那么它的运行位置是ret_from_kernel_thread,将这段代码的地址赋给thread.ip,之后准备其他寄存器信息,退出,将父进程的寄存器信息复制给子进程。将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0.子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器。

使用gdb跟踪分析do_fork

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可执行文件的装载

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

在这里插入图片描述
可重定位文件:一般是中间文件,需要和其他文件一起来创建可执行文件、静态库文件、共享目标文件。
可执行文件:文件中保存着一个用来执行的文件。
共享目标文件:指可以被可执行文件或其他库文件使用的目标。

编程使用exec*库函数加载

首先编辑一个hello.c

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

按上述步骤生成预处理文件、汇编代码、目标代码和可执行文件:

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

使用gdb追踪do_execve

首先给do_execve函数打上断点,当调用新的可执行程序时,会先进入内核态调用do_execve处理函数,并使用堆栈对原来的现场进行保护。然后,根据返回的可执行文件的地址,对当前可执行文件进行覆盖。由于返回地址为调用可执行文件的main函数入口,所以可以继续执行该文件
在这里插入图片描述

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

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

分析switch_to中的汇编代码

schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换
next = pick_next_task(rq, prev);//进程调度算法都封装这个函数内部
context_switch(rq, prev, next);//进程上下文切换
switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程
31#define switch_to(prev, next, last)                    \
32do {                                 \
 /*                              \
  * 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");                  \
77} while (0)

进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
同理,硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理,中断上下文就可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程的环境。
Linux内核工作在进程上下文或者中断上下文。提供系统调用服务的内核代码代表发起系统调用的应用程序运行在进程上下文;另一方面,中断处理程序,异步运行在中断上下文。中断上下文和特定进程无关。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值