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

基础知识概括

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

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

struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped 进程状态,-1表示不可执行,0表示可执行,大于1表示停止*/
void *stack; //内核堆栈
atomic_t usage;
unsigned int flags; /* per process flags, defined below 进程标识符 * /
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的代码:

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;
}

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

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

5.copy_process的部分代码:

static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p;
...
retval = security_task_create(clone_flags);//安全性检查
...
p = dup_task_struct(current); //复制PCB,为子进程创建内核栈、进程描述符
ftrace_graph_init_task(p);
··· retval = -EAGAIN;
// 检查该用户的进程数是否超过限制
if (atomic_read(&p->real_cred->user->processes) >=
task_rlimit(p, RLIMIT_NPROC)) {
// 检查该用户是否具有相关权限,不一定是root
if (p->real_cred->user != INIT_USER &&
!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
goto bad_fork_free;
}
...
// 检查进程数量是否超过 max_threads,后者取决于内存的大小
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count; if (!try_module_get(task_thread_info(p)->exec_domain->module))
goto bad_fork_cleanup_count;
...
spin_lock_init(&p->alloc_lock); //初始化自旋锁
init_sigpending(&p->pending); //初始化挂起信号
posix_cpu_timers_init(p); //初始化CPU定时器
···
retval = sched_fork(clone_flags, p); //初始化新进程调度程序数据结构,把新进程的状态设置为TASK_RUNNING,并禁止内核抢占
...
// 复制所有的进程信息
shm_init_task(p);
retval = copy_semundo(clone_flags, p);
...
retval = copy_files(clone_flags, p);
...
retval = copy_fs(clone_flags, p);
...
retval = copy_sighand(clone_flags, p);
...
retval = copy_signal(clone_flags, p);
...
retval = copy_mm(clone_flags, p);
...
retval = copy_namespaces(clone_flags, p);
...
retval = copy_io(clone_flags, p);
...
retval = copy_thread(clone_flags, stack_start, stack_size, p);// 初始化子进程内核栈
...
//若传进来的pid指针和全局结构体变量init_struct_pid的地址不相同,就要为子进程分配新的pid
if (pid != &init_struct_pid) {
retval = -ENOMEM;
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
if (!pid)
goto bad_fork_cleanup_io;
} ...
p->pid = pid_nr(pid); //根据pid结构体中获得进程pid
//若 clone_flags 包含 CLONE_THREAD标志,说明子进程和父进程在同一个线程组
if (clone_flags & CLONE_THREAD) {
p->exit_signal = -1;
p->group_leader = current->group_leader; //线程组的leader设为子进程的组leader
p->tgid = current->tgid; //子进程继承父进程的tgid
} else {
if (clone_flags & CLONE_PARENT)
p->exit_signal = current->group_leader->exit_signal;
else
p->exit_signal = (clone_flags & CSIGNAL);
p->group_leader = p; //子进程的组leader就是它自己 p->tgid = p->pid; //组号tgid是它自己的pid
} ... if (likely(p->pid)) {
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace); init_task_pid(p, PIDTYPE_PID, pid);
if (thread_group_leader(p)) {
...
// 将子进程加入它所在组的哈希链表中
attach_pid(p, PIDTYPE_PGID);
attach_pid(p, PIDTYPE_SID);
__this_cpu_inc(process_counts);
} else {
...
}
attach_pid(p, PIDTYPE_PID);
nr_threads++; //增加系统中的进程数目
}
...
return p; //返回被创建的子进程描述符指针P
...
}

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

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

6.dup_task_struct的代码:

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
int node = tsk_fork_get_node(orig);
int err; // 分配一个task_struct结点
tsk = alloc_task_struct_node(node);
if (!tsk)
return NULL; // 分配一个thread_info结点,其实内部分配了一个union,包含进程的内核栈
// 此时ti的值为栈底,在x86下为union的高地址处。
ti = alloc_thread_info_node(tsk, node);
if (!ti)
goto free_tsk; err = arch_dup_task_struct(tsk, orig);
if (err)
goto free_ti; // 将栈底的值赋给新结点的stack
tsk->stack = ti; ... /*
* One for us, one for whoever does the "release_task()" (usually
* parent)
*/
// 将进程描述符的使用计数器置为2
atomic_set(&tsk->usage, 2);
#ifdef CONFIG_BLK_DEV_IO_TRACE
tsk->btrace_seq = 0;
#endif
tsk->splice_pipe = NULL;
tsk->task_frag.page = NULL; account_kernel_stack(ti, 1); // 返回新申请的结点
return tsk; free_ti:
free_thread_info(ti);
free_tsk:
free_task_struct(tsk);
return NULL;
}

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

  • 先调用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的代码:

// 初始化子进程的内核栈
int copy_thread(unsigned long clone_flags, unsigned long sp,
unsigned long arg, struct task_struct *p)
{ // 取出子进程的寄存器信息
struct pt_regs *childregs = task_pt_regs(p);
struct task_struct *tsk;
int err; // 栈顶 空栈
p->thread.sp = (unsigned long) childregs;
p->thread.sp0 = (unsigned long) (childregs+1);
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps)); // 如果是创建的内核线程
if (unlikely(p->flags & PF_KTHREAD)) {
/* kernel thread */
memset(childregs, 0, sizeof(struct pt_regs));
// 内核线程开始执行的位置
p->thread.ip = (unsigned long) ret_from_kernel_thread;
task_user_gs(p) = __KERNEL_STACK_CANARY;
childregs->ds = __USER_DS;
childregs->es = __USER_DS;
childregs->fs = __KERNEL_PERCPU;
childregs->bx = sp; /* function */
childregs->bp = arg;
childregs->orig_ax = -1;
childregs->cs = __KERNEL_CS | get_kernel_rpl();
childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
p->thread.io_bitmap_ptr = NULL;
return 0;
} // 将当前进程的寄存器信息复制给子进程
*childregs = *current_pt_regs();
// 子进程的eax置为0,所以fork的子进程返回值为0
childregs->ax = 0;
if (sp)
childregs->sp = sp; // 子进程从ret_from_fork开始执行
p->thread.ip = (unsigned long) ret_from_fork;
task_user_gs(p) = get_user_gs(current_pt_regs()); p->thread.io_bitmap_ptr = NULL;
tsk = current;
err = -ENOMEM; // 如果父进程使用IO权限位图,那么子进程获得该位图的一个拷贝
if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
IO_BITMAP_BYTES, GFP_KERNEL);
if (!p->thread.io_bitmap_ptr) {
p->thread.io_bitmap_max = 0;
return -ENOMEM;
}
set_tsk_thread_flag(p, TIF_IO_BITMAP);
} ... return err;
}

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. React Native 开发豆瓣评分(七)首页组件开发

    首页内容拆分 看效果图,首页由热门影院.豆瓣热门.热门影视等列表组成,每个列表又由头加横向滑动的 电影海报列表构成. 所以可以先把页面的电影海报.评分.列表头做成组件,然后在使用 ScrollView ...

  2. Firebird 审计追踪

    Firebird 打开审计追踪功能,即在服务器上打开日志记录功能,根据配置记录不同类型的服务器执行情况. 1.首先修改Firebird.conf文件,打开审计功能: AuditTraceConfigF ...

  3. [LeetCode] 647. 回文子串 ☆☆☆(最长子串、动态规划、中心扩展算法)

    描述 给定一个字符串,你的任务是计算这个字符串中有多少个回文子串. 具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被计为是不同的子串. 示例 1: 输入: "abc" ...

  4. 大数据集群环境 zookeeper集群环境安装

    大数据集群环境 zookeeper集群环境准备 zookeeper集群安装脚本,如果安装需要保持zookeeper保持相同目录,并且有可执行权限,需要准备如下 编写脚本: vi zkInstall.s ...

  5. docker启动报错解决及分析(Cannot create container for service *******: cannot mount volume over existing file, file exists /var/lib/docker/overlay2/)

    现象: Cannot create container for service *******: cannot mount volume over existing file, file exists ...

  6. linux中container_of

    linux 驱动程序中 container_of宏解析 众所周知,linux内核的主要开发语言是C,但是现在内核的框架使用了非常多的面向对象的思想,这就面临了一个用C语言来实现面向对象编程的问题,今天 ...

  7. .pro文件部分命令详解

    #引入c++11 CONFIG += C++11 # 引入头文件的路径 INCLUDEPATH += D:\opencv\opencv3.2\configure\install\include # 引 ...

  8. 在vue项目中使用自己封装的ajax

    在 src 目录下新建 vue.extend.js ,内容如下: export default { install(Vue) { Vue.prototype.$http=function(option ...

  9. 利用Python绘制一个正方形螺旋线

    1 安装turtle Python2安装命令: pip install turtule Python3安装命令: pip3 install turtle 因为turtle库主要是在Python2中使用 ...

  10. 2019牛客多校第二场BEddy Walker 2——BM递推

    题意 从数字 $0$ 除法,每次向前走 $i$ 步,$i$ 是 $1 \sim K$ 中等概率随机的一个数,也就是说概率都是 $\frac{1}{K}$.求落在过数字 $N$ 额概率,$N=-1$ 表 ...