【kernel】从 /proc/sys/net/ipv4/ip_forward 参数看如何玩转 procfs 内核 ...

打印 上一主题 下一主题

主题 895|帖子 895|积分 2685

本文的开篇,我们先从 sysctl 这个命令开始。
sysctl 使用

sysctl 是一个 Linux 体系工具,背景实际上是 syscall,它允许用户查看和动态修改内核参数。
  1. # 查看当前设置的所有内核参数
  2. sysctl -a
  3. # 查看特定参数的值
  4. sysctl net.ipv4.conf.all.forwarding
  5. # 临时修改内核参数
  6. sysctl net.ipv4.conf.all.forwarding=1
  7. # 重新加载配置文件,默认是 /etc/sysctl.conf
  8. sysctl -p
复制代码
修改 sysctl 的三种方式:
1)sysctl 命令直接修改(重启后失效)
2)echo 1 > /proc/sys/net/ipv4/ip_forward (重启后失效)
3)vim /etc/sysctl.conf,手动加入,sysctl -p 重新加载(永久收效)
到这里,实际上可以给出一个结论:这几种方式,在原理上,都直接或间接更改了 Linux 中 /proc 文件体系下面的 /proc/sys/net/ipv4/ip_forward 文件。
那么,/proc 文件体系下的文件是如何影响到内核参数的?我们以 ip_forward 参数为例,来追踪一下。
ip_forward 参数

这个参数是内核 ip 报文转发开关。
这个参数有 2 个开关(ipv4 为例,ipv6 同理):
  1. 1 - /proc/sys/net/ipv4/ip_forward
  2. 2 - /proc/sys/net/ipv4/conf/=={all/default/enp8s0}==/forwarding
复制代码
有几条规则:
1)/proc/sys/net/ipv4/ip_forward 等价于 /proc/sys/net/ipv4/conf/all/forwarding。
可以验证,设置 sysctl net.ipv4.conf.all.forwarding=1 后,查看这两个值:
2)实际真正控制网卡启用 ip 转发的,是网卡对应的 forwarding 参数:/proc/sys/net/ipv4/conf/enp8s0/forwarding。
3)对于新创建的网卡设备,会启用 default/forwarding 参数来配置:/proc/sys/net/ipv4/conf/default/forwarding。
4)conf/all/forwarding
可以配置当前全部设备,比方将 all 参数配置从 0 修改为 1,则包括 default 在内的全部 forwarding 配置都将被改成 1。要注意的是 all 配置只有在值被修改时才有用,重复写入 all 当前值不会对其他 forwarding 配置产生任何影响。
5)all/forwarding
配置只对当前 net namespace 收效,每个 netns 有自己的独立配置。
ipforward 参数如何影响 ip 转发?

关键内核函数在 ip_route_input_slow()。
这个函数中,会根据当前网络设备 in_dev 的 forwarding 参数,来决定是继续转发,还是跳转到 ip_error。
内核通过一个宏定义 IN_DEV_FORWARD(in_dev) 来判断设备 in_dev 是否开启了转发属性。
这个宏定义在 include/linux/inetdevice.h 文件中,指向了一个 IN_DEV_CONF_GET() 宏。后者继续指向了一个 ipv4_devconf_get() 函数。
在同文件中,ipv4_devconf_get() 函数给出了以下定义:
实际上是获取了这个网络设备 in_dev 的 cnf 结构体成员的 data 数组。传入的 index 实际上是字符串 IPV4_DEVCONF_ 和 FORWARDING 的拼接。
我们来看一下这个 data 数组的结构:
在 include/uapi/linux/ip.h 中,定义了 ipv4_devconf 结构体的 data 变量 index:
最后,总结来看,内核是通过 IN_DEV_CONF_GET 宏来获取网卡设备的 forward 参数的。
pforward 参数如何被设置的

起首,我们都知道,/proc/sys 目录实际上是一个虚拟文件体系,内里生存了实时收效的内核参数。这个机制允许我们实时查看和修改内核的参数,从而影响体系的运行行为。
和 ipv4 网络相关的参数位于 /proc/sys/net/ipv4 目录下, 如下(5.10 内核):
如何修改?上文已经说了,可以通过直接 echo,或者 sysctl 体系调用,亦或修改 /etc/sysctl.conf 配置文件,即可在不同的级别使他们收效。
/proc/sys/net/ipv4 目录下生存着很多全局变量,比方全局的 ip_forward。和具体网卡设备相关的变量生存在了其子目录 conf/ 下。
内核中的 ctl_table

此中,每一个目录代表当前体系的一个网络设备。当一个新的网络设备被注册或除名时,该目录下也会随之调整。
在内核中,/proc/sys/ 中的文件和目录都是以 ctl_table 结构定义的。下面是 devinet.c 文件中对于 /proc/sys/net/ipv4/ip_forward 这个变量的定义。

此中关键字段的含义为:
  1. const char*   procname;    // 参数文件名
  2. void*         data;        // 参数文件值
  3. int           maxlen;      // 参数大小
  4. mode_t        mode;        // 文件或目录权限
  5. proc_handler* proc_handler // 处理读写请求的回调函数
复制代码
具体解释为:当前文件名为“ip_forward”;参数值绑定为ipv4_devconf的data[0]的位置;644 代表root可读写,其他只读;最后,为这个参数文件绑定了一个读写回调函数 devinet_sysctl_forward。
目录定义的 ctl_table 和文件的不太一样,多了个 child 字段:
  1. {
  2.         .procname        = "dev",
  3.         .mode                = 0555,
  4.         .child                = dev_table,
  5. }
复制代码
/proc/sys/net/ipv4/ip_forward 如何被创建的?

上一节我们了解了,比方 /proc/sys/net/ipv4/ip_forward 文件,在内核中实际上是一个 ctl_table 结构。
ctl_table 的创建,在 fs/proc/proc_sysctl.c 文件的 __register_sysctl_table() 中完成。其函数注释如下:
  1. /**
  2.  * __register_sysctl_table - register a leaf sysctl table
  3.  * @set: Sysctl tree to register on
  4.  * @path: The path to the directory the sysctl table is in.
  5.  * @table: the top-level table structure
  6.  *
  7.  * Register a sysctl table hierarchy. @table should be a filled in ctl_table
  8.  * array. A completely 0 filled entry terminates the table.
  9.  */
  10.  
  11. struct ctl_table_header *__register_sysctl_table(
  12.     struct ctl_table_set *set,
  13.     const char *path,
  14.     struct ctl_table *table
  15. ) {...}
复制代码
该函数的操作过程大体可以概述为:

  • 探求 ctl_table 合适的目录,
  • 然后将其插入。
关于这个函数,本文不再赘述了,可以去相关文件中详细了解。下面我们来看 /proc/sys/net/ipv4/ip_forward 的创建过程。
网络设备初始化函数 devinet_init 执行时,将调用 register_pernet_subsys 函数,传入 devinet_ops 结构,并执行其 init 函数。devinet_ops 结构体绑定了 init 和 exit 两个函数,其 init 函数为 devinet_init_net。当他最终被调用执行时,会依次唤起 __devnet_sysctl_register() 和 register_net_sysctl() 分别创建 all/、default/ 以及 net/ipv4/ 三个目录。如下图。

实际上,__devnet_sysctl_register() 最终调用的也是 register_net_sysctl() 函数,完成 sysctl 目录的注册。

register_net_sysctl() 函数在 sysctl_net.c 文件中最终调用 __register_sysctl_table() 接口真正去注册一个 sysctl table 子项。
/proc/sys/net/ipv4/ip_forward 如何被读写?

我们再回到 ctl_table 的结构定义:

此中一个非常重要的函数 devinet_sysctl_forward() 就是 ctl_table 结构的读写回调函数。也就是说,当 /proc/sys/net/ipv4/ip_forward 文件被读或写时,会触发这个函数的调用。
我们来详细看一下这个函数的实现:

devinet_sysctl_forward() 接收几个参数,重要的,write表示当前操作:1 代表写,0 代表读;背面几个代表用户空间缓冲区,用于通报数据(buffer:缓冲区地址,lenp:缓冲区大小,ppos:文件偏移量)。
/proc/sys/net/ipv4/ip_forward 内核变量范例为一个整数,因此其默认的读写函数为 proc_dointvec()。类似的,字符串内核变量读写函数为 proc_dostring(),整数数组读写函数为 proc_dointvec_jiffies() 等等。这些函数的具体定义在 kernel/sysctl.c 中,如下:

在写入 ip_forward 变量时,不但仅要调用 proc_dointvec() 来写入具体 proc 文件,还需要写入全部网卡设备 cnf 的 data 数组,我们在上文中给出了这部分的接口和先容。
具体流程详见上面的伪代码,当写入 ip_forward 变量时,最终会遍历全部网卡设备,并调用 IN_DEV_CONF_SET() 宏执行写入操作。
总结:网卡设备配置参数

网卡设备的结构体 in_device 中有一个配置属性 ipv4_devconf,后者的结构中定义了一个 data[] 数组,内里存储了当前网卡的配置参数实际值。
内核中读写这个 data[] 数组,一样平常会用到 IN_DEV_CONF_GET() 和 IN_DEV_CONF_SET()。
如何在 proc/sys/net/ 中自定义一个参数文件?

我们来实战一下,从现在起,下文基于 kos5.8,kernel-5.10.134。
题目,通过编写一个内核模块,实现以下功能:
1)该模块加载时,在 /proc/sys/net/ 目录下创建一个文件 flag,卸载时该文件也随之移除。
2)flag 作为一个内核参数,其参数范例为 int,全部效户可对其读写。
3)当 flag 参数被写入时,向 messages 中打印一条日志。
代码样例:
  1. #include <linux/module.h>
  2. #include <linux/kernel.h>
  3. #include <linux/init.h>
  4. #include <linux/sysctl.h>
  5. #include <linux/proc_fs.h>
  6. static int flag = 0; // 用于存储 flag 的值
  7. // 自定义的 proc_handler 函数
  8. static int flag_handler(struct ctl_table *table, int write, void __user *buffer, size_t *lenp, loff_t *ppos) {
  9.         int ret;
  10.         loff_t pos = *ppos;
  11.        
  12.         // 使用 proc_dointvec 处理实际的读取/写入操作
  13.         ret = proc_dointvec(table, write, buffer, lenp, ppos);
  14.        
  15.         // 当执行写操作时
  16.         if (write) {
  17.                 // 打印日志,指示写操作发生
  18.                 printk(KERN_INFO "Writing to /proc/sys/net/flag, new value: %s\n", (char *)buffer);
  19.         }
  20.        
  21.         return ret;
  22. }
  23. // 定义 sysctl 的控制表
  24. static struct ctl_table sysctl_table[] = {
  25.         {
  26.                 .procname = "flag",           // 创建的 sysctl 路径
  27.                 .data = &flag,                // 要处理的内核变量
  28.                 .maxlen = sizeof(flag),       // 数据的最大长度
  29.                 .mode = 0666,                 // 权限设置
  30.                 .proc_handler = flag_handler, // 使用自定义的 proc_handler
  31.         },
  32.         { } // 结束符
  33. };
  34. // 定义 sysctl 目录
  35. static struct ctl_table_header *header;
  36. static int __init proc_flag_init(void) {
  37.         printk(KERN_INFO "Initializing proc_flag_sysctl module...\n");
  38.        
  39.         // 使用 register_sysctl 创建 proc 文件
  40.         header = register_sysctl("net", sysctl_table);
  41.        
  42.         // 在 /proc/sys/net/ 目录下创建 flag 文件
  43.         if (!header) {
  44.                 printk(KERN_ERR "Unable to register sysctl table\n");
  45.                 return -ENOMEM;
  46.         }
  47.        
  48.         printk(KERN_INFO "Proc file /proc/sys/net/flag created successfully\n");
  49.         return 0;
  50. }
  51. static void __exit proc_flag_exit(void) {
  52.         // 卸载 sysctl 表
  53.         unregister_sysctl_table(header);
  54.         printk(KERN_INFO "Sysctl table for /proc/sys/net/flag removed\n");
  55. }
  56. module_init(proc_flag_init);
  57. module_exit(proc_flag_exit);
  58. MODULE_LICENSE("GPL");
  59. MODULE_AUTHOR("Hong");
  60. MODULE_DESCRIPTION("A simple kernel module for flag using custom handler and sysctl");
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

钜形不锈钢水箱

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表