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


title: sysctl

categories:

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

文章目录

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

kernel/sysctl.c & fs/proc/proc_sysctl.c 内核参数调整接口(Kernel Parameter Tuning Interface)

历史与背景

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

Sysctl(System Control)机制的诞生是为了解决一个核心的系统管理问题:如何为系统管理员和应用程序提供一个统一的、动态的接口来查看和修改正在运行的Linux内核的内部参数

在Sysctl出现之前,调整内核行为通常需要:

  1. 修改内核源代码并重新编译:这是最原始、最不灵活的方式。
  2. 在内核启动时传递引导参数(Boot Parameters):这比重新编译要好,但仍然需要在启动时就确定参数,无法在系统运行时动态调整。

随着Linux内核变得越来越复杂,需要暴露给管理员进行调整的"旋钮"(Tunables)也越来越多。这些参数涵盖了从网络协议栈行为、虚拟内存管理策略到文件系统特性等方方面面。Sysctl的出现,就是为了提供一个**在运行时(Runtime)**就能与这些内核变量进行交互的框架,从而实现:

  • 动态调优:管理员可以根据系统的实时负载和需求,动态地调整内核参数以优化性能或改变行为,而无需重启系统。
    • 系统监控:提供一个标准化的路径来读取内核的各种状态和统计信息。
  • 统一的接口:将内核中分散在各个子系统的可调参数,通过一个统一的、层次化的命名空间暴露出来。
它的发展经历了哪些重要的里程碑或版本迭代?
  • sysctl(2)系统调用的引入 :最初,Sysctl的主要接口是一个专门的sysctl(2)系统调用。这个系统调用使用一个由整数组成的、基于OID(对象标识符)的非直观路径来访问参数。例如,net.ipv4.ip_forward可能对应于{CTL_NET, NET_IPV4, NET_IPV4_IP_FORWARD}
  • /proc/sys文件系统的出现 :这是决定性的里程碑。社区认识到sysctl(2)系统调用非常不便于人类使用和脚本编写。为了提供一个更友好、更易于浏览的接口,内核引入了/proc/sys这个"伪文件系统"。procfs将Sysctl的层次化命名空间映射 为文件系统的目录和文件结构。
    • net.ipv4.ip_forward这个参数现在可以直接通过读写文件/proc/sys/net/ipv4/ip_forward来访问。
    • 这种基于文件I/O的接口非常符合Unix"一切皆文件"的哲学,使得管理员可以用标准的shell工具(cat, echo)来轻松地查看和修改内核参数。
  • sysctl(2)系统调用的废弃 :由于/proc/sys接口的巨大成功和便利性,sysctl(2)系统调用在Linux 2.6之后被废弃(Deprecated) ,不再推荐在新代码中使用。现在,sysctl机制几乎完全等同于/proc/sys文件系统。
目前该技术的社区活跃度和主流应用情况如何?

/proc/sys是现代Linux系统中进行内核参数调优和状态监控的核心标准接口

  • 主流应用
    • 系统管理员 :日常使用sysctl命令(它实际上是/proc/sys的一个包装器)或直接编辑/etc/sysctl.conf文件来持久化内核参数的修改。
    • 网络性能调优 :调整TCP/IP协议栈的各种缓冲区大小、超时时间等参数(如net.core.somaxconn, net.ipv4.tcp_rmem)是网络优化的常见操作。
    • 虚拟内存管理 :调整"脏页"回写策略(vm.dirty_ratio)、交换分区使用倾向(vm.swappiness)等。
    • 容器和虚拟化 :在创建网络命名空间时,内核会自动为新的命名空间创建一套独立的/proc/sys/net参数,实现了网络栈的隔离配置。

核心原理与设计

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

Sysctl的核心是一个基于表的注册机制 ,并由procfs提供前端展现。

  1. 内核中的注册表 (struct ctl_table)
    • 内核的各个子系统(如网络、虚拟内存)如果想暴露一个可调参数,就需要定义一个struct ctl_table实例。
    • 这个结构体描述了参数的所有信息:
      • procname: 参数的名称(即在/proc/sys中显示的文件名)。
      • data: 一个指向内核中实际变量的指针。
      • maxlen: 变量的大小。
      • mode: 文件的权限(可读/可写)。
      • proc_handler: 一个可选的处理函数指针。这使得读写操作可以不仅仅是简单的变量拷贝,还可以触发更复杂的内核动作。
      • child: 指向一个子ctl_table数组,用于构建目录层次。
  2. 注册到核心
    • 在模块初始化时,子系统调用register_sysctl_table()将这个ctl_table注册到Sysctl核心中。Sysctl核心维护着一个由这些表构成的全局树状结构。
  3. /proc/sys的映射
    • proc_sysctl.c实现了/proc/sys这个伪文件系统。
    • 当用户访问/proc/sys下的一个路径时(例如cat /proc/sys/net/ipv4/ip_forward):
      1. procfs的路径查找代码会遍历Sysctl的全局ctl_table树,匹配路径的每个部分(net, ipv4, ip_forward)。
      2. 找到匹配的ctl_table条目后,它会为这个条目动态地创建一个inode
      3. 当用户对这个文件进行readwrite操作时,procfs会调用该ctl_table条目中指定的proc_handler函数(如果存在),或者使用一个默认的处理函数。
      4. 处理函数会根据ctl_table中的data指针,读取或修改内核中对应的变量。
它的主要优势体现在哪些方面?
  • 动态性:无需重启即可实时查看和修改内核行为。
  • 统一与可发现性 :提供了一个集中的、层次化的、可自描述的(通过ls -R /proc/sys)接口。
  • 易用性:基于文件的接口非常直观,易于管理员和脚本使用。
  • 灵活性:通过自定义处理函数,Sysctl不仅能读写变量,还能触发复杂的内核动作。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
  • 非结构化数据 :Sysctl主要设计用来处理简单的、单一的值(整数、字符串)。它不适合用来导出复杂的、结构化的数据或大量的统计信息。这类需求通常由procfs下的其他文件(如/proc/net/dev)或sysfs来满足。
  • 命名空间隔离不完全 :虽然网络参数(/proc/sys/net)是每个网络命名空间独立的,但其他大多数Sysctl参数(如/proc/sys/vm, /proc/sys/kernel)是全局的,不能被容器隔离。这是容器技术中一个已知的局限性。

使用场景

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

Sysctl是调整内核全局行为策略和协议栈参数的标准解决方案。

  • 开启IP转发echo 1 > /proc/sys/net/ipv4/ip_forward。这是将一台Linux机器配置为路由器的基本步骤。
  • 调整虚拟内存行为echo 10 > /proc/sys/vm/swappiness。降低内核使用交换分区的倾向,让系统更倾向于回收文件页缓存。
  • 安全加固 :禁用ICMP重定向(net.ipv4.conf.all.accept_redirects = 0)或开启SYN Cookies(net.ipv4.tcp_syncookies = 1)以抵御某些网络攻击。
  • 增加系统限制 :增加最大文件句柄数(fs.file-max)或最大PID数(kernel.pid_max)。
是否有不推荐使用该技术的场景?为什么?
  • 控制设备状态 :不推荐。控制或查看单个设备 的状态(例如,一个LED的亮度,一个网卡的速率)应该使用sysfs (/sys)。Sysctl是用于控制内核子系统的全局行为 ,而sysfs是用于控制具体的设备实例
  • 查看进程信息 :不推荐。应使用/proc/[pid]/
  • 获取大量的、格式化的统计数据 :不推荐。例如,获取网络接口的收发包统计,直接读取/proc/net/dev比通过Sysctl更合适。

对比分析

请将其 与 其他相似技术 进行详细对比。
特性 Sysctl (/proc/sys) Sysfs (/sys) Procfs (其他部分, e.g., /proc/[pid])
核心功能 调整内核全局行为和子系统策略。 表示和控制设备模型中的具体设备和驱动。 报告进程状态 和导出格式化的、只读的系统信息。
数据模型 层次化的键值对 。主要用于读写单一值 面向对象的图结构。将kobject的层次结构映射为目录,"一文件一值"。 混合模型。部分是目录结构(进程),部分是包含复杂文本的大文件。
读写特性 可读可写。其主要目的就是"可调"。 可读可写。用于读取设备状态和写入设备配置。 大部分是只读的。用于报告状态,而非配置。
命名空间隔离 部分隔离 (只有net等少数部分)。 不隔离。反映的是全局的物理或虚拟设备。 部分隔离/proc/[pid]自然是隔离的,但/proc/meminfo等是全局的。
典型用途 调整TCP缓冲区大小 (net.core.wmem_max)。 改变LED亮度 (/sys/class/leds/led0/brightness)。 查看进程的内存映射 (/proc/self/maps)。
总结 内核的"设置"菜单 系统的"设备管理器" 系统的"任务管理器"和"信息中心"

Sysctl基础设施常量与写入模式配置

本代码片段的功能是为Linux内核的sysctl子系统定义一组基础的、全局性的常量和配置变量。它不创建任何用户可见的sysctl文件,而是为其他地方定义的ctl_table条目提供共享的数据源,并定义一个关键的全局策略------如何处理对/proc/sys/文件的写入操作,以增强系统的健壮性和安全性。

实现原理分析

此代码是sysctl框架的底层支持,其实现原理是为更高层的配置表提供数据和行为开关。

  1. 共享常量数组 (sysctl_long_vals):

    • 代码定义并导出了一个包含0, 1, LONG_MAX的全局数组。
    • EXPORT_SYMBOL_GPL意味着内核中的任何其他部分(遵循GPL许可)都可以引用这个数组。
    • 其目的是提供一个通用的、可复用的数据源。在定义许多ctl_table条目时,需要指定允许的最小值和最大值。通过让.extra1.extra2字段指向这个共享数组中的元素(例如,通过SYSCTL_ZERO, SYSCTL_ONE等宏),可以避免在内核的各个角落重复定义这些常用常量,从而节省少量内存并提高代码的一致性。
  2. 编译时限制常量 (ngroups_max, cap_last_cap):

    • ngroups_maxcap_last_cap被定义为static const int。它们的值来自于内核头文件中定义的宏NGROUPS_MAX(进程可属的最大补充组数)和CAP_LAST_CAP(最后一个有效的能力值)。
    • 将这些宏的值赋给静态常量变量的目的是为了获取一个内存地址ctl_table.data字段需要一个指针。虽然NGROUPS_MAX是一个编译时常量,但它没有内存地址。通过这个赋值,sysctl_subsys_table(在之前的示例中)就可以安全地使用 &ngroups_max 来填充.data字段,从而将这个编译时确定的内核限制暴露给用户空间。
  3. 写入模式控制 (sysctl_writes_strict):

    • 这是本片段中最核心的功能。它定义了一个枚举sysctl_writes_mode和相应的全局变量sysctl_writes_strict,用于精确控制通过procfs写入sysctl值的行为语义。
    • SYSCTL_WRITES_LEGACY : 旧的、不推荐的行为。写入操作会忽略文件的当前偏移量,每次write()系统调用都会完全覆盖整个sysctl值。
    • SYSCTL_WRITES_WARN: 与旧行为相同,但如果写入时文件偏移量不为0,内核会打印一条警告。这是一个过渡模式,旨在帮助开发者发现并修复依赖旧行为的脚本或程序。
    • SYSCTL_WRITES_STRICT : 当前的默认模式,也是最安全的模式。它强制要求对数值型sysctl的写入必须从文件偏移量0开始,并且一次write()调用必须包含完整的数值。这可以防止因部分写入或意外的文件指针位置而导致内核参数被错误地设置为截断或无效的值。
    • 这个全局变量sysctl_writes_strict会被sysctl文件处理函数(如proc_dointvec)读取,并根据其值来调整自身的行为。之前示例中定义的/proc/sys/kernel/sysctl_writes_strict文件就是用来在运行时修改这个全局变量值的用户接口。

代码分析

c 复制代码
// 定义一个包含常用 long 值的全局数组。
const unsigned long sysctl_long_vals[] = { 0, 1, LONG_MAX };
// 导出该符号,以便其他内核模块可以通过 .extra1/.extra2 字段引用这些常量。
EXPORT_SYMBOL_GPL(sysctl_long_vals);

// 仅在内核配置了SYSCTL时,以下代码才会被编译。
#if defined(CONFIG_SYSCTL)

/* 用于定义最小值和最大值的常量 */
// 将编译时宏 NGROUPS_MAX 的值赋给一个静态常量变量,以便获取其内存地址。
static const int ngroups_max = NGROUPS_MAX;
// 将编译时宏 CAP_LAST_CAP 的值赋给一个静态常量变量,以便获取其内存地址。
static const int cap_last_cap = CAP_LAST_CAP;

// 仅在内核配置了PROC_SYSCTL时,以下代码才会被编译。
#ifdef CONFIG_PROC_SYSCTL

/**
 * enum sysctl_writes_mode - 支持的sysctl写入模式
 *
 * 这些写入模式控制着在通过proc接口更新sysctl值时,
 * 当前文件位置如何影响每次写入的行为。
 */
enum sysctl_writes_mode {
	// 遗留模式:每次write()必须包含完整的sysctl值,并忽略文件位置。
	SYSCTL_WRITES_LEGACY		= -1,
	// 警告模式:与遗留模式相同,但当初始文件位置不为0时发出警告。
	SYSCTL_WRITES_WARN		= 0,
	// 严格模式:对数值的写入必须在文件位置0,且缓冲区必须包含完整的值。
	// 对字符串则尊重文件位置,允许多次写入追加。
	SYSCTL_WRITES_STRICT		= 1,
};

// 定义一个全局变量来存储当前的sysctl写入模式,并初始化为最严格的模式。
static enum sysctl_writes_mode sysctl_writes_strict = SYSCTL_WRITES_STRICT;
#endif /* CONFIG_PROC_SYSCTL */
#endif /* CONFIG_SYSCTL */

内核子系统参数初始化:创建/proc/sys/kernel/基础接口

本代码片段的功能是为Linux内核定义并注册一组基础的、与子系统和进程属性相关的运行时参数。它利用sysctl机制,在/proc/sys/kernel/目录下创建对应的文件,从而允许用户空间查询内核中关于进程组员数量上限(ngroups_max)、能力系统(capabilities)的范围(cap_last_cap)以及特定于体系结构的非对齐内存访问处理策略。

实现原理分析

此功能的实现是Linux sysctl框架的标准应用,旨在将内核内部的配置变量安全地暴露给用户空间。

  1. Sysctl表定义 (sysctl_subsys_table):

    • 代码的核心是sysctl_subsys_table数组,它是一个ctl_table结构体的集合。该数组中的每一个条目都定义了一个将在/proc/sys/kernel/下创建的文件。
    • 关键字段描述了每个接口的行为:
      • .procname: procfs中的文件名。
      • .data: 指向内核中实际存储该参数值的全局变量的指针。
      • .mode: 文件的访问权限。在此代码段中,ngroups_maxcap_last_cap被设为0444(只读),因为它们是由内核在编译时或启动时确定的常量,不应在运行时被修改。
      • .proc_handler: 指向处理该文件读写请求的内核函数。这里使用了标准的proc_dointvec(处理整数)和proc_dointvec_minmax(处理带范围检查的整数)。
  2. 条件编译 (#ifdef):

    • 该表的定义中使用了#ifdef预处理指令。例如,与非对齐访问相关的条目(unaligned-trap, ignore-unaligned-usertrap)仅在内核配置了相应的体系结构支持选项(CONFIG_SYSCTL_ARCH_UNALIGN_*)时才会被编译。这确保了sysctl接口只暴露与当前内核配置和目标体系结构相关的参数。
  3. 注册过程 (sysctl_init_bases):

    • sysctl_init_bases函数在内核初始化期间被调用,它通过register_sysctl_init("kernel", sysctl_subsys_table)将定义的sysctl_subsys_table注册到 "kernel" 命名空间下。sysctl框架会据此在procfs中创建/proc/sys/kernel/目录(如果尚不存在)并填充其中对应的文件。

代码分析

c 复制代码
// 仅在内核配置了SYSCTL时,以下代码才会被编译。
#ifdef CONFIG_SYSCTL

// 定义了在 /proc/sys/kernel/ 目录下的部分子系统sysctl条目。
static const struct ctl_table sysctl_subsys_table[] = {
// 只有在配置了PROC_SYSCTL时,此条目才有效。
#ifdef CONFIG_PROC_SYSCTL
	{
		.procname	= "sysctl_writes_strict", // 文件名: 控制对sysctl写入的严格性。
		.data		= &sysctl_writes_strict,
		.maxlen		= sizeof(int),
		.mode		= 0644,
		.proc_handler	= proc_dointvec_minmax,
		.extra1		= SYSCTL_NEG_ONE, // 允许的值范围是-1到1。
		.extra2		= SYSCTL_ONE,
	},
#endif
	{
		.procname	= "ngroups_max", // 文件名: 一个进程可以属于的最大补充组数量。
		.data		= (void *)&ngroups_max,
		.maxlen		= sizeof (int),
		.mode		= 0444, // 权限: 只读。
		.proc_handler	= proc_dointvec, // 使用标准的整数处理函数。
	},
	{
		.procname	= "cap_last_cap", // 文件名: 内核支持的最后一个(数值最高的)能力(capability)。
		.data		= (void *)&cap_last_cap,
		.maxlen		= sizeof(int),
		.mode		= 0444, // 权限: 只读。
		.proc_handler	= proc_dointvec,
	},
// 仅当内核为特定架构配置了允许非对齐陷阱时,此条目才有效。
#ifdef CONFIG_SYSCTL_ARCH_UNALIGN_ALLOW
	{
		.procname	= "unaligned-trap", // 文件名: 控制是否为非对齐访问产生陷阱。
		.data		= &unaligned_enabled,
		.maxlen		= sizeof(int),
		.mode		= 0644, // 权限: 可读写。
		.proc_handler	= proc_dointvec,
	},
#endif
// 仅当内核为特定架构配置了忽略非对齐警告时,此条目才有效。
#ifdef CONFIG_SYSCTL_ARCH_UNALIGN_NO_WARN
	{
		.procname	= "ignore-unaligned-usertrap", // 文件名: 控制是否忽略用户空间的非对齐陷阱警告。
		.data		= &no_unaligned_warning,
		.maxlen		= sizeof (int),
		.mode		= 0644,
		.proc_handler	= proc_dointvec,
	},
#endif
};

// sysctl_init_bases: sysctl基础初始化函数。
int __init sysctl_init_bases(void)
{
	// 将上面定义的 sysctl_subsys_table 注册到 "kernel" 命名空间下。
	// 这将在 /proc/sys/ 下创建 "kernel" 目录,并填充其中的文件。
	register_sysctl_init("kernel", sysctl_subsys_table);

	return 0;
}
#endif /* CONFIG_SYSCTL */

Sysctl标准整数处理程序:/proc接口的核心转换逻辑

本代码片段是Linux sysctl子系统的核心实现之一,它提供了一整套标准化的处理函数(proc handlers),用于安全、高效地处理用户空间通过/proc/sys/文件对内核整数类型(int, unsigned int, bool, u8)参数的读写操作。这些函数是绝大多数数值型sysctl接口(如proc_dointvec, proc_dointvec_minmax等)的底层基础,负责处理ASCII字符串与内核二进制整数之间的转换、错误检查、范围验证和并发安全。

实现原理分析

该代码的设计体现了高度的模块化和可扩展性,通过函数分层和回调机制来构建功能。

  1. 分层设计:

    • 转换层 (do_proc_d...vec_conv) : 这是最底层,负责在已解析出的unsigned long值和目标内核变量(如int *unsigned int *)之间进行最终的、类型安全的转换。它处理特定类型的溢出检查(例如,确保一个值在INT_MAX范围内)和符号处理。
    • 解析/格式化层 (__do_proc_d...vec) : 这是核心工作层。它负责处理来自用户空间的原始字符缓冲区。在写入 时,它调用proc_skip_spacesproc_get_long等辅助函数来解析ASCII字符串,提取出数值。在读取 时,它调用proc_put_longproc_put_char来将内核中的数值格式化为ASCII字符串。这一层通过一个函数指针参数conv来调用转换层的函数,实现了逻辑的解耦。
    • 公共API层 (proc_d...vec) : 这是最高层,是ctl_table.proc_handler字段实际使用的函数。这些函数是对解析/格式化层的简单封装,它们为调用提供了便利,并传入默认的转换函数。
  2. 通过回调实现扩展性:

    • proc_dointvec_minmaxproc_douintvec_minmax是该设计模式的最佳体现。它们不是重新实现一遍解析逻辑,而是继续调用通用的do_proc_dointvec函数。
    • 它们通过传递一个自定义的转换函数do_proc_dointvec_minmax_conv)和一个包含最小/最大值指针的参数结构体param)来实现范围检查。
    • 这个自定义的转换函数在内部先调用默认的转换函数,然后再执行额外的范围检查。这使得在不改变核心解析逻辑的情况下,轻松地为sysctl参数添加了范围验证功能。
  3. 并发安全 (READ_ONCE/WRITE_ONCE):

    • 所有对内核sysctl变量的直接读写都通过READ_ONCE()WRITE_ONCE()宏来完成。这些宏确保了内存操作的原子性(对于对齐的自然大小变量),并充当了编译器屏障。这可以防止编译器进行可能破坏并发访问的优化(如将内存访问优化为寄存器访问),从而保证了在多核或中断环境下对这些变量访问的安全性。

代码分析

核心转换函数
c 复制代码
// do_proc_dointvec_conv: 在 unsigned long 和 int 之间进行转换和类型检查。
// 这是带符号整数的默认转换回调。
static int do_proc_dointvec_conv(bool *negp, unsigned long *lvalp,
				 int *valp,
				 int write, void *data)
{
	if (write) { // 写入操作
		if (*negp) { // 如果是负数
			// 检查是否超出int的负数范围
			if (*lvalp > (unsigned long) INT_MAX + 1)
				return -EINVAL;
			// 使用WRITE_ONCE进行并发安全的写入
			WRITE_ONCE(*valp, -*lvalp);
		} else { // 如果是正数
			// 检查是否超出int的正数范围
			if (*lvalp > (unsigned long) INT_MAX)
				return -EINVAL;
			WRITE_ONCE(*valp, *lvalp);
		}
	} else { // 读取操作
		int val = READ_ONCE(*valp); // 并发安全的读取
		if (val < 0) {
			*negp = true;
			*lvalp = -(unsigned long)val;
		} else {
			*negp = false;
			*lvalp = (unsigned long)val;
		}
	}
	return 0;
}

// do_proc_douintvec_conv: 在 unsigned long 和 unsigned int 之间进行转换和类型检查。
// 这是无符号整数的默认转换回调。
static int do_proc_douintvec_conv(unsigned long *lvalp,
				  unsigned int *valp,
				  int write, void *data)
{
	if (write) {
		// 检查是否超出unsigned int的范围
		if (*lvalp > UINT_MAX)
			return -EINVAL;
		WRITE_ONCE(*valp, *lvalp);
	} else {
		unsigned int val = READ_ONCE(*valp);
		*lvalp = (unsigned long)val;
	}
	return 0;
}
通用整数向量解析与格式化
c 复制代码
// __do_proc_dointvec: 处理带符号整数向量的核心函数。
// 它可以处理一个或多个由空白符分隔的整数。
static int __do_proc_dointvec(void *tbl_data, const struct ctl_table *table,
		  int write, void *buffer,
		  size_t *lenp, loff_t *ppos,
		  int (*conv)(bool *negp, unsigned long *lvalp, int *valp,
			      int write, void *data),
		  void *data)
{
	// ... 省略了变量定义和初始检查 ...

	if (!conv) // 如果没有提供自定义转换函数,则使用默认的
		conv = do_proc_dointvec_conv;

	// 循环处理buffer中的每个数值,或者格式化内核数组中的每个值到buffer
	for (; left && vleft--; i++, first=0) {
		unsigned long lval;
		bool neg;

		if (write) {
			// 解析用户buffer中的ASCII字符串,得到数值lval和符号neg
			proc_skip_spaces(&p, &left);
			// ...
			err = proc_get_long(&p, &left, &lval, &neg, ...);
			// ...
			// 调用转换函数,将解析出的值写入内核变量
			if (conv(&neg, &lval, i, 1, data)) {
				err = -EINVAL;
				break;
			}
		} else {
			// 读取内核变量,并调用转换函数得到lval和neg
			if (conv(&neg, &lval, i, 0, data)) {
				err = -EINVAL;
				break;
			}
			// 将数值格式化为ASCII字符串并写入用户buffer
			if (!first)
				proc_put_char(&buffer, &left, '\t');
			proc_put_long(&buffer, &left, lval, neg);
		}
	}
    // ... 省略了收尾和错误处理 ...
	return err;
}

// __do_proc_douintvec: 处理单个无符号整数的核心函数。
// 注意:这个函数被简化为只处理单个值,不支持数组。
static int __do_proc_douintvec(...)
{
    // ... 省略了大部分实现细节,其逻辑与dointvec类似,但更简单 ...
}
公共Sysctl处理函数API
c 复制代码
// proc_dointvec: ctl_table中使用的标准处理函数,用于读写带符号整数向量。
int proc_dointvec(const struct ctl_table *table, int write, void *buffer,
		  size_t *lenp, loff_t *ppos)
{
	// 直接调用核心函数,并传入NULL表示使用默认的转换逻辑。
	return do_proc_dointvec(table, write, buffer, lenp, ppos, NULL, NULL);
}

// proc_douintvec: ctl_table中使用的标准处理函数,用于读写单个无符号整数。
int proc_douintvec(const struct ctl_table *table, int write, void *buffer,
		  size_t *lenp, loff_t *ppos)
{
	return do_proc_douintvec(table, write, buffer, lenp, ppos,
				 do_proc_douintvec_conv, NULL);
}

// proc_dobool: ctl_table中使用的标准处理函数,用于读写布尔值。
// 它通过将bool适配为int,复用了proc_dointvec的逻辑。
int proc_dobool(...)
{
	// ... 内部创建一个临时的ctl_table和int变量,然后调用proc_dointvec ...
}
带范围检查的封装函数
c 复制代码
// do_proc_dointvec_minmax_conv: 用于minmax检查的自定义转换函数。
static int do_proc_dointvec_minmax_conv(bool *negp, unsigned long *lvalp,
					int *valp,
					int write, void *data)
{
	// ...
	struct do_proc_dointvec_minmax_conv_param *param = data;

	// 先调用默认的转换函数,将值写入一个临时变量
	ret = do_proc_dointvec_conv(negp, lvalp, ip, write, data);
	// ...
	if (write) {
		// 在写入内核变量之前,进行范围检查
		if ((param->min && *param->min > tmp) ||
		    (param->max && *param->max < tmp))
			return -EINVAL;
		WRITE_ONCE(*valp, tmp); // 检查通过后,再安全地写入
	}
	return 0;
}

// proc_dointvec_minmax: ctl_table中使用的高级处理函数,支持范围检查。
int proc_dointvec_minmax(const struct ctl_table *table, int write,
		  void *buffer, size_t *lenp, loff_t *ppos)
{
	// 从table的extra1和extra2字段获取min/max指针
	struct do_proc_dointvec_minmax_conv_param param = {
		.min = (int *) table->extra1,
		.max = (int *) table->extra2,
	};
	// 调用通用的do_proc_dointvec,但传入自定义的minmax转换函数和参数。
	return do_proc_dointvec(table, write, buffer, lenp, ppos,
				do_proc_dointvec_minmax_conv, &param);
}
// ... 类似地,proc_douintvec_minmax 和 proc_dou8vec_minmax 也都是这种封装模式 ...

Sysctl 无符号长处理程序:单位转换和范围检查

本代码片段是Linux sysctl子系统中一个功能强大且设计精巧的处理程序,专门用于读写unsigned long类型的内核参数。其核心功能不仅包括处理ASCII字符串与unsigned long数组之间的转换和范围检查,还内建了一个通用的单位转换机制。这使得内核可以将内部使用的一种单位(如jiffies)自动、透明地转换成对用户更友好的单位(如毫秒)进行展示和配置。

代码分析

核心工作函数
c 复制代码
// __do_proc_doulongvec_minmax: 处理 unsigned long 向量的核心实现,支持单位转换和范围检查。
static int __do_proc_doulongvec_minmax(void *data,
		const struct ctl_table *table, int write,
		void *buffer, size_t *lenp, loff_t *ppos,
		unsigned long convmul, unsigned long convdiv)
{
	// ... 省略了变量定义和初始检查 ...

	// 循环处理,直到用户缓冲区耗尽或内核向量处理完毕
	for (; left && vleft--; i++, first = 0) {
		unsigned long val;

		if (write) { // 写入路径
			// ... 从用户缓冲区解析ASCII字符串为数值 val ...
			err = proc_get_long(&p, &left, &val, &neg, ...);
			if (err || neg) { // 不允许负数
				err = -EINVAL;
				break;
			}

			// *** 关键:执行单位转换 ***
			val = convmul * val / convdiv;
			// *** 关键:在转换后进行范围检查 ***
			if ((min && val < *min) || (max && val > *max)) {
				err = -EINVAL;
				break;
			}
			// 并发安全地写入内核变量
			WRITE_ONCE(*i, val);
		} else { // 读取路径
			// *** 关键:执行反向单位转换 ***
			val = convdiv * READ_ONCE(*i) / convmul;
			
			// ... 将数值 val 格式化为ASCII字符串并写入用户缓冲区 ...
			proc_put_long(&buffer, &left, val, false);
		}
	}
    // ... 省略了收尾和错误处理 ...
	return err;
}
公共API封装
c 复制代码
// do_proc_doulongvec_minmax: __do_proc_doulongvec_minmax 的一个简单封装。
static int do_proc_doulongvec_minmax(...)
{
	return __do_proc_doulongvec_minmax(table->data, table, write,
			buffer, lenp, ppos, convmul, convdiv);
}

// proc_doulongvec_minmax: 标准的、无单位转换的proc handler。
int proc_doulongvec_minmax(const struct ctl_table *table, int write,
			   void *buffer, size_t *lenp, loff_t *ppos)
{
    // 调用核心实现,乘数和除数都为1,即不进行单位转换。
    return do_proc_doulongvec_minmax(table, write, buffer, lenp, ppos, 1l, 1l);
}

// proc_doulongvec_ms_jiffies_minmax: 专门用于毫秒(ms)与jiffies之间转换的proc handler。
int proc_doulongvec_ms_jiffies_minmax(const struct ctl_table *table, int write,
				      void *buffer, size_t *lenp, loff_t *ppos)
{
    // 调用核心实现,传入HZ和1000作为转换因子。
    return do_proc_doulongvec_minmax(table, write, buffer,
				     lenp, ppos, HZ, 1000l);
}

fs/proc/proc_sysctl.c

sysctl_find_alias Sysctl 查找别名

c 复制代码
struct sysctl_alias {
	const char *kernel_param;
	const char *sysctl_param;
};

/*
 * 从历史上看,某些设置同时具有 sysctl 和命令行参数。
 * 使用通用的 sysctl.parameter 支持,我们可以在一个地方处理它们,并且只保留历史名称以实现兼容性。
 * 这并不意味着要添加全新的别名。
 * 添加现有别名时,请考虑更改值的可能不同时刻(例如,从 early_param 到调用 do_sysctl_args() 的时刻)是否是特定参数的问题。
 */
static const struct sysctl_alias sysctl_aliases[] = {
	{"hardlockup_all_cpu_backtrace",	"kernel.hardlockup_all_cpu_backtrace" },
	{"hung_task_panic",			"kernel.hung_task_panic" },
	{"numa_zonelist_order",			"vm.numa_zonelist_order" },
	{"softlockup_all_cpu_backtrace",	"kernel.softlockup_all_cpu_backtrace" },
	{ }
};

static const char *sysctl_find_alias(char *param)
{
	const struct sysctl_alias *alias;

	for (alias = &sysctl_aliases[0]; alias->kernel_param != NULL; alias++) {
		if (strcmp(alias->kernel_param, param) == 0)
			return alias->sysctl_param;
	}

	return NULL;
}

sysctl_table_root sysctl 表根目录

c 复制代码
static const struct ctl_table root_table[] = {
	{
		.procname = "",
		.mode = S_IFDIR|S_IRUGO|S_IXUGO,
	},
};
static struct ctl_table_root sysctl_table_root = {
	.default_set.dir.header = {
		{{.count = 1,
		  .nreg = 1,
		  .ctl_table = root_table }},
		.ctl_table_arg = root_table,
		.root = &sysctl_table_root,
		.set = &sysctl_table_root.default_set,
	},
};

sysctl_mount_point 系统挂载点

c 复制代码
/*
 * 支持永久空目录。
 * 必须非空,以避免与其他表共享地址。
 */
static const struct ctl_table sysctl_mount_point[] = {
	{ }
};

sysctl_set_perm_empty_ctl_header Sysctl set perm 空 ctl 头

c 复制代码
#define sysctl_set_perm_empty_ctl_header(hptr)		\
	(hptr->type = SYSCTL_TABLE_TYPE_PERMANENTLY_EMPTY)

init_header 初始化 struct ctl_table_header 结构体的实例

c 复制代码
static void init_header(struct ctl_table_header *head,
	struct ctl_table_root *root, struct ctl_table_set *set,
	struct ctl_node *node, const struct ctl_table *table, size_t table_size)
{
	head->ctl_table = table;
	head->ctl_table_size = table_size;
	head->ctl_table_arg = table;
	head->used = 0;
	head->count = 1;
	head->nreg = 1;
	head->unregistering = NULL;
	head->root = root;
	head->set = set;
	head->parent = NULL;
	head->node = node;
	INIT_HLIST_HEAD(&head->inodes);
	if (node) {
		const struct ctl_table *entry;
      /* 如果 node 非空,函数会遍历 ctl_table 数组,并将每个节点与 ctl_table_header 关联起来 */
		list_for_each_table_entry(entry, head) {
			node->header = head;
			node++;
		}
	}
   /* sysctl_mount_point 是一个特殊的 sysctl 表,用于挂载点的管理 */
	if (table == sysctl_mount_point)
		sysctl_set_perm_empty_ctl_header(head);
}

find_entry 查找 sysctl 表中的条目

c 复制代码
static const struct ctl_table *find_entry(struct ctl_table_header **phead,
	struct ctl_dir *dir, const char *name, int namelen)
{
	struct ctl_table_header *head;
	const struct ctl_table *entry;
	struct rb_node *node = dir->root.rb_node;

	lockdep_assert_held(&sysctl_lock);

	while (node)
	{
		struct ctl_node *ctl_node;
		const char *procname;
		int cmp;

		ctl_node = rb_entry(node, struct ctl_node, node);
		head = ctl_node->header;
		entry = &head->ctl_table[ctl_node - head->node];
		procname = entry->procname;

		cmp = namecmp(name, namelen, procname, strlen(procname));
		if (cmp < 0)
			node = node->rb_left;
		else if (cmp > 0)
			node = node->rb_right;
		else {
			*phead = head;
			return entry;
		}
	}
	return NULL;
}

find_subdir 查找子目录

c 复制代码
static struct ctl_dir *find_subdir(struct ctl_dir *dir,
				   const char *name, int namelen)
{
	struct ctl_table_header *head;
	const struct ctl_table *entry;

	entry = find_entry(&head, dir, name, namelen);
	if (!entry)
		return ERR_PTR(-ENOENT);
	if (!S_ISDIR(entry->mode))
		return ERR_PTR(-ENOTDIR);
	return container_of(head, struct ctl_dir, header);
}

erase_header 删除 sysctl 表头

c 复制代码
static void erase_entry(struct ctl_table_header *head, const struct ctl_table *entry)
{
	struct rb_node *node = &head->node[entry - head->ctl_table].node;

	rb_erase(node, &head->parent->root);
}

static void erase_header(struct ctl_table_header *head)
{
	const struct ctl_table *entry;

	list_for_each_table_entry(entry, head)
		erase_entry(head, entry);
}

start_unregistering 开始取消注册 sysctl 表

c 复制代码
static void start_unregistering(struct ctl_table_header *p)
{
	/* 	如果必须等待,将重新获取 */
	lockdep_assert_held(&sysctl_lock);

	/*
	 * 如果 p->used 为 0,则没有人会再次触及该条目;
	 * 在放弃之前,我们将消除所有通往它的路径sysctl_lock
	 */
	 /* 检查条目是否正在使用 */
	if (unlikely(p->used)) {
		struct completion wait;
		init_completion(&wait);
		p->unregistering = &wait;
		spin_unlock(&sysctl_lock);
		wait_for_completion(&wait);
	} else {
		/* 任何非 NULL 的内容;我们永远不会取消引用它 */
		p->unregistering = ERR_PTR(-EINVAL);
		spin_unlock(&sysctl_lock);
	}

	/*
	 * 清理与该条目相关的 dcache(目录缓存)。这是为了避免命名空间中的重复名称污染缓存
	 */
	proc_sys_invalidate_dcache(p);
	/*
	 * 在没有人持有之前,不要从列表中删除它;在 do_sysctl() 中遍历列表依赖于此。
	 */
	spin_lock(&sysctl_lock);
	/* 从内核的 sysctl 表列表中移除该条目 */
	erase_header(p);
}
c 复制代码
static void put_links(struct ctl_table_header *header)
{
	struct ctl_table_set *root_set = &sysctl_table_root.default_set;
	struct ctl_table_root *root = header->root;
	struct ctl_dir *parent = header->parent;
	struct ctl_dir *core_parent;
	const struct ctl_table *entry;

	if (header->set == root_set)
		return;

	core_parent = xlate_dir(root_set, parent);
	if (IS_ERR(core_parent))
		return;

	list_for_each_table_entry(entry, header) {
		struct ctl_table_header *link_head;
		const struct ctl_table *link;
		const char *name = entry->procname;

		link = find_entry(&link_head, core_parent, name, strlen(name));
		if (link &&
		    ((S_ISDIR(link->mode) && S_ISDIR(entry->mode)) ||
		     (S_ISLNK(link->mode) && (link->data == root)))) {
			drop_sysctl_table(link_head);
		}
		else {
			pr_err("sysctl link missing during unregister: ");
			sysctl_print_dir(parent);
			pr_cont("%s\n", name);
		}
	}
}

drop_sysctl_table 删除 sysctl 表

c 复制代码
static void drop_sysctl_table(struct ctl_table_header *header)
{
	struct ctl_dir *parent = header->parent;

	if (--header->nreg)
		return;

	if (parent) {
		put_links(header);
		start_unregistering(header);
	}

	if (!--header->count)
		kfree_rcu(header, rcu);

	if (parent)
		drop_sysctl_table(&parent->header);
}

new_dir 创建新的 sysctl 目录

c 复制代码
static struct ctl_dir *new_dir(struct ctl_table_set *set,
			       const char *name, int namelen)
{
	struct ctl_table *table;
	struct ctl_dir *new;
	struct ctl_node *node;
	char *new_name;

	new = kzalloc(sizeof(*new) + sizeof(struct ctl_node) +
		      sizeof(struct ctl_table) +  namelen + 1,
		      GFP_KERNEL);
	if (!new)
		return NULL;

	node = (struct ctl_node *)(new + 1);
	table = (struct ctl_table *)(node + 1);
	new_name = (char *)(table + 1);
	memcpy(new_name, name, namelen);
	table[0].procname = new_name;
	table[0].mode = S_IFDIR|S_IRUGO|S_IXUGO;
	init_header(&new->header, set->dir.header.root, set, node, table, 1);

	return new;
}

get_subdir 查找或创建具有指定名称的子目录

c 复制代码
/**
 * get_subdir - 查找或创建具有指定名称的子目录。
 * @dir:用于创建子目录的目录
 * @name:要查找或创建的子目录的名称
 * @namelen:名称的长度
 *
 * 采用具有提升引用计数的目录,因此我们知道如果我们删除锁,该目录不会消失。
 * 成功后,引用将从 @dir 移动到返回的子目录。出错时,将返回错误代码,并且 @dir 上的引用会被简单地删除。
 */
static struct ctl_dir *get_subdir(struct ctl_dir *dir,
				  const char *name, int namelen)
{
	struct ctl_table_set *set = dir->header.set;
	struct ctl_dir *subdir, *new = NULL;
	int err;

	spin_lock(&sysctl_lock);
	subdir = find_subdir(dir, name, namelen);
	if (!IS_ERR(subdir))
		goto found;
	if (PTR_ERR(subdir) != -ENOENT)
		goto failed;

	spin_unlock(&sysctl_lock);
	new = new_dir(set, name, namelen);
	spin_lock(&sysctl_lock);
	subdir = ERR_PTR(-ENOMEM);
	if (!new)
		goto failed;

	/* 刚做创建就被删除了?再次查找 */
	subdir = find_subdir(dir, name, namelen);
	if (!IS_ERR(subdir))
		goto found;
	if (PTR_ERR(subdir) != -ENOENT)
		goto failed;

	/* 插入新子目录目. */
	err = insert_header(dir, &new->header);
	subdir = ERR_PTR(err);
	if (err)
		goto failed;
	subdir = new;
found:
	subdir->header.nreg++;
failed:
	if (IS_ERR(subdir)) {
		pr_err("sysctl could not get directory: ");
		sysctl_print_dir(dir);
		pr_cont("%*.*s %ld\n", namelen, namelen, name,
			PTR_ERR(subdir));
	}
	drop_sysctl_table(&dir->header);
	if (new)
		drop_sysctl_table(&new->header);
	spin_unlock(&sysctl_lock);
	return subdir;
}

sysctl_mkdir_p 创建或获取 ctl_table 的目录

c 复制代码
/* 找到 ctl_table 的目录。如果未找到,请创建它。 */
static struct ctl_dir *sysctl_mkdir_p(struct ctl_dir *dir, const char *path)
{
	const char *name, *nextname;

	for (name = path; name; name = nextname) {
		int namelen;
		nextname = strchr(name, '/');
		if (nextname) {
			namelen = nextname - name;
			nextname++;
		} else {
			namelen = strlen(name);
		}
		if (namelen == 0)
			continue;

		/*
		 * namelen 确保如果 name 为 "foo/bar/yay",则首先只注册 foo。我们像使用 mkdir -p 一样遍历并返回最后一个目录条目的 ctl_dir。
		 */
		dir = get_subdir(dir, name, namelen);
		if (IS_ERR(dir))
			break;
	}
	return dir;
}

xlate_dir 将 ctl_dir 翻译为 sysctl_table_root 中的目录

c 复制代码
static struct ctl_dir *xlate_dir(struct ctl_table_set *set, struct ctl_dir *dir)
{
	struct ctl_dir *parent;
	const char *procname;
   /* 如果当前目录没有父级(dir->header.parent 为 NULL),则说明它是根目录,直接返回目标集合的根目录(set->dir) */
	if (!dir->header.parent)
		return &set->dir;
   /* 递归翻译父目录 */
	parent = xlate_dir(set, dir->header.parent);
	if (IS_ERR(parent))
		return parent;
   /* 从当前目录的 ctl_table 中获取目录名称 */
	procname = dir->header.ctl_table[0].procname;
   /* 查找对应的子目录: */
	return find_subdir(parent, procname, strlen(procname));
}
c 复制代码
static bool get_links(struct ctl_dir *dir,
		      struct ctl_table_header *header,
		      struct ctl_table_root *link_root)
{
	struct ctl_table_header *tmp_head;
	const struct ctl_table *entry, *link;

	/* header 的表大小为 0 或标记为永久空 */
	if (header->ctl_table_size == 0 ||
	    sysctl_is_perm_empty_ctl_header(header))
		return true;

	/* 表中每个条目都有可用的链接吗? */
	list_for_each_table_entry(entry, header) {
		const char *procname = entry->procname;
		/* 在 dir 中查找与条目名称(procname)匹配的链接 */
		link = find_entry(&tmp_head, dir, procname, strlen(procname));
		if (!link)
			return false;
		/* 不符合预期的类型(目录或符号链接) */
		if (S_ISDIR(link->mode) && S_ISDIR(entry->mode))
			continue;
		if (S_ISLNK(link->mode) && (link->data == link_root))
			continue;
		return false;
	}

	/*检查通过了。 增加链接的注册计数 */
	list_for_each_table_entry(entry, header) {
		const char *procname = entry->procname;
		link = find_entry(&tmp_head, dir, procname, strlen(procname));
		tmp_head->nreg++;
	}
	return true;
}
c 复制代码
static struct ctl_table_header *new_links(struct ctl_dir *dir, struct ctl_table_header *head)
{
	struct ctl_table *link_table, *link;
	struct ctl_table_header *links;
	const struct ctl_table *entry;
	struct ctl_node *node;
	char *link_name;
	int name_bytes;

	name_bytes = 0;
	/* 计算链接名称所需的内存大小 */
	list_for_each_table_entry(entry, head) {
		name_bytes += strlen(entry->procname) + 1;
	}

	links = kzalloc(sizeof(struct ctl_table_header) +
			sizeof(struct ctl_node)*head->ctl_table_size +
			sizeof(struct ctl_table)*head->ctl_table_size +
			name_bytes,
			GFP_KERNEL);

	if (!links)
		return NULL;

	node = (struct ctl_node *)(links + 1);
	link_table = (struct ctl_table *)(node + head->ctl_table_size);
	link_name = (char *)(link_table + head->ctl_table_size);
	link = link_table;

	/* 创建符号链接 */
	list_for_each_table_entry(entry, head) {
		int len = strlen(entry->procname) + 1;
		memcpy(link_name, entry->procname, len);
		link->procname = link_name;
		/* 设置链接的模式为符号链接(S_IFLNK)并赋予所有权限(S_IRWXUGO) */
		link->mode = S_IFLNK|S_IRWXUGO;
		link->data = head->root;
		link_name += len;
		link++;
	}
	init_header(links, dir->header.root, dir->header.set, node, link_table,
		    head->ctl_table_size);
	links->nreg = head->ctl_table_size;

	return links;
}
c 复制代码
static int insert_links(struct ctl_table_header *head)
{
	struct ctl_table_set *root_set = &sysctl_table_root.default_set;
	struct ctl_dir *core_parent;
	struct ctl_table_header *links;
	int err;

   /* 如果当前表属于根集合(root_set),则无需插入链接,直接返回 */
	if (head->set == root_set)
		return 0;

   /* 将当前表的父目录翻译为根集合中的对应目录 */
	core_parent = xlate_dir(root_set, head->parent);
	if (IS_ERR(core_parent))
		return 0;

   /* 检查是否已有链接 */
	if (get_links(core_parent, head, head->root))
		return 0;

	core_parent->header.nreg++;
	spin_unlock(&sysctl_lock);

   /* 为当前表创建新的链接 */
	links = new_links(core_parent, head);

	spin_lock(&sysctl_lock);
	err = -ENOMEM;
	if (!links)
		goto out;

	err = 0;
	if (get_links(core_parent, head, head->root)) {
		kfree(links);
		goto out;
	}

	err = insert_header(core_parent, links);
	if (err)
		kfree(links);
out:
	drop_sysctl_table(&core_parent->header);
	return err;
}

insert_header 用于将一个 ctl_table_header 插入到指定的 ctl_dir 目录

c 复制代码
static int insert_header(struct ctl_dir *dir, struct ctl_table_header *header)
{
	const struct ctl_table *entry;
	struct ctl_table_header *dir_h = &dir->header;
	int err;


	/* 检查目标目录是否为永久空目录 */
	if (sysctl_is_perm_empty_ctl_header(dir_h))
		return -EROFS;

	/*检查插入的表是否会创建永久空目录: */
	if (sysctl_is_perm_empty_ctl_header(header)) {
      /* 通过 RB_EMPTY_ROOT 检查红黑树是否为空 */
		if (!RB_EMPTY_ROOT(&dir->root))
			return -EINVAL;
      /* 更新目录状态 */
		sysctl_set_perm_empty_ctl_header(dir_h);
	}

	dir_h->nreg++;
	header->parent = dir;
   /* 插入表的链接 */
	err = insert_links(header);
	if (err)
		goto fail_links;
   /* 遍历表中的每个条目,并调用 insert_entry 插入条目 */
	list_for_each_table_entry(entry, header) {
		err = insert_entry(header, entry);
		if (err)
			goto fail;
	}
	return 0;
fail:
	erase_header(header);
	put_links(header);
fail_links:
	if (header->ctl_table == sysctl_mount_point)
		sysctl_clear_perm_empty_ctl_header(dir_h);
	header->parent = NULL;
	drop_sysctl_table(dir_h);
	return err;
}

sysctl_check_table: 校验Sysctl表定义的正确性

此函数是Linux sysctl子系统的一个内部辅助函数, 它不执行任何注册操作, 而是扮演一个至关重要的**"校验器"或"静态分析器"**的角色。它的核心原理是: 在将一个由驱动程序定义的ctl_table结构体数组正式注册到/proc/sys之前, 对其进行一系列的健全性检查(sanity check), 以捕捉常见的编程错误, 从而防止这些错误在运行时引发内核崩溃或未定义行为。

这是一种典型的防御性编程实践, 它极大地增强了内核的健壮性。当开发者在自己的驱动中定义一个新的内核可调参数时, 这个函数就像一个"语法警察", 确保其定义符合sysctl框架的基本要求。

c 复制代码
/*
 * 静态函数声明: sysctl_check_table
 * 检查一个 ctl_table_header 中包含的表是否有效.
 * @path: sysctl条目的路径, 主要用于打印更友好的错误信息.
 * @header: 指向包含待检查表的表头的指针.
 * @return: 返回一个错误掩码, 0表示没有错误, 非0表示有错误.
 */
static int sysctl_check_table(const char *path, struct ctl_table_header *header)
{
	/* entry: 用于遍历表中每个条目的指针. */
	const struct ctl_table *entry;
	/* err: 用于累积错误标志的整数. */
	int err = 0;

	/*
	 * list_for_each_table_entry 是一个自定义宏, 用于遍历 header 中管理的所有 ctl_table 条目.
	 */
	list_for_each_entry(entry, header) {
		/*
		 * 检查1: procname (文件名) 是否为NULL.
		 * 每个sysctl条目都必须有一个在/proc/sys下对应的文件名.
		 */
		if (!entry->procname)
			err |= sysctl_err(path, entry, "procname is null");

		/*
		 * 检查2: 如果使用了内核提供的标准"处理函数"(handler),
		 *          那么必须提供它们所需要的数据和长度信息.
		 * 这些标准handler是通用的, 它们不知道要读写哪个内核变量,
		 * 必须通过 entry->data 和 entry->maxlen 来告知.
		 */
		if ((entry->proc_handler == proc_dostring) ||
		    (entry->proc_handler == proc_dobool) ||
		    (entry->proc_handler == proc_dointvec) ||
		    (entry->proc_handler == proc_douintvec) ||
		    (entry->proc_handler == proc_douintvec_minmax) ||
		    (entry->proc_handler == proc_dointvec_minmax) ||
		    (entry->proc_handler == proc_dou8vec_minmax) ||
		    (entry->proc_handler == proc_dointvec_jiffies) ||
		    (entry->proc_handler == proc_dointvec_userhz_jiffies) ||
		    (entry->proc_handler == proc_dointvec_ms_jiffies) ||
		    (entry->proc_handler == proc_doulongvec_minmax) ||
		    (entry->proc_handler == proc_doulongvec_ms_jiffies_minmax)) {
			/* 2a: 检查 entry->data 指针是否为NULL. 如果是, handler将无的放矢. */
			if (!entry->data)
				err |= sysctl_err(path, entry, "No data");
			/* 2b: 检查 entry->maxlen (数据最大长度) 是否为0. 这对于防止缓冲区溢出至关重要. */
			if (!entry->maxlen)
				err |= sysctl_err(path, entry, "No maxlen");
			else
				/* 2c: 对数组类型的handler进行更深入的检查. */
				err |= sysctl_check_table_array(path, entry);
		}

		/*
		 * 检查3: 检查 proc_handler (处理函数) 本身是否为NULL.
		 * 每个sysctl文件都必须有一个函数来处理读写操作.
		 */
		if (!entry->proc_handler)
			err |= sysctl_err(path, entry, "No proc_handler");

		/*
		 * 检查4: 检查文件模式(权限位)是否合法.
		 * (S_IRUGO|S_IWUGO) 是所有合法的读/写权限位的并集.
		 * (entry->mode & MASK) != entry->mode 这个表达式可以检测出 entry->mode 中是否
		 * 含有 MASK 中没有的位, 例如执行位(S_IXUGO)或SUID位等, 这些对于sysctl文件是非法的.
		 */
		if ((entry->mode & (S_IRUGO|S_IWUGO)) != entry->mode)
			err |= sysctl_err(path, entry, "bogus .mode 0%o",
				entry->mode);
	}
	/* 返回所有检查结果的错误掩码. 如果没有错误, err将保持为0. */
	return err;
}

Sysctl 表注册: 在/proc/sys中创建内核参数文件

此代码片段展示了Linux内核sysctl子系统的核心注册机制。sysctl是内核提供的一种标准接口, 允许用户空间通过/proc/sys/下的虚拟文件系统在运行时读取和修改内核的可调参数

这段代码的根本原理是: 它提供了一套API, 能够将驱动程序中定义的一个C语言数据结构(struct ctl_table数组)动态地翻译并注册成/proc/sys/目录下的一个文件和目录层次结构。 这一过程涉及内存分配、目录创建、并发访问控制以及将文件操作(读/写)链接到驱动程序提供的特定处理函数上。


register_sysctl_sz: 便捷的API封装

这是一个暴露给内核其他模块使用的标准API函数。它是一个简单的封装, 目的是为了方便驱动开发者使用。

c 复制代码
/**
 * register_sysctl_sz - 注册一个 sysctl 表
 * @path: sysctl 表所在的目录路径. 如果路径不存在, 我们会为你创建它.
 * @table: 表结构. 调用者必须确保 @table 的生命周期在使用 sysctl 期间保持有效.
 *         在调用 unregister_sysctl_table() 之前决不能释放它.
 * @table_size: 表中的元素数量.
 *
 * 注册一个 sysctl 表. @table 应该是一个已填充的 ctl_table 数组.
 *
 * 更多细节请参见 __register_sysctl_table.
 */
struct ctl_table_header *register_sysctl_sz(const char *path, const struct ctl_table *table,
					    size_t table_size)
{
	/*
	 * 此函数是 __register_sysctl_table 的一个简单封装.
	 * 它总是将 sysctl 表注册到内核的"默认集合" (sysctl_table_root.default_set) 中.
	 * "集合"是支持用户命名空间(user namespace)的机制, 允许多个独立的sysctl树存在.
	 * 对于绝大多数驱动程序来说, 它们总是操作默认集合.
	 */
	return __register_sysctl_table(&sysctl_table_root.default_set,
					path, table, table_size);
}
/* 将此API导出, 使其对内核模块可用. */
EXPORT_SYMBOL(register_sysctl_sz);

/**
 * register_sysctl_mount_point() - registers a sysctl mount point
 * @path: path for the mount point
 *
 * Used to create a permanently empty directory to serve as mount point.
 * There are some subtle but important permission checks this allows in the
 * case of unprivileged mounts.
 */
struct ctl_table_header *register_sysctl_mount_point(const char *path)
{
	return register_sysctl_sz(path, sysctl_mount_point, 0);
}
EXPORT_SYMBOL(register_sysctl_mount_point);

__register_sysctl_table: 核心实现

这是实际执行所有注册工作的核心函数。它的执行流程是一个精心设计的、保证了并发安全和错误恢复的序列。

在STM32H750 (ARMv7M, 单核)上的情况:

这段代码是完全独立于硬件体系结构的内核核心代码, 其逻辑和原理在STM32H750上与在任何其他平台上完全相同。

  • 用途 : 即使在嵌入式系统中, sysctl也极其有用。它提供了一个无需重新编译固件就能在运行时调试和调整 内核行为的强大方法。例如, 一个传感器驱动可以通过/proc/sys/dev/sensor/polling_interval文件暴露其轮询间隔, 允许工程师在现场进行动态调整。
  • 并发控制 : spin_lock(&sysctl_lock)在单核系统上仍然至关重要。如果内核是抢占式的(CONFIG_PREEMPT), 一个正在修改全局sysctl树的任务可能会被另一个也想访问该树的任务抢占。spin_lock通过在临界区内禁用内核抢占, 来防止这种数据竞争, 确保了sysctl树数据结构的完整性。
  • "解锁-操作-重锁"模式 : sysctl_mkdir_p函数在创建目录时可能需要分配内存, 这是一个可能会导致当前任务睡眠的操作。在持有自旋锁时睡眠是绝对禁止的, 会导致系统死锁。因此, 代码采用了标准的"解锁->执行可能睡眠的操作->重新锁定"模式, 这是编写健壮内核代码的典范。
c 复制代码
/**
 * __register_sysctl_table - 注册一个叶子 sysctl 表
 * @set: 要在其上注册的 Sysctl 树.
 * @path: sysctl 表所在的目录路径.
 * @table: 顶层表结构. 此表在注册后不应被释放, 因此不应在栈上使用.
 *         它可以是全局的, 或由调用者动态分配并在注销后释放.
 * @table_size: 表中的元素数量.
 * ... (其他注释描述了 ctl_table 结构体的成员) ...
 * 返回: 失败时返回 NULL, 成功时返回一个指向表头的指针.
 */
struct ctl_table_header *__register_sysctl_table(
	struct ctl_table_set *set,
	const char *path, const struct ctl_table *table, size_t table_size)
{
	struct ctl_table_root *root = set->dir.header.root;
	struct ctl_table_header *header;
	struct ctl_dir *dir;
	struct ctl_node *node;

	/*
	 * 步骤1: 内存分配.
	 * 分配一块连续的内存, 用于存放表头(header)和描述表中每个条目的节点(node)数组.
	 * 这是一个优化, 减少了内存分配的次数.
	 */
	header = kzalloc(sizeof(struct ctl_table_header) +
			 sizeof(struct ctl_node)*table_size, GFP_KERNEL_ACCOUNT);
	if (!header)
		return NULL;

	/* node 指针指向紧跟在 header 结构体后面的内存区域. */
	node = (struct ctl_node *)(header + 1);
	/* 初始化 header 和 node 数组, 并检查传入的 table 是否合法. */
	init_header(header, root, set, node, table, table_size);
	if (sysctl_check_table(path, header))
		goto fail;

	/*
	 * 步骤2: 准备在sysctl树中创建目录.
	 * 在一个自旋锁保护下, 增加父目录的注册引用计数.
	 */
	spin_lock(&sysctl_lock);
	dir = &set->dir;
	dir->header.nreg++;
	spin_unlock(&sysctl_lock); /* !!关键: 立即释放锁!! */

	/*
	 * 步骤3: 创建目录.
	 * sysctl_mkdir_p 会根据 path 字符串创建所有不存在的中间目录.
	 * 此函数可能会因内存分配而睡眠, 所以必须在没有持有自旋锁的情况下调用.
	 */
	dir = sysctl_mkdir_p(dir, path);
	if (IS_ERR(dir))
		goto fail; /* 如果目录创建失败, 跳转到清理代码. */

	/*
	 * 步骤4: 原子性地插入表.
	 * 重新获取自旋锁, 以保证将新表链接到目录的操作是原子的.
	 */
	spin_lock(&sysctl_lock);
	/* insert_header 将新创建的 header 链接到目标目录 dir 的子节点列表中. */
	if (insert_header(dir, header))
		goto fail_put_dir_locked; /* 如果插入失败, 跳转到带锁的清理路径. */

	/*
	 * 插入成功后, 递减在步骤2中增加的父目录引用计数.
	 * 这是因为新表 header 自身持有了一个对父目录的引用.
	 */
	drop_sysctl_table(&dir->header);
	spin_unlock(&sysctl_lock);

	/* 成功, 返回 header 句柄, 调用者需要保存它以便将来注销. */
	return header;

fail_put_dir_locked: /* 带锁的错误处理路径 */
	drop_sysctl_table(&dir->header);
	spin_unlock(&sysctl_lock);
fail: /* 不带锁的错误处理路径 */
	kfree(header); /* 释放最初分配的内存. */
	return NULL;
}

__register_sysctl_init 将 sysctl 表注册到路径

c 复制代码
/**
 * __register_sysctl_init() - 将 sysctl 表注册到路径
 * @path:sysctl base 的路径名。如果该路径不存在,我们将创建
 * 它为你。
 * @table:这是需要注册到路径的 sysctl 表。
 * 调用方必须确保 @table 的生命周期在
 * sysctl 的终身使用。
 * @table_name:sysctl 表的名称,仅用于注册失败时的日志打印
 * @table_size:表中的元素数量
 *
 * 用户空间使用 sysctl 接口在运行时查询或修改在变量上设置的预定义值。但是,这些变量预先设置了默认值。
 * 即使 register_sysctl() 失败,依赖于这些变量的代码也将始终有效。
 * 如果 register_sysctl() 失败,你就会失去在运行时动态查询或修改 sysctl 的能力。
 * register_sysctl() 在 init 上失败的几率非常低,因此出于这两个原因,此函数不会返回任何错误,因为它被初始化代码使用。
 *
 * 上下文:如果您的基目录不存在,它将为您创建。
 */
void __init __register_sysctl_init(const char *path, const struct ctl_table *table,
				 const char *table_name, size_t table_size)
{
   /* register_sysctl_sz 是实际执行注册的函数,它返回一个指向 ctl_table_header 的指针,表示注册的结果 */
	struct ctl_table_header *hdr = register_sysctl_sz(path, table, table_size);

	if (unlikely(!hdr)) {
		pr_err("failed when register_sysctl_sz %s to %s\n", table_name, path);
		return;
	}
   /* 调用 kmemleak_not_leak 告诉内核内存泄漏检测工具(kmemleak),hdr 所指向的内存不是泄漏,避免误报 */
	kmemleak_not_leak(hdr);
}

#define register_sysctl_init(path, table)	\
	__register_sysctl_init(path, table, #table, ARRAY_SIZE(table))

proc_sys_init 初始化 proc 文件系统的 sysctl 目录

c 复制代码
int __init proc_sys_init(void)
{
	struct proc_dir_entry *proc_sys_root;

	proc_sys_root = proc_mkdir("sys", NULL);
	proc_sys_root->proc_iops = &proc_sys_dir_operations;
	proc_sys_root->proc_dir_ops = &proc_sys_dir_file_operations;
	proc_sys_root->nlink = 0;

	return sysctl_init_bases();
}

setup_sysctl_set: 初始化一个Sysctl"集合"

此函数是Linux内核sysctl子系统中一个非常底层的初始化辅助函数。它的核心作用是为一个sysctl"集合"(ctl_table_set)数据结构进行标准的、模板化的初始化

一个sysctl"集合"是sysctl框架为了支持**用户命名空间(User Namespace)**而引入的一个关键抽象。它允许多个独立的、可能包含同名文件的sysctl树并存, 而内核可以根据当前进程的上下文(即它属于哪个用户命名空间)来决定向其展示哪一个"集合"的内容。

此函数的原理可以理解为为一个新的sysctl集合对象赋予"生命"和"身份":

  1. 清零 : memset(set, 0, sizeof(*set)); 确保了结构体的所有成员都被设置为一个已知的、干净的初始状态(零或NULL)。这是一种良好的编程实践, 可以防止未初始化变量导致的随机错误。
  2. 设置可见性规则 : set->is_seen = is_seen; 这一步是为该集合赋予"身份"的核心。它将一个由调用者提供的is_seen函数指针保存到集合中。这个函数定义了**"谁能看见我"**的规则。当内核需要决定是否向某个进程展示这个集合的内容时, 就会调用这个函数。
  3. 建立根目录 : init_header(&set->dir.header, ...) 这一步为该集合创建了一个逻辑上的"根目录"。它将该集合与一个ctl_table_root关联起来, 这个root定义了整个sysctl树的通用行为(例如, 如何查找、如何检查权限)。同时, 它使用一个全局的root_table作为该集合的初始内容, 确保了即使在没有任何其他条目注册之前, 该集合也是一个结构完整的、有效的实体。

c 复制代码
/*
 * setup_sysctl_set: 初始化一个 sysctl 集合.
 * @set: 指向要被初始化的 ctl_table_set 结构体的指针.
 * @root: 指向该集合所属的 ctl_table_root 的指针. root 定义了整个sysctl树的通用行为.
 * @is_seen: 一个函数指针, 定义了判断此集合是否对当前上下文可见的规则.
 */
void setup_sysctl_set(struct ctl_table_set *set,
	struct ctl_table_root *root,
	int (*is_seen)(struct ctl_table_set *))
{
	/*
	 * 使用 memset 将 set 指向的整个 ctl_table_set 结构体的内存区域清零.
	 * 这是一个标准的初始化步骤, 确保所有成员都有一个确定的初始值 (0 或 NULL).
	 */
	memset(set, 0, sizeof(*set));

	/*
	 * 将调用者提供的 is_seen 函数指针赋值给 set 结构体的 is_seen 成员.
	 * 这个回调函数将在后续被内核用来决定此集合是否应该对当前进程可见.
	 */
	set->is_seen = is_seen;

	/*
	 * 调用 init_header 来初始化集合内部的目录头 (set->dir.header).
	 * 这相当于为这个集合创建了一个逻辑上的"根目录".
	 * - &set->dir.header: 要初始化的表头.
	 * - root: 将此集合与一个全局的sysctl树根关联起来.
	 * - set: 将表头与其所属的集合自身关联起来.
	 * - NULL, root_table, 1: 使用一个全局的、默认的根表项来初始化这个根目录,
	 *   确保它是一个有效的、非空的sysctl实体.
	 */
	init_header(&set->dir.header, root, set, NULL, root_table, 1);
}

kernel/sysctl.c

proc_dostring: 内核 sysctl 字符串处理的标准实现

本代码片段是 Linux 内核 sysctl 框架的核心部分,提供了处理字符串类型 sysctl 条目的标准、通用处理函数 proc_dostring。其核心功能是安全地在内核空间的字符串缓冲区(由 ctl_table 指定)和用户空间的缓冲区之间进行双向数据拷贝,并实现了对文件读写位置(ppos)的精细控制,同时引入了一套用于规范化写操作行为的策略机制。

实现原理分析

该实现将复杂的 VFS 文件操作抽象为一个通用的处理函数,其原理围绕着对读写路径的区分处理、对文件位置指针的策略性应用以及用户交互的健壮性设计。

  1. 写操作策略 (proc_first_pos_non_zero_ignore):

    • 内核引入了一个全局配置 sysctl_writes_strict,用于定义当用户尝试从非零位置(即非文件开头)写入 sysctl 文件时的行为。这旨在纠正一个历史遗留问题:用户可能无意中通过 echo "val" >> /proc/sys/... 这样的追加操作来修改配置,导致非预期的结果。
    • proc_first_pos_non_zero_ignore 函数是这个策略的执行者。
      • SYSCTL_WRITES_STRICT (严格模式) : 如果 ppos 非零,函数返回 true。这意味着调用者(通常是处理数字的 sysctl handler)应该拒绝这次写操作。
      • SYSCTL_WRITES_WARN (警告模式) : 如果 ppos 非零,函数调用 warn_sysctl_write 打印一条一次性的内核警告,然后返回 false,允许写操作继续。
      • 默认模式: 行为同警告模式,但不会打印警告。
    • proc_dostring 本身虽然调用了此函数(主要为了触发警告),但其内部实现 _proc_do_string 选择了自己的方式来处理 ppos,对于字符串类型,它倾向于忽略非零的写位置,以保证健壮性。
  2. 核心读写逻辑 (_proc_do_string):

    • 写路径 (write = 1) :
      a. 位置处理 : 在严格模式下,它会检查 ppos 是否超出现有字符串的末尾,如果是则直接返回,防止追加。否则,它将从 ppos 指定的位置开始覆盖。在非严格模式下,它会强制忽略 ppos,总是从缓冲区的起始位置 (len = 0) 开始写,确保每次写操作都是一次完全的覆盖。
      b. 数据拷贝 : 它从用户缓冲区逐字节地拷贝数据到内核缓冲区,直到遇到用户输入的 \0\n,或者达到用户指定的长度或内核缓冲区的最大长度。这种设计使得用户通过 echo 命令写入时,最后的换行符不会被计入配置值。
      c. 安全终止 : 拷贝结束后,它总是在内核缓冲区的末尾写入一个 \0,确保内核中的字符串始终是有效的 C 字符串。
    • 读路径 (write = 0) :
      a. 位置处理 : 它正确地实现了对 ppos 的支持,允许用户从文件的任意位置开始读取。如果 ppos 已经超出了字符串的实际长度,它会返回 0 字节,这符合标准的文件读写行为。
      b. 数据拷贝 : 它使用 memcpy 进行高效的块拷贝,将内核缓冲区中从 ppos 开始的数据拷贝到用户缓冲区。
      c. 用户体验优化 : 如果拷贝的数据没有填满用户的缓冲区(len < *lenp),它会在数据的末尾追加一个 \n 换行符。这使得用户在使用 cat /proc/sys/... 等命令时,能够得到格式美观、以换行符结尾的输出,体验与读取普通文本文件完全一致。

代码分析

c 复制代码
/**
 * @brief sysctl 字符串处理的核心实现。
 * @param data 指向内核空间的字符串缓冲区。
 * @param maxlen 内核缓冲区的最大长度。
 * @param write 非0表示写操作,0表示读操作。
 * @param buffer 指向用户空间的缓冲区。
 * @param lenp 指向用户缓冲区长度的指针,返回时被更新为实际处理的字节数。
 * @param ppos 文件的读写位置指针。
 * @return 总是返回0。
 */
static int _proc_do_string(char *data, int maxlen, int write,
		char *buffer, size_t *lenp, loff_t *ppos)
{
	size_t len;
	char c, *p;

	// 如果输入无效,则清零长度并返回。
	if (!data || !maxlen || !*lenp) {
		*lenp = 0;
		return 0;
	}

	if (write) {
		// --- 写操作路径 ---
		if (sysctl_writes_strict == SYSCTL_WRITES_STRICT) {
			// 在严格模式下,只允许从文件开头开始的覆盖写。
			len = strlen(data);
			if (len > maxlen - 1) len = maxlen - 1;
			if (*ppos > len) return 0; // 不允许追加写。
			len = *ppos;
		} else {
			// 在非严格(兼容)模式下,总是从缓冲区开头开始写,忽略 ppos。
			len = 0;
		}

		*ppos += *lenp; // 更新文件位置
		p = buffer;
		// 逐字节从用户缓冲区拷贝,直到遇到\0或\n,或达到长度限制。
		while ((p - buffer) < *lenp && len < maxlen - 1) {
			c = *(p++);
			if (c == 0 || c == '\n')
				break;
			data[len++] = c;
		}
		data[len] = 0; // 确保内核字符串以 NULL 结尾。
	} else {
		// --- 读操作路径 ---
		len = strlen(data);
		if (len > maxlen) len = maxlen;

		// 如果读的位置已经超过字符串末尾,则返回0字节。
		if (*ppos > len) {
			*lenp = 0;
			return 0;
		}

		// 根据 ppos 调整要读取的数据指针和长度。
		data += *ppos;
		len  -= *ppos;

		// 确保不超过用户请求的长度。
		if (len > *lenp)
			len = *lenp;
		if (len)
			memcpy(buffer, data, len); // 使用 memcpy 高效拷贝数据。
		// 如果用户缓冲区有剩余空间,则追加一个换行符以改善用户体验。
		if (len < *lenp) {
			buffer[len] = '\n';
			len++;
		}
		*lenp = len; // 更新返回的长度。
		*ppos += len; // 更新文件位置。
	}
	return 0;
}

/**
 * @brief 当用户从非零位置写入 sysctl 时打印警告。
 */
static void warn_sysctl_write(const struct ctl_table *table)
{
	pr_warn_once("%s wrote to %s when file position was not 0!\n"
		"This will not be supported in the future. To silence this\n"
		"warning, set kernel.sysctl_writes_strict = -1\n",
		current->comm, table->procname);
}

/**
 * @brief 检查首次写入的位置是否非零,并根据策略决定是否应忽略。
 * @return 如果在严格模式下位置非零,则返回 true (表示应忽略写操作)。
 */
static bool proc_first_pos_non_zero_ignore(loff_t *ppos,
					   const struct ctl_table *table)
{
	if (!*ppos) return false; // 位置为0,总是允许。

	switch (sysctl_writes_strict) {
	case SYSCTL_WRITES_STRICT:
		return true; // 严格模式:禁止非零位置写入。
	case SYSCTL_WRITES_WARN:
		warn_sysctl_write(table); // 警告模式:打印警告但允许。
		return false;
	default:
		return false; // 其他模式:允许。
	}
}

/**
 * @brief 处理字符串类型 sysctl 的读写操作(标准 handler)。
 * @param table 指向 sysctl 表项的指针。
 * @param write 非0表示写操作,0表示读操作。
 * @param buffer 用户空间缓冲区。
 * @param lenp 用户缓冲区长度指针。
 * @param ppos 文件读写位置指针。
 * @return 成功返回0。
 */
int proc_dostring(const struct ctl_table *table, int write,
		  void *buffer, size_t *lenp, loff_t *ppos)
{
	if (write)
		// 对于写操作,首先调用策略检查函数(主要为了触发警告)。
		proc_first_pos_non_zero_ignore(ppos, table);

	// 调用核心实现函数完成实际的读写。
	return _proc_do_string(table->data, table->maxlen, write, buffer, lenp,
			ppos);
}
相关推荐
CS_浮鱼1 小时前
【Linux】进程信号
linux·运维·服务器
Thexhy1 小时前
CentOS快速安装DockerCE指南
linux·docker
专注于大数据技术栈1 小时前
java学习--==和equals
java·python·学习
路人甲ing..2 小时前
Android Studio 快速的制作一个可以在 手机上跑的app
android·java·linux·智能手机·android studio
code monkey.3 小时前
【Linux之旅】深入 Linux Ext 系列文件系统:从磁盘物理结构到软硬链接的底层逻辑
linux·文件系统·ext2
RoboWizard5 小时前
高性能电脑热战寒冬 11月DIY配置推荐
linux·运维·服务器·电脑·金士顿
zl9798998 小时前
RabbitMQ-下载安装与Web页面
linux·分布式·rabbitmq
kitty_hi10 小时前
mysql主从配置升级,从mysql5.7升级到mysql8.4
linux·数据库·mysql·adb
智商低情商凑10 小时前
Go学习之 - Goroutines和channels
开发语言·学习·golang