本文的開篇,我們先從 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 報文轉發開關。
這個引數有 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
後,檢視這兩個值:
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。
核心透過一個宏定義 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
這個變數的定義。
其中關鍵欄位的含義為:
const char* procname; // 引數檔名
void* data; // 引數檔案值
int maxlen; // 引數大小
mode_t mode; // 檔案或目錄許可權
proc_handler* proc_handler // 處理讀寫請求的回撥函式
具體解釋為:當前檔名為“ip_forward”;引數值繫結為ipv4_devconf
的data[0]
的位置;644 代表root可讀寫,其他只讀;最後,為這個引數檔案繫結了一個讀寫回撥函式 devinet_sysctl_forward
。
目錄定義的 ctl_table 和檔案的不太一樣,多了個 child 欄位:
{ .procname = "dev", .mode = 0555, .child = dev_table, }
/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/
三個目錄。如下圖。
實際上,__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 中列印一條日誌。
程式碼樣例:
#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");