码迷,mamicode.com
首页 > 系统相关 > 详细

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

时间:2020-06-15 20:59:40      阅读:55      评论:0      收藏:0      [点我收藏+]

标签:数据段   com   限制   art   sig   from   RoCE   flag   thread   

一,实验目的:

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

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

二,实验过程:

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

  fork()函数又叫计算机程序设计中的分叉函数,fork是一个很有意思的函数,它可以建立一个新进程,把当前的进程分为父进程和子进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的PID,而子进程中的返回值则返回 0。因此,可以通过返回值来判定该进程是父进程还是子进程。还有一个很奇妙的是:fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。

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

  do_fork的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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复制父进程、获得pid、调用wake_up_new_task将子进程加入就绪队列等待调度执行等。我们知道,在Linux中,除了0号进程由手工创建外,其他进程都是通过复制已有进程创建而来,而这正是fork的主要工作,具体的任务交由copy_process完成。

简单流程如下所示:
技术图片

  copy_process函数主要完成了调用dup_task_struct复制当前进程(父进程)描述符task_struct、信息检查、初始化、把进程状态设置为TASK_RUNNING(此时?进程置为就绪态)、采?写时复制技术逐?复制所有其他进程资源、调?copy_thread_tls初始化子进程内核栈、设置子进程pid等。其中copy_thread_tls所做的工作是关键。我们知道执行fork系统调用之后,会由内核态返回两次:一次返回到父进程,这与一般的系统调用返回流程别无二致;而另一次则返回到子进程,为了实现这一点,就需要为子进程构造出合适的执行上下文,也就是初始化其内核栈和进程描述符的thread字段。这正是copy_thread_tls的任务。

  ?进程通过fork系统调?进?内核_do_fork函数,复制进程描述符及相关进程资源(采?写时复制技术)、分配?进程的内核堆栈并对内核堆栈和thread等进程关键上下?进?初始化,最后将?进程放?就绪队列,fork系统调?返回;??进程则在被调度执?时根据设置的内核堆栈和thread等进程关键上下?开始执?。

特殊之处:

  fork在陷?内核态之后有两次返回,第?次返回到原来的?进程的位置继续执?,但是在?进程中fork也返回了?次,会返回到?个特 定的点——ret_from_fork,所以它可以正常系统调?返回到?户态。

  技术图片

分析execve系统调用:

execve系统调用简介:

execve系统调?接?函数的函数原型如下:

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

?lename为可执??件的名字,argv是以NULL结尾的命令?参数数 组,envp同样是以NULL结尾的环境变量数组(使?命令man execve,可查看其说明)

 

evecve工作流程:

 Linux系统?般会提供了execl、execlp、execle、execv、execvp和execve等6个?以加载执? ?个可执??件的库函数,这些库函数统称为exec函数,差异在于对命令?参数和环境变量参数 的传递?式不同。exec函数都是通过execve系统调?进?内核,对应的系统调?内核处理函数为 sys_execve或__x64_sys_execve,它们都是通过调?do_execve来具体执?加载可执??件的 ?作。

整体的调?关系为sys_execve()或__x64_sys_execve -> do_execve() –> do_execveat_common() -> __do_execve_?le -> exec_binprm()-> search_binary_handler() -> load_elf_binary() -> start_thread()。

 

 上下文环境切换流程:

先看一下再进行evecve系统调用时有哪些上下文环境:

技术图片

 

  在布局?个新的?户态堆栈时,实际上是把命令?参数内容和环境变量的内容通过指针的? 式传到系统调?内核处理函数,再创建?个新的?户态堆栈时会把这些char *argcv[]和char *envp[]等复制到?户态堆栈中,来初始化这个新的可执?程序的执?上下?环境。所以新 的程序可以从main函数开始把对应的参数接收过来,然后执?。 值得注意的是,在调?execve系统调?时,当前的执?环境是从?进程复制过来的, execve系统调?加载完新的可执?程序之后已经覆盖了原来?进程的上下?环境。execve 系统调?在内核中帮我们重新布局了新的?户态执?环境。 执?readelf -h可以查看ELF可执??件?部信息,如下所示程序??点Entry point address:0x804887f。如果是静态链接程序在execve系统调?加载完成后,堆栈上的返回地 址会修改为程序??点的地址。当系统调?从内核态返回时,会从该地址0x804887f继续执 ?。

对于 execve 系统调用,最主要的处理过程都在 do_execve_common() 函数中,以下为该函数的主要部分:

技术图片
static int do_execve_common(struct filename *filename,struct user_arg_ptr argv,struct user_arg_ptr envp)
{
    struct linux_binprm *bprm;  // 用于解析ELF文件的结构 
    struct file *file;
    struct files_struct *displaced;
    int retval;
    current->flags &= ~PF_NPROC_EXCEEDED;  // 标记程序已被执行
    retval = unshare_files(&displaced);  // 拷贝当前运行进程的fd到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);       // 为ELF文件分配内存
    bprm->argc = count(argv, MAX_ARG_STRINGS);
    bprm->envc = count(envp, MAX_ARG_STRINGS);
    retval = prepare_binprm(bprm);     // 从打开的可执行文件中读取信息,填充bprm结构
     // 下面的4句是将运行参数和环境变量都拷贝到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);
    // 开始执行加载到内存中的ELF文件
    <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;
}
技术图片

其中最关键的exec_binprm()函数具体如下:

技术图片
static int exec_binprm(struct linux_binprm *bprm)
{
    pid_t old_pid, old_vpid;   
    int ret;
    old_pid = current->pid;
    rcu_read_lock();
    old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
    rcu_read_unlock();
    <strong>ret = search_binary_handler(bprm);</strong>
    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;
}   
技术图片

其中,search_binary_handler() 函数实现了核心功能,即当前正在执行的进程内存空间会被加载进来的可执行程序所覆盖,并根据具体情况指向新可执行程序。若新的可执行程序为静态链接的文件,main函数的入口地址为新进程的 IP 寄存器所指向的值;若为动态链接,IP 值为加载器 ld 的入口地址,ld 负责动态链接库的处理工作。

 

do_execve主要流程如下:

技术图片

 evecve的特别之处:

当前的可执?程序在执?,执?到execve系统调?时陷?内核态,在内 核???do_execve加载可执??件,把当前进程的可执?程序给覆盖掉。当execve系统调?返回 时,返回的已经不是原来的那个可执?程序了,?是新的可执?程序。execve返回的是新的可执? 程序执?的起点,静态链接的可执??件也就是main函数的?致位置,动态链接的可执??件还需 要ld链接好动态链接库再从main函数开始执?。

三,Linux系统的一般执行过程

  最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程

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

    (2)发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).

    (3)SAVE_ALL //保存现场

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

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

    (6)restore_all //恢复现场

    (7)iret - pop cs:eip/ss:esp/eflags from kernel stack

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

   Linux系统执行过程中的几个特殊情况

    (1)通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;

    (2)内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;

    (3)创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;

    (4)加载一个新的可执行程序后返回到用户态的情况,如execve;

 

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

标签:数据段   com   限制   art   sig   from   RoCE   flag   thread   

原文地址:https://www.cnblogs.com/hgsheng/p/13137546.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!