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

“平安的祝福 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”

进程在创建它的时刻开始存活。在Linux系统中,这通常是fork()系统的结果,该系统调用通过复制一个现有的进程来创建一个全新的进程。只有在创建init进程时,是通过代码实现数据结构的填充。调用fork()的进程称为父进程,新生的进程称为子进程。在系统调用结束时,在返回点这个相同位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核两次返回:一次回到父进程,另一次回到创建的新的子进程。

进程的描述符--task_struct (下面是中英文大体注释)

    

volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */ 进程的状态
void *stack;    //进程通过alloc_thread_info函数分配它的内核栈,通过free_thread_info函数释放所分配的内核栈。
unsigned int flags;    /* per process flags, defined below */ 进程的标签
 
//进程的调度
  int on_rq;
    int prio, static_prio, normal_prio;//进程优先级 实时优先级范围是0到MAX_RT_PRIO-1(即99),而普通进程的静态优先级范围是从MAX_RT_PRIO到MAX_PRIO-1(即100到139)。值越大静态优先级越低。
    unsigned int rt_priority;//rt_priority用于保存实时优先级。normal_prio值取决于静态优先级和调度策略,static_prio用于保存静态优先级,可以通过nice系统调用来进行修改。
    const struct sched_class *sched_class;//sched_class结构体表示调度类
    struct sched_entity se;
    struct sched_rt_entity rt;
  struct sched_dl_entity dl;
  unsigned int policy; //policy表示进程的调度策略
    int nr_cpus_allowed;
    cpumask_t cpus_allowed;//cpus_allowed用于控制进程可以在哪里处理器上运行。
  struct list_head tasks; //用于构建进程链表 ,内核的双向循环链表的实现方法 - 一个更简略的双向循环链表
  struct mm_struct *mm, *active_mm;//mm指向进程所拥有的内存描述符,而active_mm指向进程运行时所使用的内存描述符。对于普通进程而言,这两个指针变量的值相同。但是,内核线程不 拥有任何内存描述符,所以它们的mm成员总是为NULL。当内核线程得以运行时,它的active_mm成员被初始化为前一个运行进程的 active_mm值。
  /* per-thread vma caching */
    u32 vmacache_seqnum;
    struct vm_area_struct *vmacache[VMACACHE_SIZE];
/* task state */
    int exit_state;
    int exit_code, exit_signal;//exit_code用于设置进程的终止代号,这个值要么是_exit()或exit_group()系统调用参数(正常终止),要么是由内核提供的一个错误代号(异常终止)。
    int pdeath_signal;  /*  The signal sent when the parent dies  *///exit_signal被置为-1时表示是某个线程组中的一员。只有当线程组的最后一个成员终止时,才会产生一个信号,以通知线程组的领头进程的父进程。
    unsigned int jobctl;    /* JOBCTL_*, siglock protected */

    /* Used for emulating ABI behavior of previous Linux versions */
    unsigned int personality;

    unsigned in_execve:1;    /* Tell the LSMs that the process is doing an
                 * execve */
    unsigned in_iowait:1;

    /* Revert to default priority/policy when forking */
    unsigned sched_reset_on_fork:1;
    unsigned sched_contributes_to_load:1;
  unsigned long atomic_flags; /* Flags needing atomic access. */

    pid_t pid; //进程标识号
    pid_t tgid;
/*程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系
     * 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 */
    /*
     * children/sibling forms the list of my natural children
     */
    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 */
  cputime_t utime, stime, utimescaled, stimescaled;//utime/stime用于记录进程在用户态/内核态下所经过的节拍数(定时器)。utimescaled/stimescaled也是用于记录进程在用户态/内核态的运行时间,但它们以处理器的频率为刻度。
    cputime_t gtime;//gtime是以节拍计数的虚拟机运行时间(guest time)。
    unsigned long nvcsw, nivcsw; /* context switch counts */
    u64 start_time;        /* monotonic time in nsec */
    u64 real_start_time;    /* boot based time in nsec */
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
    unsigned long min_flt, maj_flt;

    struct task_cputime cputime_expires;
    struct list_head cpu_timers[3];

/* process credentials */
    const struct cred __rcu *real_cred; /* objective and real subjective task
                     * credentials (COW) */
    const struct cred __rcu *cred;    /* effective (overridable) subjective task
                     * credentials (COW) */
    char comm[TASK_COMM_LEN]; /* executable name excluding path
                     - access with [gs]et_task_comm (which lock
                       it with task_lock())
                     - initialized normally by setup_new_exec */
/* file system info */
    int link_count, total_link_count;
/* CPU-specific state of this task */
    struct thread_struct thread;
/* filesystem information */
    struct fs_struct *fs;
/* open file information */
    struct files_struct *files;
/* namespaces */
    struct nsproxy *nsproxy;
/* signal handlers */
    struct signal_struct *signal;//signal指向进程的信号描述符。
    struct sighand_struct *sighand;//sighand指向进程的信号处理程序描述符。

    sigset_t blocked, real_blocked;//blocked表示被阻塞信号的掩码,real_blocked表示临时掩码。
    sigset_t saved_sigmask;    /* restored if set_restore_sigmask() was used */
    struct sigpending pending; //pending存放私有挂起信号的数据结构。

    unsigned long sas_ss_sp;//  sas_ss_sp是信号处理程序备用堆栈的地址,sas_ss_size表示堆栈的大小。
    size_t sas_ss_size;
    int (*notifier)(void *priv);
    void *notifier_data;//设备驱动程序常用notifier指向的函数来阻塞进程的某些信号(notifier_mask是这些信号的位掩码),notifier_data指的是notifier所指向的函数可能使用的数据。
    sigset_t *notifier_mask;
    struct callback_head *task_works;

    struct audit_context *audit_context;//进程审计
/* Thread group tracking */
       u32 parent_exec_id;
       u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed,
 * mempolicy */
    spinlock_t alloc_lock;

    /* Protection of the PI data structures: */
    raw_spinlock_t pi_lock;
/* journalling filesystem info */
    void *journal_info;

/* stacked block device info */
    struct bio_list *bio_list;
/* VM state */
    struct reclaim_state *reclaim_state;

    struct backing_dev_info *backing_dev_info;

    struct io_context *io_context;

    unsigned long ptrace_message;
    siginfo_t *last_siginfo; /* For ptrace use.  */
    struct task_io_accounting ioac;
struct rcu_head rcu;

    /*
     * cache last used pipe for splice
     */
    struct pipe_inode_info *splice_pipe;

    struct page_frag task_frag;
/*
     * time slack values; these are used to round up poll() and
     * select() etc timeout values. These are in nanoseconds.
     */
    unsigned long timer_slack_ns;
    unsigned long default_timer_slack_ns;

 

 

分配进程描述符

Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色。

struct thread_info {
    struct task_struct    *task;        /* main task structure */
    struct exec_domain    *exec_domain;    /* execution domain */
    unsigned long        flags;        /* low level flags */
    __u32            status;        /* thread synchronous flags */
    __u32            cpu;
    int            preempt_count; /* 0 => preemptable, <0 => BUG */
    mm_segment_t        addr_limit;    /* thread address space */
    struct restart_block    restart_block;
    unsigned long        previous_sp;    /* sp of previous stack in case
                           of nested IRQ stacks */
    __u8            supervisor_stack[0];
};

进程的状态:

//include/linux/Sched.h
#define
TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define __TASK_STOPPED 4 #define __TASK_TRACED 8

进程的创建

Linux通过clone()系统调用实现fork()。然后又有clone()去调用do_fork()。实际上从kernel/fork.c文件中,我们可以看到 __ARCH_WANT_SYS_FORK,__ARCH_WANT_SYS_VFORK,__ARCH_WANT_SYS_CLONE都是调用do_fork(),只不过传递的参数不同。

do_fork()完成了创建中大部分工作,它定义在kernel/fork.c文件中。调用copy_process()函数,通过copy_process()创建子进程的描述符,并创建子进程执行时所需的其他数据结构,最终则会返回这个创建好的进程描述符(子进程的描述符)。

p = copy_process(clone_flags, stack_start, stack_size,child_tidptr, NULL, trace);//struct task_struct *p;

copy_process()

以下是对copy_process()函数分成几段,进行简单注释。

1.

/*为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同,此时父子进程的的描述符完全相同。*/
p = dup_task_struct(current);//struct task_struct *p;

2.

/*检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制*/
  retval = -EAGAIN;
    if (atomic_read(&p->real_cred->user->processes) >=
            task_rlimit(p, RLIMIT_NPROC)) {
        if (p->real_cred->user != INIT_USER &&
            !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
            goto bad_fork_free;
    }
    current->flags &= ~PF_NPROC_EXCEEDED;

    retval = copy_creds(p, clone_flags);
    if (retval < 0)
        goto bad_fork_free;
  retval = -EAGAIN;
    if (nr_threads >= max_threads)//检测系统中进程的总数量是否超过了max_threads所规定的进程最大数。
        goto bad_fork_cleanup_count;

    if (!try_module_get(task_thread_info(p)->exec_domain->module))
        goto bad_fork_cleanup_count;

 3.

 /*子进程开始与父进程区别开来。进程描述符内的许多成员都要被清0或者设为初始值。那些不是继承来的信息主要是统计信息。task_struct中的大多数数据都依然未被修改*/
 delayacct_tsk_init(p);    /* Must remain after dup_task_struct() */
    p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);//更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV被置为0
    p->flags |= PF_FORKNOEXEC;//表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
    INIT_LIST_HEAD(&p->children);
    INIT_LIST_HEAD(&p->sibling);
    rcu_copy_process(p);
    p->vfork_done = NULL;
    spin_lock_init(&p->alloc_lock);

    init_sigpending(&p->pending);

    p->utime = p->stime = p->gtime = 0;
    p->utimescaled = p->stimescaled = 0;
  p->default_timer_slack_ns = current->timer_slack_ns;

    task_io_accounting_init(&p->ioac);
    acct_clear_integrals(p);

    posix_cpu_timers_init(p);

    p->start_time = ktime_get_ns();
    p->real_start_time = ktime_get_boot_ns();
    p->io_context = NULL;
    p->audit_context = NULL;
    if (clone_flags & CLONE_THREAD)
        threadgroup_change_begin(current);
    cgroup_fork(p);

 4.

/* Perform scheduler related setup. Assign this task to a CPU. */
/*调度器设置。调用sched_fork函数执行调度器相关的设置,为这个新进程分配CPU,使得子进程的进程状态为TASK_RUNNING。并禁止内核抢占。
并且,为了不对其他进程的调度产生影响,此时子进程共享父进程的时间片。*/ retval = sched_fork(clone_flags, p);   retval = perf_event_init_task(p);   retval = audit_alloc(p);

 5.

/* copy all the process information */
    shm_init_task(p);
/*复 制进程的所有信息。根据clone_flags的具体取值来为子进程拷贝或共享父进程的某些数据结构。
在一般的情况下,这些资源会被给定的进程所有线程共享;否则,这些资源对每个进程时不同的。
比如copy_semundo()、复制开放文件描 述符(copy_files)、复制符号信息(copy_sighand 和 copy_signal)、复制进程内存(copy_mm)以及最终复制线程(copy_thread)。*/ 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_mm(clone_flags, p);   retval = copy_io(clone_flags, p);   retval = copy_thread(clone_flags, stack_start, stack_size, p);

 6.

/*分配pid。用alloc_pid函数为这个新进程分配一个pid,Linux系统内的pid是循环使用的,采用位图方式来管理。
简单的说,就是用每一位(bit)来标示该位所对应的pid是否被使用。分配完毕后,判断pid是否分配成功。成功则赋给p->pid。*/
if (pid != &init_struct_pid) { retval = -ENOMEM; pid = alloc_pid(p->nsproxy->pid_ns_for_children); if (!pid) goto bad_fork_cleanup_io; }

 7.

 1  /*最后,是一些扫尾工作并返回一个指向子进程的指针*/    
    total_forks++;//进程总数加1 2 spin_unlock(&current->sighand->siglock); 3 syscall_tracepoint_update(p); 4 write_unlock_irq(&tasklist_lock); 5 6 proc_fork_connector(p); 7 cgroup_post_fork(p); 8 if (clone_flags & CLONE_THREAD) 9 threadgroup_change_end(current); 10 perf_event_fork(p); 11 12 trace_task_newtask(p, clone_flags); 13 uprobe_copy_process(p, clone_flags); 14 15 return p;

 

 dump_task_struct()

dump_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同,此时父子进程的的描述符完全相同。

//在dump_task_struct()函数中
tsk = alloc_task_struct_node(node);//struct task_struct *tsk; ti = alloc_thread_info_node(tsk, node);//struct thread_info *ti; err = arch_dup_task_struct(tsk, orig);//int err;
tsk->stack = ti;
setup_thread_stack(tsk, orig);
#define alloc_task_struct_node(node)                        \
({                                        \
    struct page *page = alloc_pages_node(node, GFP_KERNEL | __GFP_COMP,    \
                         KERNEL_STACK_SIZE_ORDER);        \
    struct task_struct *ret = page ? page_address(page) : NULL;        \
                                        \
    ret;                                    \
})

alloc_task_struct_node(node)函数中创建页节点,其中一部分就用于存放thread_info节点,另一部分存放堆栈。

alloc_thread_info_node(tsk, node);//实际用于分配内核堆栈空间

arch_dup_task_struct(struct task_struct *dst, struct task_struct *src)函数复制一个PCB——task_struct

在arch_dup_task_struct(struct task_struct *dst, struct task_struct *src)函数
*dst = *src;//在arch_dup_task_struct()函数中,通过赋值完成复制操作

setup_thread_stack()函数只是复制thread_info,而非复制内核堆栈

static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org)
{
    *task_thread_info(p) = *task_thread_info(org);
    task_thread_info(p)->task = p;
}

copy_thread()函数

在copy_thread()函数中,完成初始化

  //在copy_thread(unsigned long clone_flags, unsigned long sp,unsigned long arg, struct task_struct *p)函数中
   struct
pt_regs *childregs = task_pt_regs(p);//pt_regs内核堆栈的栈底,p代表子进程 struct task_struct *tsk; int err; p->thread.sp = (unsigned long) childregs; p->thread.sp0 = (unsigned long) (childregs+1);

  *childregs = *current_pt_regs();//复制内核堆栈数据
    childregs->ax = 0;//为什么子进程的fork返回0,这里就是原因!
  if (sp)
        childregs->sp = sp;//调度到子进程时的内核栈顶
  p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址

 

struct pt_regs {
    unsigned long bx;
    unsigned long cx;
    unsigned long dx;
    unsigned long si;
    unsigned long di;
    unsigned long bp;
    unsigned long ax;
    unsigned long ds;
    unsigned long es;
    unsigned long fs;
    unsigned long gs;
    unsigned long orig_ax;
    unsigned long ip;
    unsigned long cs;
    unsigned long flags;
    unsigned long sp;
    unsigned long ss;
};

从 pt_regs的结构可以看出pt_regs里面存放的是一些要压入栈的内容和一些寄存器和返回值。所以pt_regs是表示内核栈底

子进程从什么时候开始执行??

就是从p->thread.ip = (unsigned long) ret_from_fork;开始的,第一条指令是ret_from_fork,那么我们查找entry_32.S得到

ENTRY(ret_from_fork)
    CFI_STARTPROC
    pushl_cfi %eax
    call schedule_tail
    GET_THREAD_INFO(%ebp)
    popl_cfi %eax
    pushl_cfi $0x0202        # Reset kernel eflags
    popfl_cfi
    jmp syscall_exit
    CFI_ENDPROC
END(ret_from_fork)

其中调用syscall_exit,

ENTRY(system_call)
    RING0_INT_FRAME            # can't unwind into user space anyway
    ASM_CLAC
    pushl_cfi %eax            # save orig_eax
    SAVE_ALL
    GET_THREAD_INFO(%ebp)
                    # system call tracing in operation / emulation
    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
    jnz syscall_trace_entry
    cmpl $(NR_syscalls), %eax
    jae syscall_badsys
syscall_call:
    call *sys_call_table(,%eax,4)
syscall_after_call:
    movl %eax,PT_EAX(%esp)        # store the return value
syscall_exit:
    LOCKDEP_SYS_EXIT
    DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
                    # setting need_resched or sigpending
                    # between sampling and the iret
    TRACE_IRQS_OFF
    movl TI_flags(%ebp), %ecx
    testl $_TIF_ALLWORK_MASK, %ecx    # current->work
    jne syscall_exit_work

restore_all:
    TRACE_IRQS_IRET

syscall_exit在system_call中已经简单分析过了,就是检测调度和出栈,用于返回用户的空间。这样就完成了子进程的用户空间的切换,可以执行子进程的程序。

 

实验结果

1.设置断点

2.menu中输入fork命令

3.在copy_thread单步调试

4.在ret_from_work中调试

 

参考文献:

http://blog.csdn.net/npy_lp/article/details/7335187

http://blog.chinaunix.net/uid-26000137-id-3767114.html

posted @ 2015-04-12 22:08  pingandezhufu  阅读(597)  评论(0编辑  收藏  举报