分析Linux内核创建一个新进程的过程

基础知识概括

  • 操作系统内核实现操作系统的三大管理功能,即进程管理功能,内存管理和文件系统。对应的三个抽象的概念是进程,虚拟内存和文件。其中,操作系统最核心的功能是进程管理。
  • 进程标识值:内核通过唯一的PID来标识每个进程。
  • 进程状态:进程描述符中state域描述了进程的当前状态。
  • iret与int 0x80指令对应,一个是离开系统调用弹出寄存器值,一个是进入系统调用压入寄存器的值。
  • fork()函数最大的特点就是被调用一次,返回两次,在父进程中返回新创建子进程的 pid;在子进程中返回 0。
  • 在Linux中,fork,vfork和clone这3个系统调用都通过do_fork来实现进程的创建
  • 在Linux中1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先

进程控制块PCB——task_struct,为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。

  1. struct task_struct {
  2. volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped 进程状态,-1表示不可执行,0表示可执行,大于1表示停止*/
  3. void *stack; //内核堆栈
  4. atomic_t usage;
  5. unsigned int flags; /* per process flags, defined below 进程标识符 * /
  6. unsigned int ptrace;

进程的创建

1.道生一(start_ kernel...rest_init),一生二(kernel_ init和kthreadd),二生三(即前面的0、1、2三个进程),三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)start_ kernel创建了rest_init,也就是0号进程。而0号进程又创建了两个线程,一个是kernel_ init,也就是1号进程,这个进程最终启动了用户态;另一个是kthreadd内核线程是所有内核线程的祖先,负责管理所有内核线程。0号进程是固定的代码,1号进程是通过复制0号进程PCB之后在此基础上做修改得到的。

2.Linux中创建进程一共有三个函数:fork,创建子进程 vfork,与fork类似,但是父子进程共享地址空间,而且子进程先于父进程运行。 clone,主要用于创建线程。Linux中所有的进程创建都是基于复制的方式,Linux通过复制父进程来创建一个新进程,通过调用do_ fork来实现。然后对子进程做一些特殊的处理。而Linux中的线程,又是一种特殊的进程。根据代码的分析,do_ fork中,copy_ process管子进程运行的准备,wake_ up_ new_ task作为子进程forking的完成。

3.fork系统调用

vfork系统调用

clone系统调用

通过上面的代码我们可以看出来fork、vfork 和 clone 三个系统调用都可以创建一个新进程,而且都是通过 do_fork 来创建进程,只不过传递的参数不同。

4.do_fork的代码:

  1. long do_fork(unsigned long clone_flags,
  2. unsigned long stack_start,
  3. unsigned long stack_size,
  4. int __user *parent_tidptr,
  5. int __user *child_tidptr)
  6. {
  7. struct task_struct *p;
  8. int trace = 0;
  9. long nr;
  10. // ...
  11. // 复制进程描述符,返回创建的task_struct的指针
  12. p = copy_process(clone_flags, stack_start, stack_size,
  13. child_tidptr, NULL, trace);
  14. if (!IS_ERR(p)) {
  15. struct completion vfork;
  16. struct pid *pid;
  17. trace_sched_process_fork(current, p);
  18. // 取出task结构体内的pid
  19. pid = get_task_pid(p, PIDTYPE_PID);
  20. nr = pid_vnr(pid);
  21. if (clone_flags & CLONE_PARENT_SETTID)
  22. put_user(nr, parent_tidptr);
  23. // 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行
  24. if (clone_flags & CLONE_VFORK) {
  25. p->vfork_done = &vfork;
  26. init_completion(&vfork);
  27. get_task_struct(p);
  28. }
  29. // 将子进程添加到调度器的队列,使得子进程有机会获得CPU
  30. wake_up_new_task(p);
  31. // ...
  32. // 如果设置了 CLONE_VFORK 则将父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间
  33. // 保证子进程优先于父进程运行
  34. if (clone_flags & CLONE_VFORK) {
  35. if (!wait_for_vfork_done(p, &vfork))
  36. ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
  37. }
  38. put_pid(pid);
  39. } else {
  40. nr = PTR_ERR(p);
  41. }
  42. return nr;
  43. }

从上面的代码中我们可以分析出来do_fork函数的作用:

  • 调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。
  • 初始化vfork的完成处理信息(如果是vfork调用)
  • 调用wake_up_new_task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。
  • 如果是vfork调用,需要阻塞父进程,知道子进程执行exec。

5.copy_process的部分代码:

  1. static struct task_struct *copy_process(unsigned long clone_flags,
  2. unsigned long stack_start,
  3. unsigned long stack_size,
  4. int __user *child_tidptr,
  5. struct pid *pid,
  6. int trace)
  7. {
  8. int retval;
  9. struct task_struct *p;
  10. ...
  11. retval = security_task_create(clone_flags);//安全性检查
  12. ...
  13. p = dup_task_struct(current); //复制PCB,为子进程创建内核栈、进程描述符
  14. ftrace_graph_init_task(p);
  15. ···
  16. retval = -EAGAIN;
  17. // 检查该用户的进程数是否超过限制
  18. if (atomic_read(&p->real_cred->user->processes) >=
  19. task_rlimit(p, RLIMIT_NPROC)) {
  20. // 检查该用户是否具有相关权限,不一定是root
  21. if (p->real_cred->user != INIT_USER &&
  22. !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
  23. goto bad_fork_free;
  24. }
  25. ...
  26. // 检查进程数量是否超过 max_threads,后者取决于内存的大小
  27. if (nr_threads >= max_threads)
  28. goto bad_fork_cleanup_count;
  29. if (!try_module_get(task_thread_info(p)->exec_domain->module))
  30. goto bad_fork_cleanup_count;
  31. ...
  32. spin_lock_init(&p->alloc_lock); //初始化自旋锁
  33. init_sigpending(&p->pending); //初始化挂起信号
  34. posix_cpu_timers_init(p); //初始化CPU定时器
  35. ···
  36. retval = sched_fork(clone_flags, p); //初始化新进程调度程序数据结构,把新进程的状态设置为TASK_RUNNING,并禁止内核抢占
  37. ...
  38. // 复制所有的进程信息
  39. shm_init_task(p);
  40. retval = copy_semundo(clone_flags, p);
  41. ...
  42. retval = copy_files(clone_flags, p);
  43. ...
  44. retval = copy_fs(clone_flags, p);
  45. ...
  46. retval = copy_sighand(clone_flags, p);
  47. ...
  48. retval = copy_signal(clone_flags, p);
  49. ...
  50. retval = copy_mm(clone_flags, p);
  51. ...
  52. retval = copy_namespaces(clone_flags, p);
  53. ...
  54. retval = copy_io(clone_flags, p);
  55. ...
  56. retval = copy_thread(clone_flags, stack_start, stack_size, p);// 初始化子进程内核栈
  57. ...
  58. //若传进来的pid指针和全局结构体变量init_struct_pid的地址不相同,就要为子进程分配新的pid
  59. if (pid != &init_struct_pid) {
  60. retval = -ENOMEM;
  61. pid = alloc_pid(p->nsproxy->pid_ns_for_children);
  62. if (!pid)
  63. goto bad_fork_cleanup_io;
  64. }
  65. ...
  66. p->pid = pid_nr(pid); //根据pid结构体中获得进程pid
  67. //若 clone_flags 包含 CLONE_THREAD标志,说明子进程和父进程在同一个线程组
  68. if (clone_flags & CLONE_THREAD) {
  69. p->exit_signal = -1;
  70. p->group_leader = current->group_leader; //线程组的leader设为子进程的组leader
  71. p->tgid = current->tgid; //子进程继承父进程的tgid
  72. } else {
  73. if (clone_flags & CLONE_PARENT)
  74. p->exit_signal = current->group_leader->exit_signal;
  75. else
  76. p->exit_signal = (clone_flags & CSIGNAL);
  77. p->group_leader = p; //子进程的组leader就是它自己
  78. p->tgid = p->pid; //组号tgid是它自己的pid
  79. }
  80. ...
  81. if (likely(p->pid)) {
  82. ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
  83. init_task_pid(p, PIDTYPE_PID, pid);
  84. if (thread_group_leader(p)) {
  85. ...
  86. // 将子进程加入它所在组的哈希链表中
  87. attach_pid(p, PIDTYPE_PGID);
  88. attach_pid(p, PIDTYPE_SID);
  89. __this_cpu_inc(process_counts);
  90. } else {
  91. ...
  92. }
  93. attach_pid(p, PIDTYPE_PID);
  94. nr_threads++; //增加系统中的进程数目
  95. }
  96. ...
  97. return p; //返回被创建的子进程描述符指针P
  98. ...
  99. }

通过上面的代码我们可以知道copy_process函数的主要作用:

  • 创建进程描述符以及子进程所需要的其他所有数据结构,为子进程准备运行环境
  • 调用dup_task_struct复制一份task_struct结构体,作为子进程的进程描述符。
  • 复制所有的进程信息
  • 调用copy_thread,设置子进程的堆栈信息,为子进程分配一个pid。

6.dup_task_struct的代码:

  1. static struct task_struct *dup_task_struct(struct task_struct *orig)
  2. {
  3. struct task_struct *tsk;
  4. struct thread_info *ti;
  5. int node = tsk_fork_get_node(orig);
  6. int err;
  7. // 分配一个task_struct结点
  8. tsk = alloc_task_struct_node(node);
  9. if (!tsk)
  10. return NULL;
  11. // 分配一个thread_info结点,其实内部分配了一个union,包含进程的内核栈
  12. // 此时ti的值为栈底,在x86下为union的高地址处。
  13. ti = alloc_thread_info_node(tsk, node);
  14. if (!ti)
  15. goto free_tsk;
  16. err = arch_dup_task_struct(tsk, orig);
  17. if (err)
  18. goto free_ti;
  19. // 将栈底的值赋给新结点的stack
  20. tsk->stack = ti;
  21. ...
  22. /*
  23. * One for us, one for whoever does the "release_task()" (usually
  24. * parent)
  25. */
  26. // 将进程描述符的使用计数器置为2
  27. atomic_set(&tsk->usage, 2);
  28. #ifdef CONFIG_BLK_DEV_IO_TRACE
  29. tsk->btrace_seq = 0;
  30. #endif
  31. tsk->splice_pipe = NULL;
  32. tsk->task_frag.page = NULL;
  33. account_kernel_stack(ti, 1);
  34. // 返回新申请的结点
  35. return tsk;
  36. free_ti:
  37. free_thread_info(ti);
  38. free_tsk:
  39. free_task_struct(tsk);
  40. return NULL;
  41. }

通过上面的部分代码我们可知:

  • 先调用alloc_task_struct_node分配一个task_struct结构体。
  • 调用alloc_thread_info_node,分配了一个union。这里分配了一个thread_info结构体,还分配了一个stack数组。返回值为ti,实际上就是栈底。
  • tsk->stack = ti将栈底的地址赋给task的stack变量。
  • 最后为子进程分配了内核栈空间。
  • 执行完dup_task_struct之后,子进程和父进程的task结构体,除了stack指针之外,完全相同

7.copy_thread的代码:

  1. // 初始化子进程的内核栈
  2. int copy_thread(unsigned long clone_flags, unsigned long sp,
  3. unsigned long arg, struct task_struct *p)
  4. {
  5. // 取出子进程的寄存器信息
  6. struct pt_regs *childregs = task_pt_regs(p);
  7. struct task_struct *tsk;
  8. int err;
  9. // 栈顶 空栈
  10. p->thread.sp = (unsigned long) childregs;
  11. p->thread.sp0 = (unsigned long) (childregs+1);
  12. memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
  13. // 如果是创建的内核线程
  14. if (unlikely(p->flags & PF_KTHREAD)) {
  15. /* kernel thread */
  16. memset(childregs, 0, sizeof(struct pt_regs));
  17. // 内核线程开始执行的位置
  18. p->thread.ip = (unsigned long) ret_from_kernel_thread;
  19. task_user_gs(p) = __KERNEL_STACK_CANARY;
  20. childregs->ds = __USER_DS;
  21. childregs->es = __USER_DS;
  22. childregs->fs = __KERNEL_PERCPU;
  23. childregs->bx = sp; /* function */
  24. childregs->bp = arg;
  25. childregs->orig_ax = -1;
  26. childregs->cs = __KERNEL_CS | get_kernel_rpl();
  27. childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
  28. p->thread.io_bitmap_ptr = NULL;
  29. return 0;
  30. }
  31. // 将当前进程的寄存器信息复制给子进程
  32. *childregs = *current_pt_regs();
  33. // 子进程的eax置为0,所以fork的子进程返回值为0
  34. childregs->ax = 0;
  35. if (sp)
  36. childregs->sp = sp;
  37. // 子进程从ret_from_fork开始执行
  38. p->thread.ip = (unsigned long) ret_from_fork;
  39. task_user_gs(p) = get_user_gs(current_pt_regs());
  40. p->thread.io_bitmap_ptr = NULL;
  41. tsk = current;
  42. err = -ENOMEM;
  43. // 如果父进程使用IO权限位图,那么子进程获得该位图的一个拷贝
  44. if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
  45. p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
  46. IO_BITMAP_BYTES, GFP_KERNEL);
  47. if (!p->thread.io_bitmap_ptr) {
  48. p->thread.io_bitmap_max = 0;
  49. return -ENOMEM;
  50. }
  51. set_tsk_thread_flag(p, TIF_IO_BITMAP);
  52. }
  53. ...
  54. return err;
  55. }

copy_thread函数的主要作用为:

  • 获取子进程寄存器信息的存放位置
  • 对子进程的thread.sp赋值,将来子进程运行,这就是子进程的esp寄存器的值。
  • 如果是创建内核线程,那么它的运行位置是ret_from_kernel_thread, - 将这段代码的地址赋给thread.ip,之后准备其他寄存器信息,退出
  • 将父进程的寄存器信息复制给子进程。
  • 将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0.
  • 子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器。

8.最后是运行新进程:从ret_from_fork处开始执行

  • dup_task_struct中为其分配了新的堆栈
  • copy_process中调用了sched_fork,将其置为TASK_RUNNING
  • copy_thread中将父进程的寄存器上下文复制给子进程,这是非常关键的一步,这里保证了父子进程的堆栈信息是一致的。
  • 将ret_from_fork的地址设置为eip寄存器的值,这是子进程的第一条指令。

实验过程

1.给MenuOS增加命令

2.用gdb进行调试,请注意此时应该回到LinuxKernel的目录下来进行

3.设置刚才所讨论的函数的断点

4.do_fork 系统内核调用:

5..copy_process 复制父进程的所有信息给子进程,dup_task_struct 中为子进程分配了新的堆栈:

6.copy_thread系统调用函数:

**copy_thread 这段代码为我们解释了两个相当重要的问题! **

  1. 为什么 fork 在子进程中返回0,原因是childregs->ax = 0;这段代码将子进程的 eax 赋值为0

  2. p->thread.ip = (unsigned long) ret_from_fork;将子进程的 ip 设置为 ret_form_fork 的首地址,因此子进程是从 ret_from_fork 开始执行的。

7.最后通过函数syscall_exit退出

本章总结

创建一个新进程在内核中的执行过程大致如下:

  1. 使用系统调用Sys_clone(或fork,vfork)系统调用创建一个新进程,而且都是通过调用do_fork来实现进程的创建;

  2. Linux通过复制父进程PCB的task_struct来创建一个新进程,要给新进程分配一个新的内核堆栈;

  3. 要修改复制过来的进程数据,比如pid、进程链表等等执行copy_process和copy_thread ;

  4. p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶 ;

  5. p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址;

20189220 余超《Linux内核原理与分析》第七周作业的更多相关文章

  1. 20169212《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 这一周学习了MOOCLinux内核分析的第一讲,计算机是如何工作的?由于本科对相关知识的不熟悉,所以感觉有的知识理解起来了有一定的难度,不过多查查资 ...

  2. 20169210《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 本周作业分为两部分:第一部分为观看学习视频并完成实验楼实验一:第二部分为看<Linux内核设计与实现>1.2.18章并安装配置内核. 第 ...

  3. 2018-2019-1 20189221 《Linux内核原理与分析》第九周作业

    2018-2019-1 20189221 <Linux内核原理与分析>第九周作业 实验八 理理解进程调度时机跟踪分析进程调度与进程切换的过程 进程调度 进度调度时机: 1.中断处理过程(包 ...

  4. 2017-2018-1 20179215《Linux内核原理与分析》第二周作业

    20179215<Linux内核原理与分析>第二周作业 这一周主要了解了计算机是如何工作的,包括现在存储程序计算机的工作模型.X86汇编指令包括几种内存地址的寻址方式和push.pop.c ...

  5. 2019-2020-1 20199329《Linux内核原理与分析》第九周作业

    <Linux内核原理与分析>第九周作业 一.本周内容概述: 阐释linux操作系统的整体构架 理解linux系统的一般执行过程和进程调度的时机 理解linux系统的中断和进程上下文切换 二 ...

  6. 2019-2020-1 20199329《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 一.上周问题总结: 未能及时整理笔记 Linux还需要多用 markdown格式不熟练 发布博客时间超过规定期限 二.本周学习内容: <庖丁解 ...

  7. 2019-2020-1 20209313《Linux内核原理与分析》第二周作业

    2019-2020-1 20209313<Linux内核原理与分析>第二周作业 零.总结 阐明自己对"计算机是如何工作的"理解. 一.myod 步骤 复习c文件处理内容 ...

  8. 2018-2019-1 20189221《Linux内核原理与分析》第一周作业

    Linux内核原理与分析 - 第一周作业 实验1 Linux系统简介 Linux历史 1991 年 10 月,Linus Torvalds想在自己的电脑上运行UNIX,可是 UNIX 的商业版本非常昂 ...

  9. 《Linux内核原理与分析》第一周作业 20189210

    实验一 Linux系统简介 这一节主要学习了Linux的历史,Linux有关的重要人物以及学习Linux的方法,Linux和Windows的区别.其中学到了LInux中的应用程序大都为开源自由的软件, ...

  10. 2018-2019-1 20189221《Linux内核原理与分析》第二周作业

    读书报告 <庖丁解牛Linux内核分析> 第 1 章 计算工作原理 1.1 存储程序计算机工作模型 1.2 x86-32汇编基础 1.3汇编一个简单的C语言程序并分析其汇编指令执行过程 因 ...

随机推荐

  1. 京信通信成功打造自动化工厂(MES应用案例)

    企业介绍: 京信通信成立于1997年,是一家集研发.生产.销售及服务于一体的移动通信外围设备专业厂商,致力于为客户提供无线覆盖和传输的整体解决方案,于2003年在香港联交所主板上市(2342.HK), ...

  2. 【转】SetWindowText 的用法

    SetWindowTextW表示设置的字符串是WCHAR (双字节字符 )SetWindowTextA表示设置的字符串是CHAR (单字节字符 )SetWindowText表示设置的字符串是自动匹配当 ...

  3. 【Java字节码】Idea中查看Java字节码的插件jclasslib Bytecode viewer

    Idea插件搜索:jclasslib Bytecode viewer 安装完后,maven install你的项目(因为该插件会读取target下的class文件),然后选中某个java文件,按下图操 ...

  4. 肖哥HCNP-正式篇笔记

    21.网工学习环境准备. 一. 关掉所有杀毒软件及管家如阿健. 二. 安装环回网卡 (一定要先安装.) 1. 计算机设备管理 2. 在右侧最上端计算机名上方右键,点击过时硬件. 3. 下一步.手动选择 ...

  5. Python_正则表达式语法

    1.正则表达式中的操作符: 2.re库的使用: import re #search方法要求只要待匹配的字符串中包含正则表达式中的字符串就可以 match = re.search('python+',' ...

  6. Mysql InnoDB行锁不使用索引锁表的时候会锁整张表

    原文:http://www.thinkphp.cn/topic/41577.html 如果使用针对InnoDB的表使用行锁,被锁定字段不是主键,也没有针对它建立索引的话.行锁锁定的也是整张表.锁整张表 ...

  7. [Reprint] Difference Between Job, Work, And Career

    https://www.espressoenglish.net/difference-between-job-work-and-career/ A lot of English learners co ...

  8. Caused by SSLError("Can’t connect to HTTPS URL because the SSL module is not available)

    window7系统: 今天刚安装的anaconda(开源的Python包管理器),把原来的python3和python2都给卸载了,结果运行爬虫程序的时候报错: Caused by SSLError( ...

  9. Oracle 中select XX_id_seq.nextval from dual 什么意思呢?

    说明 今天看别人的代码 ,遇见了 一条sql    select ctg_fault_list_id_seq.nextval from dual  不懂意思,然后就研究了下 dual :是oracle ...

  10. Python 简单批量请求接口实例

    #coding:utf-8 ''' Created on 2017年11月10日 @author: li.liu ''' import urllib import time str1=''' http ...