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

YoungkingWang 2020-06-13

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

1.fork系统调用

      fork系统调用可以建立一个新进程,把当前的进程分为父进程和子进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的PID,而子进程中的返回值则返回 0。新创建的子进程与父进程十分类似,子进程得到与父进程用户级虚拟地址空间相同(但是独立)的一份拷贝,包括文本,数据和bss段、堆以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝。这就是意味着当父进程调用fork时候,子进程还可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大区别在于他们有着不同的PID。

      先用具体调用例程分析fork:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main()
{
    FILE * fp = fopen("output.txt", "w");
    fputs("I am parent\n", fp);
    switch(fork())
    {
    case -1:
        perror("fork failed");
        return -1;
    case 0:
        fputs("I am Child\n", fp);
        break;
    default:
        break;
    }
    fclose(fp);
    return 0;
}

执行并查看输出的output.txt中的内容:

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

发现输出文件中会出现两行重复的parent文本,原因是 fputs 库函数带有缓冲,fork() 创建的子进程完全拷贝父进程用户空间内存时,fputs 库函数的缓冲区也被包含进来了。所以,fork() 执行之后,fork系统调用复制进程描述符及相关进程资源(采?写时复制技术)、分配?进程的内核堆栈并对内核堆栈和thread等进程关键上下?进?初始化,这样子进程获得了一份 fputs 缓冲区中的数据,导致“I am parent”这条消息在子进程中又被输出了一次。可见子进程确实继承了父进程大多数资源。

2.execve系统调用

execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。

一个execve系统调用例程:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
 
int main() {
    int pid;
    pid = fork();
    if (pid < 0)
    {
        fprintf(stderr, "Fork Failed\n");
        exit(-1);
    }
    else if (pid == 0)
    {
        execlp("/bin/ls", "ls", NULL);
        printf("ls command run finished\n");
    }
    else
    {
        wait(NULL);
        printf("Child Completed\n");
        exit(0);
    }
    return 0;
}

执行结果:

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

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

可见只有子进程执行了ls命令,与上面的论述一致。

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

上面提到过,execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。exec簇函数都是通过execve系统调用进入内核,对应的系统调用内核处理函数为sys_execve或__x64_sys_execve,它们都是通过调用do_execve来具体执行加载可执行文件的工作,而do_execve又通过调用do_execve_common() 工作。查看do_execve_common() 代码可知execve整体调用流程大致如下:

     a. 陷入内核

b. 加载新的进程

c. 将新的进程,完全覆盖原先进程的数据空间

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

e. 返回用户态,新程序继续执行下去。老进程的上下文被完全替换,但进程的 pid 不变,所以 execve 系统调用不会返回原进程,而是返回新进程。

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

 fork为56号系统调用,查看~/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl表得到内核函数__x64_sys_clone,由fork.c中的代码可知__x64_sys_clone调用的是do_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;
 
    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);
 
        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);

        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源码可知fork子进程启动执行时调用了 copy_process 函数,复制当前进程产生子进程,并且传入关键参数为子进程设置响应进程上下文;还调用 wake_up_new_task 函数,将子进程放入调度队列中,从而有机会 CPU 调度并得以运行。 copy_process函数主要完成了调?dup_task_struct复制当前进程(?进程)描述符task_struct、信息检查、初始化、把进程状态设置为TASK_RUNNING(此时?进程置为就绪态)、采?写时复制技术逐?复制所有其他进程资源、调?copy_thread_tls初始化?进程内核栈、设置?进程pid等。其中最关键的就是dup_task_struct复制当前进程(?进程)描述符task_struct和copy_thread_tls初始化子进程内核栈。

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

系统调用是运行在内核态的,而用户程序一般是运行在用户态的,操作系统一般通过中断从用户态切换到内核态。中断具有两个属性一个是中断号,一个是中断向量表,是一个数组,包含中断处理程序。一个中断号对应一个中断处理程序。中断分为硬件中断和软件中断,软件中断通常是一条指令,带有一个参数代表中断号。
Linux系统的一般执行过程:在linux中使用int 0x80来触发所有的系统调用。和中断一样系统调用都有一个系统调用号,系统调用号代表在系统调用表中的位置。触发中断后,系统会自动保存当前中断上下文,然后进行中断上下文切换,之后执行中断处理程序,然后恢复现场,最后以sysret或iret返回系统调?。

相关推荐