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

学号后三位:110
原创作品转载请注明出处
本实验来源https://github.com/mengning/linuxkernel/

实验内容

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

实验环境

ubuntu 18.04 虚拟机 
VMware workstation 14 Player

实验步骤

一、阅读理解task_struct数据结构

为了管理进程,操作系统使用数据结构来代表处理不同的实体,这个数据结构就是通常所说的进程描述符或进程控制块(PCB)。
在Linux中,task_struct其实就是通常所说的PCB。它定义在include/linux/sched.h文件中。

task_struct结构体中的主要信息:

1. 进程状态:记录进程是处于运行状态还是等待状态 
2. 调度信息:进程由哪个函数调度,具体怎样调度等 
3. 进程之间的通讯状况 
4. 进程之间的亲属关系:在父进程和子进程之间有task_struct类型的指针,将父进程和子进程联系起来 
5. 时间数据信息:每个进程执行所占用CPU的时间 
6. 进程的标志 
7. 进程的标识符:该进程唯一的标识符用来区别其他进程 
8. 信号处理信息 
9. 文件信息:可以进行读写操作的一些文件的信息 
10. 页面管理信息 
11. 优先级:相对于其他进程的优先级 
12. ptrace系统调用 
13. 虚拟内存处理

进程状态
Linux中的进程由多种状态,在运行的过程中,进程会随着调度在多种情况下转换,进程的状态信息是进程进行调度的对换的依据。

volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
int exit_state;

内核中状态的表示有以下几种:

#define TASK_RUNNING    0      //表示进程要么正在执行,要么正要准备执行。
#define TASK_INTERRUPTIBLE  1  //表示进程被阻塞(睡眠),直到某个条件变为真。条件一旦达成,进程的状态就被设置为TASK_RUNNING。
#define TASK_UNINTERRUPTIBLE  2  //与TASK_INTERRUPTIBLE类似,除了不能通过接受一个信号来唤醒以外。 
#define __TASK_STOPPED      4  //表示进程被停止执行。 
#define __TASK_TRACED       8  //表示进程被debugger等进程监视。
/* in tsk->exit_state */  
#define EXIT_ZOMBIE     16  //表示进程的执行被终止,但是其父进程还没有使用wait()等系统调用来获知它的终止信息。 
#define EXIT_DEAD       32  //表示进程的最终状态。
/* in tsk->state again */  
#define TASK_DEAD       64  
#define TASK_WAKEKILL       128  
#define TASK_WAKING     256  

进程的标志
当前进程的标志,用于内核识别当前进程的状态,以备下一步操作。

unsigned int flags; /* per process flags, defined below */

flags的取值有以下几种情况:

#define PF_KSOFTIRQD    0x00000001  /* I am ksoftirqd */  
#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_WQ_WORKER    0x00000020  /* I'm a workqueue worker */  
#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_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 freezable */  
#define PF_FREEZER_NOSIG 0x80000000 /* Freezer won't send signals to it */  

进程的标识符
每一个进程都拥有自己的进程标识符、用户标识符、组标识符 。
进程标识符PID是用来表示不同进程的,每一个进程都有唯一的标识符,内核就是通过这个标识符来识别不同的进程的。

pid_t pid;    //进程标识符 
pid_t tgid;    //线程的组号

进程间的亲属关系
进程的创建是具有继承关系的,一个进程可以创建多个子进程,该进程是这些子进程的父进程,这些子进程之间具有兄弟的关系。
在创建子进程的时候,子进程会继承父进程的大部分信息,也就是说子进程会将父进程的task_struct结构体中的大部分信息拷贝过来,除过pid,因而系统需要记录这些亲属关系,以便进程之间的协作。
每个进程的task_struct结构体中有许多指针,这些指针将所有的进程的task _struct结构连接起来,构成了一棵进程树。

/*
* pointers to (original) parent process, youngest child, younger sibling,
* older sibling, respectively.  (p->father can be replaced with
* p->real_parent->pid)
*/
struct task_struct __rcu *real_parent; /* real parent process */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
/*
* children/sibling forms the list of my natural children
*/
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 */

ptrace系统调用
ptrace系统调用提供了父进程可以观察和控制子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。
基本原理:当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号,都会被转发给父进程,而子进程被阻塞。而父进程收到信号后,就可以对停下来的子进程进行检查和修改,然后让子进程继续运行。我们常用的调试工具gdb就是基于ptrace来实现的。

unsigned int ptrace;  
struct list_head ptraced;  
struct list_head ptrace_entry;  
unsigned long ptrace_message;  
siginfo_t *last_siginfo; /* For ptrace use.  */  
#ifdef CONFIG_HAVE_HW_BREAKPOINT  
atomic_t ptrace_bp_refcnt;  
#endif 

进程的调度信息
进程调度是利用这部分信息来决定进程执行的有限次序,结合着进程的状态信息来保证进程合理有序的运行。

const struct sched_class *sched_class;  //调度类
struct sched_entity se;    //普通进程的调用实体
struct sched_rt_entity rt;    //实时进程的调用实体

时间数据信息

cputime_t utime, stime, utimescaled, stimescaled;  
//utime/stime用于记录进程在用户态/内核态下所经过的节拍数(定时器)
//utimescaled/stimescaled是用于记录进程在用户态/内核态的运行时间,但它们以处理器的频率为刻度
cputime_t gtime;    //是以节拍计数的虚拟机运行时间
cputime_t prev_utime, prev_stime;    //先前的运行时间
unsigned long nvcsw, nivcsw; /* context switch counts */
//nvcsw/nivcsw是自愿(voluntary)/非自愿(involuntary)上下文切换计数
struct timespec start_time;         /* monotonic time */
struct timespec real_start_time;    /* boot based time */
//start_time和real_start_time都是进程创建时间,real_start_time还包含了进程睡眠时间,常用于/proc/pid/stat

/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
unsigned long min_flt, maj_flt;
struct task_cputime cputime_expires;
struct list_head cpu_timers[3];

进程间的通信
如果多个进程在一个任务上执行协作,那么就需要这些进程可以相互访问对方的资源,相互通信。
Linux中的主要进程通信方式有:管道、信号量、内存共享、信号和消息队列

#ifdef CONFIG_SYSVIPC  
/* ipc stuff */  
    struct sysv_sem sysvsem;  
#endif

文件
进程可以打开或者关闭文件,文件属于系统资源,Linux内核要对进程使用文件的情况进行记录。
task_struct结构体中有两个数据结构用于描述进程与文件相关的信息。其中fs _struct中描述了两个VFS索引节点,这两个索引节点叫做root和pwd,分别指向进程的可执行影响所对应的根目录和当前目录或者工作目录。file _struct结构用来记录了进程打开的文件的描述符。

/* file system info */  
    int link_count, total_link_count;  
/* filesystem information */  
    struct fs_struct *fs;  
/* open file information */  
    struct files_struct *files; 

信号处理信息

struct signal_struct *signal;    //指向进程的信号描述符。 
struct sighand_struct *sighand;    //指向进程的信号处理程序描述符。 
sigset_t blocked, real_blocked;    //blocked表示被阻塞信号的掩码,real_blocked表示临时掩码
sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
struct sigpending pending;    //存放私有挂起信号的数据结构
unsigned long sas_ss_sp;    //信号处理程序备用堆栈的地址
size_t sas_ss_size;    //堆栈的大小
int (*notifier)(void *priv);
void *notifier_data;  //设备驱动程序常用notifier指向的函数来阻塞进程的某些信号,notifier_data指的是notifier所指向的函数可能使用的数据
sigset_t *notifier_mask;  //这些信号的位掩码

虚拟内存处理

struct mm_struct *mm, *active_mm;
//mm _struct用来描述每个进程的地址空间(虚拟空间)
//mm指向进程所拥有的内存描述符,active_mm指向进程运行时所使用的内存描述符
//对于普通进程而言,这两个指针变量的值相同。但是,内核线程不拥有任何内存描述符,所以它们的mm成员总是为NULL。
//当内核线程得以运行时,它的active_mm成员被初始化为前一个运行进程的active_mm值。

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

fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;
具体过程如下:fork() -> sys_clone() -> do_fork() -> dup_task_struct() -> copy_process() -> copy_thread() -> ret_from_fork()

1、分析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;
}

2、do_fork处理内容

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

3、进程的创建

3.1 do_fork()流程

  • 首先调用copy_process()为子进程复制出一份进程信息,如果是vfork()则初始化完成处理信息;
  • 然后调用wake_up_new_task将子进程加入调度器,为之分配CPU,如果是vfork(),则父进程等待子进程完成exec替换自己的地址空间。

3.2 copy_process()流程

  • 首先调用dup_task_struct()复制当前的task_struct,检查进程数是否超过限制;
  • 接着初始化自旋锁、挂起信号、CPU 定时器等;
  • 然后调用sched_fork初始化进程数据结构,并把进程状态设置为TASK_RUNNING,复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等;
  • 调用copy_thread()初始化子进程内核栈,为新进程分配并设置新的pid。

3.3 dup_task_struct()流程

  • 调用alloc_task_struct_node()分配一个 task_struct 节点;
  • 调用alloc_thread_info_node()分配一个 thread_info 节点,其实是分配了一个thread_union联合体,将栈底返回给 ti;
  • 最后将栈底的值 ti 赋值给新节点的栈。

3.4 copy_thread的流程

  • 获取子进程寄存器信息的存放位置;
  • 对子进程的thread.sp赋值,将来子进程运行,这就是子进程的esp寄存器的值;
  • 如果是创建内核线程,那么它的运行位置是ret_from_kernel_thread,将这段代码的地址赋给thread.ip,之后准备其他寄存器信息,退出;
  • 将父进程的寄存器信息复制给子进程;
  • 将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0;
  • 子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器。

3.5 新进程从ret_from_fork处开始执行,子进程的运行是由这几处保证的

  • dup_task_struct中为其分配了新的堆栈
  • copy_process中调用了sched_fork,将其置为TASK_RUNNING
  • copy_thread中将父进程的寄存器上下文复制给子进程,这是非常关键的一步,这里保证了父子进程的堆栈信息是一致的。
  • 将ret_from_fork的地址设置为eip寄存器的值,这是子进程的第一条指令。

问题:如何创建一个新进程

  • 通过调用do_fork来实现进程的创建;
  • 复制父进程PCB–task_struct来创建一个新进程,要给新进程分配一个新的内核堆栈;
  • 修改复制过来的进程数据,比如pid、进程链表等等执行copy_process和copy_thread
  • 成功创建新进程

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

设置断点到sys_clone()
在这里插入图片描述
在menuOS中调用fork()
在这里插入图片描述
函数调用栈如下:
_do_fork()->copy_process()->dup_task_struct(),copy_thread_tls()
在这里插入图片描述
先进入_do_fork()
在这里插入图片描述
再进入copy_process()
image.png-109.4kB

然后是dup_task_struct()

接下来是copy_thread_tls()

然后fork完成
在这里插入图片描述
在这里插入图片描述
问题:新进程是从哪里开始执行的?为什么从哪里能顺利执行下去?执行起点与内核堆栈如何保证一致?

1.新进程是从哪里开始执行的?为什么从哪里能顺利执行下去?
函数copy_process中的copy_thread()

int copy_thread(unsigned long clone_flags, unsigned long sp,
    unsigned long arg, struct task_struct *p)
{
    ...
    *childregs = *current_pt_regs();
    childregs->ax = 0;
    if (sp)
        childregs->sp = sp;
    p->thread.ip = (unsigned long) ret_from_fork;
    ...
}

childregs->ax = 0;这段代码将子进程的 eax 赋值为0

子进程执行ret_from_fork

ENTRY(ret_from_fork)
    CFI_STARTPROC
    pushl_cfi %eax
    call schedule_tail
    GET_THREAD_INFO(%ebp)
    popl_cfi %eax
    pushl_cfi $0x0202       # Reset kernel eflags
    popfl_cfi
    jmp syscall_exit
    CFI_ENDPROC
END(ret_from_fork)

p->thread.ip = (unsigned long) ret_from_fork;这句代码将子进程的 ip 设置为 ret_form_fork 的首地址,因此子进程是从 ret_from_fork 开始执行的。
因此,函数copy_process中的copy_thread()决定了子进程从系统调用中返回后的执行。

2.执行起点与内核堆栈如何保证一致?
在ret_from_fork之前,也就是在copy_thread()函数中:*childregs = *current_pt_regs();
该句将父进程的regs参数赋值到子进程的内核堆栈,*childregs的类型为pt_regs,里面存放了SAVE ALL中压入栈的参数。故在之后的RESTORE ALL中能顺利执行下去。

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

1、编译链接的过程
在这里插入图片描述
2、ELF可执行文件格式

一个可重定位(relocatable)文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件。
一个可执行(executable)文件保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来创建程序进程映象。
一个共享object文件保存着代码和合适的数据,用来被不同的两个链接器链接。

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

编写程序hello.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
    int pid;
    /*fork another process*/
    pid = fork();
    if(pid < 0)
    {
        /*error occurred*/
        fprintf(stderr,"Fork Failed!\n");
        exit(-1);
    }
    else if(pid == 0)
    {
        /* child process */
        execlp("/bin/ls","ls",NULL);
    }
    else
    {
        /* parent process */
        /* parent will wait for the child to complete */
        wait(NULL);
        printf("Child Complete\n");
        exit(0);
    }
}

在这里插入图片描述

六、使用gdb跟踪分析一个execve系统调用内核处理函数do_execve ,验证您对Linux系统加载可执行程序所需处理过程的理解

设置三个断点:sys_execve,load_elf_binary,start_thread
在这里插入图片描述
先sys_execve系统调用
在这里插入图片描述
然后load_elf_binary()
在这里插入图片描述
再start_thread()
在这里插入图片描述
最后调用完毕
在这里插入图片描述

七、特别关注新的可执行程序是从哪里开始执行的?为什么execve系统调用返回后新的可执行程序能顺利执行?对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同?

新的可执行程序通过修改内核堆栈eip作为新程序的起点,
从new_ip开始执行后start_thread把返回到用户态的位置从int 0x80的下一条指令变成新加载的可执行文件的入口位置。
当执行到execve系统调用时,进入内核态。
用execve()加载的可执行文件覆盖当前进程的可执行程序,当execve系统调用返回时,返回新的可执行程序的执行起点(main函数),所以execve系统调用返回后新的可执行程序能顺利执行。
execve系统调用返回时,如果是静态链接,elf_entry指向可执行文件规定的头部(main函数对应的位置0x8048***);如果需要依赖动态链接库,elf_entry指向动态链接器的起点。动态链接主要是由动态链接器ld来完成的。

八、理解Linux系统中进程调度的时机,可以在内核代码中搜索schedule()函数,看都是哪里调用了schedule(),判断我们课程内容中的总结是否准确;

调用地方:

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

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

设置四个断点:schedule,context_switch,switch_to,pick_next_task
在这里插入图片描述
函数调用栈:
__schedule->pick_next_task->context_switch->__switch_to
在这里插入图片描述

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

1.关键函数的调用关系:

schedule() --> context_switch() --> switch_to --> __switch_to()

2.汇编代码分析

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()
  • 恢复next上次被调离时推进堆栈的内容。从现在开始,next进程就成为当前进程而真正开始执行

实验总结

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值