结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
作者:互联网
实验要求
结合中断上下文切换和进程上下文切换分析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