标签:执行环境 mamicode for 内存 相关 enabled eve RoCE 复制
fork系统调用用于从已存在进程中创建一个新进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值。
下面是fork()返回的不同值。
负值:创建子进程失败。
零:返回到新创建的子进程。
正值:返回给父亲或调用者。该值包含新创建子进程的进程ID。
使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等,而子进程所独有的只有它的进程号、计时器等。因此可以看出,使用fork系统调用的代价是很大的,它复制了父进程中的数据段和堆栈段里的绝大部分内容,使得fork系统调用的执行速度并不很快。
fork的返回值这样设计是有原因的,fork在子进程中返回0,子进程仍可以调用getpid函数得到自己的进程ID,也可以调用getppid函数得到父进程的进程ID。在父进程中使用getpid函数可以得到自己的进程ID,然而要想得到子进程的进程ID,只有将fork的返回值记录下来,别无它法。
子进程代码和父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据和静态数据)。数据的复制采用的是写时复制,即只有在任一进程(父进程或者子进程)对数据执行了写操作时,复制才会发生。
此外创建子进程后,父进程打开的文件描述符(在fork之前打开的文件)在子进程中也是打开的,并且共享文件读写偏移量,且文件描述符的引用计数加1。
Linux下用于创建进程的API有三个fork,vfork和clone,这三个函数分别是通过系统调用sys_fork,sys_vfork以及sys_clone实现的。而且这三个系统调用,都是通过do_fork来实现的,只是传入了不同的参数。所以我们可以得出结论:所有的子进程是在do_fork实现创建和调用的。
long do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { 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 ((clone_flags & CSIGNAL) != SIGCHLD) trace = PTRACE_EVENT_CLONE; else trace = PTRACE_EVENT_FORK; if (likely(!ptrace_event_enabled(current, trace))) trace = 0; } p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace); //执行生成新进程的主要工作,并根据指定的标志重用父进程的一些数据 /* * Do this prior waking up the new thread - the thread pointer * might get invalid after that point, if the thread exits quickly. */ if (!IS_ERR(p)) { struct completion vfork; struct pid *pid; trace_sched_process_fork(current, p); pid = get_task_pid(p, PIDTYPE_PID); nr = pid_vnr(pid); //确定PID:注意这里不是生成新的PID,而是确定局部PID; if (clone_flags & CLONE_PARENT_SETTID) put_user(nr, parent_tidptr); if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); //初始化vfork的完成处理程序 get_task_struct(p); } wake_up_new_task(p); //保证子进程在父进程之前调用,如果子进程立即调用exec,这样可以极大的减少复制内存页的工作量 /* 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); } else { nr = PTR_ERR(p); } return nr; }
do_fork函数的主要就是复制原来的进程成为另一个新的进程。流程如下:
static __latent_entropy struct task_struct *copy_process( struct pid *pid, int trace, int node, struct kernel_clone_args *args) { ... p = dup_task_struct(current, node); ... /* copy all the process information */ shm_init_task(p); retval = security_task_alloc(p, clone_flags); if (retval) goto bad_fork_cleanup_audit; retval = copy_semundo(clone_flags, p); if (retval) goto bad_fork_cleanup_security; retval = copy_files(clone_flags, p); if (retval) goto bad_fork_cleanup_semundo; retval = copy_fs(clone_flags, p); if (retval) goto bad_fork_cleanup_files; retval = copy_sighand(clone_flags, p); if (retval) goto bad_fork_cleanup_fs; retval = copy_signal(clone_flags, p); if (retval) goto bad_fork_cleanup_sighand; retval = copy_mm(clone_flags, p); if (retval) goto bad_fork_cleanup_signal; retval = copy_namespaces(clone_flags, p); if (retval) goto bad_fork_cleanup_mm; retval = copy_io(clone_flags, p); if (retval) goto bad_fork_cleanup_namespaces; retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p, args->tls); if (retval) goto bad_fork_cleanup_io; ... return p;
copy_process()函数主要用来创建子进程的描述符以及与子进程相关数据结构。流程如下:
execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。
execve() 系统调用通常与 fork() 系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve() 来运行指定的程序。
系统调用execve的内核入口为sys_execve。
sys_execve是调用do_execve实现的。do_execve则是调用do_execveat_common实现的。
总结execve系统调用的过程:
操作系统中的状态分为核心态和用户态。大多数系统交互式操作需求在内核态执行。如设备IO操作或者进程间通信。特权指令:一类只能在核心态下运行而不能在用户态下运行的特殊指令。不同的操作系统特权指令会有所差异,但是一般来说主要是和硬件相关的一些指令。用户程序只在用户态下运行,有时需要访问系统核心功能,这时通过系统调用接口使用系统调用。
当应用程序中需要操作系统提供服务时,如请求I/O资源或执行I/O 操作,应用程序必须使用系统调用命令。由操作系统捕获到该命令后,便将CPU的状态从用户态转换到系统态,然后执行操作系统中相应的子程序(例程),完成所需的功能。执行完成后,系统又将CPU状态从系统态转换到用户态,再继续执行应用程序。
fork系统调用特殊之处在于fork创建了一个子进程,涉及进程的上下文切换:子进程的复制了父进程所有的上下文环境信息,但二者的PID不同,各自作为独立的进程被调度。fork调用一次,返回两次。第?次返回到原来的父进程的位置继续向下执行,这和其他的系统调用是?样的。第二次是子进程被创建出来后,返回到一个特定的点——ret_from_fork,通过内核构造的堆栈环境,它可以正常系统调用返回到用户态。
execve系统调用特殊之处在于execve在保持进程ID不变的情况下,使用新的上下文环境“覆盖”原上下文环境,也即原来的上下文环境不再保存,相关的环境数据直接被丢弃。 因此,当execve系统调用返回时,返回的已经不是原来的那个可执行程序了,而是新的可执行程序。execve返回的是新的可执行程序执行的起点,静态链接的可执行文件也就是main函数的?致位置,动态链接的可执行文件还需要ld链接好动态链接库再从main函数开始执行。
1、正在运行的用户态进程 X。
2、发生中断(包括异常、系统调用等),跳转到中断处理程序入口。
3、中断上下文切换,保存 EIP/ESP/EFLAGS 的值到内核态堆栈,加载EIP和ESP寄存器的值,从进程X的用户态到进程X的内核态。具体包括如下:
(1)swapgs指令:保存现场
(2)rsp point to kernel stack:加载当前进程内核堆栈栈顶地址到RSP寄存器
(3)save cs:/rip/ss:rsp/flags:将当前CPU关键上下文压入进程X的内核堆栈。
4、中断处理过程中或中断返回前调用了 schedule 函数,其中:
(1)进程调度算法选择 next 进程;
(2)进程地址空间切换;
(3)switch_to 做了关键的进程上下文切换:switch_to 调用 __switch_to_asm 汇编代码做了关键的进程上下文切换,将当前进程 X 的内核堆栈切换到进程调度算法选出来的 next 进程(假定为进程 Y)的内核堆栈,并完成了进程上下文所需的指令指针寄存器状态切换。然后进程 Y 开始运行。
5、恢复中断上下文,与步骤3的中断上下文切换相对应:
iret - pop cs:rip/ss:rsp/flags
从 Y 进程的内核堆栈中弹出步骤3对应的压栈内容,此时完成了中断上下文的切换,从Y进程的内核态返回到Y进程的用户态。
6、继续运行用户态进程X。
结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
标签:执行环境 mamicode for 内存 相关 enabled eve RoCE 复制
原文地址:https://www.cnblogs.com/yhhh/p/13137543.html