• 结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程


    实验要求

    结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程

    • 以fork和execve系统调用为例分析中断上下文的切换
    • 分析execve系统调用中断上下文的特殊之处
    • 分析fork子进程启动执行时进程上下文的特殊之处
    • 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程

    实验过程

    fork系统调用:

      fork()函数又叫计算机程序设计中的分叉函数,fork它可以建立一个新进程,把当前的进程分为父进程和子进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的PID,而子进程中的返回值则返回 0。因此,可以通过返回值来判定该进程是父进程还是子进程。fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。

      fork系统调用用于从已存在进程中创建一个新进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的进程号,而子进程中的返回值则返回 0。因此,可以通过返回值来判定该进程是父进程还是子进程。使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等,而子进程所独有的只有它的进程号、计时器等。因此可以看出,使用fork系统调用的代价是很大的,它复制了父进程中的数据段和堆栈段里的绝大部分内容,使得fork系统调用的执行速度并不很快。

    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;
    
        /*
        * Determine whether and which event to report to ptracer.  When
        * called from kernel_thread or CLONE_UNTRACED is explicitly
        * requested, no event is reported; otherwise, report if the event
        * for the type of forking is enabled.
        */
        if (!(clone_flags & CLONE_UNTRACED)) {
            if (clone_flags & CLONE_VFORK)
                trace = PTRACE_EVENT_VFORK;
            else if ((clone_flags & CSIGNAL) != SIGCHLD)
                trace = PTRACE_EVENT_CLONE;
            else
                trace = PTRACE_EVENT_FORK;
    
            if (likely(!ptrace_event_enabled(current, trace)))
                trace = 0;
        }
    
        p = copy_process(clone_flags, stack_start, stack_size,
            child_tidptr, NULL, trace);
        /*
        * Do this prior waking up the new thread - the thread pointer
        * might get invalid after that point, if the thread exits quickly.
        */
        if (!IS_ERR(p)) {
            struct completion vfork;
            struct pid *pid;
    
            trace_sched_process_fork(current, p);
    
            pid = get_task_pid(p, PIDTYPE_PID);
            nr = pid_vnr(pid);
    
            if (clone_flags & CLONE_PARENT_SETTID)
                put_user(nr, parent_tidptr);
    
            if (clone_flags & CLONE_VFORK) {
                p->vfork_done = &vfork;
                init_completion(&vfork);
                get_task_struct(p);
            }
    
            wake_up_new_task(p);
    
            /* forking complete and child started to run, tell ptracer */
            if (unlikely(trace))
                ptrace_event_pid(trace, pid);
    
            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;
    }

    整段代码涉及到很多工作的处理,但是整个创建新进程是在上述代码中的copy_process()这个函数实现的。copy_process()函数复制父进程、获得pid、调用wake_up_new_task将子进程加入就绪队列等待调度执行等。在Linux中,除了0号进程由手工创建外,其他进程都是通过复制已有进程创建而来,而这正是fork的主要工作,具体的任务交由copy_process完成。

    copy_process()的执行逻辑为:

    1)调用 dup_task_struct 复制当前进程的task_struct;

    2)将新进程相关的数据结构和进程状态初始化;

    3)复制父进程信息;

    4)调用 copy_thread_tls 初始化子进程内核栈;

    5)设置子进程pid;

    6)建立亲属关系链接,并将新进程插入全局进程队列 copy_thread_tls: 拷贝父进程系统堆栈内容;

    7)执行childregs->ax = 0语句,该代码将子进程的 eax 赋值为0,do_fork返回后会从eax读取返回值,所以为0;

    8)执行p->thread.eip = (unsigned long) ret_from_fork;将子进程的 eip 设置为 ret_form_fork 的首地址,因此子进程是从 ret_from_fork 开始执行的。

    execve系统调用:

      execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。execve() 系统调用通常与 fork() 系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve() 来运行指定的程序。

    代码如下:

    static int do_execve_common(struct filename *filename,struct user_arg_ptr argv,struct user_arg_ptr envp)
    {
        struct linux_binprm *bprm;  
        struct file *file;
        struct files_struct *displaced;
        int retval;
        current->flags &= ~PF_NPROC_EXCEEDED;  
        retval = unshare_files(&displaced);  
        bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
        retval = prepare_bprm_creds(bprm);   
        check_unsafe_exec(bprm);             
        current->in_execve = 1;
        file = do_open_exec(filename);       
        sched_exec();
        bprm->file = file;
        bprm->filename = bprm->interp = filename->name;
        retval = bprm_mm_init(bprm);       
        bprm->argc = count(argv, MAX_ARG_STRINGS);
        bprm->envc = count(envp, MAX_ARG_STRINGS);
        retval = prepare_binprm(bprm);     
        retval = copy_strings_kernel(1, &bprm->filename, bprm);
        bprm->exec = bprm->p;
        retval = copy_strings(bprm->envc, envp, bprm);
        retval = copy_strings(bprm->argc, argv, bprm);
        
        <strong>retval = exec_binprm(bprm);</strong>
        
        current->fs->in_exec = 0;
        current->in_execve = 0;
        acct_update_integrals(current);
        task_numa_free(current);
        free_bprm(bprm);
        putname(filename);
        if (displaced)
            put_files_struct(displaced);
        return retval;
    }

    execve具体的调用过程为:

    1.陷入内核

    2.加载新的进程,并且将新的进程覆盖原进程的空间

    3.将新的进程的入口地址设置为IP值

    4.切换回用户态,继续执行原来的进程。

     总结:Linux系统的一般执行过程

    中断上下文的一般处理过程

    1)正在运行的用户态进程Y

    2)发生中断——CPU完成load cs:rip(entry of a specific ISR),即跳转到中断处理程序入口。(CPU自动帮我们完成的)

    3)中断上下文切换,具体包括如下⼏点:

      swapgs指令保存现场,可以理解CPU通过swapgs指令给当前CPU寄存器状态做了⼀个快照。

      rsp point to kernel stack,加载当前进程内核堆栈栈顶地址到RSP寄存器。快速系统调⽤是由系统调用入口处的汇编代码实现⽤户堆栈和内核堆栈的切换。

      save cs:rip/ss:rsp/rflags:将当前CPU关键上下⽂压⼊进程X的内核堆栈,快速系统调用是由系统调用入口处的汇编代码实现的。

    此时完成了中断上下⽂切换,即从进程X的⽤户态到进程X的内核态。

    4)中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换

    5)标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)

    6)restore_all //恢复现场,中断上下文恢复,与(3)中断上下⽂切换相对应。注意这⾥是进程Y的中断处理过程中,而(3)中断上下文切换是在进程X的中断处理过程中,因为内核堆栈从进程X切换到进程Y了

    7)iret - pop cs:eip/ss:esp/eflags from kernel stack(返回执行的是Y进程曾经发生中断时用户态的下一条指令,恢复现场,恢复不是X进程的现场,而是曾经保存的Y进程现场)

    8)继续运行用户态进程Y

  • 相关阅读:
    【核心算法1】双指针问题
    关于博客园主题
    正交工具allpairs使用
    postman设置变量
    WebDriver驱动下载地址
    MySQL语法基础
    异或
    测试——pytest库上手
    and 和 or 的语句运算
    爬虫——Scrapy中选择器的基本使用(转)
  • 原文地址:https://www.cnblogs.com/zzydexiaowu/p/13127484.html
Copyright © 2020-2023  润新知