• 20189220 余超《Linux内核原理与分析》第七周作业


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

  • 相关阅读:
    RHEL7: How to configure a rc-local service
    安装 jemalloc for mysql
    aws rhel 7 安装GUI ,配置VNC
    官方推荐的MySQL参数设置值
    Linux HugePages及MySQL 大页配置
    Linux Transparent Huge Pages 对 Oracle 的影响
    Linux的Transparent Hugepage与关闭方法
    Linux HugePages 配置与 Oracle 性能关系说明
    How To Change Log Rate Limiting In Linux
    MySQL 8.0窗口函数
  • 原文地址:https://www.cnblogs.com/yuchao123/p/9986340.html
Copyright © 2020-2023  润新知