当前位置: 首页 > news >正文

[Linux]学习笔记系列 -- [kernel[params


title: params
categories:

  • linux
  • kernel
    tags:
  • linux
  • kernel
    abbrlink: 8fce0ef3
    date: 2025-10-03 09:01:49

文章目录

  • kernel/params.c 内核模块参数(Kernel Module Parameters) 实现模块加载时参数传递
      • 历史与背景
        • 这项技术是为了解决什么特定问题而诞生的?
        • 它的发展经历了哪些重要的里程碑或版本迭代?
        • 目前该技术的社区活跃度和主流应用情况如何?
      • 核心原理与设计
        • 它的核心工作原理是什么?
        • 它的主要优势体现在哪些方面?
        • 它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
      • 使用场景
        • 在哪些具体的业务或技术场景下,它是首选解决方案?
        • 是否有不推荐使用该技术的场景?为什么?
      • 对比分析
        • 请将其 与 其他相似技术 进行详细对比。
  • include/linux/moduleparam.h
    • `core_param`: 定义一个历史性的核心内核参数
      • `core_param` 宏的分解:
    • `__module_param_call`: 创建`kernel_param`结构体的核心宏
      • `__module_param_call` 宏的分解:
  • kernel/params.c
    • next_arg 分隔参数
    • parse_one 解析参数
    • parse_args 解析输入参数命令行
    • Linux 内核模块的 Sysfs 接口初始化
        • 核心数据结构与回调函数
        • 初始化入口函数

在这里插入图片描述

https://github.com/wdfk-prog/linux-study

kernel/params.c 内核模块参数(Kernel Module Parameters) 实现模块加载时参数传递

历史与背景

这项技术是为了解决什么特定问题而诞生的?

kernel/params.c 中的代码实现了一个核心的内核功能:模块参数(Module Parameters)。这项技术的诞生是为了解决内核模块灵活性和可配置性的问题。

在早期,内核模块的行为通常是硬编码的。如果需要调整一个参数(例如,一个驱动的调试打印级别,或者一个硬件设备的特定配置选项),唯一的办法就是修改源代码,然后重新编译整个模块。这极大地降低了软件的灵活性和可重用性。

模块参数机制的出现,就是为了提供一个标准化的接口,允许用户在加载模块时从外部向模块传递配置值,而无需重新编译。这类似于给用户空间的应用程序传递命令行参数。该机制后来也扩展到了内核自身,允许在系统启动时通过内核引导命令行传递参数。

它的发展经历了哪些重要的里程碑或版本迭代?

模块参数机制是随着Linux内核模块化系统一起演进的。

  • 基本实现:最初的实现提供了一组宏(如 MODULE_PARM),允许开发者将模块内的变量“导出”为参数,支持整数、字符串等基本类型。
  • 标准化宏的引入:后来引入了 module_param(name, type, perm)module_param_named(name, value, type, perm)module_param_array(name, type, num, perm) 等一系列更加强大和易用的宏。这些宏简化了参数的定义,并增加了对数组类型和权限控制的支持。
  • 与Sysfs的集成:这是一个关键的里程碑。当模块被加载后,它的参数会自动在sysfs文件系统中以文件的形式出现,路径通常为 /sys/module/<module_name>/parameters/<param_name>。这不仅允许用户查看参数的当前值,还允许在运行时动态地修改那些被赋予了写权限的参数,极大地增强了系统的动态可配置性。params.c 中的代码提供了实现这种读写操作的底层函数。
  • 自定义类型的支持:框架被设计为可扩展的。开发者可以通过提供自定义的 setget 回调函数,来支持任意复杂的数据类型作为模块参数。
目前该技术的社区活跃度和主流应用情况如何?

模块参数是Linux内核驱动和子系统开发中一项基础性、极其稳定且被普遍使用的功能。它不是一个可选的库,而是编写可配置内核模块的标准范式。几乎所有的内核驱动程序都使用模块参数来暴露调试选项、硬件配置、功能开关等。任何向内核提交代码的开发者都必须熟悉这一机制。

核心原理与设计

它的核心工作原理是什么?

params.c 的核心原理是基于编译时元数据生成加载时参数解析

  1. 编译时:元数据生成

    • 当开发者在模块代码中使用 module_param() 宏时,C预处理器会将其展开。
    • 这个展开的宏定义了一个 struct kernel_param 类型的静态变量。这个结构体包含了参数的所有元信息:参数名(字符串)、指向模块中实际存储参数值的变量的指针、参数类型(通过一组标准的回调函数表示)、以及在sysfs中的文件权限。
    • 最关键的一步是,编译器会将这个 struct kernel_param 实例放入一个特殊的ELF段(section)中。
  2. 加载时:解析与赋值

    • 当用户使用 insmodmodprobe 加载模块时(例如 insmod mydriver.ko debug_level=1),内核的模块加载器 (kernel/module.c) 会解析模块的ELF文件。
    • 加载器会找到那个存放 kernel_param 结构的特殊段,并遍历其中的所有参数元数据。
    • 同时,加载器会解析用户在命令行上传递的 key=value 形式的参数。
    • 对于每个用户传入的参数,加载器会在模块的参数元数据列表中按名字查找匹配项。
    • 如果找到匹配,加载器会调用与该参数类型关联的 set 函数(这些函数由 params.c 提供,如 param_set_int, param_set_bool)。set 函数负责将用户提供的字符串值(如 “1”)转换成对应的C类型(如 int 1),然后通过元数据中存储的指针,将转换后的值写入模块的全局变量中。
  3. 运行时:Sysfs交互

    • 模块成功加载后,模块子系统会为每个参数在 /sys/module/.../parameters/ 目录下创建一个文件。
    • 当用户空间程序 readwrite 这个文件时,VFS层会最终调用到与该参数类型关联的 getset 函数,从而实现对模块内部变量的运行时读写。
它的主要优势体现在哪些方面?
  • 灵活性与可配置性:无需重新编译即可改变模块行为。
  • 简单易用:为内核开发者提供了极其简单的宏接口,隐藏了所有复杂性。
  • 类型安全:框架负责处理字符串到具体类型的转换,减少了驱动中的模板代码和出错可能。
  • 运行时交互:与sysfs的无缝集成为参数提供了标准的运行时查看和修改接口。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
  • 静态定义:模块的参数必须在编译时定义好,不能在运行时动态添加或删除参数。
  • 配置而非命令:该机制主要用于设置“值”或“状态”,不适合用于触发复杂的操作或命令。对于后者,ioctl 等机制更合适。
  • 不适合大数据量:不适合用于在用户空间和内核之间传输大量或高频率的数据。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

模块参数是内核模块暴露简单配置选项的标准和首选方案。

  • 开启调试功能:一个网络驱动可以通过一个布尔型参数 debug 来控制是否打印详细的调试日志。insmod e1000.ko debug=1
  • 指定硬件资源:在硬件资源无法被内核自动探测的旧系统中,驱动可能需要通过参数来手动指定中断号(IRQ)或I/O端口地址。
  • 设置工作模式:一个无线网卡驱动可能有一个参数 wifi_mode,允许用户在加载时指定其工作在 AP(接入点)模式还是 Station(客户端)模式。
  • 覆盖默认值:模块可以有一个内部的默认配置,但允许用户通过参数在加载时覆盖它,例如一个缓冲区大小的默认值。
是否有不推荐使用该技术的场景?为什么?
  • 高频数据交换:例如,一个应用程序需要频繁地向一个驱动发送数据包。这种场景应该使用 write() 系统调用、netlink 套接字或 ioctl
  • 导出大量状态信息:如果需要向用户空间导出大量、复杂的、结构化的状态信息或统计数据,procfsdebugfs 是更合适的工具。
  • 设备特定命令:向一个打开的设备文件发送一个特定的命令(例如,让硬盘休眠),应该使用 ioctl 机制。模块参数是模块全局的,而 ioctl 是与一个具体的文件描述符关联的。

对比分析

请将其 与 其他相似技术 进行详细对比。
特性内核模块参数 (Module Parameters)Sysfs (直接创建属性)Procfsioctl
抽象层次。一个宏就完成了所有工作。。需要手动编写show/store函数并注册属性。。非常灵活,但需要编写完整的file_operations。需要定义命令号和实现ioctl回调。
主要用途模块加载时的配置,以及简单的运行时状态读/写。表示设备模型中的属性(状态和配置)。导出文本格式的进程和系统信息、统计数据。向打开的设备文件发送命令和交换数据。
作用域模块全局设备或驱动全局通常是全局每个进程每个文件描述符
数据格式强类型(int, bool, string, array等)。文本格式,由show/store函数解释。自由的文本格式。二进制,任意结构。
使用场景insmod mydrv.ko option=valcat /sys/devices/.../attrcat /proc/meminfoioctl(fd, MY_COMMAND, &data)
总结专为模块配置设计的简化版sysfs接口。内核对象属性的标准表示方法。用于信息导出和调试的传统接口。面向设备的命令通道。

include/linux/moduleparam.h

core_param: 定义一个历史性的核心内核参数

core_param 宏的作用是定义一个核心内核参数。这些参数不隶属于任何特定的可加载模块,而是属于内核的核心部分。它们可以直接在内核启动的命令行中被设置(例如,由U-Boot传递),并且不像模块参数那样有模块名前缀。

正如注释所说,它主要用于兼容历史悠久的 __setup() 机制,为核心代码提供一个类型安全、有sysfs接口的现代化参数定义方式。

core_param 宏的分解:

#define core_param(name, var, type, perm)				\/* 1. 类型检查 */param_check_##type(name, &(var));				\/* 2. 调用底层宏 */__module_param_call("", name, &param_ops_##type, &var, perm, -1, 0)
  1. param_check_##type(name, &(var));:

    • 这是一个编译时的类型安全检查
    • ## 是C预处理器的“记号粘贴”(Token Pasting)操作符。它会将 param_check_ 和宏参数 type 的值(例如 int)粘贴在一起,形成一个新的函数名,如 param_check_int
    • 内核为每种基本类型都定义了这样的检查函数。param_check_int 会确保你传递给它的 var 变量确实是一个 int 类型的指针。如果类型不匹配,编译器会在这里产生一个警告或错误,从而在早期就捕捉到潜在的bug。
  2. __module_param_call("", name, &param_ops_##type, &var, perm, -1, 0):

    • 这是对底层核心宏的调用,传递了特定的参数来表现出 core_param 的行为。
    • "": 第一个参数 prefix 被设置为空字符串。这就是 core_param 定义的参数没有前缀的核心原因
    • name: 参数的名称。
    • &param_ops_##type: 传递一个指向操作函数集的指针。同样使用了记号粘贴,对于 int 类型,它会变成 &param_ops_int。这个结构体包含了将字符串转换为整数(set函数)和将整数转换为字符串(get函数)的回调函数指针。
    • &var: 你的C变量的地址,内核会将解析后的参数值存放在这里。
    • perm: sysfs中对应文件的权限。
    • -1: level 参数被设置为-1。这是一个特殊值,意味着这个参数不与任何特定的initcall级别绑定,可以在启动过程的任何时候被处理。这对于需要非常早期生效的核心参数(如 console)是必需的。
    • 0: flags 参数为0,没有特殊标志。

__module_param_call: 创建kernel_param结构体的核心宏

这个宏是所有参数定义宏(module_param, core_param 等)的最终目的地。它的唯一工作就是在编译时静态地定义并初始化一个 struct kernel_param 实例,并使用特殊的编译器属性将其放入名为 __param 的内存段中。

__module_param_call 宏的分解:

#define __module_param_call(prefix, name, ops, arg, perm, level, flags)	\/* 1. 创建参数全名字符串 */static const char __param_str_##name[] = prefix #name;		\/* 2. 定义并初始化 kernel_param 结构体 */static struct kernel_param __moduleparam_const __param_##name	\/* 3. 添加编译器属性 */__used __section("__param")					\__aligned(__alignof__(struct kernel_param))			\/* 4. 初始化结构体成员 */= { __param_str_##name, THIS_MODULE, ops,			\VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } }
  1. static const char __param_str_##name[] = prefix #name;:

    • 这一行创建了一个字符串常量,包含了参数的完整名称
    • #name 是C预处理器的“字符串化”(Stringification)操作符,它会将宏参数 name 的内容变成一个字符串字面量。例如,如果 namemy_value#name 就是 "my_value"
    • prefix #name 是C语言的一个特性,两个相邻的字符串字面量会被编译器自动合并成一个。
    • 示例:
      • 对于 core_param("root", ...)prefix""nameroot,结果是 "" "root",合并为 "root"
      • 对于模块 my_drv 中的 module_param(my_var, ...)prefix 会是 "my_drv."namemy_var,结果是 "my_drv." "my_var",合并为 "my_drv.my_var"
  2. static struct kernel_param ... __param_##name:

    • 这是在静态地定义一个 struct kernel_param 类型的变量。变量名是通过记号粘贴生成的,例如 __param_root
    • __moduleparam_const 在非模块化编译时通常就是 const,表示这个结构体是只读的。
  3. 编译器属性:

    • __used: 告诉编译器,即使你在当前文件中看不到任何对这个变量的引用,也绝对不能把它优化掉。这是必需的,因为引用它的代码(parse_args)在另一个文件中,并且是通过链接器定义的地址符号来找到它的。
    • __section("__param"): 这是整个机制的魔法核心。它指示编译器将这个 struct kernel_param 变量放入一个特殊的、名为 __param 的ELF段中。
    • __aligned(...): 确保结构体按其自然边界对齐,以获得最佳性能。
  4. 结构体初始化:

    • __param_str_##name: 指向我们第一步创建的全名字符串。
    • THIS_MODULE: 这是一个宏,对于内置代码(如core_param的情况),它的值是NULL;对于可加载模块,它是一个指向该模块struct module实例的指针。
    • ops: 指向类型特定的操作函数集。
    • VERIFY_OCTAL_PERMISSIONS(perm): 一个用于在编译时验证权限值是否为有效八进制数的宏,增加了代码的健壮性。
    • level: 要处理此参数的initcall级别。
    • flags: 任何额外的标志。
    • { arg }: 指向最终要被修改的C变量的指针。花括号用于C99风格的联合体成员初始化。

kernel/params.c

next_arg 分隔参数

/** 解析字符串以获取参数值对。* 您可以在空格周围使用 “,但不能转义 ”。* 参数名称中的连字符和下划线等效。*/
char *next_arg(char *args, char **param, char **val)
{unsigned int i, equals = 0;int in_quote = 0, quoted = 0;if (*args == '"') {args++;in_quote = 1;quoted = 1;}for (i = 0; args[i]; i++) {if (isspace(args[i]) && !in_quote)break;if (equals == 0) {if (args[i] == '=')equals = i;}if (args[i] == '"')in_quote = !in_quote;}*param = args;if (!equals)*val = NULL;else {args[equals] = '\0';*val = args + equals + 1;/*不要在 value 中包含引号. */if (**val == '"') {(*val)++;if (args[i-1] == '"')args[i-1] = '\0';}}if (quoted && i > 0 && args[i-1] == '"')args[i-1] = '\0';if (args[i]) {args[i] = '\0';args += i + 1;} elseargs += i;/* Chew up trailing spaces. */return skip_spaces(args);
}
EXPORT_SYMBOL(next_arg);

parse_one 解析参数

static int parse_one(char *param,char *val,const char *doing,const struct kernel_param *params,unsigned num_params,s16 min_level,s16 max_level,void *arg, parse_unknown_fn handle_unknown)
{unsigned int i;int err;/* Find parameter */for (i = 0; i < num_params; i++) {if (parameq(param, params[i].name)) {if (params[i].level < min_level|| params[i].level > max_level)return 0;/* No one handled NULL, so do it here. */if (!val &&!(params[i].ops->flags & KERNEL_PARAM_OPS_FL_NOARG))return -EINVAL;pr_debug("handling %s with %p\n", param,params[i].ops->set);kernel_param_lock(params[i].mod);if (param_check_unsafe(&params[i]))err = params[i].ops->set(val, &params[i]);elseerr = -EPERM;kernel_param_unlock(params[i].mod);return err;}}if (handle_unknown) {pr_debug("doing %s: %s='%s'\n", doing, param, val);return handle_unknown(param, val, doing, arg);}pr_debug("Unknown argument '%s'\n", param);return -ENOENT;
}

parse_args 解析输入参数命令行


/* Args looks like "foo=bar,bar2 baz=fuz wiz". */
char *parse_args(const char *doing,char *args,const struct kernel_param *params,unsigned num,s16 min_level,s16 max_level,void *arg, parse_unknown_fn unknown)
{char *param, *val, *err = NULL;/* Chew leading spaces */args = skip_spaces(args);if (*args)pr_debug("doing %s, parsing ARGS: '%s'\n", doing, args);while (*args) {int ret;int irq_was_disabled;args = next_arg(args, &param, &val);/* Stop at -- */if (!val && strcmp(param, "--") == 0)return err ?: args;irq_was_disabled = irqs_disabled();ret = parse_one(param, val, doing, params, num,min_level, max_level, arg, unknown);if (irq_was_disabled && !irqs_disabled())pr_warn("%s: option '%s' enabled irq's!\n",doing, param);switch (ret) {case 0:continue;case -ENOENT:pr_err("%s: Unknown parameter `%s'\n", doing, param);break;case -ENOSPC:pr_err("%s: `%s' too large for parameter `%s'\n",doing, val ?: "", param);break;default:pr_err("%s: `%s' invalid for parameter `%s'\n",doing, val ?: "", param);break;}err = ERR_PTR(ret);}return err;
}

Linux 内核模块的 Sysfs 接口初始化

此代码片段是Linux内核模块子系统与sysfs文件系统集成的核心部分。它的根本原理是利用内核的kobjectkset对象模型, 在系统启动的早期阶段, 创建一个顶层的/sys/module/目录。同时, 它定义了一整套的回调函数和类型信息, 用来规定未来任何内核模块加载时, 其在sysfs中的表现形式和行为——即如何创建模块自己的子目录(如/sys/module/nfsd/)以及目录下的属性文件(如refcnt, version), 以及这些文件在被读写时应该执行什么操作。

这个机制是实现内核模块与用户空间交互、监控和管理的关键。它允许用户空间的工具(如lsmod, modinfo)或系统管理员通过简单的文件操作来查看模块的状态、引用计数, 甚至修改模块的参数。


核心数据结构与回调函数

这些是定义模块sysfs行为的构建块。

/** module_attr_show: 当从sysfs读取一个模块属性文件时被调用的函数(例如 cat /sys/module/nfsd/refcnt).* 这是一个 "分发器" 函数.*/
static ssize_t module_attr_show(struct kobject *kobj,struct attribute *attr,char *buf)
{const struct module_attribute *attribute;struct module_kobject *mk;int ret;/* 将通用的 attribute 和 kobject 指针转换为模块特定的类型. */attribute = to_module_attr(attr);mk = to_module_kobject(kobj);/* 检查具体的属性是否定义了 show 方法, 如果没有则返回IO错误. */if (!attribute->show)return -EIO;/* 调用该属性自己的show方法, 由它来填充buf并返回结果. */ret = attribute->show(attribute, mk, buf);return ret;
}/** module_attr_store: 当向一个模块属性文件写入时被调用的函数(例如 echo 1 > /sys/module/nfsd/parameters/some_param).* 这同样是一个 "分发器" 函数.*/
static ssize_t module_attr_store(struct kobject *kobj,struct attribute *attr,const char *buf, size_t len)
{const struct module_attribute *attribute;struct module_kobject *mk;int ret;/* 将通用的 attribute 和 kobject 指针转换为模块特定的类型. */attribute = to_module_attr(attr);mk = to_module_kobject(kobj);/* 检查具体的属性是否定义了 store 方法, 如果没有则返回IO错误. */if (!attribute->store)return -EIO;/* 调用该属性自己的store方法, 由它来解析buf并执行操作. */ret = attribute->store(attribute, mk, buf, len);return ret;
}/** module_sysfs_ops: 将 show 和 store 分发器函数打包成一个 sysfs_ops 结构体.* 这个结构体将被关联到所有模块的kobject上.*/
static const struct sysfs_ops module_sysfs_ops = {.show = module_attr_show,.store = module_attr_store,
};/** uevent_filter: 一个过滤器函数, 用于决定是否为一个kobject生成uevent.*/
static int uevent_filter(const struct kobject *kobj)
{const struct kobj_type *ktype = get_ktype(kobj);/* 只有当kobject的类型是模块类型(module_ktype)时,才返回1(表示允许生成uevent). */if (ktype == &module_ktype)return 1;return 0;
}/** module_uevent_ops: 将 uevent 过滤器打包.* 当模块被加载或卸载时, 这个过滤器会确保向用户空间(如udev)发送一个事件.*/
static const struct kset_uevent_ops module_uevent_ops = {.filter = uevent_filter,
};/* module_kset: 一个全局指针, 它将指向代表 /sys/module/ 目录的kset对象. */
struct kset *module_kset;/** module_kobj_release: 当一个模块kobject的引用计数降为0时, 内核调用的最终释放函数.*/
static void module_kobj_release(struct kobject *kobj)
{struct module_kobject *mk = to_module_kobject(kobj);/* 如果有其他代码正在等待这个kobject被释放(例如在模块卸载过程中), 就唤醒它. */if (mk->kobj_completion)complete(mk->kobj_completion);
}/** module_ktype: 定义了所有"模块kobject"的共同行为和属性.* 它像一个 "类" 的定义.*/
const struct kobj_type module_ktype = {.release   =	module_kobj_release, /* 指定释放函数. */.sysfs_ops =	&module_sysfs_ops,   /* 指定sysfs文件操作. */
};

初始化入口函数

这是将所有部件组合在一起并实际创建/sys/module目录的函数。

/** param_sysfs_init: 创建 "module" kset.** 这必须在initramfs被解压、request_module()变得可能之前完成,* 否则模块加载将在 mod_sysfs_init 中失败.*/
static int __init param_sysfs_init(void)
{/** 调用 kset_create_and_add() 来创建一个名为 "module" 的 kset, 并将其添加到sysfs中.* 这个单一的调用就创建了 /sys/module/ 目录.* 它还将 module_uevent_ops 与这个kset关联起来.*/module_kset = kset_create_and_add("module", &module_uevent_ops, NULL);if (!module_kset) {/* 如果创建失败, 打印警告并返回错误. 这通常是致命的. */printk(KERN_WARNING "%s (%d): error creating kset\n",__FILE__, __LINE__);return -ENOMEM;}return 0;
}
/** subsys_initcall() 是一个宏, 它将 param_sysfs_init 函数注册为一个在内核启动早期阶段* (在核心驱动和文件系统初始化之后, 但在大部分设备驱动初始化之前)就要被调用的函数.* 这确保了模块的sysfs基础设施在任何模块需要被加载之前就已经准备就绪.*/
subsys_initcall(param_sysfs_init);
http://www.dtcms.com/a/609360.html

相关文章:

  • AI 多模态全栈应用项目描述
  • SpringMVC(2)学习
  • 面向智能教育的生成式AI个性化学习内容生成研究
  • C语言编程代码编译 | 学习如何高效编译和调试C语言程序
  • 多模态学习与多模态模型
  • 网站建设费的税率网页设计制作用什么软件
  • Flutter Material 3设计语言详解
  • 天猫魔盒M19_晶晨S912H当贝桌面线刷机包_adb开启
  • 长沙seo优化排名东营优化网站
  • Python 编程实战 · 实用工具与库 — Flask 基础入门
  • supOS工厂操作系统 | 像“拼乐高”一样做数据分析
  • 青岛营销型网站推广wordpress doc导入
  • upload-labs(1-13)(配合源码分析)
  • Kubernetes-架构安装
  • 【剑斩OFFER】算法的暴力美学——二维前缀和
  • 网站开发教程全集哪些网站做的好看
  • 2025IPTV 源码优化版实测:双架构兼容 + 可视化运维
  • 建设一个网站步骤揭阳专业网站建设
  • ftp下的内部网站建设竞价培训课程
  • 技术观察 | 语音增强技术迎来新突破!TFCM模型如何攻克“保真”与“降噪”的难题?
  • FPGA系统架构设计实践5_IP的封装优化
  • UDP服务端绑定INADDR_ANY后,客户端该用什么IP访问?
  • 不同传感器前中后融合方案简介
  • 《C++在LLM系统中的核心赋能与技术深耕》
  • sward V2.1.5 版本发布,支持文档导出为html\PDF,社区版新增多种账号集成与认证
  • 东莞建站网站模板怎么做电脑网站后台
  • 物联网赋能互联网医院:构建智慧医疗新生态
  • node.js+npm的环境配置以及添加镜像(保姆级教程)
  • Java 大视界 -- 基于 Java 的大数据联邦学习在跨行业数据协同创新中的实践突破
  • 企业做网站电话约见客户的对话北京网站建设 一流