fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。它不需要参数并返回一个整数值。下面是fork()返回的不同值。一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。因此,可以通过返回值来判定该进程是父进程还是子进程。
fork系统调用是通过do_fork来实现的,具体过程如下:首先是用户程序调用fork(),然后是库函数fork(),系统调用fork(通过系统调用号),通过sys_call_table中寻到sys_fork()的函数地址,调用sys_fork,最后调用do_fork();
do_fork()的关键代码:
long _do_fork(struct kernel_clone_args *args) { //复制进程描述符和执行时所需的其他数据结构 p = copy_process(NULL, trace, NUMA_NO_NODE, args); //将子进程添加到就绪队列 wake_up_new_task(p); //返回子进程pid(父进程中fork返回值为子进程的pid) return nr; }
_do_fork以调用copy_process开始, 后者执行生成新的进程的实际工作, 并根据指定的标志复制父进程的数据。
copy_process函数代码:
static struct task_struct *copy_process(struct pid *pid, int trace, int node, struct kernel_clone_args *args) { //复制进程描述符task_struct、创建内核堆栈等 p = dup_task_struct(current, node); /* copy all the process information */ shm_init_task(p); ... // 初始化子进程内核栈和thread retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p, args->tls); ... return p;//返回被创建的子进程描述符指针 }
copy_process函数主要完成了调用dup_task_struct复制当前进程(父进程)描述符task_struct、信息检查、初始化、把进程状态设置为TASK_RUNNING(此时子进程置为就绪态)、采用写时复制技术逐一复制所有其他进程资源、调用copy_thread_tls初始化子进程内核栈、设置子进程pid等。其中最关键的就是dup_task_struct复制当前进程(父进程)描述符task_struct和copy_thread_tls初始化子进程内核栈。
copy_thread_tls函数代码:
int copy_thread_tls(unsigned long clone_flags, unsigned long sp, unsigned long arg, struct task_struct *p, unsigned long tls) { struct pt_regs *childregs = task_pt_regs(p); struct task_struct *tsk; int err; /* 获取寄存器的信息 */ p->thread.sp = (unsigned long) childregs; p->thread.sp0 = (unsigned long) (childregs+1); memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps)); if (unlikely(p->flags & PF_KTHREAD)) { /* kernel thread 内核线程的设置 */ memset(childregs, 0, sizeof(struct pt_regs)); p->thread.ip = (unsigned long) ret_from_kernel_thread; task_user_gs(p) = __KERNEL_STACK_CANARY; childregs->ds = __USER_DS; childregs->es = __USER_DS; childregs->fs = __KERNEL_PERCPU; childregs->bx = sp; /* function */ childregs->bp = arg; childregs->orig_ax = -1; childregs->cs = __KERNEL_CS | get_kernel_rpl(); childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED; p->thread.io_bitmap_ptr = NULL; return 0; } /* 将当前寄存器信息复制给子进程 */ *childregs = *current_pt_regs(); /* 子进程 eax 置 0,因此fork 在子进程返回0 */ childregs->ax = 0; if (sp) childregs->sp = sp; /* 子进程ip 设置为ret_from_fork,因此子进程从ret_from_fork开始执行 */ p->thread.ip = (unsigned long) ret_from_fork; task_user_gs(p) = get_user_gs(current_pt_regs()); p->thread.io_bitmap_ptr = NULL; tsk = current; err = -ENOMEM; if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) { p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr, IO_BITMAP_BYTES, GFP_KERNEL); if (!p->thread.io_bitmap_ptr) { p->thread.io_bitmap_max = 0; return -ENOMEM; } set_tsk_thread_flag(p, TIF_IO_BITMAP); } err = 0; /* * Set a new TLS for the child thread? * 为进程设置一个新的TLS */ if (clone_flags & CLONE_SETTLS) err = do_set_thread_area(p, -1, (struct user_desc __user *)tls, 0); if (err && p->thread.io_bitmap_ptr) { kfree(p->thread.io_bitmap_ptr); p->thread.io_bitmap_max = 0; } return err; }
copy_thread_tls是一个特定于体系结构的函数,用于复制进程中特定于线程(thread-special)的数据, 重要的就是填充task_struct->thread的各个成员,这是一个thread_struct类型的结构, 其定义是依赖于体系结构的。它包含了所有寄存器(和其他信息),内核在进程之间切换时需要保存和恢复的进程的信息。该函数用于设置子进程的执行环境,如子进程运行时各CPU寄存器的值、子进程的内核栈的起始地址(指向内核栈的指针通常也是保存在一个特别保留的寄存器中)
总结来说,进程的创建过程?致是?进程通过fork系统调?进?内核_do_fork函数,如下图所示复制进程描述符及相关进程资源(采?写时复制技术)、分配?进程的内核堆栈并对内核堆栈和thread等进程关键上下?进?初始化,最后将?进程放?就绪队列, fork系统调?返回;??进程则在被调度执?时根据设置的内核堆栈和thread等进程关键上下?开始执?。
2.以execve为例分析中断上下文的切换
当前的可执?程序在执?,执?到execve系统调?时陷?内核态,在内核???do_execve加载可执??件,把当前进程的可执?程序给覆盖掉。当execve系统调?返回时,返回的已经不是原来的那个可执?程序了,?是新的可执?程序。 execve返回的是新的可执?程序执?的起点,静态链接的可执??件也就是main函数的?致位置,动态链接的可执??件还需要ld链接好动态链接库再从main函数开始执?。
系统调用execve的内核入口为sys_execve,定义在<arch/kernel/process.c>中:
asmlinkage int sys_execve(struct pt_regs regs) { int error; char * filename; filename = getname((char *) regs.ebx); error = PTR_ERR(filename); if (IS_ERR(filename)) goto out; error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, ®s); if (error == 0) current->ptrace &= ~PT_DTRACE; putname(filename); out: return error; }
execve系统调用的执行过程:
最终execve系统调用从内核返回到用户态时,返回到的不是触发execve触发的下一条指令,下一条指令已经不存在了,已经被覆盖掉了,最终返回到new_ip,也就是ELF entry的位置。特殊之处像fork一样修改了内核堆栈栈低的关键的cpu上下文,但与fork不同,execve修改了中断上下文,fork修改的是进程上下文。
系统调用可以看做是一种特殊的中断,因此自然涉及中断上下文,也就是切换到用户内核栈,同时保存相关的寄存器使得中断结束后能够正常返回。
fork系统调用,特殊之处在于其创建了一个新的进程,并且在父子进程中各有一次返回。对于fork的父进程来说,fork系统调用和普通的系统调用基本相同。但是对fork子进程来说,需要设置子进程的进程上下文环境,这样子进程才能从fork系统调用后返回。
execve系统调用,由于execve使得新加载可执?程序已经覆盖了原来?进程的上下?环境,而原来的中断上下文就是保存的是原来的、被覆盖的进程的上下文,因此需要修改原来的中断上下文,使得系统调用返回后能够指向现在加载的这个可执行程序的入口。
4.Linux 系统的一般执行过程
以正在运行的用户态进程X切换到用户态进程Y为例具体表述如下:
结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程
原文:https://www.cnblogs.com/zhanxunghefei/p/13130109.html