Linux调度域与调度组

引入调度域的讨论可以参考这篇文章。这篇笔记重点分析了内核调度域相关的数据结构以及内核用于构建调度域的代码实现,以此来加深对调度域的理解。调度域是调度器进行负载均衡的基础。

调度域拓扑层级

整个系统的调度域组成一个层级结构,内核设计了struct sched_domain_topology_level来描述一层调度域拓扑。

typedef const struct cpumask *(*sched_domain_mask_f)(int cpu);
typedef int (*sched_domain_flags_f)(void);

struct sched_domain_topology_level {
    sched_domain_mask_f mask;
    sched_domain_flags_f sd_flags;

    int    flags;
    int    numa_level;
    struct sd_data      data;
#ifdef CONFIG_SCHED_DEBUG
    char *name;
#endif
};
  • mask:该回调用于指定该层级的调度域的CPU掩码。
  • sd_flags:该回调用于获取该层级的调度域标记。
  • data:该层级的调度域对象,见下面单独分析。
  • name:层级名字,如MC、DIE,这些名字会在用户态的/proc/sys/kernel/sched_domain目录中体现。

系统的调度域拓扑用sched_domain_topology_level[]数组表示,保存在全局变量sched_domain_topology中,数组的每一个元素代表一个层级。default_topology是系统定义的默认调度域拓扑层级数组。各体系结构可以定义自己的调度域拓扑层级数组,然后通过set_sched_topology()函数替换该默认值。

// 不考虑超线程,默认的由MC(多核)和DIE(socket)两个层级组成,手机产品基本上只有这两个层级
static struct sched_domain_topology_level default_topology[] = {
#ifdef CONFIG_SCHED_SMT
    { cpu_smt_mask, cpu_smt_flags, SD_INIT_NAME(SMT) }, // 支持超线程时开启
#endif
#ifdef CONFIG_SCHED_MC
    { cpu_coregroup_mask, cpu_core_flags, SD_INIT_NAME(MC) },
#endif
    { cpu_cpu_mask, SD_INIT_NAME(DIE) },
    { NULL, },
};

struct sched_domain_topology_level *sched_domain_topology = default_topology;

// MC层级包含了属于同一个cluster的所有CPU
const struct cpumask *cpu_coregroup_mask(int cpu)
{
    return &cpu_topology[cpu].core_sibling;
}

// DIE层级包含了系统所有的CPU(不考虑NUMA)
static inline const struct cpumask *cpu_cpu_mask(int cpu)
{
    return cpumask_of_node(cpu_to_node(cpu));
}

sd_data

该结构用于辅助定义调度域拓扑层级,包含了一个拓扑层级所有的调度域对象,这些对象每个CPU一份。由于Per-CPU变量也是动态分配的,所以类型是二级指针。此外,由于整个调度域拓扑层级一旦建立就基本不会再发生变化,每个CPU一份可以提高访问效率。

struct sd_data {
    struct sched_domain **__percpu sd;
    struct sched_group **__percpu sg;
    struct sched_group_capacity **__percpu sgc;
};

构建系统调度域拓扑结构时,会使用__sdt_alloc()为sd_data分配内存,对应的释放函数为__sdt_free()

static int __sdt_alloc(const struct cpumask *cpu_map)
{
    struct sched_domain_topology_level *tl;
    int j;

    for_each_sd_topology(tl) { // 遍历所有的拓扑层级,为每一个tl->sd_data分配空间
        struct sd_data *sdd = &tl->data;

        // 分配调度域指针对象,这些指针每个CPU一份
        sdd->sd = alloc_percpu(struct sched_domain *);
        sdd->sg = alloc_percpu(struct sched_group *);
        sdd->sgc = alloc_percpu(struct sched_group_capacity *);

        // 为每个CPU分配这三个调度域对象
        for_each_cpu(j, cpu_map) {
            struct sched_domain *sd;
            struct sched_group *sg;
            struct sched_group_capacity *sgc;

            sd = kzalloc_node(sizeof(struct sched_domain) + cpumask_size(),
                GFP_KERNEL, cpu_to_node(j));
            *per_cpu_ptr(sdd->sd, j) = sd;

            sg = kzalloc_node(sizeof(struct sched_group) + cpumask_size(),
                GFP_KERNEL, cpu_to_node(j));
            sg->next = sg;
            *per_cpu_ptr(sdd->sg, j) = sg;

            sgc = kzalloc_node(sizeof(struct sched_group_capacity) + cpumask_size(),
                GFP_KERNEL, cpu_to_node(j));
            *per_cpu_ptr(sdd->sgc, j) = sgc;
        }
    }
    return 0;
}

调度域对象

有3个调度域对象:

  1. struct sched_domain:调度域代表可以共享属性和调度参数的一组CPU。每个CPU在每一个调度域拓扑层级中都有一个shced_domain对象。
  2. struct sched_group:调度域可以由一个或多个调度组构成,每个调度组也代表可以共享属性和调度参数的一组CPU,属于同一个调度域的调度组的集合组成了调度域代表的CPU。调度域进行负载均衡的目的就是要保证其内部各个调度组之间的负载均衡。
  3. struct sched_capacity:包含了调度组的captacity内容,和调度组管理的CPU的能力强相关。

调度域: sched_domain

struct sched_domain {
    // 组成调度域拓扑层级
    struct sched_domain *parent;    /* top domain must be null terminated */
    struct sched_domain *child;    /* bottom domain must be null terminated */
    struct sched_group *groups;    /* the balancing groups of the domain */
    // 暂时忽略诸多调度参数 
...
    int level; // 代表的调度域拓扑等级,MC为0,其它依次加1

#ifdef CONFIG_SCHED_DEBUG
    char *name;
#endif
    union {
        void *private; /* used during construction */
        struct rcu_head rcu; /* used during destruction */
    };

    unsigned int span_weight; // 该调度域包含的CPU个数和CPU掩码
    unsigned long span[0];
};
  1. parent、child、groups:组成了调度域拓扑等级。parent指针指向更高层的sched_domain对象,最低层的sched_domain对象(如MC层)的该指针为NULL。child则是和parent相反。groups指向了属于该调度域的所有调度组对象链表,见sched_group部分的介绍。
  2. private:指向所属的sched_domain_topology_level.sd_data。
  3. span_weight、span:包含了该调度域包含的CPU信息,span数组在构建系统调度域拓扑结构时根据sched_domain_topology中定义的系统调度域拓扑等级描述动态分配。

构建调度域: build_sched_domain()

sched_domain对象的分配是在__sdt_alloc()函数中完成的,但是其大多数字段的设置以及系统拓扑层级结构中sched_domain之间的父子关系是在build_sched_domain()函数中完成的。

// 构建cpu上tl层级的sched_domain对象(tl->sd_data.sd[cpu]),其包含的CPU为cpu_map,child为其下一级
struct sched_domain *build_sched_domain(struct sched_domain_topology_level *tl,
        const struct cpumask *cpu_map, struct sched_domain_attr *attr,
        struct sched_domain *child, int cpu)
{
    struct sched_domain *sd = sd_init(tl, cpu); // 初始化该sched_domain中的各字段
    if (!sd)
        return child;

    // 设置sched_domain的CPU掩码,即span数组
    cpumask_and(sched_domain_span(sd), cpu_map, tl->mask(cpu));
    if (child) {        
        sd->level = child->level + 1;
        sched_domain_level_max = max(sched_domain_level_max, sd->level);
        
        child->parent = sd; // 相邻层级的sched_domain对象之间建立父子关系
        sd->child = child;

        // 检查确保低层级的sched_domain的CPU掩码必须是高层级的sched_domain的CPU掩码的子集
        if (!cpumask_subset(sched_domain_span(child),
            sched_domain_span(sd))) {

#ifdef CONFIG_SCHED_DEBUG
            pr_err("     the %s domain not a subset of the %s domain\n",
                child->name, sd->name);
#endif

            cpumask_or(sched_domain_span(sd),
               sched_domain_span(sd),
               sched_domain_span(child));
        }
    }
    set_domain_attribute(sd, attr);
    return sd;
}

如上可见,sched_domain对象的大部分字段都是在sd_init()函数中设置的,下面我们先忽略其中调度策略参数的设置,这部分内容在负载均衡相关的笔记中再来分析(结合负载均衡上下文更容易理解)。

static struct sched_domain *
sd_init(struct sched_domain_topology_level *tl, int cpu)
{
    // 找到要初始化的sched_domain对象
    struct sched_domain *sd = *per_cpu_ptr(tl->data.sd, cpu);
    int sd_weight, sd_flags = 0;

    *sd = (struct sched_domain) {
...
#ifdef CONFIG_SCHED_DEBUG
        .name = tl->name,
#endif
    };
...
    sd->private = &tl->data;
    // private指向sched_domain_topology_level.sd_data
    return sd;
}

调度组: sched_group

调度域进一步可以划分为若干个调度组,这些调度组的CPU的并集就是调度域的CPU集合。

struct sched_group {
    struct sched_group *next;    /* Must be a circular list */
    atomic_t ref;

    unsigned int group_weight; // cpumask中CPU的个数
    struct sched_group_capacity *sgc;

    // 调度组的CPU掩码
    unsigned long cpumask[0];
};
  1. next:同一个调度域下的多个调度组用该指针组成一个单向循环链表,链表的表头结点为sched_domain.groups指针。
  2. ref:同一个sched_group对象可能被多个CPU上的sched_domain对象引用,所以需要一个引用计数。
  3. sgc:用来描述该调度组的capacity,见下面介绍。
  4. group_weight、cpumask:该调度组包含了哪些CPU和CPU的个数。

构建调度组: build_sched_groups()

__sdt_alloc()函数中完成sched_group对象的分配,每个CPU在每个调度域拓扑层级都分配了一个sched_group对象,但是这些对象并不会全部被使用。在构建系统调度域拓扑时,会调用build_sched_groups()函数为指定CPU的sched_domain对象构建其所有的sched_group对象。

static int build_sched_groups(struct sched_domain *sd, int cpu)
{
    struct sched_group *first = NULL, *last = NULL;
    struct sd_data *sdd = sd->private;
    const struct cpumask *span = sched_domain_span(sd); // span为该调度域的CPU集合
    struct cpumask *covered;
    int i;

    // 为调度域对象sdd->sd_data.sd[cpu]确定调度组,将确定的调度组对象及其sgc指针
    // 保存在sdd->sd_data.groups[cpu]中,该函数会修改sd->groups的指向
    get_group(cpu, sdd, &sd->groups);
    atomic_inc(&sd->groups->ref);

    // 后面的逻辑只有在处理调度域对象的第一个CPU时才会执行,
    // 对于MC层就是每个cluster的第一个CPU,对于DIE层就是整个系统的第一个CPU
    if (cpu != cpumask_first(span))
        return 0;

    lockdep_assert_held(&sched_domains_mutex);

    covered = sched_domains_tmpmask;
    cpumask_clear(covered);
    for_each_cpu(i, span) {
        // 检查调度域内的每个CPU,判断其是否可以作为一个独立的sched_group
        struct sched_group *sg;
        int group, j;

        if (cpumask_test_cpu(i, covered))
            continue;

        group = get_group(i, sdd, &sg);
        cpumask_setall(sched_group_mask(sg)); // 这个逻辑非常奇怪,为什么要设置所有的CPU到调度组?

        // 找到那些属于同一个sched_group的CPU,将其设置到调度组对象的CPU掩码中
        for_each_cpu(j, span) {
            if (get_group(j, sdd, NULL) != group)
                continue;
            cpumask_set_cpu(j, covered);
            cpumask_set_cpu(j, sched_group_cpus(sg));
        }
        // 将同一个调度域对象下面的若干个调度组对象通过sched_group.next指针组织成单向循环链表
        if (!first)
            first = sg;
        if (last)
            last->next = sg;
        last = sg;
    }
    last->next = first;
    return 0;
}

// 为调度域对象sdd->sd[cpu]确定调度组,将确定的调度组对象及其sgc指针保存在sg中
static int get_group(int cpu, struct sd_data *sdd, struct sched_group **sg)
{
    struct sched_domain *sd = *per_cpu_ptr(sdd->sd, cpu);
    struct sched_domain *child = sd->child;

    if (child)
        cpu = cpumask_first(sched_domain_span(child));

    if (sg) {
        *sg = *per_cpu_ptr(sdd->sg, cpu);
        (*sg)->sgc = *per_cpu_ptr(sdd->sgc, cpu);
        atomic_set(&(*sg)->sgc->ref, 1); /* for claim_allocations */
    }
    return cpu;
}

get_group()函数非常关键,该函数对第一层拓扑和更高层拓扑的逻辑不同。对于第一层拓扑,其child为NULL,这时传入的cpu不会发生变化,这样调度域对象sdd->sd[cpu]关联的调度组对象就是该CPU的调度组对象。对于更高层拓扑,其child不是NULL,这时调度组对象来自其第一层拓扑的第一个CPU对应的对象。这样的get_group()函数实现的效果见下面"构建系统调度域拓扑"中第二组for循环后的示意图。

调度组能力: sched_group_capacity

每个sched_group对象都关联一个sched_group_capacity对象来描述调度组的能力。

struct sched_group_capacity {
    atomic_t ref; // 同sched_group中的ref
    
    // 调度组的capacity    
    unsigned int capacity, capacity_orig;
    unsigned long next_update;
...
    unsigned long cpumask[0]; /* iteration mask */
};

初始化调度组能力: init_sched_groups_capacity()

在构建系统调度域拓扑过程中,会调用该函数将初始化指定调度域的所有调度组能力。调度组的能力和调度组中CPU的capacity强相关,这里不详细展开,在CPU能力相关笔记中再详细分析。

static void init_sched_groups_capacity(int cpu, struct sched_domain *sd)
{
    struct sched_group *sg = sd->groups;

    // 为该调度域的所有调度组计算其管理的CPU个数
    do {
        sg->group_weight = cpumask_weight(sched_group_cpus(sg));
        sg = sg->next;
    } while (sg != sd->groups);

    // 只有调度组中的第一个CPU才需要执行计算capacity的过程,因为其他CPU都会共享该sgc 
    if (cpu != group_balance_cpu(sg))
        return;

    update_group_capacity(sd, cpu); // 计算capacity
    atomic_set(&sg->sgc->nr_busy_cpus, sg->group_weight);
}

根域: root_domain

根域定义了一些全局信息,所有CPU共用一个根域对象。初始时系统会定义一个def_root_domain作为系统根域,后续整个调度域拓扑结构建立时会重新建立一个根域对象来替换默认的根域对象。

/*
 * By default the system creates a single root-domain with all cpus as
 * members (mimicking the global state we have today).
 */
struct root_domain def_root_domain;

struct root_domain {
    atomic_t refcount;
...
    struct rcu_head rcu;
    cpumask_var_t span;
    cpumask_var_t online;
};

// 最终系统拓扑层级中的调度域和根域对象会关联到rq中
struct rq {
...
#ifdef CONFIG_SMP
    struct root_domain *rd;
    struct sched_domain *sd;
#endif
}

cpu_attach_domain()

系统调度域拓扑建立完毕后会调用该函数将每个CPU上的调度域对象(第一级)和根域对象保存到CPU运行队列rq中。

static void
cpu_attach_domain(struct sched_domain *sd, struct root_domain *rd, int cpu)
{
    struct rq *rq = cpu_rq(cpu);
    struct sched_domain *tmp;

    // 遍历系统调度域拓扑结构,去掉那些对调度没有意义的荣誉层级(如相邻两个层级的调度域管理的CPU相同)
    for (tmp = sd; tmp; ) {
        struct sched_domain *parent = tmp->parent;
        if (!parent)
            break;

        if (sd_parent_degenerate(tmp, parent)) {
            tmp->parent = parent->parent;
            if (parent->parent)
                parent->parent->child = tmp;

            if (parent->flags & SD_PREFER_SIBLING)
                tmp->flags |= SD_PREFER_SIBLING;
            destroy_sched_domain(parent, cpu);
        } else
            tmp = tmp->parent;
    }

    if (sd && sd_degenerate(sd)) {
        tmp = sd;
        sd = sd->parent;
        destroy_sched_domain(tmp, cpu);
        if (sd)
            sd->child = NULL;
    }

    // 将sd和rd指针保存到rq->sd和rq->rd中
    rq_attach_root(rq, rd);
    tmp = rq->sd;
    rcu_assign_pointer(rq->sd, sd);
    destroy_sched_domains(tmp, cpu);

    update_top_cache_domain(cpu);
}

构建系统调度域拓扑

在开机过程中,会调用init_sched_domains()函数根据定义的系统调度域拓扑层级结构构建系统调度域拓扑。

// 传入的cpu_map为cpu_active_mask
static int init_sched_domains(const struct cpumask *cpu_map)
{
    int err;

    arch_update_cpu_topology(); // 构建系统调度域拓扑之前让体系结构更新一次自己的cpu_map

    ndoms_cur = 1;
    doms_cur = alloc_sched_domains(ndoms_cur);
    if (!doms_cur)
        doms_cur = &fallback_doms;
    cpumask_andnot(doms_cur[0], cpu_map, cpu_isolated_map); // 去掉隔离的CPU

    err = build_sched_domains(doms_cur[0], NULL); // 构建系统调度域拓扑结构

    // 系统调度域拓扑构建完毕,根据结果创建/proc/sys/kernel/domain目录
    register_sched_domain_sysctl();
    return err;
}

核心的系统调度域拓扑建立由build_sched_domains()函数完成。

// 该临时数据结构用来保存分配的调度域对象
struct s_data {
    struct sched_domain ** __percpu sd;
    struct root_domain    *rd;
};

static int build_sched_domains(const struct cpumask *cpu_map,
                   struct sched_domain_attr *attr)
{
    enum s_alloc alloc_state;
    struct sched_domain *sd;
    struct s_data d;
    int i, ret = -ENOMEM;

    // 为调度域拓扑层级数组sched_domain_topology分配调度域对象
    alloc_state = __visit_domain_allocation_hell(&d, cpu_map);
    if (alloc_state != sa_rootdomain)
        goto error;

    // 为每个CPU都建立一个sched_domain对象组成的层级结构(通过sched_domain的parent和child字段)
    for_each_cpu(i, cpu_map) {
        struct sched_domain_topology_level *tl;

        sd = NULL;
        for_each_sd_topology(tl) { // 拓扑层级从低到高遍历,sd为child
            // 每构建一层sched_domain对象都是下一层的child
            sd = build_sched_domain(tl, cpu_map, attr, sd, i);
            // 保存每个CPU的最低层次sched_domain对象到临时变量d.sd中,后面有用处
            if (tl == sched_domain_topology)
                *per_cpu_ptr(d.sd, i) = sd;
            if (tl->flags & SDTL_OVERLAP || sched_feat(FORCE_SD_OVERLAP))
                sd->flags |= SD_OVERLAP;
            // 构建的拓扑层级最高层次只需要覆盖所有active_cpu_mask即可,这样可以防止数组sched_domain_topology
            // 定义多余的高层次拓扑层级,比如在DIE上面再定义一层,但是DIE层已经可以覆盖系统所有CPU了
            if (cpumask_equal(cpu_map, sched_domain_span(sd)))
                break;
        }
    }

    // 为每个CPU的各层sched_domain对象构建调度组对象
    for_each_cpu(i, cpu_map) {
        for (sd = *per_cpu_ptr(d.sd, i); sd; sd = sd->parent) {
            // 由低到高为每一层调度域拓扑层级的sched_domain对象构建调度组对象
            sd->span_weight = cpumask_weight(sched_domain_span(sd));
            if (sd->flags & SD_OVERLAP) {
                // 调度域中各调度组的CPU存在重叠的情况,我们不考虑
                if (build_overlap_sched_groups(sd, i))
                    goto error;
            } else {
                // 为CPU i上的调度域对象sd构建调度组对象
                if (build_sched_groups(sd, i))
                    goto error;
            }
        }
    }

    // 1. 为每个CPU的各层sched_domain对象中的sched_group对象设置capacity。
    // 2. 标记系统调度域拓扑中不需要的调度域对象,后续根据标记结果释放。
    for (i = nr_cpumask_bits-1; i >= 0; i--) {
        if (!cpumask_test_cpu(i, cpu_map))
            continue;
        for (sd = *per_cpu_ptr(d.sd, i); sd; sd = sd->parent) {
            claim_allocations(i, sd);
            init_sched_groups_capacity(i, sd);
        }
    }

    // 将调度域对象和根域对象保存到CPU运行队列中
    rcu_read_lock();
    for_each_cpu(i, cpu_map) {
        sd = *per_cpu_ptr(d.sd, i);
        cpu_attach_domain(sd, d.rd, i);
    }

    rcu_read_unlock();
    ret = 0;
error:
    __free_domain_allocs(&d, alloc_state, cpu_map);
    return ret;
}
  • __visit_domain_allocation_hell()函数只负责调度对象的分配。下图是6小核+2大核的系统在该函数调用之后,系统调度域拓扑层级结构中的调度域对象分配示意图。

  • 第一组for循环中,通过build_sched_domain()函数为每个CPU都建立一个sched_domain对象组成的层级结构(通过sched_domain的parent和child字段)。下图是6小核+2大核的系统在该组for循环之后,系统调度域拓扑层级结构中的调度域对象关系示意图。

  • 第二组for循环中,通过build_sched_groups()函数为每个CPU的各层sched_domain对象构建调度组对象(保存到sched_domain的groups字段中)。下图是6小核+2大核的系统在该组for循环之后,系统调度域拓扑层级结构中的调度域对象关系示意图。

上图中置灰部分表示这些对象没有关联到拓扑结构中。在MC层,每个CPU为一个调度组,同一个cluster内的调度组组成该层的调度域。在DIE层,每个cluster内的CPU为一个调度组,多个cluster的调度组组成该层的调度域。

  • 第三组for循环中,通过claim_allocations()函数将系统调度域拓扑中不需要的调度域对象标记为NULL(上图中置灰部分调度域对象),这样最后的__free_domain_allocs()函数根据标记结果释放这些对象。通过init_sched_groups_capacity()函数为所有的sched_group对象初始化capacity。
  • 第四组for循环中,通过cpu_attach_domain()函数将调度域对象和根域对象保存到CPU运行队列中。
  • 最后,通过__free_domain_allocs()函数释放那些不需要的调度域对象。下图是6小核+2大核的系统在多余的调度域对象被释放之后的示意图。

__visit_domain_allocation_hell()

该函数为系统调度域拓扑层级数组sched_domain_topology分配调度域对象。

static enum s_alloc __visit_domain_allocation_hell(struct s_data *d,
                                                   const struct cpumask *cpu_map)
{
    memset(d, 0, sizeof(*d));

    // 为所有的sched_domain_topology_level.sd_data分配对象
    if (__sdt_alloc(cpu_map))
        return sa_sd_storage;
    // 该临时的Per-CPU变量用于的后续流程,用于指向__sdt_alloc()中分配的调度对象
    d->sd = alloc_percpu(struct sched_domain *);
    if (!d->sd)
        return sa_sd_storage;
    // 分配并初始化根域对象
    d->rd = alloc_rootdomain();
    if (!d->rd)
        return sa_sd;
    return sa_rootdomain;
}

claim_allocations()

将调度域sd中使用到的相关调度域对象指针标记为NULL,这样最后的__free_domain_allocs()函数就可以通过判断非NULL指针来将那些未使用到的调度域对象进行回收。需要注意的是这里将tl->sdd.xxx指针设置为NULL并不会导致内存泄漏,因为这些对象的指针在build_sched_domains()函数的临时变量d中还保存了一份,而且最后会通过cpu_attach_domain()函数将sd指针保存到CPU运行队列中。

static void claim_allocations(int cpu, struct sched_domain *sd)
{
    struct sd_data *sdd = sd->private;

    WARN_ON_ONCE(*per_cpu_ptr(sdd->sd, cpu) != sd);
    *per_cpu_ptr(sdd->sd, cpu) = NULL;

    if (atomic_read(&(*per_cpu_ptr(sdd->sg, cpu))->ref))
        *per_cpu_ptr(sdd->sg, cpu) = NULL;

    if (atomic_read(&(*per_cpu_ptr(sdd->sgc, cpu))->ref))
        *per_cpu_ptr(sdd->sgc, cpu) = NULL;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/174716.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

909-2015-T2

文章目录 1.原题2.算法思想3.关键代码4.完整代码5.运行结果 1.原题 编写算法,删除二叉搜索树(二叉排序树)的最小元素。叙述算法思想并给出算法实现,分析算法复杂性。二叉树采用链式存储结构,节点结构如下:…

编写函数求定积分的通用函数

归纳编程学习的感悟, 记录奋斗路上的点滴, 希望能帮到一样刻苦的你! 如有不足欢迎指正! 共同学习交流! 🌎欢迎各位→点赞 👍 收藏⭐ 留言​📝 不积跬步无以至千里,…

子虔科技亮相2023工业软件生态大会 以先进理念赋能工业软件发展

作为云化工业软件领先企业,子虔科技携多项全新云原生产品亮相2023工业软件生态大会。 本届大会以“共建新一代工业软件体系,引领制造业高质量发展”为主题,集结行业领先企业、行业专家探究工业软件在核心技术、产业链创新和生态建设等方面创…

navicat --CSV导出数据乱码情况(三种情况解决方式)

CSV导出数据乱码情况分析及处理 在navicat 中有很多导出方式,大家都知道csv导出要比xlse要快很多,但是在使用csv导出时要防止乱码情况, 下面我列出三种处理方式(如有其他方式大家可以帮忙补充一下): 文章目…

seismicunix基础-声波波动方程推导

seismicunix基础-声波波动方程推导 接触波动方程的研究人员都绕不开这个公式,这是在一维状态下波动方程 但是对于这个方程是怎样来的很少有人能说清楚,其中涉及到牛顿第二运动定律,物体的加速度与受到的力有关。 假设一维弦是大量紧密连接的质…

Spark---介绍及安装

一、Spark介绍 1、什么是Spark Apache Spark 是专为大规模数据处理而设计的快速通用的计算引擎。Spark是UC Berkeley AMP lab (加州大学伯克利分校的AMP实验室)所开源的类Hadoop MapReduce的通用并行计算框架,Spark拥有Hadoop MapReduce所具有的优点;但…

.nvmrc 文件使用详解

文章目录 1. 前言2. .nvmrc 是什么3. 创建 .nvmrc 文件4. 使用 .nvmrc 文件5. 终端自动切换版本 1. 前言 当开发多个项目时,每个项目运行环境要求的 node 版本不一样,那么我们就需要给每个项目指定 node 版本,也就是通过终端执行 nvm install…

用Auth Analyzer插件批量测试接口越权,安全测试快人一步!

随着信息化技术的不断发展,软件安全成了软件行业的重大挑战,因此安全测试也成为了测试人员必备的技能之一。 沐沐在安全测试过程中较为常见的就是接口越权漏洞,在尝试过多种工具进行越权漏洞测试后,最终找到了个人认为最便捷最有…

[C++ 从入门到精通] 12.重载运算符、赋值运算符重载、析构函数

📢博客主页:https://loewen.blog.csdn.net📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!📢本文由 丶布布原创,首发于 CSDN,转载注明出处🙉📢现…

c++|引用

目录 一、引用概念 二、引用特性 三、常引用 (具有常属性的引用变量) 四、使用场景 一、引用概念 引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,他和他引用的变量共用同…

Java动态代理JKD版本

1、ISale.java package com.atguigu; public interface ISale {void saleShaoBing();void saleJianBing();void saleYueBing();void saleManTou(); }2、WuDa.java package com.atguigu;//Target:目标类、目标对象 public class WuDa implements ISale{//target method:目标方法…

Polygon Miden VM架构总览

1. 计算类型 Programs程序有2种类型: 1)Circuit电路:即,程序即电路。将程序转换为电路。2)Virtual machine虚拟机:即,程序为电路的输入。【Miden VM属于此类型】 2. 何为ZK virtual machine…

用Markdown Nice写作

网址:https://www.mdnice.com/ 代码 表格 第二行用来对齐: -表示左对齐 :-:表示居中 -:表示右对齐 数学 上下标 分数 累加 幂 对数 根式 微积分 交集、并集 格式 标题 缩进 删除线 斜体 加粗 参考文献

【ArcGIS Pro二次开发】(77):ArcGIS Pro中图层的获取与解析

一、最简单的获取图层方式 通常情况下,如果要获取当前地图中的图层,可以用2种方法获取。 以下图为例: 一种是【map.Layers】属性获取,结果如下: 可以看出,这里只获取到了第一层级的图层,图层组…

目标检测 详解SSD原理,数据处理与复现

原理详解 前言 今天我们要读的这篇VGGNet(《Very Deep Convolutional Networks For Large-Scale Image Recognition》),就是在AlexNet基础上对深度对网络性能的影响做了进一步的探索。它是ImageNet 2014年亚军,相比于AlexNet&am…

inBuilder低代码平台新特性推荐-第九期

各位知乎的友友们,大家好~ 今天来给大家带来的是inBuilder低代码平台特性推荐系列第九期——子表弹出新增! 01 概述 子表弹出新增,是低代码平台提供的一种前端输入组件,在子表字段较多的场景中,有时为了方便…

代码随想录刷题】Day16 二叉树03

文章目录 1.【104】二叉树的最大深度(优先掌握递归)1.1 前言1.2 题目描述1.3 递归法java代码实现1.4 迭代法java代码实现1.5 相关练习题【559】N叉树的最大深度 2.【111】二叉树的最小深度(优先掌握递归)2.1 题目描述2.2 递归法ja…

智能高效的转运机器人,为物流行业注入新动力

在当今社会,随着科技的不断发展,机器人已经逐渐融入到我们的生活中。其中,转运机器人作为物流行业的新秀,正以其高效、智能的特点,引起了广泛的关注。 转运机器人,是指能够自主进行物品搬运和运输的机器人…

说一下类的生命周期

👽System.out.println(“👋🏼嗨,大家好,我是代码不会敲的小符,双非大四,Java实习中…”); 📚System.out.println(“🎈如果文章中有错误的地方,恳请大家指正&a…

开始通过 Amazon SageMaker JumpStart 在亚马逊云科技上使用生成式 AI

目前,生成式 AI 正受到公众的广泛关注,人们围绕着许多人工智能技术展开讨论。很多客户一直在询问有关亚马逊云科技生成式 AI 解决方案的更多信息,本文将为您进行解答。 这篇文章通过一个真实的客户使用案例概述了生成式 AI,提供了…