fork系统调用
fork系统调用主要是通过_do_fork来完成的。
_do_fork主要代码片段如下
long _do_fork(struct kernel_clone_args *args)
{
u64 clone_flags = args->flags;
struct completion vfork;
struct pid *pid;
struct task_struct *p;
int trace = 0;
long nr;
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if (args->exit_signal != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
// 复制进程描述符
p = copy_process(NULL, trace, NUMA_NO_NODE, args);
add_latent_entropy();
if (IS_ERR(p))
return PTR_ERR(p);
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, args->parent_tid);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
// 将子进程插入到就绪队列
wake_up_new_task(p);
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);
return nr;
}
其中_do_fork完成的最主要的功能是利用函数copy_process()来创建进程描述符以及子进程所需要的所有其他内核数据结构。
然后再利用wake_up_new_task()函数,调整子进程和父进程的参数,然后检查子进程与父进程,如果它们在同一CPU上且不共享同一组页表,就把子进程插入到父进程所在的运行队列。并且恰好插在父进程的前面。如果它们不再同一CPU上或不共享同一组页表,就将子进程插在父进程所在运行队列的队尾。
当_do_fork()结束之后,调度程序会将子进程内核态堆栈的地址装入esp寄存器,把ret_from_fork()的地址装入eip寄存器。然后当fork系统调用执行结束时,新进程将开始执行,系统调用的返回值存放在eax中,其中子进程的返回值为0,父进程的返回值为子进程的pid.
execeve系统调用
execve系统调⽤对应的内核处理函数为__x64_sys_execve().该函数最终通过__do_execve_file()来具体执⾏加载可执⾏⽂件的⼯作。
整体的调用关系为如下
__x64_sys_execve - > do_execve() - > do_execveat_common() - > __do_execve_file - > exec_binprm() - > search_binary_handler() - > load_elf_binary() - > start_thread()
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;
if (IS_ERR(filename))
return PTR_ERR(filename);
if ((current->flags & PF_NPROC_EXCEEDED) &&
atomic_read(¤t_user()->processes) > rlimit(RLIMIT_NPROC)) {
retval = -EAGAIN;
goto out_ret;
}
current->flags &= ~PF_NPROC_EXCEEDED;
retval = unshare_files(&displaced);
if (retval)
goto out_ret;
retval = -ENOMEM;
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_files;
retval = prepare_bprm_creds(bprm);
if (retval)
goto out_free;
check_unsafe_exec(bprm);
current->in_execve = 1;
if (!file)
file = do_open_execat(fd, filename, flags);
retval = PTR_ERR(file);
if (IS_ERR(file))
goto out_unmark;
sched_exec();
bprm->file = file;
if (!filename) {
bprm->filename = "none";
} else if (fd == AT_FDCWD || filename->name[0] == '/') {
bprm->filename = filename->name;
} else {
if (filename->name[0] == '\0')
pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d", fd);
else
pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d/%s",
fd, filename->name);
if (!pathbuf) {
retval = -ENOMEM;
goto out_unmark;
}
if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))
bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
bprm->filename = pathbuf;
}
bprm->interp = bprm->filename;
retval = bprm_mm_init(bprm);
if (retval)
goto out_unmark;
retval = prepare_arg_pages(bprm, argv, envp);
if (retval < 0)
goto out;
retval = prepare_binprm(bprm);
if (retval < 0)
goto out;
retval = copy_strings_kernel(1, &bprm->filename, bprm);
if (retval < 0)
goto out;
bprm->exec = bprm->p;
retval = copy_strings(bprm->envc, envp, bprm);
if (retval < 0)
goto out;
retval = copy_strings(bprm->argc, argv, bprm);
if (retval < 0)
goto out;
would_dump(bprm, bprm->file);
retval = exec_binprm(bprm);
if (retval < 0)
goto out;
current->fs->in_exec = 0;
current->in_execve = 0;
rseq_execve(current);
acct_update_integrals(current);
task_numa_free(current, false);
free_bprm(bprm);
kfree(pathbuf);
if (filename)
putname(filename);
if (displaced)
put_files_struct(displaced);
return retval;
out_free:
free_bprm(bprm);
kfree(pathbuf);
out_files:
if (displaced)
reset_files_struct(displaced);
}
在 __do_execve_file()函数中,主要是创建了一个bprm的数据结构用来表示可执行文件,这里会根据可执行文件对bprm做一些填充,然后再调用search_binary_handler()函数来扫描能够处理相应可执行文件格式的处理器,调用相对应的load_binary()函数
在load_binary()函数中主要完成以下工作
- 校验文件
- 加载文件到内存中并根据ELF文件中的Program header table和Section head table映射到进程的地址空间
- 判断是否需要动态链接
- 配置进程上下文启动环境start_thread
void start_thread(struct pt_regs *regs, unsigned int pc, unsigned long usp)
{
/*
* The binfmt loader will setup a "full" stack, but the C6X
* operates an "empty" stack. So we adjust the usp so that
* argc doesn't get destroyed if an interrupt is taken before
* it is read from the stack.
*
* NB: Library startup code needs to match this.
*/
usp -= 8;
regs->pc = pc;
regs->sp = usp;
regs->tsr |= 0x40; /* set user mode */
current->thread.usp = usp;
}
start_thraed()修改保存在内核态堆栈的eip和esp的值,使它们分别指向动态链接程序的入口点和新的用户态堆栈的栈顶。
所以当execve()系统调用返回后,返回的已经是新的程序的起点了。
一般执行过程
- 正在运⾏的⽤户态进程X。
- 发⽣中断(包括异常、系统调⽤等),硬件完成
- 当前CPU上下文压入用户态进程X的内核堆栈。
- 加载当前进程内核堆栈相关信息,跳转到中断处理程序,即中断执行路径的起点。
- 保存现场,完成中断上下文切换,从进程X的用户态到进程X的内核态
- 中断处理过程中或中断返回前调⽤了schedule函数,其中完成了进程调度算法选择next进程、进程地址空间切换、关键的进程上下⽂切换等。
- switch_to调⽤了__switch_to_asm汇编代码做了关键的进程上下⽂切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程Y)的内核堆栈,并完成了进程上下⽂所需的指令指针寄存器状态切换。之后开始运⾏进程Y。
- 中断上下⽂恢复,与(3)中断上下⽂切换相对应。注意这⾥是进程Y的中断处理过程中,⽽(3)中断上下⽂切换是在进程X的中断处理过程中,因为内核堆栈从进程X切换到进程Y了。
- iret pop cs:rip/ss:rsp/rflags,从Y进程的内核堆栈中弹出(3)中对应的压栈内容。此时完成了中断上下⽂的切换,即从进程Y的内核态返回到进程Y的⽤户态。
- 继续运⾏⽤户态进程Y。
ffork子进程启动执行时进程上下文的特殊之处
fork一个子进程时,子进程不是从switch_to下一行代码或者label1的位置开始执行的,而是从ret_from_fork开始执行的
execve系统调用中断上下文的特殊之处
execve系统调用加载新的可执行程序,在execve系统调用处理过程中修改了触发该系统调用保存的中断上下文,使得返回到用户态的位置修改为新程序的elf_entry或者ld动态连接器的起点地址
参考资料《深入理解Linux内核(第三版)》