linux進程調度(二)-進程創建

文章目錄

  • 2.進程創建和終止
    • 2.1 進程創建的4種方法
    • 2.2 進程創建過程分析
      • 2.2.1 copy_process函數分析
        • 2.2.1.1 dup_task_struct函數分析
        • 2.2.1.2 sched_fork函數分析
        • 2.2.1.3 copy_mm函數分析
        • 2.2.1.4 copy_thread函數分析
      • 2.2.2 wake_up_new_task函數分析

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。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/166637.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/166637.shtml
英文地址,請注明出處:http://en.pswp.cn/news/166637.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

常用數據存儲格式介紹:Excel、CSV、JSON、XML

在現代數字時代&#xff0c;數據經過提煉后可以推動創新、簡化運營并支持決策流程。然而&#xff0c;在提取數據之后&#xff0c;并將其加載到數據庫或數據倉庫之前&#xff0c;需要將數據轉化為可用的數據存儲格式。本文將介紹開發者常用的4種數據存儲格式&#xff0c;包括 Ex…

布局下一個時代,UTONMOS夯實元宇宙發展基礎

從 PC 互聯網到移動互聯網&#xff0c;再到元宇宙&#xff0c;互聯網的發展在一直不斷演變和升級著。元宇宙的時代紅利將帶來從底層基礎設施向外延展到用戶體驗的全面升級。 人們以各自不同視角理解元宇宙。但我們認為&#xff0c;目前學術界和產業界對元宇宙雖然沒有統一規范的…

JavaScript 閉包技巧

什么是閉包&#xff1f; MDN&#xff1a;“閉包是捆綁在一起&#xff08;封閉&#xff09;的函數及其周圍狀態&#xff08;詞法環境&#xff09;的引用的組合。換句話說&#xff0c;閉包使您可以從內部函數訪問外部函數的作用域。在 JavaScript 中&#xff0c;每次創建函數時都…

css引入的三種方式

css引入的三種方式 一、內聯樣式二、外部樣式表三、 內部樣式表總結trouble 一、內聯樣式 內聯樣式也被稱為行內樣式。它是將 CSS 樣式直接應用于 HTML 元素的 style 屬性中的一種方式 <p style"color: blue; font-size: 16px;">這是一個帶有內聯樣式的段落。&…

Modbus RTU轉Profinet網關連接PLC與變頻器通訊在機床上應用案例

背景&#xff1a;以前在機床加工車間里&#xff0c;工人們忙碌地操作著各種機床設備。為了使整個生產過程更加高效、流暢&#xff0c;進行智能化改造。 方案&#xff1a;在機床上&#xff0c;PLC通過Modbus RTU轉Profinet網關連接變頻器進行通訊&#xff1a;PLC作為整個生產線…

實現簡單的操作服務器和客戶端(下)

一、說明 描述:本教程介紹如何使用 simple_action_client 庫創建斐波那契操作客戶端。此示例程序創建一個操作客戶端并將目標發送到操作服務器。 內容 代碼代碼解釋編譯運行操作客戶端連接服務器和客戶端二、代碼 首先,在您喜歡的編輯器中創建actionlib_tutorials/src/fib…

【封裝UI組件庫系列】封裝Icon圖標組件

封裝UI組件庫系列第三篇封裝Icon圖標組件 &#x1f31f;前言 &#x1f31f;封裝Icon 1.創建Icon組件 2.引用svg圖標庫 第一步 第二步 第三步 3.二次封裝 4.封裝自定義屬性 &#x1f31f;總結 &#x1f31f;前言 在前端開發中&#xff0c;大家可能已經用過各種各樣的UI組…

VUE項目部署過程中遇到的錯誤:POST http://124.60.11.183:9090/test/login 405 (Not Allowed)

我當初報了這個405錯誤&#xff0c;再網上查了半天&#xff0c;他們都說什么是nginx部署不支持post訪問靜態資源。 但后面我發現我是因為另一個原因才導致的無法訪問。 我再vue中有使用devServer:{ proxy:{} }進行路由轉發。 但是&#xff01;&#xff01; 在這個配置只…

接口測試學習路線

接口測試分為兩種&#xff1a; 測試外部接口&#xff1a;系統和外部系統之間的接口 如&#xff1a;電商網站&#xff1a;支付寶支付 測試內部接口&#xff1a;系統內部的模塊之間的聯調&#xff0c;或者子系統之間的數據交互 測試重點&#xff1a;測試接口參數傳遞的正確性&…

node與 pnpm、node-sass 等工具的版本兼容關系

1. node & pnpm 2. node & node-sass 3. node-sass & sass-loader sass-loader依賴于node-sass&#xff0c;以下是部分版本號對應

Zookeeper 集群中是怎樣選舉leader的

zookeeper集群中服務器被劃分為以下四種狀態&#xff1a; LOOKING&#xff1a;尋找Leader狀態。處于該狀態的服務器會認為集群中沒有Leader&#xff0c;需要進行Leader選舉&#xff1b;FOLLOWING&#xff1a;跟隨著狀態&#xff0c;說明當前服務器角色為Follower&#xff1b;LE…

Jmeter基礎和概念

JMeter 介紹&#xff1a; 一個非常優秀的開源的性能測試工具。 優點&#xff1a;你用著用著就會發現它的重多優點&#xff0c;當然不足點也會呈現出來。 從性能工具的原理劃分&#xff1a; Jmeter工具和其他性能工具在原理上完全一致&#xff0c;工具包含4個部分&#xff1a; …

綠色能源守護者:光伏運維無人機

隨著我國太陽能光伏產業被納入戰略性新興產業&#xff0c;光伏發電成為實現“雙碳”目標的關鍵之一。在政策支持下&#xff0c;光伏產業維持高速發展&#xff0c;為迎接“碳達峰、碳中和”大勢注入了強大動力。在這一背景下&#xff0c;復亞智能與安徽一家光伏企業合作&#xf…

LeetCode78. Subsets

文章目錄 一、題目二、題解 一、題目 Given an integer array nums of unique elements, return all possible subsets (the power set). The solution set must not contain duplicate subsets. Return the solution in any order. Example 1: Input: nums [1,2,3] Outpu…

size和shape的區別與聯系

對于Numpy數據類型 shape和size都是屬于Numpy的屬性 arr.shape 將返回一個包含兩個元素的元組&#xff0c;例如 (m, n)&#xff0c;其中 m 表示數組的行數&#xff0c;n 表示數組的列數。arr.size 將返回數組中元素的總數。 舉例: 輸入&#xff1a; import numpy as np# 創…

JavaScript之DOM操作

第一章 API介紹 ?API是一種事先定義好的函數&#xff0c;用來提供應用程序與開發人員基于某軟件或硬件得以訪問的一組例程&#xff0c;而又無需訪問源碼&#xff0c;或理解內部工作機制的細節。 ?Web API接口&#xff1a;瀏覽器提供的一系列操作瀏覽器功能和頁面元素的API(BO…

【Linux】Linux的常用基本指令

Linux常用基本指令 Linux指令的歷史背景前言說明一、 ls 列出文件中的所有內容常用選項 二、pwd 顯示當前所在目錄進程三、cd 將當前工作目錄改變到指定的目錄下常用樣例 四、touch 1. 更改文檔或目錄的日期時間 2. 新建一個不存在的文件常用選項 四、mkdir 1. 更改文檔或目錄的…

牛客劍指offer刷題回溯篇

文章目錄 矩陣中的路徑題目思路代碼實現 機器人的運動范圍題目思路代碼實現 矩陣中的路徑 題目 請設計一個函數&#xff0c;用來判斷在一個n乘m的矩陣中是否存在一條包含某長度為len的字符串所有字符的路徑。路徑可以從矩陣中的任意一個格子開始&#xff0c;每一步可以在矩陣…

TensorFlow實戰教程(二十五)-基于BiLSTM-CRF的醫學命名實體識別研究(下)模型構建

這篇文章寫得很冗余,但是我相信你如果真的看完,并且按照我的代碼和邏輯進行分析,對您以后的數據預處理和命名實體識別都有幫助,只有真正對這些復雜的文本進行NLP處理后,您才能適應更多的真實環境,堅持!畢竟我寫的時候也看了20多小時的視頻,又寫了20多個小時,別抱怨,加…

JS按順序逐個發送 請求

1.使用Promise鏈 當需要按順序逐個發送 POST 請求時&#xff0c;可以使用 Axios 庫的 Promise 鏈來實現。在每個 POST 請求成功后&#xff0c;可以觸發下一個請求。這里有一個簡單的示例&#xff1a; 首先&#xff0c;確保已經在 HTML 文件中引入了 Axios 庫&#xff1a; &l…