Linux内核源码阅读之do_fork()

news/2024/7/20 11:38:30 标签: Linux内核, 子进程, fork()

前言

本文主要介绍Linux内核中,一个新的进程/线程是如何诞生的。

主要涉及复制父进程各类资源、设置子进程系统堆栈、构造子进程调度时运行点等。

函数原型

Linux系统中,除第一个进程是被捏造出来的,其他进程都是通过do_fork()复制出来的。

int do_fork(unsigned long clone_flags, unsigned long stack_start,struct pt_regs *regs, unsigned long stack_size)

do_fork()根据其参数不同,对父进程的复制过程也不同,主要区别是子进程与父进程共享资源还是单独从父进程复制出来一份。

这里我们介绍复制父进程全部资源的情况,即复制父进程的task_struct结构、进程用户空间等。

我们这里为了叙述do_fork()的主体结构,避免干扰视线,省略了许多细节。

1.创建子进程task_struct结构体

既然是创建新的进程,首先需要申请进程最基本的单位task_struct结构

570 p = alloc_task_struct();
571 if (!p)
572 goto fork_out;
573
574 *p = *current;

然后需要将父进程task_struct结构中的各种参数复制到子进程task_struct中。

2.获取一个空闲的pid

static int get_pid(unsigned long flags)

pid是进程的独一无二的标识,因此必须获取一个。

3.复制各种资源

642 /* copy all the process information */
643 if (copy_files(clone_flags, p))             //复制文件描述符
644 goto bad_fork_cleanup;
645 if (copy_fs(clone_flags, p))
646 goto bad_fork_cleanup_files;
647 if (copy_sighand(clone_flags, p))            //复制信号量
648 goto bad_fork_cleanup_fs;                    
649 if (copy_mm(clone_flags, p))                 //复制虚存空间
650 goto bad_fork_cleanup_sighand;
651 retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);  //复制系统堆栈

3.1copy_files(clone_flags, p)

父进程中可能打开了一系列文件,因此要复制给子进程,注意这不是通过指针共享。

3.2copy_fs(clone_flags, p)

复制父进程当前目录环境,如当前文件系统,当前目录pwd

3.3复制父进程的用户空间

static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)

这里复制父进程的 vm_area_struct,即用户虚存空间,包括目录表、页面表。事实上do_fork()中并不会真正的复制页面,推迟到访问时通过缺页异常真正申请页面。

copy_mm()首先会申请一个mm_struct结构体,复制父进程的mm_struct内容。我们可以想象即使是复制用户空间,子进程和父进程能够具有相同的目录表吗?显然不能!因此,在申请一个mm_struct后,会调用mm_init(mm)为子进程申请一个新的目录表。

接下来就是二层循环将父进程的页表项填入到子进程相应的页表中,这个过程中包括子进程页表的申请。

总之,子进程复制父进程的用户空间,仅仅是将父进程页面表项内容填到子进程的页表中,因为页面表项的内容才是真正指向页面的地址。

4.copy_thread()

526 #define savesegment(seg,value) \
527 asm volatile("movl %%" #seg ",%0":"=m" (*(int *)&(value)))
528
529 int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
530 unsigned long unused,
531 struct task_struct * p, struct pt_regs * regs)
532 {
533 struct pt_regs * childregs;
534    //获取子进程系统堆栈顶部指针
535 childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1;    
536 struct_cpy(childregs, regs);    //从父进程拷贝系统堆栈状态
537 childregs->eax = 0;             //子进程返回值设0
538 childregs->esp = esp;           //子进程用户堆栈
539
540 p->thread.esp = (unsigned long) childregs;        //初次运行时,子进程系统堆栈位置
541 p->thread.esp0 = (unsigned long) (childregs+1);    //子进程系统空间堆栈顶部
542
543 p->thread.eip = (unsigned long) ret_from_fork;     //下次运行时的切入点
544
545 savesegment(fs,p->thread.fs);
546 savesegment(gs,p->thread.gs);
547
548 unlazy_fpu(current);
549 struct_cpy(&p->thread.i387, ¤t->thread.i387);
550
551 return 0;
552 }

这个函数比较关键。这里拷贝父进程内核堆栈,541行将子进程内核堆栈地址保存到thread.esp0,子进程被调度时,thread.esp0会写入到TSS中的esp0,用于进程堆栈切换。接着543设置子进程初次运行时指令执行起点。

537行将0放到eax中,而do_fork()在返回后,会从eax中读取返回地址,子进程的返回地址就是0。

这时,一个完整的子进程已经诞生了,这时子进程还不在进程可执行队列中,不能接受调度,但是随后就会通过wake_up_process(p)将其加入可执行队列接受调度。子进程系统堆栈空间如下图所示。

而父进程也就是当前进程继续执行,最后,do_fork()子进程的pid作为返回值返回。

retval = p->pid;
....省略....
return retval;

这样子进程被调度时的返回值和父进程从do_fork()返回时就不一样了。

参考资料:

Linux内核情景分析》毛德操,胡希明


http://www.niftyadmin.cn/n/1750233.html

相关文章

程序猿摘要:帮您清除更早一些道理

1、 分享第一条经验:“学历代表过去、能力代表如今、学习力代表未来。”事实上这是一个来自国外教育领域的一个研究结果。相信工作过几年、十几年的朋友对这个道 理有些体会吧。但我相信这一点也非常重要:“重要的道理明确太晚将抱憾终生。”所以放在每一…

Data Manager 数据结构+STL

/*优先队列的小根堆的写法。水题。 还有一个方法是将所有的值都变为负数&#xff0c;然后直接用优先队列。 输出输入和输出还有字符串用c&#xff0c;不然tle。*/#include <iostream> #include <queue> #include <cstring> #include <stdio.h> using n…

真香!使用 Goland 网页版实现真正的云开发

云原生玩家往往都是左手 MacBook&#xff0c;右手 Goland&#xff0c;但由于大部分人的 MacBook 硬件资源有限&#xff0c;基本上无法丝滑地使用 Goland。即使你是 8C16G 的高富帅&#xff0c;多开几个 PornHub 标签页也会撑不住的&#xff0c;许多人不得不忍痛转向 VSCode。现…

Linux内核源码阅读之系统调用execve()

asmlinkage int sys_execve(struct pt_regs regs) 前言 sys_execve()根据参数中指定的二进制文件路径&#xff0c;执行相应的二进制文件。我们可能会疑惑&#xff0c;参数中是一个pt_regs结构&#xff0c;哪里有文件路径&#xff1f;事实上&#xff0c;系统调用也属于中断&…

又超时了!Etcd分布式锁你用明白了吗?

现象线上程序报错&#xff0c;错误信息&#xff1a;lock failed: context deadline exceeded, retry问题排查很明显的是获取锁超时了&#xff0c;由于用的 etcd 的分布式锁&#xff0c;可能是 etcd 出问题了&#xff0c;此时看到大量 etcd 日志&#xff0c;rejected connection…

Artificial Intelligence? 编码好题!

/*稍有繁琐。 先确定号&#xff0c;再确定单位。判断单位的前一个字符是否为前缀。然后确定数字的范围。 用atof将范围内的字符串放到一个字符数组里&#xff0c;再将其转化为数字。*/ #include <iostream> #include <stdio.h> #include <string.h> #include…

实战weblogic集群之创建节点和集群

一、启动weblogic&#xff0c;访问控制台 weblogic的domain创建完成后&#xff0c;接下来就可以启动它&#xff0c;步骤如下&#xff1a; $ cd /app/sinova/domains/base_domain/bin $ ./startWebLogic.sh 按照提示输入创建domain时设置的用户名及密码&#xff08;weblogic&…

Linux源码阅读进程通信之管道(详)

管道是进程通信的一种方式&#xff0c;这里主要介绍无名管道&#xff08;以后简称管道&#xff09;。 特点 1.这种管道只能在父子进程或兄弟进程之间建立&#xff0c;而命名管道就没有这种限制。 2.管道只能单向流通&#xff0c;对于A,B两个进程&#xff0c;要不A写B读&#xf…