分析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; //调度到子进程时的第一条指令地址;

最新文章

  1. 缓存AsimpleCache -- 解决Android中Sharedpreferences无法存储List数据/ASimpleCache
  2. SQLServer用sql语句怎么返回一个月所有的天数
  3. BZOJ 3527 力
  4. ReactEurope Conf 参会感想
  5. /usr/lib64/python2.6/site-packages/pycurl.so: undefined symbol: CRYPTO_set_locking_callback
  6. ruby中输入命令行编译sass(ruby小白)
  7. vs2013 ADO联系SQL server2012数据库
  8. Angular杂谈系列2-Angular2升级Angular4指南
  9. Nexus 私有仓库搭建与 Maven 集成
  10. appium+Python 启动app(三)登录
  11. Redis数据库云端最佳技术实践
  12. gstreamer如何查看相关插件信息(src/sink)?
  13. MVC的HTTP请求处理过程(IIS应用程序池、CLR线程池)
  14. 【读书笔记】iOS-button只显示在一个界面的右下角,不管界面大小怎么变化(xib,没有使用自动布局)(一)
  15. centos7 上配置Javaweb---MySQL的安装与配置、乱码解决
  16. Lambda表达式select()和where()的区别
  17. How to calculate bits per character of a string? (bpc) to read
  18. Swift3 重写一个带占位符的textView
  19. Major GC和Full GC的区别是什么?触发条件呢?
  20. convert-a-number-to-hexadecimal

热门文章

  1. jQuery 页面加载后执行的事件(3 种方式)
  2. tree - 列出树状目录结构
  3. Django-视图函数view
  4. 【HICP Gauss】数据库 数据库管理(文件 用户管理 系统权限 对象权限 profile)-7
  5. 【笔记】MAML-模型无关元学习算法
  6. 《TensorFlow2深度学习》学习笔记(三)Tensorflow进阶
  7. hdu1384Intervals(差分约束)
  8. windows(hexo)使用git时出现:warning: LF will be replaced by CRLF
  9. wordpress更新出现Briefly unavailable for scheduled maintenance. Check back in a minute.
  10. 【Selenium-WebDriver实战篇】ScreenRecorder的实际输出路径,自己的解决方案