linux进程调度(二)-进程创建

2.进程创建和终止

在 Linux 系统中,每个进程都有独立的内存空间和上下文环境,并且可以独立地执行、分配资源和与其他进程进行通信。线程是进程中的一种执行单元,它也有自己的栈、程序计数器和寄存器等,但是它们共享进程的内存和大部分上下文环境,可以方便地进行数据共享和协作。线程实际上也是一种特殊的进程,轻量级进程,它们与普通进程的区别只是在于它们共享内存和上下文环境的方式。因此,每个线程都会有自己的进程标识符和堆栈,但它们使用相同的地址空间并共享同一组文件描述符、信号处理器和进程调度器等,所以这里的进程创建包括了线程。

2.1 进程创建的4种方法

我们一般使用fork系统调用创建进程,其实Linux操作系统还提供了vfork、clone这两个系统调用让我们创建进程。
vfork系统调用和fork系统调用类似,但是vfork的父进程会一直阻塞,直到子进程调用exit()或者execve()为止。clone系统调用通常用于创建用户线程。在Linux内核中没有专门的线程,而是把线程当成普通进程来看待,在内核中还以task_struct数据结构来描述线程,并没有使用特殊的数据结构或者调度算法来描述线程。Clone允许创建一个与父进程共享某些资源的子进程,这些资源可以是内存、文件或其他系统资源。所以,clone与fork相比具有更高的灵活性,可以更细粒度地控制子进程与父进程之间的资源共享,支持创建轻量级进程,即占用比较少的内存和资源的进程。我们看看这几个系统调用的代码:

1.SYSCALL_DEFINE0(fork)  
2.{  
3.    struct kernel_clone_args args = {  
4.        .exit_signal = SIGCHLD,  
5.    };  
6.  
7.    return kernel_clone(&args);  
8.}  
9.  
10.SYSCALL_DEFINE0(vfork)  
11.{  
12.    struct kernel_clone_args args = {  
13.        .flags      = CLONE_VFORK | CLONE_VM,  
14.        .exit_signal    = SIGCHLD,  
15.    };  
16.  
17.    return kernel_clone(&args);  
18.}  
19.  
20.SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,  
21.         int __user *, parent_tidptr,  
22.         unsigned long, tls,  
23.         int __user *, child_tidptr)  
24.{  
25.    struct kernel_clone_args args = {  
26.        .flags      = (lower_32_bits(clone_flags) & ~CSIGNAL),  
27.        .pidfd      = parent_tidptr,  
28.        .child_tid  = child_tidptr,  
29.        .parent_tid = parent_tidptr,  
30.        .exit_signal    = (lower_32_bits(clone_flags) & CSIGNAL),  
31.        .stack      = newsp,  
32.        .tls        = tls,  
33.    };  
34.  
35.    return kernel_clone(&args);  
36.}  

我们可以直观看到kthread_run → kthread_create → kthread_create_on_node → __kthread_create_on_node的调用流程。我们继续看__kthread_create_on_node函数:

1.static __printf(4, 0)  
2.struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data),  
3.                            void *data, int node,  
4.                            const char namefmt[],  
5.                            va_list args)  
6.{  
7.    DECLARE_COMPLETION_ONSTACK(done);  
8.    struct task_struct *task;  
9.    struct kthread_create_info *create = kmalloc(sizeof(*create),  
10.                             GFP_KERNEL);  
11.  
12.    if (!create)  
13.        return ERR_PTR(-ENOMEM);  
14.    create->threadfn = threadfn;  
15.    create->data = data;  
16.    create->node = node;  
17.    create->done = &done;  
18.  
19.    spin_lock(&kthread_create_lock);  
20.    list_add_tail(&create->list, &kthread_create_list);  
21.    spin_unlock(&kthread_create_lock);  
22.  
23.    wake_up_process(kthreadd_task);  
24.  
25.    if (unlikely(wait_for_completion_killable(&done))) {  
26.        wait_for_completion(&done);  
27.    }  
28.    task = create->result;  
29.    return task;  
30.}  

我们可以看到__kthread_create_on_node函数主要做了一下几件事:

  1. 创建了一个struct kthread_create_info数据类型的变量create,同时把要运行的函数和参数写入create中;
  2. 把create加入了kthread_create_list队列;
  3. 唤醒kthreadd_task进程后等待kthreadd_task执行完成;
  4. 从create中获取到task_struct结构体,并且返回这个结构体。
    返回的这个结构体就是我们已经创建成功的进程了,那么kthreadd_task进程是怎么创建进程的呢?继续看下去,kthreadd_task是一个全局变量,他的初始化在init/main.c文件的rest_init函数中:
1.noinline void __ref rest_init(void)  
2.{  
3....  
4.        pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);  
5.        rcu_read_lock();  
6.        kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);  
7....  
8.}  

我们可以看到系统通过kernel_thread函数创建了kthreadd进程得到进程的pid,并且通过这个pid找到kthreadd进程的进程描述符赋值给全局变量kthreadd_task。我们前面唤醒kthreadd_task其实就是唤醒kthreadd进程,我们在看看kthreadd吧:

1.int kthreadd(void *unused)  
2.{  
3.    struct task_struct *tsk = current;  
4.  
5.    /* Setup a clean context for our children to inherit. */  
6.    set_task_comm(tsk, "kthreadd");//把本进程的名字改为kthreadd  
7.    ignore_signals(tsk);//屏蔽进程的所有信号  
8.    //设置进程的 CPU 亲和性,去除了ISOLATION的cpu  
9.    set_cpus_allowed_ptr(tsk, housekeeping_cpumask(HK_FLAG_KTHREAD));  
10.    //设置进程的可用NUMA节点  
11.    set_mems_allowed(node_states[N_MEMORY]);  
12.  
13.    current->flags |= PF_NOFREEZE;//设置标志位表示这个进程不可以冻结  
14.    cgroup_init_kthreadd();//禁止用户发起cgroup迁移  
15.  
16.    for (;;) {//死循环  
17.        set_current_state(TASK_INTERRUPTIBLE);//设置当前进程为轻度睡眠  
18.        if (list_empty(&kthread_create_list))//如果链表为空  
19.            schedule();//主动发起调度  
20.        __set_current_state(TASK_RUNNING);//设置当前进程为可运行状态  
21.  
22.        spin_lock(&kthread_create_lock);  
23.        //当kthread_create_list不为空的时候进入while循环  
24.        while (!list_empty(&kthread_create_list)) {  
25.            struct kthread_create_info *create;  
26.            //从kthread_create_list链表中取出一个create  
27.            create = list_entry(kthread_create_list.next,  
28.                        struct kthread_create_info, list);  
29.            list_del_init(&create->list);//把create踢出链表  
30.            spin_unlock(&kthread_create_lock);  
31.  
32.            create_kthread(create);//根据create创建进程  
33.  
34.            spin_lock(&kthread_create_lock);  
35.        }  
36.        spin_unlock(&kthread_create_lock);  
37.    }  
38.  
39.    return 0;  
40.}  

我么可以看到kthreadd一开始是初始化自己,然后在死循环中取出kthread_create_list链表数据create,最后调用create_kthread(create)创建进程了。create_kthread函数仅仅是调用kernel_thread函数而已,我们看看kernel_thread:

1.pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)  
2.{  
3.    struct kernel_clone_args args = {  
4.        .flags      = ((lower_32_bits(flags) | CLONE_VM |  
5.                    CLONE_UNTRACED) & ~CSIGNAL),  
6.        .exit_signal    = (lower_32_bits(flags) & CSIGNAL),  
7.        .stack      = (unsigned long)fn,  
8.        .stack_size = (unsigned long)arg,  
9.    };  
10.  
11.    return kernel_clone(&args);  
12.} 

到这里我们又看到很熟悉的画面了,就是kernel_clone_args和kernel_clone函数。原来内核创建线程也是通过kernel_clone函数的。
到目前为止,我们无论是用户态的系统调用还是内核提供的函数,他们都是通过设置struct kernel_clone_args结构体为传入参数,然后调用kernel_clone函数来创建进程的。

2.2 进程创建过程分析

我们已经知道系统是通过kernel_clone函数来创建进程的,这里仅仅是创建进程,并且加入到就绪队列中,至于运行是由调度器决定是否运行的,我们来看看kernel_clone函数吧:

1.pid_t kernel_clone(struct kernel_clone_args *args)  
2.{  
3....  
4.    //函数创建一个新的子进程  
5.    p = copy_process(NULL, trace, NUMA_NO_NODE, args);  
6.    add_latent_entropy();  
7.  
8.    //根据子进程的task_struct数据结构获取 pid结构体  
9.    pid = get_task_pid(p, PIDTYPE_PID);  
10.    nr = pid_vnr(pid);//由子pid结构体来计算PID值  
11.  
12.    if (clone_flags & CLONE_VFORK) {  
13.        p->vfork_done = &vfork;  
14.        init_completion(&vfork);//初始化完成量  
15.        get_task_struct(p);  
16.    }  
17.  
18.    //唤醒新创建的进程,把进程加入就绪队列里并接受调度、运行  
19.    wake_up_new_task(p);  
20.  
21.    if (clone_flags & CLONE_VFORK) {  
22.        //等待子进程调用exec()或者exit()  
23.        if (!wait_for_vfork_done(p, &vfork))  
24.            ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);  
25.    }  
26.  
27.    put_pid(pid);//减少pid数据得应用计数  
28.    return nr;//返回值为子进程的ID  
29.}  

从代码可以看到kernel_clone函数主要做了一下几件事:

  1. 调用函数copy_process创建一个新的子进程,返回进程描述符;
  2. 调用函数get_task_pid根据进程描述符得到pid结构体,然后计算出pid值;
  3. 调用函数wake_up_new_task唤醒进程,其实也就是把进程加入到就绪队列中,并且设置进程的状态,等待调度器调度;
  4. 如果有CLONE_VFORK标志位表示是vfork创建的进程,需要设置父进程的完成量,要等待子进程运行完毕;
  5. 最后返回进程的PID值。
    这里比较重要的函数有创建进程描述符的copy_process函数和把进程加入就绪队列的wake_up_new_task。

2.2.1 copy_process函数分析

1.static __latent_entropy struct task_struct *copy_process(  
2.                    struct pid *pid,  
3.                    int trace,  
4.                    int node,  
5.                    struct kernel_clone_args *args)  
6.{  
7.    int pidfd = -1, retval;  
8.    struct task_struct *p;  
9.    struct multiprocess_signals delayed;  
10.    struct file *pidfile = NULL;  
11.    u64 clone_flags = args->flags;  
12.    struct nsproxy *nsp = current->nsproxy;  
13.    ...  
14.    //为新进程分配一个task_struct数据结构  
15.    p = dup_task_struct(current, node);  
16.    if (!p)  
17.        goto fork_out;  
18.  
19.    rt_mutex_init_task(p);//初始化新task_struct中的几个实时互斥量  
20.  
21.    retval = copy_creds(p, clone_flags);//初始化进程的凭据,实现权限和安全相关  
22.    if (retval < 0)  
23.        goto bad_fork_free;  
24.  
25.    delayacct_tsk_init(p);//初始化延迟任务,用于计算延迟信息  
26.    INIT_LIST_HEAD(&p->children);//初始化子进程链表  
27.    INIT_LIST_HEAD(&p->sibling);//初始化兄弟进程链表  
28.    rcu_copy_process(p);//初始化task_struct中rcu相关数据和链表  
29.    p->utime = p->stime = p->gtime = 0;//初始化task的用户态、系统态和子进程消耗时间  
30.    prev_cputime_init(&p->prev_cputime);//初始化prev_cputime结构体,用于计算cpu时间  
31.  
32.    //初始化ioac成员,用于存储进程的 I/O 操作统计信息  
33.    task_io_accounting_init(&p->ioac);  
34.    acct_clear_integrals(p);//清除task中的acct*信息  
35.  
36.    posix_cputimers_init(&p->posix_cputimers);  
37.  
38.    p->io_context = NULL;//初始化io_context  
39.    audit_set_context(p, NULL);//初始化审计上下文  
40.    cgroup_fork(p);//初始化cgroup相关字段  
41.  
42.    /* Perform scheduler related setup. Assign this task to a CPU. */  
43.    //初始化与进程调度相关的数据结构。将此任务分配给CPU。  
44.    retval = sched_fork(clone_flags, p);  
45.    if (retval)  
46.        goto bad_fork_cleanup_policy;  
47.  
48.    //初始化task_struct中的perf_event上下文  
49.    retval = perf_event_init_task(p);  
50.    if (retval)  
51.        goto bad_fork_cleanup_policy;  
52.    retval = audit_alloc(p);//为任务分配审计上下文块  
53.    if (retval)  
54.        goto bad_fork_cleanup_perf;  
55.    /* copy all the process information */  
56.    shm_init_task(p);//初始化sysvshm成员  
57.    //申请task的security资源  
58.    retval = security_task_alloc(p, clone_flags);  
59.    if (retval)  
60.        goto bad_fork_cleanup_audit;  
61.    //初始化task的sysvsem.undo_list  
62.    retval = copy_semundo(clone_flags, p);  
63.    if (retval)  
64.        goto bad_fork_cleanup_security;  
65.    //复制父进程打开的文件等信息  
66.    retval = copy_files(clone_flags, p);  
67.    if (retval)  
68.        goto bad_fork_cleanup_semundo;  
69.    //复制父进程的fs_struct数据结构  
70.    retval = copy_fs(clone_flags, p);  
71.    if (retval)  
72.        goto bad_fork_cleanup_files;  
73.    //复制父进程的信号处理函数,主要是sighand成员的初始化  
74.    retval = copy_sighand(clone_flags, p);  
75.    if (retval)  
76.        goto bad_fork_cleanup_fs;  
77.    //复制父进程的信号系统,主要是signal成员的初始化  
78.    retval = copy_signal(clone_flags, p);  
79.    if (retval)  
80.        goto bad_fork_cleanup_sighand;  
81.    //复制父进程的进程地址空间的页表  
82.    retval = copy_mm(clone_flags, p);  
83.    if (retval)  
84.        goto bad_fork_cleanup_signal;  
85.    //复制父进程的命名空间  
86.    retval = copy_namespaces(clone_flags, p);  
87.    if (retval)  
88.        goto bad_fork_cleanup_mm;  
89.    //复制父进程中与I/O相关的内容,主要是io_context成员  
90.    retval = copy_io(clone_flags, p);  
91.    if (retval)  
92.        goto bad_fork_cleanup_namespaces;  
93.    //复制父进程的内核堆信息,主要是thread成员  
94.    retval = copy_thread(clone_flags, args->stack, args->stack_size, p, args->tls);  
95.    if (retval)  
96.        goto bad_fork_cleanup_io;  
97.  
98.    stackleak_task_init(p);//设置最低栈地址lowest_stack,用于栈溢出检查  
99.  
100.    if (pid != &init_struct_pid) {  
101.        //为新进程分配一个pid数据结构和PID  
102.        pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid,  
103.                args->set_tid_size);  
104.        if (IS_ERR(pid)) {  
105.            retval = PTR_ERR(pid);  
106.            goto bad_fork_cleanup_thread;  
107.        }  
108.    }  
109.  
110.    futex_init_task(p);//初始化进程的Futex相关数据,包括robust_list、futex_state、futex_exit_mutex、pi_state_list、pi_state_cache  
111.  
112.  
113.    /* ok, now we should be set up.. */  
114.    p->pid = pid_nr(pid);//分配一个全局PID号  
115.    if (clone_flags & CLONE_THREAD) {//创建一个线程  
116.        p->group_leader = current->group_leader;  
117.        p->tgid = current->tgid;  
118.    } else {//创建一个进程  
119.        p->group_leader = p;  
120.        p->tgid = p->pid;  
121.    }  
122.  
123.    //初始化脏页相关  
124.    p->nr_dirtied = 0;  
125.    p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);  
126.    p->dirty_paused_when = 0;  
127.  
128.    p->pdeath_signal = 0;  
129.    INIT_LIST_HEAD(&p->thread_group);//初始化线程组链表  
130.    p->task_works = NULL;//回调任务链表初始化  
131.    clear_posix_cputimers_work(p);//空  
132.  
133.    //初始化进程的开始时间  
134.    p->start_time = ktime_get_ns();  
135.    p->start_boottime = ktime_get_boottime_ns();  
136.  
137.    spin_lock(¤t->sighand->siglock);  
138.    copy_seccomp(p);//初始化seccomp数据,用于安全计算  
139.  
140.    rseq_fork(p, clone_flags);//初始化task的rseq_fork、rseq_sig和rseq_event_mask,作用不了解  
141.  
142.    init_task_pid_links(p);//初始化task的pid_links数组  
143.  
144.    if (pidfile)  
145.        fd_install(pidfd, pidfile);//将文件描述符和文件/管道的指针关联起来  
146.  
147.    sched_post_fork(p, args);//为新进程设置调度属性  
148.    cgroup_post_fork(p, args);//为新进程进行组设置  
149.    perf_event_fork(p);//设置新进程性能事件  
150.  
151.    trace_task_newtask(p, clone_flags);//trace相关  
152.    uprobe_copy_process(p, clone_flags);//复制父进程的用户层断点  
153.  
154.    copy_oom_score_adj(clone_flags, p);//复制父进程的signal->oom_score_adj  
155.  
156.    return p;//返回子进程描述符  
157.  
158.  
159.}  

copy_process函数主要做了一下几件事:

  1. 根据标志位kernel_clone_args.flags,判断是否存在不合理的地方,如果是则返回错误码;比如CLONE_NEWNS和CLONE_FS不可以同时出现,因为不允许不同命名空间共享根目录;又比如CLONE_THREAD和CLONE_SIGHAND需要同时出现,因为线程组必须共享信号相关;
  2. 调用dup_task_struct函数为新进程分配一个task_struct数据结构;
  3. 初始化task_struct数据结构,有一些成员是赋初值,有一些成员是从父进程拷贝;从父进程拷贝的下面介绍;
  4. 调用函数sched_fork根据父进程情况初始化进程调度相关的数据结构;
  5. 调用函数copy_files复制父进程打开的文件等信息;
  6. 调用函数copy_fs复制父进程的fs_struct数据结构;
  7. 调用函数copy_sighand复制父进程的信号处理函数,初始化sighand成员;
  8. 调用函数copy_signal复制父进程的信号系统,初始化signal成员;
  9. 调用函数copy_mm复制父进程的进程地址空间的页表;(重点)
  10. 调用函数copy_thread复制父进程的内核堆信息,初始化thread成员;
  11. 调用函数alloc_pid为新进程分配一个pid数据结构和PID;
  12. 初始化其他成员完毕后返回进程描述符。
    我们比较关注的是dup_task_struct、sched_fork和copy_mm函数。
2.2.1.1 dup_task_struct函数分析
1.static struct task_struct *dup_task_struct(struct task_struct *orig, int node)  
2.{  
3.    struct task_struct *tsk;  
4.    unsigned long *stack;  
5.    struct vm_struct *stack_vm_area __maybe_unused;  
6.    int err;  
7.  
8.    //为新进程分配一个进程描述符  
9.    tsk = alloc_task_struct_node(node);  
10.    if (!tsk)  
11.        return NULL;  
12.  
13.    //为新进程分配内核栈空间  
14.    stack = alloc_thread_stack_node(tsk, node);  
15.    if (!stack)  
16.        goto free_tsk;  
17.    //将一个内核线程的内存栈大小计入内存控制组的限额中  
18.    if (memcg_charge_kernel_stack(tsk))  
19.        goto free_stack;  
20.  
21.    //复制父进程的内核栈所在的内存区域,也就是stack_vm_area,  
22.    stack_vm_area = task_stack_vm_area(tsk);  
23.  
24.    //处理进程描述符体系结构相关部分,arm64更新了thread_info.flags  
25.    err = arch_dup_task_struct(tsk, orig);  
26.  
27.    tsk->stack = stack;//设置栈内存地址  
28.#ifdef CONFIG_VMAP_STACK  
29.    tsk->stack_vm_area = stack_vm_area;//设置内核栈所在的vma  
30.#endif  
31.#ifdef CONFIG_THREAD_INFO_IN_TASK  
32.    refcount_set(&tsk->stack_refcount, 1);//设置stack_refcount为1  
33.#endif  
34.  
35.    clear_tsk_need_resched(tsk);//清除TIF_NEED_RESCHED标志位  
36.    set_task_stack_end_magic(tsk);//设置任务栈结束标记  
37.  
38.#ifdef CONFIG_STACKPROTECTOR  
39.    tsk->stack_canary = get_random_canary();//初始化stack_canary  
40.#endif  
41.    if (orig->cpus_ptr == &orig->cpus_mask)  
42.        tsk->cpus_ptr = &tsk->cpus_mask;  
43.  
44.    refcount_set(&tsk->rcu_users, 2);//初始化rcu引用计数  
45.    /* One for the rcu users */  
46.    refcount_set(&tsk->usage, 1);//task的引用计数为1  
47.#ifdef CONFIG_BLK_DEV_IO_TRACE  
48.    tsk->btrace_seq = 0;  
49.#endif  
50.    //初始化task_struct的几个成员而已  
51.    tsk->splice_pipe = NULL;  
52.    tsk->task_frag.page = NULL;  
53.    tsk->wake_q.next = NULL;  
54.  
55.    account_kernel_stack(tsk, 1);//更新内核栈这块内存在lrc链表的热度  
56.  
57.    kcov_task_init(tsk);//初始化kcov相关,用于统计内核代码覆盖率,这里是空  
58.  
59.  
60.#ifdef CONFIG_BLK_CGROUP  
61.    //初始化IO限流相关  
62.    tsk->throttle_queue = NULL;  
63.    tsk->use_memdelay = 0;  
64.#endif  
65.  
66.#ifdef CONFIG_MEMCG  
67.    //初始化mem cgroup  
68.    tsk->active_memcg = NULL;  
69.#endif  
70.    return tsk;//返回task_struct结构体  
71.  
72.free_stack:  
73.    free_thread_stack(tsk);  
74.free_tsk:  
75.    free_task_struct(tsk);  
76.    return NULL;  
77.}  

dup_task_struct函数申请进程描述符的内存,同时初始化进程描述符,这几工作主要分为以下几步:

  1. 调用函数alloc_task_struct_node从kmem中分配进程描述符的内存
  2. 调用函数alloc_thread_stack_node分配新进程的占空间,优先从cached_stacks缓存栈分配,分配失败再通过vmalloc分配,分配成功会设置到进程描述符中;
  3. 调用函数memcg_charge_kernel_stack把内核线程的内存栈大小计入内存控制组的限额中,避免内核态栈使用过多内存,导致内存不足或者内存负载不均衡的问题;
  4. 调用函数task_stack_vm_area复制父进程的内核栈所在的vma,也就是stack_vm_area,后面会设置到新创建的进程描述符中
  5. 调用函数arch_dup_task_struct处理进程描述符体系结构相关部分,arm64主要是拷贝了thread_info.flags,然后清除了TIF_SVE和TIF_MTE_ASYNC_FAULT,这部分主要是跟架构相关,x86会有很大的差别;
  6. 调用函数clear_tsk_need_resched清除clear_tsk_need_resched标志位,这样子系统回到用户态会让父进程继续运行,直到式时间片的到来;
  7. 调用函数set_task_stack_end_magic设置任务栈结束标记,每次占空间被使用都会检查这个标记,保证发生栈溢出的时候会报警;
  8. 后面还会初始化一些成员,这些初始化都很直观,就不细说,最后会返回进程描述符。
2.2.1.2 sched_fork函数分析
1.int sched_fork(unsigned long clone_flags, struct task_struct *p)  
2.{  
3.    __sched_fork(clone_flags, p);//设置cfs、rt和dl调度实体,主要是se、rt、dl这三个成员  
4.  
5.    p->state = TASK_NEW;//设置进程的状态:TASK_NEW,它还没被添加到调度器里  
6.  
7.    p->prio = current->normal_prio;//继承父进程优先级  
8.  
9.    //如果调度信息需要重置  
10.    if (unlikely(p->sched_reset_on_fork)) {  
11.        if (task_has_dl_policy(p) || task_has_rt_policy(p)) {  
12.            p->policy = SCHED_NORMAL;  
13.            p->static_prio = NICE_TO_PRIO(0);  
14.            p->rt_priority = 0;  
15.        } else if (PRIO_TO_NICE(p->static_prio) < 0)  
16.            p->static_prio = NICE_TO_PRIO(0);  
17.  
18.        p->prio = p->normal_prio = p->static_prio;  
19.        set_load_weight(p);  
20.  
21.        p->sched_reset_on_fork = 0;  
22.    }  
23.  
24.    //根据优先级设置调度类  
25.    if (dl_prio(p->prio))//如果是dl进程  
26.        return -EAGAIN;  
27.    else if (rt_prio(p->prio))//如果是rt进程  
28.        //选用RT的调度类rt_sched_class  
29.        p->sched_class = &rt_sched_class;  
30.    else//那就是普通进程  
31.        //选用CFS的调度类fair_sched_class  
32.        p->sched_class = &fair_sched_class;  
33.  
34.    init_entity_runnable_average(&p->se);//初始化与子进程的调度实体,主要是se->avg  
35.  
36.#ifdef CONFIG_SCHED_INFO  
36.    //初始化sched_info数据  
37.    if (likely(sched_info_on()))  
38.        memset(&p->sched_info, 0, sizeof(p->sched_info));  
40.#endif  
41.#if defined(CONFIG_SMP)  
39.    p->on_cpu = 0;//还没有进入就绪队列  
43.#endif  
40.    init_task_preempt_count(p);//初始化task的thread_info的preempt_count  
45.#ifdef CONFIG_SMP  
41.    //初始化pushable_tasks和pushable_dl_tasks  
42.    plist_node_init(&p->pushable_tasks, MAX_PRIO);  
43.    RB_CLEAR_NODE(&p->pushable_dl_tasks);  
49.#endif  
44.    return 0;  
51.}  

sched_fork函数主要是初始化进程调度相关的数据结构,包括了:

  1. 调用函数__sched_fork初始化cfs、rt和dl调度实体,包括是否在就绪队列、虚拟运行时间、迁移此时还有统计调度的其他信息等等;
  2. 设置进程的状态为TASK_NEW,这是一个临时的状态;
  3. 继承父进程的优先级;
  4. 判断sched_reset_on_fork参数决定是否重置调度优先级,一般不会重置的;
  5. 根据进程的优先级设置进程的调度类,也就是sched_class;
  6. 调用函数init_entity_runnable_average初始化新进程的调度实体se,也就是把整个se设置为0,然后把进程的平均负载设置为最低负载;
  7. 初始化sched_info和on_cpu成员等等,最后返回0。
2.2.1.3 copy_mm函数分析
1.static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)  
2.{  
3.    struct mm_struct *mm, *oldmm;  
4.    int retval;  
5.  
6.    //初始化进程的缺页情况  
7.    tsk->min_flt = tsk->maj_flt = 0;  
8.    tsk->nvcsw = tsk->nivcsw = 0;  
9.#ifdef CONFIG_DETECT_HUNG_TASK  
9.    //更新进程调度次数和调度时间  
10.    tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;  
11.    tsk->last_switch_time = 0;  
13.#endif  
12.  
13.    tsk->mm = NULL;  
14.    tsk->active_mm = NULL;  
15.  
16.    //借用父进程的mm  
17.    oldmm = current->mm;  
18.    if (!oldmm)  
19.        return 0;  
20.  
21.    /* initialize the new vmacache entries */  
22.    vmacache_flush(tsk);//初始化vmacache.vmas  
23.      
24.    //如果创建的是线程  
25.    if (clone_flags & CLONE_VM) {  
26.        mmget(oldmm);//父进程的user数量+1  
27.        mm = oldmm;  
28.        goto good_mm;  
29.    }  
30.  
31.    retval = -ENOMEM;  
32.    //复制父进程的进程地址空间  
33.    mm = dup_mm(tsk, current->mm);  
34.    if (!mm)  
35.        goto fail_nomem;  
36.  
39.good_mm:  
37.    //设置mm和active_mm  
38.    tsk->mm = mm;  
39.    tsk->active_mm = mm;  
40.    return 0;  
41.  
45.fail_nomem:  
42.    return retval;  
47.}  

copy_mm主要是初始化进程的内存相关数据,主要是mm_struct,具体作用一下几件事:

  1. 初始化进程的缺页情况和更新进程的调用次数;
  2. 找到父进程的mm_struct。如果父进程的mm_struct为空,说明父进程是内核进程,那子进程也是内核进程,不需要mm_struct,直接返回0即可;
  3. 调用函数vmacache_flush初始化vmacache的vmas指针数组,它是进程查找vma的快速路径;
  4. 如果clone_flags的CLONE_VM被置位了,说明创建的是线程,父子进程共用一个mm_struct,所以要把mm_struct的user数量加一,设置好进程的mm和active_mm成员就可以返回0了;
  5. 到这里说明创建的是进程,需要调用函数dup_mm复制父进程的地址空间,然后设置好进程的mm和active_mm成员再返回;
    我们继续看看dup_mm函数是怎么复制父进程的地址空间的:
1.static struct mm_struct *dup_mm(struct task_struct *tsk,  
48.                struct mm_struct *oldmm)  
3.{  
49.    struct mm_struct *mm;  
50.    int err;  
51.  
52.    mm = allocate_mm();//子进程分配一个内存描述符mm  
53.    if (!mm)  
54.        goto fail_nomem;  
55.  
56.    //把父进程的内存描述符的内容全部复制到子进程  
57.    memcpy(mm, oldmm, sizeof(*mm));  
58.  
59.    //初始化子进程的内存描述符的一些成员  
60.    if (!mm_init(mm, tsk, mm->user_ns))  
61.        goto fail_nomem;  
62.  
63.    //复制父进程的进程地址空间的页表到子进程  
64.    err = dup_mmap(mm, oldmm);  
65.    if (err)  
66.        goto free_pt;  
67.  
68.    //初始化内存水位  
69.    mm->hiwater_rss = get_mm_rss(mm);  
70.    mm->hiwater_vm = mm->total_vm;  
71.  
72.    if (mm->binfmt && !try_module_get(mm->binfmt->module))  
73.        goto free_pt;  
74.  
75.    return mm;//返回mm_struct  
76.  
32.free_pt:  
77.    /* don't put binfmt in mmput, we haven't got module yet */  
78.    mm->binfmt = NULL;  
79.    mm_init_owner(mm, NULL);  
80.    mmput(mm);  
81.  
38.fail_nomem:  
82.    return NULL;  
40.}  

dup_mm函数主要做了以下几件事:

  1. 调用函数allocate_mm从kmem中申请mm_struct数据结构的内存;
  2. 调用函数memcpy把父进程的内存描述符的内容全部复制到子进程;
  3. 调用函数mm_init初始化mm_struct,毕竟是新进程,有一些数据还是要初始化的,函数把mm_users、owner、flags和context等成员,然后申请一级页表写入pgd成员;
  4. 调用函数dup_mmap首先初始化vm和红黑树相关的数据,如果有大页,把大页的数据设置为共享,然后遍历mm的所有vma,把共享的vma一个个拷贝后插入到子进程的mm中;
  5. 初始化内存水位后返回mm_struct数据结构体。
2.2.1.4 copy_thread函数分析
1.int copy_thread(unsigned long clone_flags, unsigned long stack_start,  
2.        unsigned long stk_sz, struct task_struct *p, unsigned long tls)  
3.{  
4.    //找到进程的栈底,这里保存了发生异常时寄存器的信息  
5.    struct pt_regs *childregs = task_pt_regs(p);  
6.  
7.    //清空cpu_context,调度进程时使用这个成员保存通用寄存器的值的  
8.    memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context));  
9.  
10.    fpsimd_flush_task_state(p);  
11.  
12.    ptrauth_thread_init_kernel(p);  
13.  
14.    //处理子进程是用户进程的情况  
15.    if (likely(!(p->flags & PF_KTHREAD))) {  
16.        //把当前进程内核栈底部的 pt_regs 结构体复制一份  
17.        *childregs = *current_pt_regs();  
18.        //把子进程的X0寄存器设置为0,也就是fork返回的0  
19.        childregs->regs[0] = 0;  
20.  
21.        //设置子进程的TPIDR_EL0寄存器跟为父进程的一样  
22.        *task_user_tls(p) = read_sysreg(tpidr_el0);  
23.  
24.        if (stack_start) {//如果指定了用户栈起始地址,  
25.        //需要设置子进程的sp  
26.            if (is_compat_thread(task_thread_info(p)))  
27.                childregs->compat_sp = stack_start;  
28.            else  
29.                childregs->sp = stack_start;  
30.        }  
31.  
32.        //如果设置了CLONE_SETTLS标志  
33.        if (clone_flags & CLONE_SETTLS)  
34.            //把传入的参数tls设置到子进程中  
35.            p->thread.uw.tp_value = tls;  
36.    } else {//处理子进程是内核线程的情况  
37.        //把子进程内核栈底部的 pt_regs 结构体清零  
38.        memset(childregs, 0, sizeof(struct pt_regs));  
39.        //设置子进程的处理器状态为异常级别1,也就是内核态  
40.        childregs->pstate = PSR_MODE_EL1h;  
41.        if (IS_ENABLED(CONFIG_ARM64_UAO) &&  
42.            cpus_have_const_cap(ARM64_HAS_UAO))  
43.            childregs->pstate |= PSR_UAO_BIT;  
44.  
45.        spectre_v4_enable_task_mitigation(p);  
46.  
47.        if (system_uses_irq_prio_masking())  
48.            childregs->pmr_save = GIC_PRIO_IRQON;  
49.        //把子进程的x19寄存器设置为线程函数的地址  
50.        p->thread.cpu_context.x19 = stack_start;  
51.        //把子进程的x20寄存器设置为传给线程函数的参  
52.        p->thread.cpu_context.x20 = stk_sz;  
53.    }  
54.    //设置子进程的进程硬件上下文(struct cpu_context)中pc和sp成员的值  
55.    p->thread.cpu_context.pc = (unsigned long)ret_from_fork;//新进程内核开始运行的地方  
56.    p->thread.cpu_context.sp = (unsigned long)childregs;//新进程的内核栈  
57.  
58.    ptrace_hw_copy_thread(p);  
59.  
60.    return 0;  
61.}  

copy_thread函数主要是初始化子进程的堆栈信息,也是调度器调度子进程的状态信息,主要是以下几件事:

  1. 调用函数task_pt_regs找到子进程的栈底,这里应该保存发生异常时寄存器的信息,调度器调度的时候可以恢复的上下文就在这里;
  2. 清空cpu_context,调度进程时使用这个成员保存通用寄存器的值的;
  3. 如果创建的子进程是用户进程,拷贝父进程的栈底,也就是pt_regs结构体信息,然后把子进程的X0寄存器设置为0,这是fork系统调用的返回值,子进程返回的0就是X0寄存器设置的0;
  4. 设置子进程的TPIDR_EL0寄存器跟为父进程的一样,TPIDR_EL0 是用户读写线程标识符寄存器,用来存放每线程数据的基准地址,存放每线程数据的区域通常被称为线程本地存储(Thread Local Storage,TLS);
  5. 如果指定了用户栈起始地址,需要设置子进程的sp,正常操作我们都不会设置这个参数;
  6. 如果创建的是内核进程,需要把子进程内核栈底部的 pt_regs 结构体清零,因为内核进程跟父进程基本是独立的,不需要继承其堆栈;
  7. 设置子进程的处理器状态为异常级别1,也就是内核态;把子进程的x19寄存器设置为线程函数的地址;把子进程的x20寄存器设置为传给线程函数的参数;
  8. 最后设置子进程的进程硬件上下文(struct cpu_context)中pc和sp成员的值,其中设置的cp值为ret_from_fork函数;
    我们的子进程的PC是ret_from_fork函数,也就是新进程是从ret_from_fork函数开始执行的,这是汇编代码:
1.SYM_CODE_START(ret_from_fork)  
2.        bl      schedule_tail   //对prev进程做收尾工作  
3.        cbz     x19, 1f         //用户进程跳到1         // not a kernel thread  
4.        mov     x0, x20  
5.        blr     x19             //异常返回,跳到内核线程回调函数  
6.1:      get_current_task tsk  
7.        b       ret_to_user     //调用函数返回用户态  
8.SYM_CODE_END(ret_from_fork)  

ret_from_fork函数主要做了以下几件事情:

  1. 首先调用函数schedule_tail对父进程做一些收尾工作,主要是保存现场;
  2. 判断x19寄存器是否为0,如果x19不为0,表示子进程是内核进程,把x20寄存器的值移动到x0寄存器后跳转到x19寄存器的地址处,我们还记得前面的copy_thread函数中,对x19和x20寄存器的初始化吗?那时候的x19寄存器保存的是线程函数的地址,x20寄存器保存的是线程函数的参数,这就正好跳转的内核线程的地址了;
  3. 如果x19为0,说明子进程是用户进程,跳转到1处,调用函数get_current_task,把读取sp_el0写入tsk参数中,然后跳转到ret_to_user函数通过kernel_exit回到用户态运行;

2.2.2 wake_up_new_task函数分析

1.void wake_up_new_task(struct task_struct *p)  
2.{  
3.    struct rq_flags rf;  
4.    struct rq *rq;  
5.  
6.    raw_spin_lock_irqsave(&p->pi_lock, rf.flags);  
7.    p->state = TASK_RUNNING;//进程的状态变为TASK_RUNNING  
8.#ifdef CONFIG_SMP  
8.    p->recent_used_cpu = task_cpu(p);//设置recent_used_cpu  
9.    rseq_migrate(p);  
10.    //设置子进程将来要在哪个CPU上运行  
11.    __set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));  
13.#endif  
12.    rq = __task_rq_lock(p, &rf);  
13.    update_rq_clock(rq);//更新运行队列中的计时器  
14.    post_init_entity_util_avg(p);  
15.  
16.    //调用enqueue_task把子进程放入就绪队列并且设置状态(on_rq)  
17.    activate_task(rq, p, ENQUEUE_NOCLOCK);  
18.    trace_sched_wakeup_new(p);  
19.    //检查是否需要抢占父进程  
20.    check_preempt_curr(rq, p, WF_FORK);  
23.#ifdef CONFIG_SMP  
21.    //调用cfs的ops的task_woken处理进程被唤醒的情况,毕竟是第一次唤醒  
22.    if (p->sched_class->task_woken) {  
23.        /* 
24.         * Nothing relies on rq->lock after this, so its fine to 
25.         * drop it. 
26.         */  
27.        rq_unpin_lock(rq, &rf);  
28.        p->sched_class->task_woken(rq, p);  
29.        rq_repin_lock(rq, &rf);  
30.    }  
34.#endif  
31.    task_rq_unlock(rq, p, &rf);  
36.}  

wake_up_new_task函数主要作用是用于唤醒一个新创建的进程,把进程加入就绪队列里并接受调度器的调度,它主要做了以下几件事:

  1. 把保护进程描述符的自旋锁上锁
  2. 把进程的状态设置为TASK_RUNNING,表示进程已经就绪;
  3. 设置进程的recent_used_cpu成员为进程当前运行的cpu,也是父进程运行的cpu,便于后续找到最适合的cpu运行;
  4. 调用函数select_task_rq进行选择适合运行的cpu,这个函数是根据根据调度类来选择的,如果可运行的cpu数量大于1,则调用调度类的select_task_rq函数来找到合适的cpu;否则就返回那个唯一能运行的cpu;
  5. 调用函数__set_task_cpu设置子进程将来要在哪个CPU上运行,主要是设置se.cfs_rq和rt.rt_rq的调度组信息,还要设置cpu和wake_cpu;
  6. 调用函数__task_rq_lock锁住进程所在的运行队列;
  7. 调用函数post_init_entity_util_avg根据调度队列的util_avg初始化调度实体的util_avg;
  8. 调用函数check_preempt_curr检查是否需要抢占,这个函数判断父子进程的调度类是否一致,一致则使用调度类的check_preempt_curr函数判断是否需要抢占;否则再判断如果子进程的调度类比父进程的打打, 则调用函数resched_curr设置抢占标志;
  9. 调用调度类的task_woken方法对进程被唤醒的情况进行处理;
    上面的说明已经比较详细了,不过继续分析check_preempt_curr函数师怎么检测是否需要抢占的:
1.void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)  
2.{  
41.    //如果父子进程的调度类相同  
42.    if (p->sched_class == rq->curr->sched_class)  
43.        //调用调度类的check_preempt_curr函数  
44.        rq->curr->sched_class->check_preempt_curr(rq, p, flags);  
45.    //如果子进程的调度类比父进程的调度类大  
46.    else if (p->sched_class > rq->curr->sched_class)  
47.        resched_curr(rq);//说明需要重新调度  
48.  
49.    //如果父进程在队列中并且父进程需要调度  
50.    if (task_on_rq_queued(rq->curr) && test_tsk_need_resched(rq->curr))  
51.        rq_clock_skip_update(rq);//需要更新CPU运行队列的时钟  
14.}  

check_preempt_curr函数的作用是检测当前进程是否需要调度,并且设置是否调度的标志位,工作顺序如下:

  1. 如果子进程的调度类和父进程的相同,则调用调度类的check_preempt_curr函数判断是否需要调度,在check_preempt_curr函数中会根据情况来设置抢占标志位的;
  2. 如果子进程的调度类比父进程的大,也就是子进程的优先级肯定更高,说明肯定需要重新调度,那么调用函数resched_curr函数设置调度标志位;
  3. 如果父进程在队列中并且父进程需要调度,那么调用函数rq_clock_skip_update来更新CPU运行队列的时钟;因为当CPU上没有正在运行的进程时,内核会将该CPU的运行队列的时钟暂停,以节省系统资源,为了避免这种情况,我们需要修正cpu的时钟。
    到了这里我们只需要看看resched_curr函数函数就知道系统是怎么设置重新调度的标志位的了:
1.void resched_curr(struct rq *rq)  
2.{  
55.    struct task_struct *curr = rq->curr;  
56.    int cpu;  
57.      
58.    //防止死锁的WARN_ON,  
59.    lockdep_assert_held(&rq->lock);  
60.      
61.    //通过判断TIF_NEED_RESCHED标志位查看进程是否需要调度  
62.    if (test_tsk_need_resched(curr))  
63.        return;  
64.      
65.    cpu = cpu_of(rq);//获取rq执行的cpu号  
66.      
67.    //如果进程在当前cpu上执行  
68.    if (cpu == smp_processor_id()) {  
69.        set_tsk_need_resched(curr);//设置thread_info.flags置位TIF_NEED_RESCHED  
70.        set_preempt_need_resched();//设置thread_info.preempt.need_resched置位  
71.        return;  
72.    }  
73.    //如果进程在其他cpu上执行  
74.      
75.    if (set_nr_and_not_polling(curr))//设置TIF_NEED_RESCHED标志  
76.        smp_send_reschedule(cpu);//使用IPI_RESCHEDULE通知其他cpu  
77.    else  
78.        trace_sched_wake_idle_without_ipi(cpu);  
27.}  

resched_curr函数主要是做了三件事:

  1. 首先调用函数test_tsk_need_resched查看当前进程的调度标志位已经置位,如果已经置位则可以直接返回了;
  2. 如果rq队列在当前进程执行,调用函数set_tsk_need_resched设置TIF_NEED_RESCHED标志和调用函数给thread_info.preempt.need_resched置位后返回;
  3. 否则就是rq队列在其他cpu上执行了,这时候需要调用函数set_nr_and_not_polling给thread_info.flags置位TIF_NEED_RESCHED,然后调用函数smp_send_reschedule使用IPI_RESCHEDULE中断通知其他cpu。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小坚学Linux

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值