本文目標:
在先前的文章中已經探討過 socket buffer(sk_buff),sk_buff 代表著一塊網路封包,它記載了封包的 meta data 以及封包的原始資料,相關的資料結構定義在 Linux 的原始程式碼中(路徑為 include/linux/sk_buff.h
)。
讓我們簡單的認識一下 sk_buff 結構,一個 sk_buff 結構包含了多個 header 以及 pointer,它們分別用於記錄:
這些 pointer 用於記錄每個網路層在封包中的起始位址,舉例來說:
在 Linux kernel 中,如果需要存取、操作裝置,必須事先安裝裝置的驅動程式(device driver)。
參考上圖,我們不難發現 Linux 將裝置分成了兩大類,分別是:
在上面我們已經提到,Linux 將裝置分成了兩大類:Block Devices
以及 Character Devices
。
兩者最主要的差異在於 Block Devices
使用固定長度的方式傳輸資料,而 Character devices
可接受非固定長度的資料。
對於兩者的操作方式,在 user space 並沒有差異。
Character Devices 可以視為一個資料流,它可以像是檔案一樣被存取。因此,Character device driver 至少需要負責以下工作:
對於某些特殊案例,它還需要提供 ioctl 操作,比較常見的場景是 text console 以及 serial ports,兩者皆屬於 streaming structure。
/* In sample.c */
# include <linux/module.h>
# include <linux/kernel.h>
# include <linux/init.h>
static int __init dev_drv_init(void)
{
pr_info("Initializing the module.\n");
return 0;
}
static void __exit dev_drv_exit(void)
{
pr_info("Unloading the module.\n");
return;
}
module_init(dev_drv_init);
module_exit(dev_drv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Ian Chen");
上方程式碼是一個非常簡易的 kernel module。
當 kernel module 被載入後,它會印出 Initializing the module
訊息(需要使用 dmesg 工具查看)。
反之,當 kernel module 被移除,它會印出 Unloading the module
。
module_init()
用於註冊模組的 initializer。module_exit()
當使用者使用 rmmod
移除模組,它會調用 cleanup_module()
將與模組相關的程式碼從 kernel space 清除。MODULE_LICENSE()
是一個特別的巨集,它用來表示模組的 license。MODULE_AUTHOR()
用於宣告模組的開發者。pr_info()
會將訊息寫入系統的日誌當中,其路徑為 /var/log/messages
。使用 Make 可以幫助我們提高每次修改程式碼後的測試效率,Make 會根據我們自定義的 makefile 自動化的編譯原始程式碼:
KERNEL_DIR := /lib/modules/$(shell uname -r)/build
MODULE_NAME = sample
obj-m := $(MODULE_NAME).o
all:
make -C $(KERNEL_DIR) M=$(shell pwd) modules
clean:
make -C $(KERNEL_DIR) M=$(shell pwd) clean
建立 Makefile 後,我們使用以下命令即可產生 kernel module 的 kernel object 檔案 sample.ko
:
$ make all
當你修改了模組的原始程式碼,可以使用以下命令清除先前編譯產出的檔案:
$ make clean
開發一個 Network Device Driver development 最快的方式就是調用 alloc_netdev()
或是 alloc_etherdev()
讓 OS 分配一個 network device,並且建立 net_device_ops
structure 註冊裝置的 hook functions。
當 device driver 載入到 Linux kernel,kernel 會根據不同的時機調用我們註冊的 hook function 操作 network device 的行為,基本的 network device operations 包含了:
ndo_init()
會在 device 註冊後被呼叫。ndo_open()
會在 device 的狀態轉變為 up
時被呼叫。ndo_validate_addr()
用於驗證 device 的 MAC(Mdedia Access Control)是否有效。ndo_stop()
i 會在 device 的狀態轉變為 down
時被呼叫。ndo_start_xmit()
用於傳送網路封包。
ndo_change_mtu()
用於改變裝置的 MTU(Maximum Transfer Unit)。ndo_set_mac_address()
用於改變裝置的 MAC。Dummy module 內建於 Linux kernel,它可以用於建立虛擬的網路環境,讓開發者使用它進行端對端的測試。
舉例來說,對於封閉的網路環境,主機僅有 loopback address 127.0.0.1
可以用於分析,其餘的 IP 都是不可用的。
因此,dummy interface 為了解決這個問題誕生了!
使用下方的命令建立 dummy interface nodelocaldns
:
# ip link adds: add virtual link DEVICE specifies the physical device to act operate on.
$ sudo ip link add nodelocaldns type dummy
以下命令會為 nodelocaldns
分配 IP:
$ sudo ip addr add 168.254.10.10 dev nodelocaldns
$ sudo ip addr add 10.20.0.10 dev nodelocaldns
當我們成功建立 dummy interface 並且為它分配 IP,我們就可以使用 ping 向它發送 icmp echo request:
ping 10.20.0.10
當我們不再需要 dummy interface,可以使用以下命令移除它:
$ sudo ip link delete nodelocaldns
Dummy module 的原始程式碼可以在 Linux kernel 專案找到。
在這個小節中,我們會藉由閱讀 Dummy module 的原始程式碼學習如何開發 network device driver:
module_init(dummy_init_module);
module_exit(dummy_cleanup_module);
MODULE_LICENSE("GPL");
MODULE_ALIAS_RTNL_LINK(DRV_NAME);
The codes above list the initializer, clean-up function, module license, and module aliases.
Let's get into dummy_init_module() and dummy_cleanup_module():
static struct rtnl_link_ops dummy_link_ops __read_mostly = {
.kind = DRV_NAME,
.setup = dummy_setup,
.validate = dummy_validate,
};
// ...
static int __init dummy_init_module(void)
{
int i, err = 0;
down_write(&pernet_ops_rwsem);
rtnl_lock();
err = __rtnl_link_register(&dummy_link_ops);
if (err < 0)
goto out;
for (i = 0; i < numdummies && !err; i++) {
err = dummy_init_one();
cond_resched();
}
if (err < 0)
__rtnl_link_unregister(&dummy_link_ops);
out:
rtnl_unlock();
up_write(&pernet_ops_rwsem);
return err;
}
static void __exit dummy_cleanup_module(void)
{
rtnl_link_unregister(&dummy_link_ops);
}
/* Number of dummy devices to be set up by this module. */
module_param(numdummies, int, 0);
MODULE_PARM_DESC(numdummies, "Number of dummy pseudo devices");
static int __init dummy_init_one(void)
{
struct net_device *dev_dummy;
int err;
dev_dummy = alloc_netdev(0, "dummy%d", NET_NAME_ENUM, dummy_setup);
if (!dev_dummy)
return -ENOMEM;
dev_dummy->rtnl_link_ops = &dummy_link_ops;
err = register_netdevice(dev_dummy);
if (err < 0)
goto err;
return 0;
err:
free_netdev(dev_dummy);
return err;
}
使用 alloc_netdev()
以分配 network device 需要使用的資源。
當分配工作完成,調用 register_netdevice()
將 device 註冊到 kernel 之中。
此外,它同時讓 dev_dummy 建立了 dummy_link_ops 的關聯。
到這裡,我們已經大致了解如何分配並且註冊一個網路裝置到 Linux kernel 上,讓我們繼續追蹤 dummy 的 setup 與 validate 函式:
static void dummy_setup(struct net_device *dev)
{
ether_setup(dev);
/* Initialize the device structure. */
dev->netdev_ops = &dummy_netdev_ops;
dev->ethtool_ops = &dummy_ethtool_ops;
dev->needs_free_netdev = true;
/* Fill in device structure with ethernet-generic values. */
dev->flags |= IFF_NOARP;
dev->flags &= ~IFF_MULTICAST;
dev->priv_flags |= IFF_LIVE_ADDR_CHANGE | IFF_NO_QUEUE;
dev->features |= NETIF_F_SG | NETIF_F_FRAGLIST;
dev->features |= NETIF_F_GSO_SOFTWARE;
dev->features |= NETIF_F_HW_CSUM | NETIF_F_HIGHDMA | NETIF_F_LLTX;
dev->features |= NETIF_F_GSO_ENCAP_ALL;
dev->hw_features |= dev->features;
dev->hw_enc_features |= dev->features;
eth_hw_addr_random(dev);
dev->min_mtu = 0;
dev->max_mtu = 0;
}
static int dummy_validate(struct nlattr *tb[], struct nlattr *data[],
struct netlink_ext_ack *extack)
{
if (tb[IFLA_ADDRESS]) {
if (nla_len(tb[IFLA_ADDRESS]) != ETH_ALEN)
return -EINVAL;
if (!is_valid_ether_addr(nla_data(tb[IFLA_ADDRESS])))
return -EADDRNOTAVAIL;
}
return 0;
}
dummy_setup()
初始化 netdev_ops
、dummy_ethtool_ops
以及填寫 ethernet-generic values。eth_hw_addr_random()
用於產生隨機的 Ethernet address(MAC)並設置 addr_assign_type 以便 sysfs 可以讀取狀態並提供 user space 使用。此外,在 dummy_setup()
函式中,它會註冊 dummy_netdev_ops
將其作為網路裝置的 operations:
static const struct net_device_ops dummy_netdev_ops = {
.ndo_init = dummy_dev_init,
.ndo_uninit = dummy_dev_uninit,
.ndo_start_xmit = dummy_xmit,
.ndo_validate_addr = eth_validate_addr,
.ndo_set_rx_mode = set_multicast_list,
.ndo_set_mac_address = eth_mac_addr,
.ndo_get_stats64 = dummy_get_stats64,
.ndo_change_carrier = dummy_change_carrier,
};
net_device_ops
結構描述了所有當網路裝置運作時會使用到的操作,讓我們來看看 dummy_xmit
這個函式做了些什麼工作:
static netdev_tx_t dummy_xmit(struct sk_buff *skb, struct net_device *dev)
{
dev_lstats_add(dev, skb->len);
skb_tx_timestamp(skb);
dev_kfree_skb(skb);
return NETDEV_TX_OK;
}
dev_lstats_add()
用於更新封包的長度。skb_tx_timestamp()
用於紀錄封包發送的 timestamp 的 hook function,它應該要在封包送往 MAC hardware 之前被呼叫。dev_kfree_skb()
socket buffer 是由 kernel space 分配的記憶體空間,當封包傳送後需要手動的釋放這塊記憶體。包含本篇文章,系列文已經花了超過 3 篇文章在探討 Linux Networking 的相關知識,這些知識可以幫助我們:
雖然筆者本身在這個領域也是個新手,但還是希望這些資源可以幫助到更多人!