结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
Posted tlxclmm
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程相关的知识,希望对你有一定的参考价值。
结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
1.什么叫中断上下文?
硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的 一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“ 中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境。
在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中继服务结束时能恢复被中断进程 的执行。
2.中断的具体过程
首先说明中断处理程序与中断服务例程的关系:
当一个中断发生时,内核应该有相应的处理方法,这个方法就是中断处理程序,一个中断处理程序对应一个中断号。中断处理程序是管理硬件的驱动程序的一部分,如果设备需要中断,相应的设备驱动程序就需注册中断处理程序。注册方式:使用request_irq()函数。
而一条中断线对应一个一个中断处理程序,而多个设备可能共享一条中断线,那么如何让中断处理程序为不同的设备提供不同的处理方法。这就引出了中断服务例程。一个中断处理程序对应若干个中断服务例程。
中断处理程序就相当于某个中断向量的总的处理程序,比如IRQ0x09_interrupt()是中断号为9的总处理程序,假如这个9号中断由5个设备共享,那么这5个设备都分别有其对应的中断服务例程。也就是说当有多个设备需要共享某个中断线时,中断处理程序必须要调用ISR,此时会调用handle_IRQ_event()
整个中断的总体流程为:首先是硬件通过中断控制器发出中断信号,中断信号对应中断向量,然后利用中断向量在IDT中找到对应中断门,在中断门中得到段选择符从而可以从GDT中找到中断服务例程的段基址。之后在栈中保存eflags、cs和eip的内容,就要选择对应的中断服务例程来完成中断所需要的服务。其中所有的中断都需要在中断服务真正开始之前执行SAVE_ALL保存上下文环境。
3.分析fork与execve的中断上下文切换
fork系统调?创建了?个?进程,?进程复制了父进程中所有的进程信息,包括内核堆栈、进程描述符等,?进程作为?个独?的进程也会被调度。
fork系统调用进入内核态,通过do_fork来完成,_do_dork具体代码:
1 // kernel/fork.c 2 3 long _do_fork(struct kernel_clone_args *args) 4 { 5 u64 clone_flags = args->flags; 6 struct completion vfork; 7 struct pid *pid; 8 struct task_struct *p; 9 int trace = 0; 10 long nr; 11 12 /* 13 * Determine whether and which event to report to ptracer. When 14 * called from kernel_thread or CLONE_UNTRACED is explicitly 15 * requested, no event is reported; otherwise, report if the event 16 * for the type of forking is enabled. 17 */ 18 if (!(clone_flags & CLONE_UNTRACED)) { 19 if (clone_flags & CLONE_VFORK) 20 trace = PTRACE_EVENT_VFORK; 21 else if (args->exit_signal != SIGCHLD) 22 trace = PTRACE_EVENT_CLONE; 23 else 24 trace = PTRACE_EVENT_FORK; 25 26 if (likely(!ptrace_event_enabled(current, trace))) 27 trace = 0; 28 } 29 30 p = copy_process(NULL, trace, NUMA_NO_NODE, args);//复制进程描述符和执?时所需的其他数据结构31 add_latent_entropy(); 32 33 if (IS_ERR(p)) 34 return PTR_ERR(p); 35 36 /* 37 * Do this prior waking up the new thread - the thread pointer 38 * might get invalid after that point, if the thread exits quickly. 39 */ 40 trace_sched_process_fork(current, p); 41 42 pid = get_task_pid(p, PIDTYPE_PID); 43 nr = pid_vnr(pid); 44 45 if (clone_flags & CLONE_PARENT_SETTID) 46 put_user(nr, args->parent_tid); 47 48 if (clone_flags & CLONE_VFORK) { 49 p->vfork_done = &vfork; 50 init_completion(&vfork); 51 get_task_struct(p); 52 } 53 54 wake_up_new_task(p);//将?进程添加到就绪队列 55 56 /* forking complete and child started to run, tell ptracer */ 57 if (unlikely(trace)) 58 ptrace_event_pid(trace, pid); 59 60 if (clone_flags & CLONE_VFORK) { 61 if (!wait_for_vfork_done(p, &vfork)) 62 ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); 63 } 64 65 put_pid(pid); 66 return nr;//返回?进程pid(?进程中fork返回值为?进程的pid) 67 }
_do_fork主要调用了两个关键函数:copy_process和wake_up_new_task。
copy_process:克隆一个当前进程的副本并为之分配id。
execve() 系统调用通常与 fork() 系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve() 来运行指定的程序。
execve也比较特殊。当前的可执行程序在执行,执行到execve系统调用时陷入内核态,在内核里面用do_execve加载可执行文件,把当前进程的可执行程序给覆盖掉。当execve系统调用返回时,返回的已经不是原来的那个可执行程序了,而是新的可执行程序。
execve系统调用时通过通过调?do_execve来具体执?加载可执??件的?作:
1 //filename为可执行文件的名字 2 int do_execve(struct filename *filename, 3 const char __user *const __user *__argv, 4 const char __user *const __user *__envp) 5 { 6 struct user_arg_ptr argv = { .ptr.native = __argv }; 7 struct user_arg_ptr envp = { .ptr.native = __envp }; 8 return do_execveat_common(AT_FDCWD, filename, argv, envp, 0); 9 }
其又调用了do_execveat_common
1 static int do_execveat_common(int fd, struct filename *filename, 2 struct user_arg_ptr argv, 3 struct user_arg_ptr envp, 4 int flags) 5 { 6 return __do_execve_file(fd, filename, argv, envp, flags, NULL); 7 }
1 /* 2 * sys_execve() executes a new program. 3 */ 4 static int do_execveat_common(int fd, struct filename *filename, 5 struct user_arg_ptr argv, 6 struct user_arg_ptr envp, 7 int flags) 8 { 9 char *pathbuf = NULL; 10 struct linux_binprm *bprm; /* 这个结构当然是非常重要的,下文,列出了这个结构体以便查询各个成员变量的意义 */ 11 struct file *file; 12 struct files_struct *displaced; 13 int retval; 14 15 if (IS_ERR(filename)) 16 return PTR_ERR(filename); 17 18 /* 19 * We move the actual failure in case of RLIMIT_NPROC excess from 20 * set*uid() to execve() because too many poorly written programs 21 * don‘t check setuid() return code. Here we additionally recheck 22 * whether NPROC limit is still exceeded. 23 */ 24 if ((current->flags & PF_NPROC_EXCEEDED) && 25 atomic_read(¤t_user()->processes) > rlimit(RLIMIT_NPROC)) { 26 retval = -EAGAIN; 27 goto out_ret; 28 } 29 30 /* We‘re below the limit (still or again), so we don‘t want to make 31 * further execve() calls fail. */ 32 current->flags &= ~PF_NPROC_EXCEEDED; 33 34 // 1. 调用unshare_files()为进程复制一份文件表; 35 retval = unshare_files(&displaced); 36 if (retval) 37 goto out_ret; 38 39 retval = -ENOMEM; 40 41 // 2、调用kzalloc()在堆上分配一份structlinux_binprm结构体; 42 bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); 43 if (!bprm) 44 goto out_files; 45 46 retval = prepare_bprm_creds(bprm); 47 if (retval) 48 goto out_free; 49 50 check_unsafe_exec(bprm); 51 current->in_execve = 1; 52 53 // 3、调用open_exec()查找并打开二进制文件; 54 file = do_open_execat(fd, filename, flags); 55 retval = PTR_ERR(file); 56 if (IS_ERR(file)) 57 goto out_unmark; 58 59 // 4、调用sched_exec()找到最小负载的CPU,用来执行该二进制文件; 60 sched_exec(); 61 62 // 5、根据获取的信息,填充structlinux_binprm结构体中的file、filename、interp成员; 63 bprm->file = file; 64 if (fd == AT_FDCWD || filename->name[0] == ‘/‘) { 65 bprm->filename = filename->name; 66 } else { 67 if (filename->name[0] == ‘