《Linux内核分析》第六周学习笔记

《Linux内核分析》第六周学习笔记 进程的描述和创建

郭垚 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

【学习视频时间:1小时 撰写博客时间:2小时】

【学习内容:进程创建的过程、使用gdb跟踪分析内核处理函数sys_clone】

一、进程的描述

1.1 进程描述符task_struct数据结构(一)

1. 进程控制块PCB——task_struct

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

struct task_struct{
volatile long state; //进程状态,-1表示不可执行,0表示可执行,大于1表示停止
void *stack; //内核堆栈
atomic_t usage;
unsigned int flags; //进程标识符
unsigned int ptrace;
……
}

struct task_ struct数据结构很庞大

  • Linux进程的状态与操作系统原理中的描述的进程状态似乎有所不同,比如就绪状态和运行状态都是TASK_ RUNNING,为什么呢?
  • 进程的标示pid
  • 所有进程链表struct list_ head tasks;
    • 内核的双向循环链表的实现方法
    • 一个更简略的双向循环链表
  • 程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系
  • Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_ info和进程的内核堆栈
    • 进程处于内核态时使用,不同于用户态堆栈,即PCB中指定了内核栈,那为什么PCB中没有用户态堆栈?用户态堆栈是怎么设定的?
    • 内核控制路径所用的堆栈很少,因此对栈和Thread_ info来说,8KB足够了
  • struct thread_ struct thread; //CPU-specific state of this task
  • 文件系统和文件描述符
  • 内存管理——进程的地址空间

《Linux内核分析》第六周学习笔记Linux进程状态转换图

1.2 进程描述符task_struct数据结构(二)

#ifdef CONFIG_SMP //条件编译,多处理器会用到
struct llist_node wake_entry;
int on_cpu;
struct task_struct *last_wakee;
unsigned long wakee_flips;
unsigned long wakee_flip_decay_ts; int wake_cpu;
#endif
int on_rq; int prio, static_prio, normal_prio;
unsigned int rt_priority; //与优先级相关
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt; …… struct list_head tasks; //进程链表
#ifdef CONFIG_SMP
struct plist_node pushable_tasks;
struct rb_node pushable_dl_tasks;
#endif

《Linux内核分析》第六周学习笔记

Linux的状态图

    struct mm_struct *mm, *active_mm; //内存管理进程的地址空间
#ifdef CONFIG_COMPAT_BRK
unsigned brk_randomized:;
#endif
/* per-thread vma caching */
u32 vmacache_seqnum;
struct vm_area_struct *vmacache[VMACACHE_SIZE];
#if defined(SPLIT_RSS_COUNTING)
struct task_rss_stat rss_stat;
#endif
/* task state */
int exit_state; //任务的状态
int exit_code, exit_signal;
int pdeath_signal; /* The signal sent when the parent dies */
unsigned int jobctl; /* JOBCTL_*, siglock protected */ /* Used for emulating ABI behavior of previous Linux versions */
unsigned int personality; unsigned in_execve:; /* Tell the LSMs that the process is doing an
1321 * execve */
unsigned in_iowait:; …… //进程的父子关系
* pointers to (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/
struct task_struct __rcu *real_parent; /* real parent process */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
/*
1345 * children/sibling forms the list of my natural children
1346 */
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */

《Linux内核分析》第六周学习笔记

《Linux内核分析》第六周学习笔记

    struct list_head ptraced; //调试
struct list_head ptrace_entry; /* PID/PID hash table linkage. */
struct pid_link pids[PIDTYPE_MAX];
struct list_head thread_group;
struct list_head thread_node; //将进程链表连接起来 …… /* 与当前任务CPU状态相关,对进程上下文切换有关键性作用 */
struct thread_struct thread;
/* filesystem information */
struct fs_struct *fs;
/* 打开文件描述符列表 */
struct files_struct *files;
/* namespaces */
struct nsproxy *nsproxy;
/* 信号处理 */
struct signal_struct *signal;
struct sighand_struct *sighand; sigset_t blocked, real_blocked;
sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
struct sigpending pending;
……
struct pipe_inode_info *splice_pipe;
//与管道相关
struct page_frag task_frag; #ifdef CONFIG_TASK_DELAY_ACCT
struct task_delay_info *delays;
#endif
#ifdef CONFIG_FAULT_INJECTION
int make_it_fail;
#endif

二、进程的创建

2.1 进程的创建概览及fork一个进程的用户态代码

回顾:

  • 道生一(start_ kernel...cpu_ idle)
  • 一生二(kernel_ init和kthreadd)
  • 二生三(即前面的0、1、2三个进程)
  • 三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)
  • start_ kernel创建了cpu_ idle,也就是0号进程。而0号进程又创建了两个线程,一个是kernel_ init,也就是1号进程,这个进程最终启动了用户态;另一个是kthreadd。0号进程是固定的代码,1号进程是通过复制0号进程PCB之后在此基础上做修改得到的
  • iret与int 0x80指令对应,一个是弹出寄存器值,一个是压入寄存器的值
  • 如果将系统调用类比于fork();那么就相当于系统调用创建了一个子进程,然后子进程返回之后将在内核态运行,而返回到父进程后仍然在用户态运行

fork一个子进程的代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < )
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-);
}
else if (pid == ) //pid == 0和下面的else都会被执行到(一个是在父进程中即pid ==0的情况,一个是在子进程中,即pid不等于0) {
/* child process */
printf("This is Child Process!\n");
}
else
{
/* parent process */
printf("This is Parent Process!\n");
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
}

2.2 理解进程创建过程复杂代码的方法

《Linux内核分析》第六周学习笔记

《Linux内核分析》第六周学习笔记

Linux中创建进程一共有三个函数:

  • fork,创建子进程
  • vfork,与fork类似,但是父子进程共享地址空间,而且子进程先于父进程运行。
  • clone,主要用于创建线程

  这里值得注意的是,Linux中得线程是通过模拟进程实现的,较新的内核使用的线程库一般都是NPTL。

  • 复制一个PCB——task_struct

err = arch_dup_task_struct(tsk, orig);
  • 给新进程分配一个新的内核堆栈

ti = alloc_ thread_ info_ node(tsk, node);
tsk->stack = ti;
setup_ thread_ stack(tsk, orig); //这里只是复制thread_ info,而非复制内核堆栈
  • 要修改复制过来的进程数据,比如pid、进程链表等。具体见copy _process内部
  • 从用户态的代码看fork(),函数返回了两次,即在父子进程中各返回一次。

2.3 浏览进程创建过程相关的关键代码

通过之前的学习,我们知道fork是通过触发0x80中断,陷入内核,来使用内核提供的提供调用:

SYSCALL_DEFINE0(fork)
{
return do_fork(SIGCHLD, , , NULL, NULL);
}
#endif SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, ,
, NULL, NULL);
} SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
{
return do_fork(clone_flags, newsp, , parent_tidptr, child_tidptr);
}

通过以上精简后的代码,我们可以看出,fork、vfork和clone这三个函数最终都是通过do_ fork函数实现的。

追踪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 = ;
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大概做了如下几件事:

  1. 调用copy_ process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。
  2. 初始化vfork的完成处理信息(如果是vfork调用)
  3. 调用wake_ up_ new_ task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。
  4. 如果是vfork调用,需要阻塞父进程,知道子进程执行exec。上面的过程对vfork稍微做了处理,因为vfork必须保证子进程优先运行,执行exec,替换自己的地址空间。

抛开vfork,进程创建的大部分过程都在copy_ process函数中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; // 分配一个新的task_struct,此时的p与当前进程的task,仅仅是stack地址不同
p = dup_task_struct(current); // 检查该用户的进程数是否超过限制
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;
} retval = -EAGAIN;
// 检查进程数量是否超过 max_threads,后者取决于内存的大小
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count; // 初始化自旋锁 // 初始化挂起信号 // 初始化定时器 // 完成对新进程调度程序数据结构的初始化,并把新进程的状态设置为TASK_RUNNING
retval = sched_fork(clone_flags, p);
// ..... // 复制所有的进程信息
// copy_xyz // 初始化子进程的内核栈
retval = copy_thread(clone_flags, stack_start, stack_size, p);
if (retval)
goto bad_fork_cleanup_io; if (pid != &init_struct_pid) {
retval = -ENOMEM;
// 这里为子进程分配了新的pid号
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
if (!pid)
goto bad_fork_cleanup_io;
} /* ok, now we should be set up.. */
// 设置子进程的pid
p->pid = pid_nr(pid);
// 如果是创建线程
if (clone_flags & CLONE_THREAD) {
p->exit_signal = -;
// 线程组的leader设置为当前线程的leader
p->group_leader = current->group_leader;
// tgid是当前线程组的id,也就是main进程的pid
p->tgid = current->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;
// tgid和pid相同
p->tgid = p->pid;
} if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
// 如果是创建线程,那么同一线程组内的所有线程、进程共享parent
p->real_parent = current->real_parent;
p->parent_exec_id = current->parent_exec_id;
} else {
// 如果是创建进程,当前进程就是子进程的parent
p->real_parent = current;
p->parent_exec_id = current->self_exec_id;
} // 将pid加入PIDTYPE_PID这个散列表
attach_pid(p, PIDTYPE_PID);
// 递增 nr_threads的值
nr_threads++; // 返回被创建的task结构体指针
return p;
}

根据这份精简代码,我总结出copy_process的大体流程

  1. 检查各种标志位(已经省略)
  2. 调用dup_ task_ struct复制一份task_ struct结构体,作为子进程的进程描述符。
  3. 检查进程的数量限制。
  4. 初始化定时器、信号和自旋锁。
  5. 初始化与调度有关的数据结构,调用了sched_ fork,这里将子进程的state设置为TASK_ RUNNING。
  6. 复制所有的进程信息,包括fs、信号处理函数、信号、内存空间(包括写时复制)等。
  7. 调用copy_ thread,这又是关键的一步,这里设置了子进程的堆栈信息。
  8. 为子进程分配一个pid
  9. 设置子进程与其他进程的关系,以及pid、tgid等。这里主要是对线程做一些区分。

2.4 创建的新进程从哪里开始执行?

一个新创建的子进程,(当它获得CPU之后)是从哪一行代码进程执行的?

  • 与之前写过的my_ kernel相比较,kernel中是可以指定新进程开始的位置(也就是通过eip寄存器指定代码行)。fork中也有相似的机制
  • 这涉及子进程的内核堆栈数据状态和task_ struct中thread记录的sp和ip的一致性问题,这是在copy_ thread in copy_ process设定的

 *childregs = *current_pt_regs(); //复制内核堆栈
childregs->ax = ; //子进程的fork返回0
p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址

2.5 使用gdb跟踪创建新进程的过程(实验)

1. 更新menu,删除test_fork.c和test.c文件,重新执行make rootfs

《Linux内核分析》第六周学习笔记

2. 编译内核查看fork命令

《Linux内核分析》第六周学习笔记

3. 启动gdb调试,并对主要的函数设置断点

《Linux内核分析》第六周学习笔记

《Linux内核分析》第六周学习笔记

《Linux内核分析》第六周学习笔记

执行一个fork,会发现只输出一个fork的命令描述,后面并没有执行,因为它停在了sys_ clone这个位置。

4. 特别关注新进程是从哪里开始执行的?为什么从哪里能顺利执行下去?即执行起点与内核堆栈如何保证一致。

  答:ret_ from_ fork决定了新进程的第一条指令地址。子进程从ret_ from_ fork处开始执行。因为在ret_ from_ fork之前,也就是在copy_ thread()函数中* childregs = * current_ pt_ regs();该句将父进程的regs参数赋值到子进程的内核堆栈。* childregs的类型为pt_ regs,里面存放了SAVE_ ALL中压入栈的参数,因此在之后的RESTORE ALL中能顺利执行下去。

总结

  Linux中所有的进程创建都是基于复制的方式,Linux通过复制父进程来创建一个新进程,通过调用do_ fork来实现。然后对子进程做一些特殊的处理。而Linux中的线程,又是一种特殊的进程。根据代码的分析,do_ fork中,copy_ process管子进程运行的准备,wake_ up_ new_ task作为子进程forking的完成。fork()函数最大的特点就是被调用一次,返回两次。

上一篇:[druid]大数据挑战——如何使用Druid实现数据聚合


下一篇:NuGet 程序源包