【kernel】從 /proc/sys/net/ipv4/ip_forward 引數看如何玩轉 procfs 核心引數

_hong發表於2024-12-06

本文的開篇,我們先從 sysctl 這個命令開始。

sysctl 使用

sysctl 是一個 Linux 系統工具,後臺實際上是 syscall,它允許使用者檢視和動態修改核心引數。

# 檢視當前設定的所有核心引數
sysctl -a
# 檢視特定引數的值
sysctl net.ipv4.conf.all.forwarding
# 臨時修改核心引數
sysctl net.ipv4.conf.all.forwarding=1
# 重新載入配置檔案,預設是 /etc/sysctl.conf
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 報文轉發開關。

【kernel】從 /proc/sys/net/ipv4/ip_forward 引數看如何玩轉 procfs 核心引數

這個引數有 2 個開關(ipv4 為例,ipv6 同理):

1 - /proc/sys/net/ipv4/ip_forward
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 後,檢視這兩個值:

【kernel】從 /proc/sys/net/ipv4/ip_forward 引數看如何玩轉 procfs 核心引數

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()<以下核心版本為 4.18>

這個函式中,會根據當前網路裝置 in_dev 的 forwarding 引數,來決定是繼續轉發,還是跳轉到 ip_error。

【kernel】從 /proc/sys/net/ipv4/ip_forward 引數看如何玩轉 procfs 核心引數

核心透過一個宏定義 IN_DEV_FORWARD(in_dev) 來判斷裝置 in_dev 是否開啟了轉發屬性。

這個宏定義在 include/linux/inetdevice.h 檔案中,指向了一個 IN_DEV_CONF_GET() 宏。後者繼續指向了一個 ipv4_devconf_get() 函式。

【kernel】從 /proc/sys/net/ipv4/ip_forward 引數看如何玩轉 procfs 核心引數

在同檔案中,ipv4_devconf_get() 函式給出了以下定義:

【kernel】從 /proc/sys/net/ipv4/ip_forward 引數看如何玩轉 procfs 核心引數

實際上是獲取了這個網路裝置 in_devcnf 結構體成員的 data 陣列。傳入的 index 實際上是字串 IPV4_DEVCONF_FORWARDING 的拼接。

我們來看一下這個 data 陣列的結構:

【kernel】從 /proc/sys/net/ipv4/ip_forward 引數看如何玩轉 procfs 核心引數

include/uapi/linux/ip.h 中,定義了 ipv4_devconf 結構體的 data 變數 index

【kernel】從 /proc/sys/net/ipv4/ip_forward 引數看如何玩轉 procfs 核心引數

最後,總結來看,核心是透過 IN_DEV_CONF_GET 宏來獲取網路卡裝置的 forward 引數的。

【kernel】從 /proc/sys/net/ipv4/ip_forward 引數看如何玩轉 procfs 核心引數

pforward 引數如何被設定的

首先,我們都知道,/proc/sys 目錄實際上是一個虛擬檔案系統,裡面儲存了實時生效的核心引數。這個機制允許我們實時檢視和修改核心的引數,從而影響系統的執行行為。

和 ipv4 網路相關的引數位於 /proc/sys/net/ipv4 目錄下, 如下(5.10 核心):

【kernel】從 /proc/sys/net/ipv4/ip_forward 引數看如何玩轉 procfs 核心引數

如何修改?上文已經說了,可以透過直接 echo,或者 sysctl 系統呼叫,亦或修改 /etc/sysctl.conf 配置檔案,即可在不同的級別使他們生效。

/proc/sys/net/ipv4 目錄下儲存著很多全域性變數,例如全域性的 ip_forward。和具體網路卡裝置相關的變數儲存在了其子目錄 conf/ 下。

【kernel】從 /proc/sys/net/ipv4/ip_forward 引數看如何玩轉 procfs 核心引數

核心中的 ctl_table

其中,每一個目錄代表當前系統的一個網路裝置。當一個新的網路裝置被註冊或除名時,該目錄下也會隨之調整。

在核心中,/proc/sys/ 中的檔案和目錄都是以 ctl_table 結構定義的。下面是 devinet.c 檔案中對於 /proc/sys/net/ipv4/ip_forward 這個變數的定義。

image

其中關鍵欄位的含義為:

const char*   procname;    // 引數檔名
void*         data;        // 引數檔案值
int           maxlen;      // 引數大小
mode_t        mode;        // 檔案或目錄許可權
proc_handler* proc_handler // 處理讀寫請求的回撥函式

具體解釋為:當前檔名為“ip_forward”;引數值繫結為ipv4_devconfdata[0]的位置;644 代表root可讀寫,其他只讀;最後,為這個引數檔案繫結了一個讀寫回撥函式 devinet_sysctl_forward

目錄定義的 ctl_table 和檔案的不太一樣,多了個 child 欄位:

{
	.procname	= "dev",
	.mode		= 0555,
	.child		= dev_table,
}
【kernel】從 /proc/sys/net/ipv4/ip_forward 引數看如何玩轉 procfs 核心引數

/proc/sys/net/ipv4/ip_forward 如何被建立的?

上一節我們瞭解了,例如 /proc/sys/net/ipv4/ip_forward 檔案,在核心中實際上是一個 ctl_table 結構。

ctl_table 的建立,在 fs/proc/proc_sysctl.c 檔案的 __register_sysctl_table() 中完成。其函式註釋如下:

/**
 * __register_sysctl_table - register a leaf sysctl table
 * @set: Sysctl tree to register on
 * @path: The path to the directory the sysctl table is in.
 * @table: the top-level table structure
 *
 * Register a sysctl table hierarchy. @table should be a filled in ctl_table
 * array. A completely 0 filled entry terminates the table.
 */
 
struct ctl_table_header *__register_sysctl_table(
    struct ctl_table_set *set,
    const char *path, 
    struct ctl_table *table
) {...}

該函式的操作過程大體可以概述為:

  • 尋找 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/ 三個目錄。如下圖。

image

實際上,__devnet_sysctl_register() 最終呼叫的也是 register_net_sysctl() 函式,完成 sysctl 目錄的註冊。

image

register_net_sysctl() 函式在 sysctl_net.c 檔案中最終呼叫 __register_sysctl_table() 介面真正去註冊一個 sysctl table 子項。

/proc/sys/net/ipv4/ip_forward 如何被讀寫?

我們再回到 ctl_table 的結構定義:

image

其中一個非常重要的函式 devinet_sysctl_forward() 就是 ctl_table 結構的讀寫回撥函式。也就是說,當 /proc/sys/net/ipv4/ip_forward 檔案被讀或寫時,會觸發這個函式的呼叫。

我們來詳細看一下這個函式的實現:

image

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 中,如下:

image

在寫入 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 中列印一條日誌。

程式碼樣例:

#include <linux/module.h> 
#include <linux/kernel.h> 
#include <linux/init.h> 
#include <linux/sysctl.h> 
#include <linux/proc_fs.h> 

static int flag = 0; // 用於儲存 flag 的值 

// 自定義的 proc_handler 函式 
static int flag_handler(struct ctl_table *table, int write, void __user *buffer, size_t *lenp, loff_t *ppos) { 
	int ret; 
	loff_t pos = *ppos; 
	
	// 使用 proc_dointvec 處理實際的讀取/寫入操作 
	ret = proc_dointvec(table, write, buffer, lenp, ppos); 
	
	// 當執行寫操作時 
	if (write) { 
		// 列印日誌,指示寫操作發生 
		printk(KERN_INFO "Writing to /proc/sys/net/flag, new value: %s\n", (char *)buffer); 
	} 
	
	return ret; 
} 

// 定義 sysctl 的控制表 
static struct ctl_table sysctl_table[] = { 
	{ 
		.procname = "flag",           // 建立的 sysctl 路徑 
		.data = &flag,                // 要處理的核心變數 
		.maxlen = sizeof(flag),       // 資料的最大長度 
		.mode = 0666,                 // 許可權設定 
		.proc_handler = flag_handler, // 使用自定義的 proc_handler 
	}, 
	{ } // 結束符 
};

// 定義 sysctl 目錄 
static struct ctl_table_header *header; 

static int __init proc_flag_init(void) { 
	printk(KERN_INFO "Initializing proc_flag_sysctl module...\n"); 
	
	// 使用 register_sysctl 建立 proc 檔案 
	header = register_sysctl("net", sysctl_table); 
	
	// 在 /proc/sys/net/ 目錄下建立 flag 檔案 
	if (!header) { 
		printk(KERN_ERR "Unable to register sysctl table\n"); 
		return -ENOMEM; 
	} 
	
	printk(KERN_INFO "Proc file /proc/sys/net/flag created successfully\n"); 
	return 0; 
} 

static void __exit proc_flag_exit(void) { 
	// 解除安裝 sysctl 表 
	unregister_sysctl_table(header); 
	printk(KERN_INFO "Sysctl table for /proc/sys/net/flag removed\n"); 
} 

module_init(proc_flag_init); 
module_exit(proc_flag_exit); 

MODULE_LICENSE("GPL"); 
MODULE_AUTHOR("Hong"); 
MODULE_DESCRIPTION("A simple kernel module for flag using custom handler and sysctl");

相關文章