sysctl 接口
sysctl 系统调用是 Linux 内核一个有趣的特性;在 Unix 世界中它非常独特。该系统调用导出了微调内核参数的能力,并与 /proc 文件系统紧密结合,/proc 文件系统是一个更简单的、基于文件的接口,可用于执行与系统调用相同的任务。sysctl 出现在内核 1.3.57 版本中,并从此得到完全支持。本文解释了如何在 2.0.0 到 2.1.35 之间的任何内核中使用 sysctl。
在运行 Unix 内核时,系统管理员经常需要根据其特定需求微调一些底层特性。通常,系统定制需要您重新构建内核镜像并重启计算机。这些任务耗时较长,需要良好的技能和一点运气才能成功完成。Linux 开发者偏离了这种方法,选择使用可变参数代替硬编码的常量;运行时配置可以通过使用 sysctl 系统调用或更轻松地利用 /proc 文件系统来执行。sysctl 的内部设计不仅可以读取和修改配置参数,还可以支持动态设置此类变量。换句话说,模块编写者可以在 sysctl 树中插入新条目,并允许运行时配置驱动程序特性。
大多数 Linux 用户都熟悉 /proc 文件系统。简而言之,该文件系统可以被认为是内核内部的网关:其文件是某些内核信息的入口点。此类信息通常以文本形式交换,以方便交互使用,尽管在需要时交换可以涉及二进制数据。二进制 /proc 文件的典型示例是 /proc/kcore,它是一个代表当前内核的 core 文件。因此,您可以执行命令
gdb /usr/src/linux/vmlinux /proc/kcore
并窥探您正在运行的内核。当然,如果 vmlinux 是使用 -g 编译器选项编译的,则在 /proc/kcore 上使用 gdb 会产生更好的结果。
大多数 /proc 文件是只读的:写入它们没有任何效果。例如,这适用于 /proc/interrupts、/proc/ioports、/proc/net/route 和所有其他信息节点。另一方面,目录 /proc/sys 的行为有所不同;它是与系统控制相关的文件树的根目录。/proc/sys 中的每个子目录都处理一个内核子系统,例如 net/ 和 vm/,而 kernel/ 子目录是特殊的,因为它包含内核范围的参数,例如文件 kernel/hostname。
每个 sysctl 文件都包含数值或字符串值——有时是单个值,有时是它们的数组。例如,如果您转到 /proc/sys 目录并给出命令
grep . kernel/*
内核 2.1.32 返回类似于以下内容的数据
kernel/ctrl-alt-del:0 kernel/domainname:systemy.it kernel/file-max:1024 kernel/file-nr:128 kernel/hostname:morgana kernel/inode-max:3072 kernel/inode-nr:384 263 kernel/osrelease:2.1.32 kernel/ostype:Linux kernel/panic:0nn kernel/printk:6 4 1 7 kernel/securelevel:0 kernel/version:#9 Mon Apr 7 23:08:18 MET DST 1997值得强调的是,使用 less 读取 /proc 项目不起作用,因为对于 stat 系统调用,它们显示为零长度文件,而 less 在读取文件之前会检查文件的属性。stat 的不准确性是 /proc 的一个特性,而不是错误。这是人力资源(在编写代码方面)和内核大小(在携带代码方面)的节省。stat 信息对于大多数文件来说是完全不相关的,因为 cat、grep 和所有其他工具都可以正常工作。如果您确实需要使用 less 来查看 /proc 文件的内容,您可以求助于
cat如果您想更改系统参数,您只需将新值写入 /proc/sys 中的正确文件即可。如果文件包含值数组,它们将按顺序被覆盖。让我们以 kernel/printk 文件为例。printk 首次在内核版本 2.1.32 中引入。/proc/sys/kernel/printk 中的四个数字控制 printk 内核函数的“verbosity”级别。数组中的第一个数字是 console_loglevel:优先级小于或等于指定值的内核消息将打印到系统控制台(即,活动的虚拟控制台,除非您已更改它)。此参数不影响 klogd 的操作,无论如何 klogd 都会接收所有消息。以下命令显示如何更改日志级别
# cat kernel/printk 6 4 1 7 # echo 8 > kernel/printk # cat kernel/printk 8 4 1 7级别 8 对应于调试消息,默认情况下调试消息不会打印在控制台上。上面显示的示例会话更改了默认行为,以便打印每条消息,包括调试消息。
类似地,您可以通过将新值写入 /proc/kernel/hostname 来更改主机名——如果 hostname 命令不可用,这是一个有用的功能。
即使 /proc 文件系统是一个很好的资源,但它并非总是内核中可用的。由于它对于系统操作不是至关重要的,因此有时您会选择将其从内核镜像中排除,或者只是不挂载它。例如,在构建嵌入式系统时,节省 40 到 50KB 可能是有利的。此外,如果您担心安全性,您可能会决定通过不挂载 /proc 来隐藏系统信息。
内核调优的系统调用接口,即 sysctl,是窥探可配置参数并修改它们的另一种方法。sysctl 的一个优点是它更快,因为它不涉及 fork/exec(即,不生成外部程序),也不涉及任何目录查找。但是,除非您运行的是旧平台,否则性能节省是无关紧要的。
要在 C 程序中使用系统调用,必须包含头文件 sys/sysctl.h;它将 sysctl 函数声明为
int sysctl (int *name, int nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen);
如果您的标准库不是最新的,则 sysctl 函数既不会在头文件中原型化,也不会在库中定义。我不太清楚库函数最初是在何时引入的,但我知道 libc-5.0 没有它,而 libc-5.3 有。如果您有一个旧库,您必须直接调用系统调用,使用如下代码
#include <linux/unistd.h> #include <linux/sysctl.h> /* now "_sysctl(struct __sysctl_args *args)" can be called */ _syscall1(int, _sysctl, struct __sysctl_args *, args);系统调用获取一个参数而不是六个参数,并且通过在系统调用名称前面加上下划线来解决原型中的不匹配。因此,系统调用是 _sysctl 并获取一个参数,而库函数是 sysctl 并获取六个参数。本文中介绍的示例代码使用库函数。
sysctl 库函数的六个参数具有以下含义
name 指向一个整数数组:每个整数值标识一个 sysctl 项,可以是目录或叶节点文件。此类值的符号名称在文件 linux/sysctl.h 中定义。
nlen 说明数组 name 中列出了多少个整数。要访问特定条目,您需要指定通过子目录的路径,因此您需要指定此路径的长度。
oldval 是指向数据缓冲区的指针,必须在其中存储 sysctl 项的旧值。如果它是 NULL,则系统调用不会将值返回给用户空间。
oldlenp 指向一个整数,该整数说明 oldval 缓冲区的长度。系统调用会更改该值以反映已写入多少数据,这可能小于缓冲区长度。
newval 指向一个数据缓冲区,该缓冲区托管替换数据。内核将读取此缓冲区以更改正在操作的 sysctl 条目。如果它是 NULL,则内核值不会更改。
newlen 是 newval 的长度。内核将从 newval 读取不超过 newlen 个字节。
现在,让我们编写一些 C 代码来访问 /proc/sys/kernel/printk 中包含的四个参数。该文件的数字名称是 KERN_PRINTK,位于目录 CTL_KERN/ 中(这两个符号都在 linux/sysctl.h 中定义)。清单 1 pkparms.c 中显示的代码是访问这些值的完整程序。
更改 sysctl 值类似于读取它们——只需使用 newval 和 newlen。一个类似于 pkparms.c 的程序可以用于更改控制台日志级别,即 kernel/printk 中的第一个数字。该程序名为 setlevel.c,其核心代码如下所示
int newval[1]; int newlen = sizeof(newval); /* assign newval[0] */ error = sysctl (name, namelen, NULL /* oldval */, 0 /* len */, newval, newlen);
该程序仅覆盖内核条目的前 sizeof(int) 个字节,这正是我们想要的。
请记住,printk 参数未导出到内核 2.0 版本的 sysctl 中。由于缺少 KERN_PRINTK 符号,这些程序在 2.0 版本下将无法编译;此外,如果您针对更高版本编译其中任何一个程序,然后在 2.0 版本下运行,则在调用 sysctl 时会收到错误。
pkparms.c、setlevel.c 和 hname.c(稍后介绍)的源文件位于 2365.tgz1 文件中。
上面介绍的两个程序的简单运行如下所示
# ./pkparms len is 16 bytes 6 4 1 7 # cat /proc/sys/kernel/printk 6 4 1 7 # ./setlevel 8 # ./pkparms len is 16 bytes 8 4 1 7
如果您运行的是内核 2.0,请不要失望——作用于 kernel/printk 的文件只是示例,相同的代码可以用于访问 2.0 内核中可用的任何 sysctl 项,只需进行最小的修改。
在同一个 ftp 站点上,您还会找到 hname.c,这是一个基于 sysctl 的精简版 hostname 命令。该源代码适用于 2.0 内核,并演示了如何在没有库支持的情况下调用系统调用,因为我的 Linux-2.0 在基于 libc-5.0 的 PC 上运行。
尽管内核的可调参数是底层的,但它们非常有趣,可以进行调整,并且可以帮助优化 Linux 在不同环境中的系统性能。
以下列表概述了 /proc/sys 中一些相关的 /kernel 和 /vm 文件。(此信息适用于 2.0 到 2.1.35 的所有内核。)
kernel/panic - 整数值是系统在系统崩溃时自动重启之前等待的秒数。值为 0 表示“禁用”。自动重启是对于无人值守系统来说很有趣的功能。命令行选项 panic=value 可用于在启动时设置此参数。
kernel/file-max - 系统中打开文件的最大数量。另一方面,file-nr 是每个进程的最大值,并且无法修改,因为它受到硬件页面大小的限制。inodes 也存在类似的条目:系统范围的条目和不可变的每个进程的条目。具有许多进程和许多打开文件的服务器可能会受益于增加这两个条目的值。
kernel/securelevel - 这是系统中安全功能的钩子。securelevel 文件当前是只读的,即使对于 root 用户也是如此,因此只能通过程序代码(例如,模块)更改。只有 EXT2 文件系统使用 securelevel——如果 securelevel 大于 0,它会拒绝更改文件标志(例如 immutable 和 append-only)。这意味着,预编译了非零 securelevel 且不支持模块的内核可用于保护珍贵文件免受网络入侵情况下的损坏。但请继续关注 securelevel 的新功能。
vm/freepages - 包含三个数字,都是空闲页面的计数。第一个数字是系统中的最小可用空间。需要空闲页面来满足原子分配请求,例如传入的网络数据包。第二个数字是开始重度交换的级别,第三个是开始轻度交换的级别。具有高带宽的网络服务器受益于更高的数字,以避免由于可用内存短缺而丢弃数据包。默认情况下,保留百分之一的内存为空闲。
vm/bdflush - 此文件中的数字可以微调缓冲区缓存的行为。它们记录在 fs/buffer.c 中。
vm/kswapd - 此文件存在于所有 2.0.x 内核中,但在 2.1.33 中已删除,因为它没有用。可以安全地忽略它。
vm/swapctl - 这个大文件包含了用于微调交换算法的所有参数。这些字段在 include/linux/swapctl.h 中列出,并在 mm/swap.c 中使用。
模块编写者可以通过使用编程接口扩展控制树,轻松地将他们自己的可调功能添加到 /proc/sys 中。内核向模块导出以下两个函数
struct ctl_table_header * register_sysctl_table(ctl_table * table, int insert_at_head); void unregister_sysctl_table( struct ctl_table_header * table);
前一个函数用于注册条目的“表”,并返回一个令牌,后一个函数使用该令牌来分离(注销)您的表。参数 insert_at_head 告知新表必须插入在其他表之前还是之后,您可以轻松地忽略该问题并指定 0,这意味着“不在头部”。
什么是 ctl_table 类型?它是一个由以下字段组成的结构
int ctl_name - 这是一个数字 ID,在每个表中都是唯一的。
const char *procname - 如果条目必须通过 /proc 可见,则这是相应的名称。
void *data - 指向数据的指针。例如,对于整数项,它将指向一个整数值。
int maxlen - 前一个字段指向的数据的大小;例如,sizeof(int)。
mode_t mode - 文件的模式。目录应启用可执行位(例如,0555 八进制)。
ctl_table *child - 对于目录,这是子表。对于叶节点,为 NULL。
proc_handler *proc_handler - 处理程序负责执行由 /proc 文件生成的任何读/写操作。如果该项目没有 procname,则不使用此字段。
ctl_handler *strategy - 当使用系统调用时,此处理程序读取/写入数据。
struct proc_dir_entry *de - 内部使用。
void *extra1, *extra2 - 这些字段是在版本 1.3.69 中引入的,用于为特定处理程序指定额外信息。例如,内核有一个用于整数向量的处理程序,该处理程序使用 extra 字段来通知每个数组中允许的最小值和最大值。
好吧,前面的列表可能吓坏了大多数读者。因此,我不会显示处理函数的原型,而是直接切换到一些示例代码。编写代码比理解代码容易得多,因为您可以从复制现有文件中的行开始。结果代码将属于 GPL——当然,我不认为这是一个缺点。
让我们编写一个带有两个整数参数(名为 ontime 和 offtime)的模块。该模块将忙循环几个定时器滴答,并休眠更多时间;这些参数控制每个状态的持续时间。是的,这确实很傻,但这是我可以想象到的最简单的独立于硬件的示例。
这些参数将放在 /proc/sys/kernel/busy 中,这是一个新目录。为此,我们需要注册如图 1 所示的树。/kernel 目录不会由 register_sysctl_table 创建,因为它已经存在。此外,它不会在 unregister 时删除,因为它仍然有活动的子文件;因此,通过指定整个目录树,您可以将文件添加到 /proc/sys 中的每个目录。
清单 2 是 busy.c 的有趣部分,它完成了与 sysctl 相关的所有工作。这里的技巧是将所有繁重的工作留给 proc_dointvec 和 sysctl_intvec。这些处理程序仅由内核的 2.1.8 及更高版本导出,因此在为旧内核编译时,您需要将它们复制到您的模块中(或实现类似的东西)。
我不会在此处显示与忙循环相关的代码,因为它完全超出了本文的范围。一旦您从 FTP 站点 1 下载了源代码,就可以在您自己的系统上编译它。它适用于 Intel、Alpha 和 SPARC 平台上的 2.0 和 2.1 版本。
尽管 sysctl 非常有用,但很难找到文档。对于习惯于查看源代码以提取信息的系统程序员来说,这并不是问题。sysctl 内部的主要入口点是 kernel/sysctl.c 和 net/sysctl_net.c。sysctl 表中的大多数项仅作用于字符串或整数数组。因此,要在整个源代码树中搜索一个项目,您最终将使用 data 字段作为 grep 的参数。我看不到这种方法的捷径。
例如,让我们跟踪 /proc/sys/net/ipv4 中 ip_log_martians 的含义。您首先会发现 sysctl_net.c 引用了 ipv4_table,而 ipv4_table 又由 sysctl_net_ipv4.c 导出。该文件又在其表中包含以下条目
{NET_IPV4_LOG_MARTIANS, "ip_log_martians", &ipv4_config.log_martians, sizeof(int), 0644, NULL, &proc_dointvec},
因此,理解我们的控制文件的作用就简化为在整个源代码中查找字段 ipv4config.log_martians。一些 grepping 将显示该字段用于控制此主机接收到的错误数据包的详细报告(通过 printk)。
不幸的是,许多系统管理员不是程序员,需要其他信息来源。为了他们的利益,内核开发人员有时会编写一些文档,作为编写代码的休息,并且此文档与内核源代码一起分发。坏消息是,sysctl 在设计上是相当新的,并且此类额外文档几乎不存在。
文件 Documentation/networking/Configurable 是 sysctl 的简短介绍(比本文短得多),并指向 net/TUNABLE,而 net/TUNABLE 又是一个巨大的网络子树中可配置参数列表。不幸的是,每个项目的描述都非常技术性,因此不了解网络细节的人无法熟练地调整网络参数。在我写作时,如果您不算 C 源文件,则此文件是关于系统控制的唯一信息来源。
Alessandro Rubini 阅读电子邮件 rubini@linux.it,并喜欢培育橡树和玩内核代码。他目前正在寻找这两个领域的工作。