结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程
Posted hesetone
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程相关的知识,希望对你有一定的参考价值。
1、普通系统调用
系统调用是一种特殊的中断,中断分外部中断(硬件中断)和内部中断(软件中断),内部中断?称为异常(Exception),异常?分为故障(fault)和陷阱(trap),系统调?就是利?陷阱(trap)这种软件中断?式是主动从?户态进?内核态的。但是,一般从用户态进入内核态,是由两种方式触发,第一种是硬件中断,就是当硬件中断信号来到的时候,就会执行这个中断对应的中断服务例程。第二种是用户态程序在执行的过程当中,调用了一个系统调用,产生了一个内部中断,陷入内核态,也称为陷阱。
C库函数内部使用了系统调用的封装例程,所以当用户态程序调用一个系统调用时,该封装例程发布系统调用,通过特定的陷阱向内核发出服务请求,且int 0x80和sysacall指令会触发一个系统调用,因为这条汇编指令时产生中断向量为128的编程异常。CPU切换到内核态,并开始执行system_call对应汇编代码,也就是entry_INT80_32或者entry_SYSCALL_64(分别对应于int 0x80和syscall),并根据系统调用号,调用对应的内核处理函数,用户态程序通过给EAX寄存器传递一个名为系统调用号的参数来告知CPU应该执行哪个系统调用,除此之外,系统调用也需要传递其他参数。32位x86体系结构下,系统调用通过寄存器的方式传递参数,除了EAX传递系统调用号之外,参数依次赋值给EBX、ECX、EDX、ESI、EDI、EBP。在64位x86体系结构下,系统调用也是通过寄存器传递参数,使用RDI、RSI、RDX、RCX、R8、R9这6个寄存器作为参数传递寄存器。
int $0x80指令或syscall指令触发系统调?机制会在堆栈上保存?些寄存器的值,会保存中断发?时当前执?程序的栈顶地址(ESP、RSP)、当时的状态字(EFlags、RFlags)、当时的 CS:EIP/RIP 的值。同时会将当前进程内核态的栈顶地址、内核态的状态字放? CPU 对应的寄存器,并且 CS:EIP/RIP 寄存器的值会指向中断处理程序的??,对于系统调?来讲是指向系统调?处理的??。系统调用的内核堆栈展示如下:
中断是在?个进程当中从进程的?户态到进程的内核态,或从进程的内核态返回到进程的?户态,中断上下?代表当前进程执?,所以中断上下?中的get_current可获取?个指向当前进程描述符的指针,即指向被中断进程,相应的中断上下?切换的信息存储于该进程的内核堆栈中。中断有多种类型,?如有不可屏蔽中断、可屏蔽中断、异常、陷阱(系统调?)等。
2、fork系统调用
fork?进程的内核堆栈示意图中struct pt_regs就是内核堆栈中保存的中断上下?,struct inactive_task_frame就是fork?进程的进程上下?。__switch_to_asm汇编代码中完成内核堆栈切换后的代码,正好与struct inactive_task_frame对应??出栈。fork子进程的内核堆栈展示如下,对应的文件位置是/arch/x86/include/asm/switch_to.h:
在文件/kernel/fork.c中,有如下声明,通过上?的代码可以看出fork创建一个新进程,是通过_do_fork函数来创建进程的,传递结构体kernel_clone_args类型变量args就行。
1 SYSCALL_DEFINE0(fork) 2 { 3 #ifdef CONFIG_MMU 4 struct kernel_clone_args args = { 5 .exit_signal = SIGCHLD, 6 }; 7 8 return _do_fork(&args); 9 #else 10 /* can not support in nommu mode */ 11 return -EINVAL; 12 #endif 13 }
_do_fork具体进程的创建?概就是把当前进程的描述符等相关进程资源复制?份,从?产??个?进程,并根据?进程的需要对复制的进程描述符做?些修改,然后把创建好的?进程放?运?队列(操作系统原理中的就绪队列),更细致点说,它主要完成了调?copy_process()复制?进程、获得pid、调?wake_up_new_task将?进程加?就绪队列等待调度执?等,它在/kernel/fork.c中有如下定义:
1 long _do_fork(struct kernel_clone_args *args) 2 { 3 //复制进程描述符和执?时所需的其他数据结构 4 p = copy_process(NULL, trace, NUMA_NO_NODE, args); 5 wake_up_new_task(p); //将?进程添加到就绪队列 6 return nr; //返回?进程pid(?进程中fork返回值为?进程的pid) 7 }
因此,可以总结,fork系统调用的执行过程?致是当前进程通过调用fork()系统调?函数进?内核态,执行_do_fork函数,如下图所示复制进程描述符pid及相关进程资源(采?写时复制技术)、调?copy_thread_tls初始化?进程内核栈、设置?进程pid等。其中最关键的就是dup_task_struct复制当前进程(?进程)描述符task_struct和copy_thread_tls初始化?进程内核栈。最后将?进程 放?就绪队列,fork系统调?返回;??进程则在被调度执?时根据设置的内核堆栈和thread等进程关键上下?开始执?。
写一段代码a.c来验证一下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #include <sys/types.h> 5 #include <unistd.h> 6 7 int main(void) 8 { 9 int count = 1; 10 int child; 11 12 child = fork(); 13 14 if (child < 0) 15 { 16 perror("error fork"); 17 } 18 else if (child == 0) 19 { 20 printf("Child process : %d (%p), %d ", ++count, &count, getpid()); 21 } 22 else 23 { 24 printf("Parent process : %d (%p), %d ", count, &count, getpid()); 25 } 26 27 return EXIT_SUCCESS; 28 }
1 Parent process : 1 (0x7ffeeeb5e7c8), 9408 2 Child process : 2 (0x7ffeeeb5e7c8), 9409
结果显示如上,可见,fork?个?进程的过程中,复制?进程的资源时采?了Copy On Write(写时复制)技术,不需要修改的进程资源??进程是共享内存存储空间的。事实显示的确如此,不同于普通的系统调用,fork系统调用的一个奇妙之处就是函数仅仅被调用一次,却能够返回两次,有两个返回结果,而且它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程,辨别方法如上述。
3、分析execve系统调用
execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,当前进程的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和main函数开始运行。但是,进程的ID将保持不变。函数原型是sys_execve(),sys_execve()根据参数中指定的二进制文件路径,执行相应的二进制文件。在tools/include/nolibc/nolibc.h中sys_execve函数有有如下定义:
1 int sys_execve(const char *filename, char *const argv[], char *const envp[]) 2 { 3 return my_syscall3(__NR_execve, filename, argv, envp); 4 }
在tools/include/nolibc/nolibc.h文件中,my_syscall3函数有如下定义,num中记录系统调用号,其余则是触发该系统调用的各种参数。
1 #define my_syscall3(num, arg1, arg2, arg3) 2 ({ 3 long _ret; 4 register long _num asm("rax") = (num); 5 register long _arg1 asm("rdi") = (long)(arg1); 6 register long _arg2 asm("rsi") = (long)(arg2); 7 register long _arg3 asm("rdx") = (long)(arg3); 8 9 asm volatile ( 10 "syscall " 11 : "=a" (_ret) 12 : "r"(_arg1), "r"(_arg2), "r"(_arg3), 13 "0"(_num) 14 : "rcx", "r8", "r9", "r10", "r11", "memory", "cc" 15 ); 16 _ret; 17 })
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 #include <sys/types.h> 5 #include <unistd.h> 6 7 int main(void) 8 { 9 const char *oldpath = "execl_ln.c"; 10 const char *newpath = "a.c"; 11 int count = 1; 12 int child; 13 14 child = fork(); 15 16 if (child < 0) 17 { 18 perror("error fork"); 19 } 20 else if (child == 0) 21 { 22 execlp("ls", "ls", NULL); 23 printf("Child process : %d (%p), %d ", ++count, &count, getpid()); 24 } 25 else 26 { 27 printf("Parent process : %d (%p), %d ", count, &count, getpid()); 28 } 29 30 return EXIT_SUCCESS; 31 }
1 Parent process : 1 (0x7ffee5ab97d4), 11041 2 a 3 a.c 4 execl_ln 5 execl_ln.c 6 forktest 7 ln.s
事实上,内核中实际执行execv()或execve()系统调用的程序是do_execve(),这个函数先打开目标映像文件,并从目标文件的头部(第一个字节开始)读入若干字节,然后调用另一个函数search_binary_handler(),在此函数里面,它会搜索我们上面提到的Linux支持的可执行文件类型队列,让各种可执行程序的处理程序前来认领和处理。如果类型匹配,则调用load_binary函数指针所指向的处理函数来处理目标映像文件。在ELF文件格式中,处理函数是load_elf_binary函数,调用链为sys_execve()->do_execve()->do_execveat_common()->__do_execve_file()->exec_binprm()->search_binary_handler()->load_binary(),代码段展示如下:
1 int do_execve(struct filename *filename, 2 const char __user *const __user *__argv, 3 const char __user *const __user *__envp) 4 { 5 struct user_arg_ptr argv = { .ptr.native = __argv }; 6 struct user_arg_ptr envp = { .ptr.native = __envp }; 7 return do_execveat_common(AT_FDCWD, filename, argv, envp, 0); 8 }
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 static int __do_execve_file(int fd, struct filename *filename, 2 struct user_arg_ptr argv, 3 struct user_arg_ptr envp, 4 int flags, struct file *file) 5 { 6 char *pathbuf = NULL; 7 struct linux_binprm *bprm; 8 struct files_struct *displaced; 9 int retval; 10 /* We‘re below the limit (still or again), so we don‘t want to make 11 * further execve() calls fail. */ 12 current->flags &= ~PF_NPROC_EXCEEDED; 13 14 ...... 15 16 retval = bprm_mm_init(bprm); //为ELF文件分配内存
17 18 would_dump(bprm, bprm->file); 19 retval = exec_binprm(bprm);//开始执行加载到内存中的ELF文件 20 if (retval < 0) 21 goto out; 22 23 /* execve succeeded */ 24 25 return retval; 26 ...... 27 }
1 static int exec_binprm(struct linux_binprm *bprm) 2 { 3 pid_t old_pid, old_vpid; 4 5 ...... 6 int ret; 7 ret = search_binary_handler(bprm); 8 9 ...... 10 return ret; 11 }
1 int search_binary_handler(struct linux_binprm *bprm) 2 { 3 bool need_retry = IS_ENABLED(CONFIG_MODULES); 4 struct linux_binfmt *fmt; 5 int retval; 6 7 ...... 8 retval = security_bprm_check(bprm);//检查是否具有运行权限 9 if (retval) 10 return retval; 11 12 retval = -ENOENT; 13 retry: 14 read_lock(&binfmt_lock); 15 list_for_each_entry(fmt, &formats, lh) {//尝试每一种格式的解析函数
16 if (!try_module_get(fmt->module)) 17 continue; 18 read_unlock(&binfmt_lock); 19 20 bprm->recursion_depth++; 21 retval = fmt->load_binary(bprm);//最关键的步骤,调用合适格式的处理函数加载该可执行文件 22 bprm->recursion_depth--; 23 24 ......... 25 return retval; 26 }
在load_binary函数中,加载进来的可执行文件,也就是系统调用"ls",将把当前正在执行的进程的内存空间,也就是fork出来的子进程,完全覆盖掉,由于当前的进程都会被新加载进来的ls系统调用程序完全替换掉,所以我们的测试程序的第23行没有在terminal上打印信息。
3、Linux系统的一般执行过程
以上是关于结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程的主要内容,如果未能解决你的问题,请参考以下文章
结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程