• 《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进程状态转换图

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

    1242#ifdef CONFIG_SMP //条件编译,多处理器会用到
    1243    struct llist_node wake_entry;
    1244    int on_cpu;
    1245    struct task_struct *last_wakee;
    1246    unsigned long wakee_flips;
    1247    unsigned long wakee_flip_decay_ts;
    1248
    1249    int wake_cpu;
    1250#endif
    1251    int on_rq;
    1252
    1253    int prio, static_prio, normal_prio;
    1254    unsigned int rt_priority; //与优先级相关
    1255    const struct sched_class *sched_class;
    1256    struct sched_entity se;
    1257    struct sched_rt_entity rt;
    
    ……
    
    1295    struct list_head tasks; //进程链表
    1296#ifdef CONFIG_SMP
    1297    struct plist_node pushable_tasks;
    1298    struct rb_node pushable_dl_tasks;
    1299#endif

    Linux的状态图

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

    1356    struct list_head ptraced; //调试
    1357    struct list_head ptrace_entry;
    1358
    1359    /* PID/PID hash table linkage. */
    1360    struct pid_link pids[PIDTYPE_MAX];
    1361    struct list_head thread_group;
    1362    struct list_head thread_node; //将进程链表连接起来
    
    ……
    
    1411/* 与当前任务CPU状态相关,对进程上下文切换有关键性作用 */
    1412    struct thread_struct thread;
    1413/* filesystem information */
    1414    struct fs_struct *fs;
    1415/* 打开文件描述符列表 */
    1416    struct files_struct *files;
    1417/* namespaces */
    1418    struct nsproxy *nsproxy;
    1419/* 信号处理 */
    1420    struct signal_struct *signal;
    1421    struct sighand_struct *sighand;
    1422
    1423    sigset_t blocked, real_blocked;
    1424    sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
    1425    struct sigpending pending;
    ……
    1598    struct pipe_inode_info *splice_pipe;
    1599    //与管道相关
    1600    struct page_frag task_frag;
    1601
    1602#ifdef  CONFIG_TASK_DELAY_ACCT
    1603    struct task_delay_info *delays;
    1604#endif
    1605#ifdef CONFIG_FAULT_INJECTION
    1606    int make_it_fail;
    1607#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 < 0) 
        { 
            /* error occurred */
            fprintf(stderr,"Fork Failed!");
            exit(-1);
        } 
        else if (pid == 0) //pid == 0和下面的else都会被执行到(一个是在父进程中即pid ==0的情况,一个是在子进程中,即pid不等于0)
    
        {
            /* child process */
            printf("This is Child Process!
    ");
        } 
        else 
        {  
            /* parent process  */
            printf("This is Parent Process!
    ");
            /* parent will wait for the child to complete*/
            wait(NULL);
            printf("Child Complete!
    ");
        }
    }

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

    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, 0, 0, NULL, NULL);
    }
    #endif
    
    SYSCALL_DEFINE0(vfork)
    {
        return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
                0, 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, 0, 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 = 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大概做了如下几件事:

    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 = -1;
            // 线程组的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 = 0; //子进程的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

    2. 编译内核查看fork命令

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

    执行一个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()函数最大的特点就是被调用一次,返回两次。

  • 相关阅读:
    redis系列之------过期策略
    总结与期盼
    服务不可用排查思路
    Spring Boot Starters到底怎么回事?
    redis系列之------主从复制
    redis系列之------对象
    redis系列之------数据库
    DirectX11 With Windows SDK--00 目录
    DirectX11 With Windows SDK--34 位移贴图
    DirectX11 With Windows SDK--33 曲面细分阶段(Tessellation)
  • 原文地址:https://www.cnblogs.com/20135228guoyao/p/5334985.html
Copyright © 2020-2023  润新知