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.mzph.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…