ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

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

2020-06-15 23:06:39  阅读:351  来源: 互联网

标签:fork do 调用 struct 切换 Linux 进程 上下文 execve


实验要求

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

  • 以fork和execve系统调用为例分析中断上下文的切换

  • 分析execve系统调用中断上下文的特殊之处

  • 分析fork子进程启动执行时进程上下文的特殊之处

  • 以系统调用作为特殊的中断,结合中断上下文切换和进程上下文切换分析Linux系统的一般执行过程

 


以fork和execve系统调用为例分析中断上下文的切换

1、fork()函数

我们知道,进程是程序执行的最小单位,一个进程有完整的地址空间、程序计数器等,如果想创建一个新的进程,使用函数 fork 就可以。

函数原型:

pit_t fork(void)

返回:在子进程中为0,在父进程中为子进程ID,若出错则为-1

在调用该函数的进程(即为父进程)中返回的是新派生的进程 ID 号,在子进程中返回的值为 0。想要知道当前执行的进程到底是父进程,还是子进程,只能通过返回值来进行判断。fork 函数实现的时候,实际上会把当前父进程的所有相关值都克隆一份,包括地址空间、打开的文件描述符、程序计数器等,就连执行代码也会拷贝一份,新派生的进程的表现行为和父进程近乎一样,就好像是派生进程调用过 fork 函数一样。为了区别两个不同的进程,实现者可以通过改变 fork 函数的栈空间值来判断,对应到程序中就是返回值的不同。

if(fork() == 0){
  do_child_process(); //子进程执行代码
}else{
  do_parent_process();  //父进程执行代码
}

fork()系统调用与普通的系统调用的区别在于,普通的系统调用会返回一次,而fork()系统调用会返回两次,分别从父进程和子进程返回。

写一个简单的程序来验证fork()的行为

#include<iostream>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
int main() {

            pid_t pid;
            pid = fork();
            if (pid < 0) {
                    cout<<"fork error!"<<endl;
            } else if (pid == 0) {
                    cout<<"This is child process"<<endl;
            } else{
                    cout<<"This is parent process"<<endl;
            }
            return 0;
}

  运行后显示条件语句if下的两种情况都被打印出来了,这是因为一个是父进程输出的,而另外一个是子进程输出的

 

 

查阅linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl可知,在64位linux下,fork库函数的系统调用号为57,并且入口地址为___x64_sys_fork。其实现位于kernel/fork.c源文件中。

SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
    struct kernel_clone_args args = {
        .exit_signal = SIGCHLD,
    };
​
    return _do_fork(&args);
#else
    /* can not support in nommu mode */
    return -EINVAL;
#endif
}

可以发现,fork的底层实现是调用了__do_fork()函数。

/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 *
 * args->exit_signal is expected to be checked for sanity by the caller.
 */
long _do_fork(struct kernel_clone_args *args)
{
     ...
    
    struct pid *pid;
    struct task_struct *p;
    int trace = 0;
    long nr;
​
     ...
​
    p = copy_process(NULL, trace, NUMA_NO_NODE, args);//复制父进程的相关资源到子进程
    
     ...
​
    wake_up_new_task(p);//将子进程转换为就绪态
​
     ...
    
    put_pid(pid);
    return nr;
}

我们可以看到__do_fork函数主要作用是调用copy_process函数复制父进程和获得子进程pid、调用wake_up_new_task函数将子进程唤醒为就绪态等待调度。

/*
 * This creates a new process as a copy of the old one,
 * but does not actually start it yet.
 *
 * It copies the registers, and all the appropriate
 * parts of the process environment (as per the clone
 * flags). The actual kick-off is left to the caller.
 */
static __latent_entropy struct task_struct *copy_process(
                    struct pid *pid,
                    int trace,
                    int node,
                    struct kernel_clone_args *args)
{
    int pidfd = -1, retval;
    struct task_struct *p;
    
    ...
    
    p = dup_task_struct(current, node);//调用dup_task_struct()为子进程复制一份进程描述符,包括复制父进程的thread_info结构和内核栈

   // 检查该用户的进程数是否超过限制    if (atomic_read(&p->real_cred->user->processes) >=     task_rlimit(p, RLIMIT_NPROC)) {       // 检查该用户是否具有相关权限,不一定是root       if (p->real_cred->user != INIT_USER &&         !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))             goto bad_fork_free;     }    ... /* copy all the process information */ shm_init_task(p); //初始化子进程的内核栈 retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p, args->tls); ... ​ return p; ... }  

copy_process函数的主要作用是调用了dup_task_struct函数为子进程复制一份进程描述符,包括复制父进程的thread_info结构和内核栈。

在执行fork系统调用之后,会由内核态返回到两次:一次返回到父进程用户态,这相当于一般的系统调用返回;而另一次则返回到子进程用户态,为了实现这一点,就需要为子进程构造出合适的执行上下文,也就是初始化子进程的内核栈和进程描述符的thread字段,这就是copy_thread_tls的作用。

int copy_thread_tls(unsigned long clone_flags, unsigned long sp,unsigned long arg, struct task_struct *p, unsigned long tls)
{
    // ...
    
    frame->ret_addr = (unsigned long) ret_from_fork;
    p->thread.sp = (unsigned long) fork_frame;
    *childregs = *current_pt_regs();
    childregs->ax = 0;
    
    // ...
}

其中,ret_addr字段指定了子进程返回时的执行地址,其被设置为ret_from_fork。

thread.sp字段设置成了fork_frame起始地址,这是子进程内核栈的栈顶位置。

 

 

 

fork子进程启动执行时进程上下文的特殊之处:

调用fork系统调用的特殊之处在于,调用一次fork陷入到内核态后,会返回两次,一次返回是从父进程内核态返回到用户态,另一次返回是子进程从内核态返回到用户态。

2、execve()函数

execve系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 保持不变。execve系统调用通常与 fork系统调用配合使用。从一个进程中启动另一个程序时,通常是先fork一个子进程,然后在子进程中使用 execve变为运行指定程序的进程。在64位linux下,execve系统调用号为56,函数入口为__x64_sys_execve。

函数原型:

int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

成功则函数不会返回;失败返回-1。

函数实现:

SYSCALL_DEFINE3(execve,
        const char __user *, filename,
        const char __user *const __user *, argv,
        const char __user *const __user *, envp)
{
    return do_execve(getname(filename), argv, envp);
}

 可以看到execve调用了do_execve,而do_execve又调用了do_execveat_common,最后再调用__do_execve_file完成。

/*
 * sys_execve() executes a new program.
 */
static int __do_execve_file(int fd, struct filename *filename,
                struct user_arg_ptr argv,
                struct user_arg_ptr envp,
                int flags, struct file *file)
{
    char *pathbuf = NULL;
    struct linux_binprm *bprm;
    struct files_struct *displaced;
    int retval;
     ...
    bprm->file = file;
     ...
    retval = prepare_binprm(bprm);
     ...
    retval = copy_strings(bprm->envc, envp, bprm);
     ...
    retval = exec_binprm(bprm);
    ...
    return retval;
}

__do_execve_file的主要功能是从文件中载入ELF可执行文件并执行。其中exec_binprm函数实际执行了文件。而exec_binprm调用了search_binary_handler,这是真正替换进程镜像的地方。

static int exec_binprm(struct linux_binprm *bprm)
{
    pid_t old_pid, old_vpid;
    int ret;
​
    /* Need to fetch pid before load_binary changes it */
    old_pid = current->pid;
    rcu_read_lock();
    old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
    rcu_read_unlock();
​
    ret = search_binary_handler(bprm);
    if (ret >= 0) {
        audit_bprm(bprm);
        trace_sched_process_exec(current, old_pid, bprm);
        ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
        proc_exec_connector(current);
    }
​
    return ret;
}

 

 

execve的调用过程:

1.execve系统调用陷入内核

2.execve调用do_execve函数,将相关命令行参数和shell上下文封装起来

3.do_execve调用do_execveat_common,后者调用__do_execve_file

4.__do_execve_file打开ELF文件并把相关的信息装入linux_binprm结构体

5.__do_execve_file中调用search_binary_handler,寻找解析ELF文件的函数

6.search_binary_handler找到ELF文件解析函数load_elf_binary

7.load_elf_binary解析ELF文件,把ELF文件装入内存,修改进程的用户态堆栈(主要是把命令行参数和shell上下文加入到用户态堆栈),修改进程的数据段代码段

8.load_elf_binary调用start_thread修改进程内核堆栈(特别是内核堆栈的ip指针)

9.进程从execve返回到用户态后ip指向ELF文件的main函数地址,用户态堆栈中包含了命令行参数和shell上下文环境

 

execve系统调用中断上下文的特殊之处:

在执行execve系统调用陷入内核态时,在内核中用do_execve加载可执行文件,把当前进程的可执行程序给覆盖掉了。在执行execve系统调用返回时,返回的不是之前那个可执行程序了,而是一个新的可执行程序起始地址。


 

 Linux系统的一般执行过程

1.正在运行的用户态进程X

2.发生中断(包括异常、系统调用等),跳转到中断处理程序入口。

3.中断上下文切换,包括保存CPU寄存器状态(保存现场),加载当前进程内核堆栈栈顶地址到RSP寄存器,将CPU上下文压入进程X的内核堆栈

4.中断处理过程中或中断返回前调⽤了schedule函数,按照进程调度算法选择要切换的进程

5.switch_to调⽤了__switch_to_asm汇编代码做了关键的进程上下⽂切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程Y)的内核堆栈,并完成了进程上下⽂所需的指令指针寄存器状态切换。之后开始运⾏进程Y(这⾥进程Y曾经通过以上步骤被切换出去,因此可以从switch_to下⼀⾏代码继续执⾏)

6.回复中断上下文

7.从进程Y的内核态返回到进程Y的用户态

8.继续运行用户态Y

参考:

https://www.cnblogs.com/smarxdray/p/13095850.html

标签:fork,do,调用,struct,切换,Linux,进程,上下文,execve
来源: https://www.cnblogs.com/zyc1234/p/13138384.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有