咏月东南 2020-06-14
结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程
完成一篇博客总结分析Linux系统的一般执行过程,以期对Linux系统的整体运作形成一套逻辑自洽的模型,并能将所学的各种OS和Linux内核知识/原理融通进模型中。
图来自http://static.cyblogs.com/3433091-63269eb8f87c2bb9.png
内核态:当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。其他的属于用户态。用户程序运行在用户态,操作系统运行在内核态
用户态:当进程在执行用户自己的代码时,那么他就处于用户态(用户态)
由于用户态不能干扰内核态.所以CPU指令就有两种,特权指令和非特权指令.不同的状态对应不同的指令
特权指令:只能由操作系统内核部分使用,不允许用户直接使用的指令。如,I/O指令、置终端屏蔽指令、清内存、建存储保护、设置时钟指令)。
非特权指令:所有程序均可直接使用。
从用户态到内核态切换可以通过三种方式:
系统调用: 这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。
异常: 当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
以下面这个简单的例子为例:
#include <sys/types.h> #include <unistd.h> #include <stdio.h> int main (){ pid_t pid; pid = fork(); int count = 0; if (pid < 0){ printf("error in fork!\n"); } else if (pid == 0){ printf("child process whose process id is %d\n",getpid()); count ++; } else{ printf("parent process whose process id is %d\n",getpid()); count ++; } printf("count is %d/n",count); return 0; }
运行结果为:
child process whose process id is 14531count is 1
parent process whose process id is 14532count is 1结论很明显:子进程号大于父进程号,两个进程执行的代码相同。
fork函数内部执行的是do_fork系统调用,do_fork的代码:
long _do_fork(struct kernel_clone_args *args) { u64 clone_flags = args->flags; struct completion vfork; struct pid *pid; 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 (args->exit_signal != SIGCHLD) trace = PTRACE_EVENT_CLONE; else trace = PTRACE_EVENT_FORK; if (likely(!ptrace_event_enabled(current, trace))) trace = 0; } p = copy_process(NULL, trace, NUMA_NO_NODE, args); add_latent_entropy(); if (IS_ERR(p)) return PTR_ERR(p); /* * Do this prior waking up the new thread - the thread pointer * might get invalid after that point, if the thread exits quickly. */ 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, args->parent_tid); 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); return nr; }
do _fork的大致执行流程为:
---调用 copy_process 为子进程复制出一份进程信息
-------调用 dup_task_struct 复制当前的 task_struct
--------检查进程数是否超过最大数目(默认32678)
-------加入新进程到一个调度器类,并更新调度器时钟
-------调用 sched_fork 初始化进程数据结构。
-------复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
-------初始化新进程的内核栈
---为新进程分配并设置新的 pid
---如果执行的是 vfork。初始化完成。
---调用 wake_up_new_task方法 将子进程加入调度器,为之分配 CPU
---如果是 vfork,父进程等待子进程完成 exec。然后随之替换自己的地址空间
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; }
search_binary_handler() 是最重要的函数,即加载进来的可执行程序会将当前正在执行的进程内存空间覆盖,指向新的可执行程序。
所以我们可以得知,通过execve系统调用新进程,原先的进程都会被替换掉。
execve系统调用过程及其上下文的变化情况:
1.陷入内核
2.加载新的进程,并且将新的进程覆盖原进程的空间
3.将新的进程的入口地址设置为IP值
4.切换回用户态,继续执行原来的进程。
do_execve大致流程为:
图片来源于https://my.oschina.net/u/3857782/blog/1854572
进程调度的时机?般都是在中断处理后和中断返回前的某个时机点,只有内核线程可以直接调?schedule函数主动发起进程调度。对于用户态进程的相互之间切换,主要有如下步骤:
1.发生中断或者是陷入,将当前进程的某些寄存器比如(eip、esp、eflags)保存到内核栈中(保存现场)
2.将要切换的新进程的eip、esp加载进来
3 .中断处理程序中会调用schedule()函数 进行进程上下文切换,这里的switch_to()是核心方法
4.运行新的用户态进程。
用户进程->INT 0x80->system_call->系统调用进程->内核
下面列举一下由用户态转向和心态的例子:
1.用户进行系统调用
2.发生一次中断
3.用户程序中企图调用特权指令,从内核态转向用户态由一条指令实现,这条指令也是特权指令。
这样,linux操作系统的运行环境可以理解为:用户通过操作系统运行上层程序,而这个上层程序的运行依赖于操作系统底层管理程序提供的服务。当需要这个服务的时候,系统通过中断机制进入内核态,运行中断处理程序,这时就是由中断进制进入核心态,同时通过异常也可以进入和心态
进程上下文,就是一个进程在将要发生切换的时候(比如时间片到),CPU的某些寄存器中的值、进程的状态以及堆栈中的内容都需要被保存下来,以便切换回来不会丢失执行的位置断点,这就是所谓的保存当前进程的上下文,以便切换回来可以继续执行该进程。用户空间的进程要传递很多变量、参数给内核,与此同时,内核也要将这些变量,寄存器的值,参数等保存起来。以便切换回来还原现场之后继续执行该进程。