第六课 linux下进程描述与进程创建

来源:互联网 发布:java开源微信商城系统 编辑:程序博客网 时间:2024/05/29 10:07

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

本次实验课程内容和文章的题目是一致的,包括进程如何描述和进程的创建过程。
在实验中需要我们跟踪fork系统调用过程来验证上述所有的分析。

分析task_struct

在操作系统下,对一个进程管理,使用结构体来表示一个进程叫做pcb,process control block,进程控制块。在linux下定义结构体task_struct来表示一个进程,或被成为任务结构体。
1. 重要字段解释
进程描述符的结构名称叫做task_struct,在文件include/linux/sched.h中定义。
结构体定义的开始部分如下:

struct task_struct {    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */    void *stack;    atomic_t usage;    unsigned int flags; /* per process flags, defined below */    unsigned int ptrace;

在代码运行运行过程中,进程的可分为不同的状态,在linux下,进程分为三种主要状态状态:就绪态、运行态和阻塞状态。每种状态将会依据外部条件不断的切换。
刚开始创建的进程叫做就绪态,但是叫做task_running
当被调用之后,开始运行叫做运行态,也叫做task_running
这两个状态都用了task_runing同一个标志,可理解为可运行状态,是否运行要看时间片等信息。
如果进程终止后进入僵尸状态,最终被回收。
如果等待某件事情将进入阻塞状态。如果被唤醒重新进入就绪态。

void * stack表示内核栈。在代码运行的过程中需要中断关注内核的堆栈的创建过程与切换过程。
ptrace是用来跟踪线程的,比如当进程运行出现异常时,是否回溯当前的状态,判断的依据就是这个变量,在syscall中我们就能够看到这个对这个变量进行了判断。
代码继续:

#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

这里是对对称多处理器的情况下代码如何设置。字面上的意思有wake_cpu表示唤醒哪一个cpu,说明了这里用来管理多cpu的运行同步相关。具体细节不明确。暂时不关心。

    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;

on_rq表示正在正在运行的队列,running queue的缩写。还有进程的优先级,比如静态的优先级还是正常优先级或者实时优先级。什么时候需要用到进程优先级呢?进程调度的时候判断进程优先级,这是一个重要的依据。因此下面有sched相关的定义。具体的使用方法待以后再学习研究。

struct list_head tasks;

此处是将进程以链表的形式管理起来。

struct mm_struct *mm, *active_mm;

一个进程对应着固定的虚拟内存空间但是如何映射到物理空间上,因此需要内存管理相关。

    pid_t pid;    pid_t tgid;

进程号用来唯一的描述一个进程,在linux下,每一个进程都被称作轻量级的线程。tgid是为了进程派生出来线程后,进行线程管理时而作的工作。tgid的值等于第一个进程的pid,以后派生的所有的线程都具有相同的进程id,就是tgid。而每一个线程的pid是不同的,也叫做轻量级的进程。

/*     * 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 */

进程之间的关系,父进程,子进程,子进程之间形成兄弟关系。

/* CPU-specific state of this task */    struct thread_struct thread;

线程的注释是当前任务的cpu状态,下面子进程创建将会看到。

/* filesystem information */    struct fs_struct *fs;/* open file information */    struct files_struct *files;/* namespaces */    struct nsproxy *nsproxy;

与当前进程相关的文件描述符信息

/* signal handlers */    struct signal_struct *signal;    struct sighand_struct *sighand;

进程有关的信号处理。比如系统调用退出到用户空间的时候,就会处理信号。

#ifdef CONFIG_TRACING    /* state flags for use by tracers */    unsigned long trace;    /* bitmask and counter of trace recursion */    unsigned long trace_recursion;#endif /* CONFIG_TRACING */

进程跟踪相关。
其他的内容,由于能力有限不能进一步的分析。

创建进程

  • 复制task_struct
    在copy_process函数中调用函数:dup_task_struct实现结构体复制。
  • 复制内核堆栈
    alloc_thread_info_node
  • 复制后修改
    比如修改pid等信息。
    进程描述符的复制过程如下:
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;   //分配节点空间    tsk = alloc_task_struct_node(node);    if (!tsk)        return NULL;    //分配thread_info节点空间    ti = alloc_thread_info_node(tsk, node);    if (!ti)        goto free_tsk;    //复制数据    err = arch_dup_task_struct(tsk, orig);    if (err)        goto free_ti;    //指向新的堆栈    tsk->stack = ti;#ifdef CONFIG_SECCOMP    /*     * We must handle setting up seccomp filters once we're under     * the sighand lock in case orig has changed between now and     * then. Until then, filter must be NULL to avoid messing up     * the usage counts on the error path calling free_task.     */    tsk->seccomp.filter = NULL;#endif    //复制thread_info    setup_thread_stack(tsk, orig);    clear_user_return_notifier(tsk);    clear_tsk_need_resched(tsk);    set_task_stack_end_magic(tsk);#ifdef CONFIG_CC_STACKPROTECTOR    tsk->stack_canary = get_random_int();#endif    /*     * One for us, one for whoever does the "release_task()" (usually     * parent)     */    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;}

复制函数调用:arch_dup_task_struct实现。

int __weak arch_dup_task_struct(struct task_struct *dst,                           struct task_struct *src){    *dst = *src;    return 0;}

仅仅有一个结构体的直接赋值实现。因为两个结构体的都能够以完全相同,所以直接取值赋值完成。

在内核中,thread_info和堆栈是合并在一起的一个联合体。alloc_thread_info_node函数只是完成页面的创建,

static struct thread_info *alloc_thread_info_node(struct task_struct *tsk,                          int node){    struct page *page = alloc_kmem_pages_node(node, THREADINFO_GFP,                          THREAD_SIZE_ORDER);    return page ? page_address(page) : NULL;}

THREAD_SIZE_ORDER的值为1,所以此处创建2*4k=8K的内存空间也就是两个连续的页面空间。

tsk->stack = ti;

ti创建之后被赋值给了stack,表示了内核堆栈的空间。将来会从高地址向低地址进行压栈。但是总的空间大小肯定要小于8KB的大小。这里也说明了内核中栈空间大小很小。
修改进程相关内容

p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);    p->flags |= 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;#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE    p->prev_cputime.utime = p->prev_cputime.stime = 0;#endif#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN    seqlock_init(&p->vtime_seqlock);    p->vtime_snap = 0;    p->vtime_snap_whence = VTIME_SLEEPING;#endif

p是刚刚创建的内核任务的结构体,为了新的进程开始修改其中的内容,比如对信号的初始化,spilock的初始化等等,详细的内容不再分析。在本文中复制修改一部分。

copy_thread

进入关键的内容copy_thread函数

intcopy_thread(unsigned long clone_flags, unsigned long stack_start,        unsigned long stk_sz, struct task_struct *p)

函数的声明如上,输入参数中最后一个是创建的task_struct结构体。

struct thread_info *thread = task_thread_info(p);

task_thread_info的定义如下:

#define task_thread_info(task)  ((struct thread_info *)(task)->stack)

可以知道从p中找出stack赋值给thread_info,而stack就是前面调用函数alloc_thread_info_node创建的。

struct pt_regs *childregs = task_pt_regs(p);

子进程的栈指向新的开辟的栈空间。task_pt_regs将会对栈空间进行偏移,并且返回栈顶的位置。老师在课程中说是saveall的部分内容。具体细节还要研究。
pt_regs中的内容就是复制的内容:是栈底的内容

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

以上是栈底是int系统调用时调用的部分内容,上面是saveall保存的内容。

p->thread.sp = (unsigned long) childregs;p->thread.sp0 = (unsigned long) (childregs+1);

把当前线程的sp也就是栈顶指针指向childreg是位置,

    *childregs = *current_pt_regs();    childregs->ax = 0;

把父进程的堆栈中的内容拷贝到子进程的堆栈中。ax表示的返回值,这是子进程为什么返回0的原因所在。

第一条指令

p->thread.ip = (unsigned long) ret_from_fork;

将返回指令赋值给ip这个变量,这也是子进程将会执行的第一条指令。因为调度到新的子进程的时候,thread.ip中的值将会被写入到eip寄存器中,靠的就是ret来完成。
在entry_32.s中我们查看ret_from_fork函数:

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_ENDPROCEND(ret_from_fork)

CFI_STARTPROC和CFI_ENDPROC是一对,用来开始profile和结束profile。在中间将会记录每一个函数的调用者和被调用者。
pushl_cfi都理解成压栈,对后面的参数保存一个副本。
跳转到syscall_exit中开始退出处理,此时的退出处理和父进程的退出处理正好是相同的退出,会返回到用户态上去,但是子进程的进程信息却改变了,堆栈信息和父进程的堆栈信息时相同的。

结论:

为什么在创建子进程的时候,直接拷贝父进程的堆栈信息到子进程中,为了返回时是一致的状态。
子进程退出时是否发生进程切换呢?
在syscall_exit中会调用syscall_exit_work函数。然后调用resume_userspace的时候就有可能会发生进程切换。

实验跟踪
实验过程中
git clone没有成功
全部是自己的写的源文件,单独执行qemu可正常运行kennel和init,但是跟踪模式下,却进入了循环状态。不太明白。
实验图片保存在实验报告中,列出地址如下:
https://www.shiyanlou.com/courses/reports/1031867

0 0