cpufreq子系统

cpufreq是linux上负责实现动态调频的关键,这篇笔记总结了linux内核cpufreq子系统的关键实现(Linux 3.18.140)。

概述

借用一张网络上的图片来看cpufreq子系统的整体结构:

  • 用户态接口:cpufreq通过sysfs向用户态暴露接口,这些节点部分是为了展示内核的配置,部分节点是可以配置的,通过这些节点可以控制cpufreq的一些行为。
  • core层:cpufreq子系统的核心层,负责管理子系统中的policy、governor和driver组件,是三者的纽带,通过core层,实现了调频策略和调频机制的分离。
  • policy:调频策略,每个CPU都有一个调频策略,规定了该CPU的最大、最小可运行频率等信息。
  • governor:可以独立于core层实现,通过规定的接口和core层交互。policy必须和某个governor关联,由governor实现具体的调频策略。可以认为policy负责管理调频参数,governor基于调频参数实现调频策略。
  • driver:平台相关的调频驱动,core层使用driver中的接口完成具体的调频操作。
  • notifier:cpufreq基于通知链机制对外发布的通知事件,外部模块可以通过监听这些事件在CPU的调频策略或者频率发生变化时做出一些处理。
  • stats:一些频率调整的统计信息,我们不关注。

这篇笔记我们重点关注core层的实现,其它部分模块仅作简要介绍。

core层关键实现

core层是cpufreq子系统的关键,它主要包含下面内容:

  1. driver的管理。
  2. policy的管理。
  3. governor的管理。
  4. 事件通知。
  5. 用户态接口。
  6. 为了方便driver、governor实现而提供的一些公共的API。

core层初始化

开机过程中,core层最开始的初始化非常简单,仅仅是创建一个kobject对象,该对象在sysfs中对应到目录/sys/devices/system/cpu/cpufreq

struct kobject *cpufreq_global_kobject;static int __init cpufreq_core_init(void)
{if (cpufreq_disabled()) // cpu动态调频功能未使能return -ENODEV;// 创建kobject代表cpufreqcpufreq_global_kobject = kobject_create();return 0;
}
core_initcall(cpufreq_core_init);

driver的管理

cpufreq驱动的实现需要实例化一个struct cpufreq_dirver对象,然后通过cpufreq_register_driver()函数向core层注册自己。系统只需要一个cpufreq驱动来实现具体的CPU频率调整功能,所以core层只允许注册一个驱动。

struct cpufreq_driver

下面是cpufreq_driver的一些核心字段:

  • init():该回调必须实现,core层在为CPU设置policy时会用分配的policy为参数调用驱动的该回调,驱动需要在实现中设置policy的部分字段。
  • verify():该回调必须实现,core层会通过该回调让驱动校验policy中的参数设置是否正确。
  • setpolicy()、target()、target_index():这几个回调必须实现一个。当实现setpolicy()时,驱动表示软件只需要设置一个策略参数(如功耗优先or性能优先)即可,硬件来决定具体的CPU工作频率。当实现后两个回调时,表示需要由governor来决定CPU的具体工作频率,然后通过这两个接口将CPU工作频率设定为指定值。
struct cpufreq_driver {char name[CPUFREQ_NAME_LEN]; // 驱动名称会在sysfs中体现u8 flags; // 表明一些驱动的featurevoid *driver_data; // cpufreq驱动自己的指针,core层不关注int    (*init)    (struct cpufreq_policy *policy);int    (*verify)    (struct cpufreq_policy *policy);int    (*setpolicy)    (struct cpufreq_policy *policy);int    (*target)    (struct cpufreq_policy *policy,    /* Deprecated */unsigned int target_freq,unsigned int relation);int    (*target_index)    (struct cpufreq_policy *policy,
unsigned int index);// 可选的,但一般都会实现。获取指定CPU的当前工作频率unsigned int    (*get)    (unsigned int cpu);struct freq_attr    **attr; // 驱动可以定义一些自己需要在sysfs中体现的参数
...    
};

注册cpufreq驱动

core层为驱动提供了cpufreq_register_driver()函数来注册cpufreq驱动,cpufreq_unregister_driver()是去注册接口,不再展开。注册过程包含三个关键逻辑:

  1. 检查drver实例中的参数设置是否正确。
  2. 保存driver实例到全局变量cpufreq_driver中,相当于完成注册。
  3. 触发为系统中的CPU设置policy的流程,该流程见下文分析。
static struct subsys_interface cpufreq_interface = {.name = "cpufreq",.subsys = &cpu_subsys, // cpu子系统.add_dev = cpufreq_add_dev, // 向cpufreq子系统添加和移除cpu的回调.remove_dev    = cpufreq_remove_dev,
};int cpufreq_register_driver(struct cpufreq_driver *driver_data)
{unsigned long flags;// 检查驱动的回调实现是否正确if (!driver_data || !driver_data->verify || !driver_data->init ||!(driver_data->setpolicy || driver_data->target_index ||driver_data->target) ||(driver_data->setpolicy && (driver_data->target_index ||driver_data->target)) ||(!!driver_data->get_intermediate != !!driver_data->target_intermediate))return -EINVAL;if (driver_data->setpolicy)driver_data->flags |= CPUFREQ_CONST_LOOPS;// cpufreq驱动只能注册一个,保存到全局变量cpufreq_driver中write_lock_irqsave(&cpufreq_driver_lock, flags);if (cpufreq_driver) {write_unlock_irqrestore(&cpufreq_driver_lock, flags);return -EEXIST;}cpufreq_driver = driver_data;write_unlock_irqrestore(&cpufreq_driver_lock, flags);// 这一步会为系统的所有CPU设置policyret = subsys_interface_register(&cpufreq_interface);// 监听CPU热插拔事件register_hotcpu_notifier(&cpufreq_cpu_notifier);
}

policy的管理

调频策略用struct cpufreq_policy来描述。系统中每个CPU都必须有一个调频策略,由于可能存在多个CPU共用一个调频策略的情况(如手机上常见的一个cluster中的CPU频率必须保持一致),所以系统中实际的cpufreq_policy对象数量可能少于CPU个数。core层用Per-CPU变量为每个CPU保存对应的cpufreq_policy对象指针。

static DEFINE_PER_CPU(struct cpufreq_policy *, cpufreq_cpu_data);
// 系统中所有的cpufreq_policy对象组织到全局链表中
static LIST_HEAD(cpufreq_policy_list);

struct cpufreq_policy

cpufreq_policy代表一个CPU调频策略,其关键字段如下:

struct cpufreq_policy {// 共享该policy的CPU掩码,cpus为online状态的CPU集合,related_cpus为所有共享该plocy的CPU集合cpumask_var_t cpus;cpumask_var_t related_cpus;unsigned int cpu; // 每个policy都有一个管理cpuunsigned int last_cpu; // 由于CPU可以热插拔,保存前一个管理该policy的CPU// CPU支持的最大、最小等频率信息,由驱动设置struct cpufreq_cpuinfo cpuinfo;/* see above */unsigned int min;    /* in kHz */unsigned int max;    /* in kHz */unsigned int cur;    /* in kHz, only needed if cpufreqgovernors are used */unsigned int policy; // 硬件自己控制具体的CPU频率时才使用该字段struct cpufreq_governor    *governor; // 软件控制频率时,具体的governor实现void *governor_data;bool governor_enabled; /* governor start/stop flag */// CPU的工作频率并非是可以调节为任意值的,驱动提供了可调节的频率挡位struct cpufreq_frequency_table    *freq_table;struct list_head policy_list; // 将该policy组织到全局链表中
};// 所有的policy对象保存到全局链表中
DEFINE_MUTEX(cpufreq_governor_lock);
static LIST_HEAD(cpufreq_policy_list);

设置CPU的policy

在驱动注册的最后,会调用subsys_interface_register()函数遍历系统中所有CPU,将每个CPU以设备的方式通过cpufreq_add_dev()函数添加到core层,这时会为CPU设置调频策略。

static int cpufreq_add_dev(struct device *dev, struct subsys_interface *sif)
{return __cpufreq_add_dev(dev, sif);
}static int __cpufreq_add_dev(struct device *dev, struct subsys_interface *sif)
{unsigned int j, cpu = dev->id;int ret = -ENOMEM;struct cpufreq_policy *policy;unsigned long flags;bool recover_policy = cpufreq_suspended;if (cpu_is_offline(cpu)) // 只为online的CPU分配调频策略return 0;#ifdef CONFIG_SMP// 可能存在多个CPU共用一个策略的情况,所以如果该CPU已经设置了策略,则处理结束policy = cpufreq_cpu_get(cpu);if (unlikely(policy)) {cpufreq_cpu_put(policy);return 0;}
#endif// 分配一个policy对象policy = recover_policy ? cpufreq_policy_restore(cpu) : NULL;if (!policy) {recover_policy = false;policy = cpufreq_policy_alloc();}// 设置该CPU为policy的管理CPUif (recover_policy && cpu != policy->cpu)WARN_ON(update_policy_cpu(policy, cpu, dev));elsepolicy->cpu = cpu;cpumask_copy(policy->cpus, cpumask_of(cpu)); // 当前cpu属于该policy管理init_completion(&policy->kobj_unregister);INIT_WORK(&policy->update, handle_update);// 调用驱动的init回调。驱动会设置policy中的参数ret = cpufreq_driver->init(policy);// 驱动在init()回调中必须正确的设置哪些CPU会共享该policy,这样core层才能正确设置policy中的cpus字段cpumask_or(policy->related_cpus, policy->related_cpus, policy->cpus);cpumask_and(policy->cpus, policy->cpus, cpu_online_mask);if (!recover_policy) {policy->user_policy.min = policy->min;policy->user_policy.max = policy->max;}// 为共用同一个policy的其它CPU设置Per-CPU指针,指向分配的policy对象write_lock_irqsave(&cpufreq_driver_lock, flags);for_each_cpu(j, policy->cpus)per_cpu(cpufreq_cpu_data, j) = policy;write_unlock_irqrestore(&cpufreq_driver_lock, flags);// 获取CPU的当前工作频率if (cpufreq_driver->get && !cpufreq_driver->setpolicy) {policy->cur = cpufreq_driver->get(policy->cpu);}// 对于那种开机时不以可调节频率表中频率运行的情况,这里将CPU的频率调整为一个已知的频率if ((cpufreq_driver->flags & CPUFREQ_NEED_INITIAL_FREQ_CHECK) && has_target()) {/* Are we running at unknown frequency ? */ret = cpufreq_frequency_table_get_index(policy, policy->cur);if (ret == -EINVAL) {ret = __cpufreq_driver_target(policy, policy->cur - 1, CPUFREQ_RELATION_L);}}// 发送CPUFREQ_START通知blocking_notifier_call_chain(&cpufreq_policy_notifier_list, CPUFREQ_START, policy);if (!recover_policy) {// 为policy在sysfs中创建属性文件,即/sys/devices/system/cpu/cpufreq/policyX/XXX文件ret = cpufreq_add_dev_interface(policy, dev);// 发送CPUFREQ_CREATE_POLICY通知blocking_notifier_call_chain(&cpufreq_policy_notifier_list,CPUFREQ_CREATE_POLICY, policy);}// 将policy对象加入到全局链表中write_lock_irqsave(&cpufreq_driver_lock, flags);list_add(&policy->policy_list, &cpufreq_policy_list);write_unlock_irqrestore(&cpufreq_driver_lock, flags);// 为policy关联governorcpufreq_init_policy(policy);if (!recover_policy) {policy->user_policy.policy = policy->policy;policy->user_policy.governor = policy->governor;}
}
  1. 只有online状态的cpu才会设置调频策略。
  2. cpufreq_cpu_get()函数根据Per-CPU变量cpufreq_cpu_data的设置,检查该CPU是否和其它CPU复用了一个policy对象,如果是则不需要进行后续的处理。
  3. 分配一个policy对象,将当前cpu设置为该policy的管理CPU。然后调用驱动的init()回调,驱动必须在该回调中对policy中的cpu、频率的信息进行正确的设置。
  4. 继续用驱动设置好的参数设置policy中的其它字段。特别重要的一步是将该共享的policy对象设置到其它CPU的cpufreq_cpu_data中。
  5. 对外发送通知,并在sysfs中为该policy创建属性节点。
  6. 将policy加入全局的cpufreq_policy_list链表中。
  7. 为该policy设置governor,该流程见下文介绍。

governor的管理

policy的调频策略实际由governor实现的,系统可以有多个governor,这些governor注册到core层后,用户态可以通过sysfs节点来为CPU指定具体使用哪个governor。系统用链表保存所有支持的governor。

static LIST_HEAD(cpufreq_governor_list);

core层提供了governor的注册函数cpufreq_register_governor(),实现很简单,不再展开。

struct governor

每个governor都需要实例化一个该对象,然后将其注册到core层。

struct cpufreq_policy {
...struct cpufreq_governor    *governor;void *governor_data;
}struct cpufreq_governor {char name[CPUFREQ_NAME_LEN]; // 每个governor都有一个唯一的名字int    initialized;// 处理governor的启动和停止事件int    (*governor)    (struct cpufreq_policy *policy,unsigned int event);ssize_t    (*show_setspeed)    (struct cpufreq_policy *policy,char *buf);int    (*store_setspeed)    (struct cpufreq_policy *policy,unsigned int freq);// governor实现对驱动的约束,要求驱动切换频率的最大时延必须小于该值unsigned int max_transition_latency;struct list_head governor_list; // 将governor组织到全局链表中};

为policy设置governor

policy的调频策略需要由governor来实现,如前面“设置CPU的policy”中分析,CPU设置了policy后,需要为policy关联一个governor。这是通过cpufreq_init_policy()函数完成的。用户态可以通过sysfs修改policy的governor,此时也会触发类似的流程。

// 记录每个CPU的governor,如果不配置则为其选择默认的governor
static DEFINE_PER_CPU(char[CPUFREQ_NAME_LEN], cpufreq_cpu_governor);static void cpufreq_init_policy(struct cpufreq_policy *policy)
{struct cpufreq_governor *gov = NULL;struct cpufreq_policy new_policy; // 借助一个临时变量完成设置过程int ret = 0;memcpy(&new_policy, policy, sizeof(*policy));// 根据名字从全局链表中选择governor,开机时可能尚未配置,选择一个默认的配置,默认配置来自系统configgov = __find_governor(per_cpu(cpufreq_cpu_governor, policy->cpu));if (gov)pr_debug("Restoring governor %s for cpu %d\n", policy->governor->name, policy->cpu);elsegov = CPUFREQ_DEFAULT_GOVERNOR;new_policy.governor = gov;if (cpufreq_driver->setpolicy)cpufreq_parse_governor(gov->name, &new_policy.policy, NULL);// 用新的配置更新当前policyret = cpufreq_set_policy(policy, &new_policy);
}// policy : current policy.new_policy: policy to be set.
static int cpufreq_set_policy(struct cpufreq_policy *policy,struct cpufreq_policy *new_policy)
{struct cpufreq_governor *old_gov;int ret;memcpy(&new_policy->cpuinfo, &policy->cpuinfo, sizeof(policy->cpuinfo));// 让驱动校验policy的参数设置是否正确ret = cpufreq_driver->verify(new_policy);// 发送CPUFREQ_ADJUST通知blocking_notifier_call_chain(&cpufreq_policy_notifier_list,CPUFREQ_ADJUST, new_policy);blocking_notifier_call_chain(&cpufreq_policy_notifier_list,CPUFREQ_INCOMPATIBLE, new_policy);// 上述的通知过程中可以调整policy,重新校验policy参数设置ret = cpufreq_driver->verify(new_policy);/* notification of the new policy */blocking_notifier_call_chain(&cpufreq_policy_notifier_list,CPUFREQ_NOTIFY, new_policy);policy->min = new_policy->min;policy->max = new_policy->max;if (cpufreq_driver->setpolicy) { // 驱动只需要设置策略的情形policy->policy = new_policy->policy;return cpufreq_driver->setpolicy(new_policy);}if (new_policy->governor == policy->governor)goto out;// 更新governor,对旧的governor执行STOP和EXIT,对新的governor执行INIT和STARTold_gov = policy->governor;if (old_gov) {__cpufreq_governor(policy, CPUFREQ_GOV_STOP);up_write(&policy->rwsem);__cpufreq_governor(policy, CPUFREQ_GOV_POLICY_EXIT);down_write(&policy->rwsem);}policy->governor = new_policy->governor;if (!__cpufreq_governor(policy, CPUFREQ_GOV_POLICY_INIT)) {if (!__cpufreq_governor(policy, CPUFREQ_GOV_START))goto out;}
}

事件通知

事件通知虽然也是在core层实现的,但是它是相对独立的内容,这里单独对其进行分析。

cpufreq子系统利用Linux标准的通知链机制,实现了两种通知:policy通知Transition通知。外部模块可以监听这两种通知事件,在事件发生时执行一些需要的逻辑。

// 通知监听接口
int cpufreq_register_notifier(struct notifier_block *nb, unsigned int list);
int cpufreq_unregister_notifier(struct notifier_block *nb, unsigned int list);// list参数代表了要监听的通知类型
#define CPUFREQ_TRANSITION_NOTIFIER    (0)
#define CPUFREQ_POLICY_NOTIFIER        (1)static BLOCKING_NOTIFIER_HEAD(cpufreq_policy_notifier_list);
static struct srcu_notifier_head cpufreq_transition_notifier_list;

policy通知

cpufreq子系统在CPU的policy发生变化时,会依次发送三个policy通知事件:

  1. 首先发出CPUFREQ_ADJUST事件,给通知接收者一个机会来修改policy中的参数,比如温控模块可能会修改其最大频率。
  2. 然后发出CPUFREQ_INCOMPATIBLE事件,给通知接收者一个机会来检查policy的参数是否和硬件不兼容,如果不兼容可以修改。这两个事件时挨着依次发出的,除了事件含义不同外,效果完全相同,所以事件接收者完全可以在一个事件中将所有该处理的事情执行完毕。
  3. 最后发出CPUFREQ_NOTIFY事件告诉通知接收者,将以该policy作为最终配置。
/* Policy Notifiers  */
#define CPUFREQ_ADJUST            (0)
#define CPUFREQ_INCOMPATIBLE        (1)
#define CPUFREQ_NOTIFY            (2)// 另外几个事件用来通知policy对象的生命周期变化
#define CPUFREQ_START            (3)
#define CPUFREQ_UPDATE_POLICY_CPU    (4)
#define CPUFREQ_CREATE_POLICY        (5)
#define CPUFREQ_REMOVE_POLICY        (6)

Transition通知

cpufreq子系统在CPU的频率发生变化前后,会依次发送两个Transition通知事件,携带的参数表明了修改的CPU及其变化前后的频率。

/* Transition notifiers */
#define CPUFREQ_PRECHANGE        (0)
#define CPUFREQ_POSTCHANGE        (1)struct cpufreq_freqs {unsigned int cpu;    /* cpu nr */unsigned int old;unsigned int new;u8 flags;        /* flags of cpufreq_driver, see below. */
};

用户态接口

cpufreq子系统在sysfs中有如下接口,通过这些既可以展示一些配置信息,也给用户态提供了一些可调整的参数。

cpufreq子系统的总目录是/sys/devices/system/cpu/cpufreq,下面是系统中所有的policy实例,每个policy目录名中的数字是该policy的管理CPU ID。其中schedutil是一种基于调度器的governor,该目录保存了该governor在sysfs中的内容。

每个policyX目录下是该policy暴露的节点,这些节点大部分是只读的,其含义如下:

  • affected_cpusreleated_cpu分别对应内核中policy->cpus和policy->releated_cpus,是该policy管理的CPU掩码。
  • cpuinfo_xxx_freq字段表示该CPU可以支持CPU频率,单位为kHZ。
  • cpuinfo_transition_latency字段表示该CPU频率切换需要的时延,单位为ns。
  • scaling_available_frequencies表示该组CPU可以调整的频率挡位;scaling_available_governors表示该组CPU可以选择的governor;scaling_driver表示当前的驱动名称;scaling_governor表示当前的governor名称。
  • scaling_xxx_freq表示当前该CPU上配置的可调节频率信息。
  • 用户态可以通过向scaling_setspeed节点写入频率来调整CPU频率,当然内核可能会不支持该操作。

每个CPU都必须有一种policy,所以在cpu的目录下会有一个软链接指向对应的policy目录:

参考资料

  1. cpufreq schedutil原理剖析-CSDN博客;
  2. https://www.cnblogs.com/LoyenWang/p/11385811.html;
  3. 内核Doc:Documentation/cpu-freq/core.txt;

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

添加jdk 11到环境变量的一种方法

添加jdk 11到环境变量的一种方法 1.jdk11可以直接在android studio 中下载, File --> Settings --> Build, Execution, Deployment --> Build Tools --> Gradle 下载jdk 11 ,确认好下载路径 2.jdk11 添加到环境变量添加到环境变量 多个…

java基于ssm的房源管理系统+vue论文

目 录 目 录 I 摘 要 III ABSTRACT IV 1 绪论 1 1.1 课题背景 1 1.2 研究现状 1 1.3 研究内容 2 2 系统开发环境 3 2.1 vue技术 3 2.2 JAVA技术 3 2.3 MYSQL数据库 3 2.4 B/S结构 4 2.5 SSM框架技术 4 3 系统分析 5 3.1 可行性分析 5 3.1.1 技术可行性 5 3.1.2 操作可行性 5 3…

web前端——clear可以清除浮动产生的影响

clear可以解决高度塌陷的问题&#xff0c;产生的副作用要小 未使用clear之前 <!DOCTYPE html> <head><meta charset"UTF-8"><title>高度塌陷相关学习</title><style>div{font-size:50px;}.box1{width:200px;height:200px;backg…

PN协议下,上位机如何通过RJ45口远程控制PLC?

在实际系统中&#xff0c;车间里分布多台PLC&#xff0c;需要用上位机软件集中控制。通常所有设备距离在几十米到上百米不等。在有通讯需求的时候&#xff0c;如果布线的话&#xff0c;工程量较大且不美观&#xff0c;这种情况下比较适合采用无线通信方式。 本方案以组态王和2…

绩效管理与绩效考核有何区别?

绩效管理跟绩效考核的区别&#xff1f;用图说话&#xff0c;就是这么个关系&#xff1a; 下面展开来说说—— 01 定位不同 绩效管理是一个完整的“系统” 它注重的是对”过程“的管理&#xff0c;包括完善的管理计划、监督规则、控制手段等。映射到员工管理上&#xff0c;更…

算法基础之合并果子

合并果子 核心思想&#xff1a; 贪心 Huffman树(算法): 每次将两个最小的堆合并 然后不断向上合并 #include<iostream>#include<algorithm>#include<queue> //用小根堆实现找最小堆using namespace std;int main(){int n;cin>>n;priority_queue&l…

C++string类的介绍及常用函数用法总结

&#x1f389;个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名乐于分享在学习道路上收获的大二在校生&#x1f43b;‍❄个人主页&#x1f389;&#xff1a;GOTXX &#x1f43c;个人WeChat&#xff1a;ILXOXVJE&#x1f43c;本文由GOTXX原创&#xff0c;首发CSDN&a…

C语言-蓝桥杯2013年第四届真题-公式求值

题目描述 输入n, m, k&#xff0c;输出下面公式的值。 其中C_n^m是组合数&#xff0c;表示在n个人的集合中选出m个人组成一个集合的方案数。组合数的计算公式如下&#xff1a; 输入格式 输入的第一行包含一个整数n&#xff1b;第二行包含一个整数m&#xff0c;第三行包含…

无监督学习(K-Means)的认识

目录 一、无监督学习 二、无监督学习和有监督学习的区别 三、K-Means 3.1数据分析 3.2k-meas算法 3.3数据正态化后k-means 3.4找最佳k&#xff08;Elbow Plot&#xff09; 四、k-means算法的优缺点 一、无监督学习 无监督学习是一种机器学习的方法&#xff0c;…

L1-078:吉老师的回归

题目描述 曾经在天梯赛大杀四方的吉老师决定回归天梯赛赛场啦&#xff01; 为了简化题目&#xff0c;我们不妨假设天梯赛的每道题目可以用一个不超过 500 的、只包括可打印符号的字符串描述出来&#xff0c;如&#xff1a;Problem A: Print "Hello world!"。 众所周知…

java基于VUE3+SSM框架的在线宠物商城+vue论文

摘 要 信息数据从传统到当代&#xff0c;是一直在变革当中&#xff0c;突如其来的互联网让传统的信息管理看到了革命性的曙光&#xff0c;因为传统信息管理从时效性&#xff0c;还是安全性&#xff0c;还是可操作性等各个方面来讲&#xff0c;遇到了互联网时代才发现能补上自古…

JRT表格元素完全体

之前分享的表格绘制是一个表格是实现雏形&#xff0c;周边把表格完全体实现&#xff0c;后面很多打印和绘制逻辑将借助此表格实现&#xff0c;所以需要表格够稳定够强大。 表格定义&#xff0c;后面借助模板设计器定义&#xff0c;现在是写死的测试定义对象。 1.PageList定义每…