iT邦幫忙

2022 iThome 鐵人賽

DAY 21
0
DevOps

5G 核心網路與雲原生開發之亂彈阿翔系列 第 22

以 Dummy module 為例來學習 device driver 開發

  • 分享至 

  • xImage
  •  

本文目標:

  • 複習 socket buffer
  • 認識 Device
  • 學習 network device 的概念
  • 介紹開發 network device 的基本知識

Recap:什麼是 socket buffer?

在先前的文章中已經探討過 socket buffer(sk_buff),sk_buff 代表著一塊網路封包,它記載了封包的 meta data 以及封包的原始資料,相關的資料結構定義在 Linux 的原始程式碼中(路徑為 include/linux/sk_buff.h)。

讓我們簡單的認識一下 sk_buff 結構,一個 sk_buff 結構包含了多個 header 以及 pointer,它們分別用於記錄:

  • transport_header
  • network_header
  • mac_header
  • 封包的 tail & end

這些 pointer 用於記錄每個網路層在封包中的起始位址,舉例來說:

  • 對於 transportation layer,它應包含它自己的 header 以及 user data。
  • 對 Network layer 來說,它除了需要包含 transportation layer 的資料,還需要包含 Network layer 自身的 header。
  • 對 the data link layer 來說,它就包含了所有的資料(user data 以及上兩層的封包頭以及自身的封包頭)。

進入正題

在 Linux kernel 中,如果需要存取、操作裝置,必須事先安裝裝置的驅動程式(device driver)。

參考上圖,我們不難發現 Linux 將裝置分成了兩大類,分別是:

  • Block Devices
  • Character Devices

Device 的種類

在上面我們已經提到,Linux 將裝置分成了兩大類:Block Devices 以及 Character Devices

Block Devices

兩者最主要的差異在於 Block Devices 使用固定長度的方式傳輸資料,而 Character devices 可接受非固定長度的資料。
對於兩者的操作方式,在 user space 並沒有差異。

Character Devices

Character Devices 可以視為一個資料流,它可以像是檔案一樣被存取。因此,Character device driver 至少需要負責以下工作:

  • open
  • close
  • read
  • write

對於某些特殊案例,它還需要提供 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

編譯 kernel module

使用 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

開發一個 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() 用於傳送網路封包。
    • NETDEV_TX_OK
    • NETDEV_TX_BUSY
  • ndo_change_mtu() 用於改變裝置的 MTU(Maximum Transfer Unit)。
  • ndo_set_mac_address() 用於改變裝置的 MAC。

研究實際專案:Dummy module

Dummy module 內建於 Linux kernel,它可以用於建立虛擬的網路環境,讓開發者使用它進行端對端的測試。
舉例來說,對於封閉的網路環境,主機僅有 loopback address 127.0.0.1 可以用於分析,其餘的 IP 都是不可用的。
因此,dummy interface 為了解決這個問題誕生了!

使用 Dummy module

使用下方的命令建立 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);
}
  • Line 9 & 28:使用 semaphore 避免 race condition。
  • Line 14 & 27:Netlink mutex lock,用於保護 network devices 列表。
  • Line 15: 將 dummy_link_ops 註冊到 rtnl_link。
    • Rtnetlink 允許 kernel's routing tables 被存取或是替換。
    • Rtnetlink 維護了記載 link_ops 的 linked-list,其中的每一個節點可以使用 kind 識別。
    • netdev 就像是 rtnl_link 的客戶,它使用 rtnl_link_ops 與 dummy_link_ops 建立關聯。
    • 如果有任何 rtnl_link_ops 從 linked-list 被移除,Linux kernel 需要處理每一個與該操作有關連的模組。
  • Line 19 - 22: Dummy module 允許使用者一口起建立多個 dummies。
/* 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_opsdummy_ethtool_ops 以及填寫 ethernet-generic values。
  • eth_hw_addr_random() 用於產生隨機的 Ethernet address(MAC)並設置 addr_assign_type 以便 sysfs 可以讀取狀態並提供 user space 使用。
  • IFLA_ADDRESS 代表 interface L2 address。

此外,在 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 的相關知識,這些知識可以幫助我們:

  • 將計算機網路學習到的抽象概念轉為實務經驗:對初學者來說,要將理論書籍或是課本上的抽象概念並不是一件簡單的事情,透過觀察 Linux 作業系統如何處理網路這個棘手的問題,可以幫助我們克服陌生的網路概念。
  • 站在巨人的肩膀上看世界:筆者之所以花費這麼長的篇幅在 Linux Networking,除了明天要介紹的 gtp5g 專案本身是個 network device driver 外,時下最熱門的 Docker、Kubernetes 底層其實也都使用到了這些東西去兜出龐大的系統,像是:namespace 做到的資源隔離、netfilter 控制網路、CNI 的實作與 kernel space 息息相關...等等。

雖然筆者本身在這個領域也是個新手,但還是希望這些資源可以幫助到更多人!

References


上一篇
你知道 Linux 如何處理網路封包嗎? - 以 socket programming 為例
下一篇
gtp5g 原始程式碼解說
系列文
5G 核心網路與雲原生開發之亂彈阿翔36
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言