Linux进程创建、执行和切换过程理解

Linux进程创建、执行和切换过程理解

学号:282

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

实验内容

  • 进程的创建
    1. 阅读理解task_struct数据结构
    2. 分析fork函数对应的内核处理过程do_fork,理解创建一个新进程如何创建和修改task_struct数据结构
    3. 使用gdb跟踪分析一个fork系统调用内核处理函数do_fork
  • 可执行文件的加载
    1. 理解编译链接的过程和ELF可执行文件格式
    2. 编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接
    3. 使用gdb跟踪分析一个execve系统调用内核处理函数do_execve
  • 进程切换
    1. 分析一个schedule()函数
    2. 仔细分析switch_to中的汇编代码

实验过程

一、进程的创建

task_struct分析

为了描述进程的信息,我们引入了进程控制块这个数据结构。进程控制块至少应该包含进程标识(是进程的唯一标识,PID),还有进程的优先级,记录进程的上下文信息,记录进程下一次下一条指令的地址,进程中的程序的地址,等等。下面就task_struct结构体的数据成员进行简单分类:

  1. volatile long state. 进程的状态,可能取值如下

    #define TASK_RUNNING        0        //进程要么正在执行,要么准备执行
    #define TASK_INTERRUPTIBLE  1        //可中断的睡眠,可以通过一个信号唤醒
    #define TASK_UNINTERRUPTIBLE    2    //不可中断睡眠,不可以通过信号进行唤醒
    #define __TASK_STOPPED      4        //进程停止执行
    #define __TASK_TRACED       8        //进程被追踪
    /* in tsk->exit_state */ 
    #define EXIT_ZOMBIE     16           //僵尸状态的进程,表示进程被终止,但是父进程还没有获取它的终止信息,比如进程有没有执行完等信息。                     
    #define EXIT_DEAD       32           //进程的最终状态,进程死亡。
    /* in tsk->state again */ 
    #define TASK_DEAD       64           //死亡
    #define TASK_WAKEKILL       128  //唤醒并杀死的进程
    #define TASK_WAKING     256      //唤醒进程 
  2. 进程的唯一标识

     pid_t pid;      //进程的唯一标识
     pid_t tgid; // 线程组的领头线程的pid成员的值
  3. unsigned int flags.进程的标记

    #define PF_ALIGNWARN    0x00000001    /* Print alignment warning msgs */
                        /* Not implemented yet, only for 486*/
    #define PF_STARTING    0x00000002    /* being created */
    #define PF_EXITING    0x00000004    /* getting shut down */
    #define PF_EXITPIDONE    0x00000008    /* pi exit done on shut down */
    #define PF_VCPU        0x00000010    /* I'm a virtual CPU */
    #define PF_FORKNOEXEC    0x00000040    /* forked but didn't exec */
    #define PF_MCE_PROCESS  0x00000080      /* process policy on mce errors */
    #define PF_SUPERPRIV    0x00000100    /* used super-user privileges */
    #define PF_DUMPCORE    0x00000200    /* dumped core */
    #define PF_SIGNALED    0x00000400    /* killed by a signal */
    #define PF_MEMALLOC    0x00000800    /* Allocating memory */
    #define PF_FLUSHER    0x00001000    /* responsible for disk writeback */
    #define PF_USED_MATH    0x00002000    /* if unset the fpu must be initialized before use */
    #define PF_FREEZING    0x00004000    /* freeze in progress. do not account to load */
    #define PF_NOFREEZE    0x00008000    /* this thread should not be frozen */
    #define PF_FROZEN    0x00010000    /* frozen for system suspend */
    #define PF_FSTRANS    0x00020000    /* inside a filesystem transaction */
    #define PF_KSWAPD    0x00040000    /* I am kswapd */
    #define PF_OOM_ORIGIN    0x00080000    /* Allocating much memory to others */
    #define PF_LESS_THROTTLE 0x00100000    /* Throttle me less: I clean memory */
    #define PF_KTHREAD    0x00200000    /* I am a kernel thread */
    #define PF_RANDOMIZE    0x00400000    /* randomize virtual address space */
    #define PF_SWAPWRITE    0x00800000    /* Allowed to write to swap */
    #define PF_SPREAD_PAGE    0x01000000    /* Spread page cache over cpuset */
    #define PF_SPREAD_SLAB    0x02000000    /* Spread some slab caches over cpuset */
    #define PF_THREAD_BOUND    0x04000000    /* Thread bound to specific cpu */
    #define PF_MCE_EARLY    0x08000000      /* Early kill for mce process policy */
    #define PF_MEMPOLICY    0x10000000    /* Non-default NUMA mempolicy */
    #define PF_MUTEX_TESTER    0x20000000    /* Thread belongs to the rt mutex tester */
    #define PF_FREEZER_SKIP    0x40000000    /* Freezer should not count it as freezeable */
    #define PF_FREEZER_NOSIG 0x80000000    /* Freezer won't send signals to it */
  4. 进程之间的亲属关系。

    struct task_struct *real_parent; /* real parent process 父进程*/
    struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports 终止时,向父进程发送信号*/
    struct list_head children;    /* list of my children 子进程链表*/
    struct list_head sibling;    /* linkage in my parent's children list */
    struct task_struct *group_leader;    /* threadgroup leader */
  5. 进程调度信息

    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;
    unsigned int policy;                 // 进程调度策略
  6. 时间数据成员

    cputime_t utime, stime, utimescaled, stimescaled;
    cputime_t gtime;
    cputime_t prev_utime, prev_stime;    // 记录当前的运行时间(用户态和内核态)
    unsigned long nvcsw, nivcsw;         // 自愿/非自愿上下文切换计数
    struct timespec start_time;          // 进程的开始执行时间    
    struct timespec real_start_time;     // 进程真正的开始执行时间
    unsigned long min_flt, maj_flt;
    struct task_cputime cputime_expires;// cpu执行的有效时间
    struct list_head cpu_timers[3];      // 用来统计进程或进程组被处理器追踪的时间
    struct list_head run_list;
    unsigned long timeout;               // 当前已使用的时间(与开始时间的差值)
    unsigned int time_slice;         // 进程的时间片的大小
    int nr_cpus_allowed;

gdb跟踪分析do_fork

  1. 修改MenuOS中的test.c,加入系统调用的代码

    #include <stdio.h>
    #include <unistd.h>
    int Fork(int argc, char **argv){
        pid_t pid;
    
        pid=fork();
        if(pid==-1)
            printf("fork error\n");
        else if(pid==0){
            printf("the returned value is %d\n",pid);   
            printf("The pid is %d,now in child process\n",getpid());
        }
        else{
            printf("the returned value is %d\n",pid);   
            printf("The pid is %d,now in farther process\n",getpid());  
        }
        return 0;
    }
  2. 启动MenuOS,使用gdb追踪

    打断点

    追踪断点

  3. do_fork分析

    Linux系统中,除第一个进程是被捏造出来的,其他进程都是通过do_fork()复制出来的,方法声明如下。

    int do_fork(unsigned long clone_flags, unsigned long stack_start,struct pt_regs *regs, unsigned long stack_size)

    为了关注do_fork主体,省略了部分细节

    第一步:然是创建新的进程,首先需要申请进程最基本的单位task_struct结构

    p = alloc_task_struct();
    if (!p)
    goto fork_out;
    
    *p = *current;

    第二步:获取一个空闲的pid

    static int get_pid(unsigned long flags)

    第三步:复制各种资源

    /* copy all the process information */
    if (copy_files(clone_flags, p))             //复制文件描述符
    goto bad_fork_cleanup;
    if (copy_fs(clone_flags, p))
    goto bad_fork_cleanup_files;
    if (copy_sighand(clone_flags, p))            //复制信号量
    goto bad_fork_cleanup_fs;                    
    if (copy_mm(clone_flags, p))                 //复制虚存空间
    goto bad_fork_cleanup_sighand;
    retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);  //复制系统堆栈

    第四步:copy_thread

    #define savesegment(seg,value) \
    asm volatile("movl %%" #seg ",%0":"=m" (*(int *)&(value)))
    
    int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
    unsigned long unused,
    struct task_struct * p, struct pt_regs * regs)
    {
    struct pt_regs * childregs;
       //获取子进程系统堆栈顶部指针
    childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1;    
    struct_cpy(childregs, regs);    //从父进程拷贝系统堆栈状态
    childregs->eax = 0;             //子进程返回值设0
    childregs->esp = esp;           //子进程用户堆栈
    
    p->thread.esp = (unsigned long) childregs;        //初次运行时,子进程系统堆栈位置
    p->thread.esp0 = (unsigned long) (childregs+1);    //子进程系统空间堆栈顶部
    
    p->thread.eip = (unsigned long) ret_from_fork;     //下次运行时的切入点
    
    savesegment(fs,p->thread.fs);
    savesegment(gs,p->thread.gs);
    
    unlazy_fpu(current);
    struct_cpy(&p->thread.i387, ¤t->thread.i387);
    
    return 0;
    }

    一个完整的子进程已经诞生了,这时子进程还不在进程可执行队列中,不能接受调度,但是随后就会通过wake_up_process(p)将其加入可执行队列接受调度。

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

gdb跟踪do_execve

  1. 修改MenuOS中test.c源文件,加入execlp("/bin/ls","ls",NULL);重新编译后使用gdb追踪。

  2. do_execve断点

小结:

可执行程序的elf格式

我们编译获得一个可执行程序,会被加载到内存进行执行。这个可执行程序是有一定的格式的,其中现在用的比较多的就是elf格式。对于一个可执行文件,必须保护程序运行必要的相关信息,比如说这个程序依赖那些动态链接库,程序的入口地址等,我们通过这样一种格式化的方式,保存了这些信息共系统进行解析,从而可以加载我们的代码段和数据段进入内存。

程序的加载以及执行过程的关键行为分析

execve函数会获得一些shell通过函数调用的机制传递的参数进行执行,经历上面调试过程讲解的一系列步骤:**execve=>do_execve=>do_execve_common=>(do_open_exec/exec_binprm)=>**

search_binary_handler=>list_for_each_entry=>load_elf_binary=>start_thread

总体来讲,就是通过构造一些结构体,首先对文件进行打开操作,并且在结构体中保存一些必要的信息,然后根据文件的类型,使用不同的模块对文件进行解析,解析的过程中知道了这是一个动态链接的程序还是一个静态链接的程序,根据这个设置内核栈中的ip,这样在进程调度的时候,就有了合适的入口。

三、进程切换

​ fork会创建一个新的进程,加载文件并进行执行。在这个过程中,涉及到了两个进程之间的切换。我们依然使用上一步的环境,对fork系统调用进行调试,来完成这个分析。

context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next){
  struct mm_struct *mm, *oldmm;
  prepare_task_switch(rq, prev, next);
  mm = next->mm;<br>   switch_to(prev,next,prev);//切换寄存器的状态和堆栈的切换

我们主要来看switch_to函数

#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   */ \//保存到了output里面,栈顶里面.
             "movl %[next_sp],%%esp\n\t"    /* restore ESP   */ \//完成了内核堆栈的切换,接下来就是在另外一个进程的栈空间了
             "movl $1f,%[prev_ip]\n\t"  /* save    EIP   */ \//当前进程的eip保持
             "pushl %[next_ip]\n\t" /* restore EIP   */
             __switch_canary                    \
             "jmp __switch_to\n"    /* regparm call  */ \//jmp的函数完成以后,需要iret,把ip弹出来了,这样就到了下一行代码执行.
 
             "1:\t"                     \//新设置的IP是从这里开始的,也就是movl $1f,从这里开始就说明是另外一个进程了。所以内核堆栈先切换好,执行了两句,用的是新的进程的内核堆栈,但是确是在原来的进程的ip继续执行。
             "popl %%ebp\n\t"       /* restore EBP   */ \
             "popfl\n"          /* restore flags */ \ //原来的进程切换的时候,曾经设置过save ebp和save flags,所以这里就需要pop来恢复
                                    \
             /* output parameters */                \
             : [prev_sp] "=m" (prev->thread.sp),     \ //分别表示内核堆栈以及当前进程的eip
               [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)                \  //用a和d来传递参数...
                                    \
               __switch_canary_iparam               \
                                    \
             : /* reloaded segment registers */         \
            "memory");                  \
} while (0)

小结:

先是pushfl \n\t等语句,用来在当前的栈中保持flags,以及当前的ebp,准备进行进程的切换。然后是当前的esp,会保存在当前进程的thread结构体中。其中的movl $1f,%[prev_ip]则是保存当前进程的ip为代码中标号1的位置。然后是resotre ebp和flags的语句,用于恢复ebp和flags到寄存器中,这些值是保持在内核栈中的。这样,对于新的进程,我们使用c继续执行,就可以走到ret_from_fork中了。

猜你喜欢

转载自www.cnblogs.com/hitomeng/p/10599189.html
今日推荐