标签:覆盖 read child 物理内存 hand 引用 lse 文件的 命令
fork() 系统调用将创建一个与父进程几乎一样的新进程,之后继续执行下面的指令。程序可以根据 fork() 的返回值,确定当前处于父进程中,还是子进程中——在父进程中,返回值为新创建子进程的进程 ID,在子进程中,返回值是 0。一些使用多进程模型的服务器程序(比如 sshd),就是通过 fork() 系统调用来实现的,每当新用户接入时,系统就会专门创建一个新进程,来服务该用户。
fork() 系统调用所创建的新进程,与其父进程的内存布局和数据几乎一模一样。在内核中,它们的代码段所在的只读存储区会共享相同的物理内存页,可读可写的数据段、堆及栈等内存,内核会使用写时拷贝技术,为每个进程独立创建一份。
在 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; // ... // 复制进程描述符,返回创建的task_struct的指针 p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace); if (!IS_ERR(p)) { struct completion vfork; struct pid *pid; trace_sched_process_fork(current, p); // 取出task结构体内的pid pid = get_task_pid(p, PIDTYPE_PID); nr = pid_vnr(pid); if (clone_flags & CLONE_PARENT_SETTID) put_user(nr, parent_tidptr); // 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行 if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); } // 将子进程添加到调度器的队列,使得子进程有机会获得CPU wake_up_new_task(p); // ... // 如果设置了 CLONE_VFORK 则将父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间 // 保证子进程优先于父进程运行 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; }
进程的创建过程大致是父进程通过fork系统调用进入内核_ do_fork函数,如下图所示复制进程描述符及相关进程资源(采用写时复制技术)、分配子进程的內核堆栈并对內核堆栈和 thread等进程关键上下文进行初始化,最后将子进程放入就绪队列,fork系统调用返回;而子进程则在被调度执行时根据设置的內核堆栈和thread等进程关键上下文开始执行。
特殊之处:fork在陷?内核态之后有两次返回,第?次返回到原来的?进程的位置继续执?,但是在?进程中fork也返回了?次,会返回到?个特 定的点——ret_from_fork,所以它可以正常系统调?返回到?户态。
execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main 函数开始运行。同时,进程的 ID 将保持不变。
execve() 系统调用通常与 fork() 系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。 例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve() 来运行指定的程序。
asmlinkage int sys_execve(struct pt_regs regs) { int error; char * filename; // 将可执行文件的名称装入到一个新分配的页面中 filename = getname((char __user *) regs.ebx); error = PTR_ERR(filename); if (IS_ERR(filename)) goto out; // 执行可执行文件 error = do_execve(filename, (char __user * __user *) regs.ecx, (char __user * __user *) regs.edx, ®s); if (error == 0) { task_lock(current); current->ptrace &= ~PT_DTRACE; task_unlock(current); /* Make sure we don‘t return using sysenter.. */ set_thread_flag(TIF_IRET); } putname(filename); out: return error; }
execve系统调用执行过程:
特殊之处:
当execve在调用时陷入内核态,就执行do_execve文件,覆盖当前的可执行程序,所以 返回的是新的可执行程序的起点。main函数位置是静态链接的可执行文件,动态链接的可执行文件需要连接动态链接库后在开始执行。
几种特殊的情况:
1. 通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;
2. 内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;
3. 创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;
4. 加载一个新的可执行程序后返回到用户态的情况,如execve;
结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程
标签:覆盖 read child 物理内存 hand 引用 lse 文件的 命令
原文地址:https://www.cnblogs.com/geekshawn/p/13129859.html