Linux设备驱动开发浅尝
写在前面
前段时间由于开发裸金属服务器与智能网卡的项目,对于Linux内核及设备驱动产生很大的兴趣,也看了基本相关的书籍。借着过年前的周六、周日两天,我准备从一个初学者的角度,尝试写基本的驱动,最终目标实现一个简单的网络设备驱动。
Taylor on 2022/1/22.
一、环境搭建
1.1 搭建kvm + qemu + libvirt 开发调试环境
手头上有一台物理机器,实验也是在这个上面完成:
# hostnamectl
Static hostname: taylor-host
Icon name: computer-desktop
Chassis: desktop
Machine ID: d9ccc1a1ded74231bcef880aa245b864
Boot ID: 766200cf5e6b472b86b89b3186479f78
Operating System: Ubuntu 21.10
Kernel: Linux 5.13.0-27-generic
Architecture: x86-64
Hardware Vendor: System manufacturer
Hardware Model: System Product Name
推荐搭建并使用虚拟机环境:
sudo virt-install --name=kernel-env --memory=16384,maxmemory=16384 \
--vcpus=8,maxvcpus=8 --os-type=linux --os-variant=ubuntu20.04 \
--location=/home/base-image/ubuntu-20.04.2-live-server-amd64.iso \
--disk path=/var/lib/libvirt/images/kernel-env.img,size=50 \
--bridge=br-lan --graphics=vnc,port=5991 --console=pty,target_type=serial \
--extra-args="console=tty0 console=ttyS0"
1.2 编译器环境搭建
搭建Clion + WSL开发调试环境,也可以参考官网的文档。
1.3 准备makefile
makefile是执行一组操作的特殊文件,其中最重要的操作是程序的编译。
make工具用于解析makefile。
介绍下obj-<X> kbuild变量:
obj-y += base_driver.o
- 如果
<X> = m,则使用变量obj-m,表示将base_driver.o编译为模块 - 如果
<X> = y,则使用变量obj-y,表示将base_driver.o编译为内核的内置模块 - 如果
<X> = n,则使用变量obj-n,则不会构建base_driver.o
给出Makefile的demo:
obj-m := base_driver.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
all default: modules
install: modules_install
modules modules_install help clean:
$(MAKE) -C $(KERNELDIR) M=$(shell pwd) $@
二、设备驱动demo
2.1 驱动程序框架
# vim base_driver.c
//
// Created by taylor Tao on 2022/1/22.
//
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int __init base_init(void) {
pr_info("load base driver successfully!");
return 0;
}
static void __exit base_exit(void) {
pr_info("unload base driver end.");
}
module_init(base_init);
module_exit(base_exit);
MODULE_AUTHOR("Taylor <taoruizhe100@163.com>");
MODULE_LICENSE("GPL");
2.2 入口和出口
内核驱动程序有入口和出口,分别对应模块加载函数与模块卸载的函数。
函数名可以是任意,只需要通过module_init()和module_exit()宏,将对应函数注册为加载和卸载函数即可。
2.3 属性
__init和__exit属性,是在include/linux/init.h中定义的内核宏:
#define __init __section(".init.text")
#define __exit __section(".exit.text")
首先理解可执行和可链接格式(ELF)的目标文件。ELF目标文件由不同的命名部分组成,其中一些部分是必需的。通过objdump可以打印出模块的不同组成部分:
# objdump -h base_driver.ko
base_driver.ko: file format elf64-x86-64
...
属于ELF标准的包括:
- .text:代码
- .data:数据段
- .rodata:只读数据
- .comment:注释
总结下,__init和__exit是Linux指令(宏),使用C编译器属性来指定符号(数据、代码)的位置。这些指定指示编译器,将以它们为前缀的代码分别放在.init.text和.exit.text部分。
2.4 模块信息
内核模块使用.modinfo部分来存储模块信息。所有MODULE_*的宏都用来表示模块信息,这里定义的宏是MODULE_AUTHOR()和MODULE_LICENSE()。真正底层宏是MODULE_INFO(tag, info),以tag = info的方式添加信息。
我们可以通过objdump转储模块信息:
# objdump base_driver.ko -d -j .modinfo
这里顺便提一下许可,Linux开源许可是GPL,模块必须兼容GPL才行:
GPL --> GNU 公共许可证v2或更高版本
2.5 错误处理
错误代码由内核或用户空间应用程序,通过errno变量给出解释。根据错误代码,我们可以查找内核源码的定义文件,找到对应的错误:
/usr/src/linux-headers-$(uname -r)/include/uapi/asm-generic/errno-base.h/usr/src/linux-headers-$(uname -r)/include/uapi/asm-generic/errno.h
一般在系统调用中,错误的返回方式如:
dev = init(&ptr);
if(!dev)
return -EIO;
在用户空间内的程序,出现系统调用失败,需要先加载头函数:
#include <errno.h>
#include <string.h>
if (write(fd, buf, 1) < 0)
printf("sth mess up %s \n", strerror(error));
同时,处理错误需要撤销这个错误发生之前的设置,使用goto:
ptr = kmalloc(sizeof (device_t));
if (!ptr)
ret = -ENOMEM
goto err_alloc;
dev = init(&ptr);
if(!dev)
return -EIO;
return 0;
err_init:
free(ptr);
err_alloc:
return ret;
2.6 消息打印
printk()是在内核空间中使用,作用于用户空间的printf()相同。通过dmesg命令可以看到printk()写入的行。
内核日志分为8个等级,优先级与数值成反比:
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_SOH_ASCII '\001'
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
#define KERN_DEFAULT "" /* the default kernel loglevel */
源码见:
/usr/src/linux-headers-$(uname -r)/include/linux/kern_levels.h
打印内核消息的方式,如果省略日志级别,则使用默认日志级别,一般是调试级别:
printk(KERN_ERR "this is error log\n");
如demo中所示,可以使用包装宏来简化打印方式,更加简单直接:
- pr_emerg、pr_alert、pr_crit、pr_err、pr_warning、pr_notice、pr_info、pr_debug
printk()的实现方式是:内核将消息日志级别和当前控制台的日志级别进行比较,如果前者比后者更高(数值更低),消息立即打印到控制台。
查看控制台的日志级别,表示当前日志级别(4),默认日志级别(4):
# cat /proc/sys/kernel/printk
4 4 1 7
printk()不会阻塞,即使在原子上下文中调用也足够安全,它会尝试锁定控制台并打印消息;如果锁定失败,输出则写入缓冲区,函数返回。
2.7 编译过程
root@taylor-host:/home/c-project# make
make -C /lib/modules/5.13.0-27-generic/build M=/home/c-project modules
make[1]: Entering directory '/usr/src/linux-headers-5.13.0-27-generic'
scripts/Makefile.build:44: /home/c-project/Makefile: No such file or directory
make[2]: *** No rule to make target '/home/c-project/Makefile'. Stop.
make[1]: *** [Makefile:1879: /home/c-project] Error 2
make[1]: Leaving directory '/usr/src/linux-headers-5.13.0-27-generic'
make: *** [makefile:9: modules] Error 2
修改makefile文件名为Makefile,继续:
root@taylor-host:/home/c-project# make
make -C /lib/modules/5.13.0-27-generic/build M=/home/c-project modules
make[1]: Entering directory '/usr/src/linux-headers-5.13.0-27-generic'
CC [M] /home/c-project/base_driver.o
In file included from ./include/linux/module.h:21,
from /home/c-project/base_driver.c:6:
./include/linux/moduleparam.h:24:9: error: expected ‘,’ or ‘;’ before ‘static’
24 | static const char __UNIQUE_ID(name)[] \
| ^~~~~~
./include/linux/module.h:165:32: note: in expansion of macro ‘__MODULE_INFO’
165 | #define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
| ^~~~~~~~~~~~~
./include/linux/module.h:229:46: note: in expansion of macro ‘MODULE_INFO’
229 | #define MODULE_LICENSE(_license) MODULE_FILE MODULE_INFO(license, _license)
| ^~~~~~~~~~~
/home/c-project/base_driver.c:21:1: note: in expansion of macro ‘MODULE_LICENSE’
21 | MODULE_LICENSE("GPL")
| ^~~~~~~~~~~~~~
make[2]: *** [scripts/Makefile.build:281: /home/c-project/base_driver.o] Error 1
make[1]: *** [Makefile:1879: /home/c-project] Error 2
make[1]: Leaving directory '/usr/src/linux-headers-5.13.0-27-generic'
make: *** [Makefile:9: modules] Error 2
demo代码中缺少;,继续:
root@taylor-host:/home/c-project# make
make -C /lib/modules/5.13.0-27-generic/build M=/home/c-project modules
make[1]: Entering directory '/usr/src/linux-headers-5.13.0-27-generic'
CC [M] /home/c-project/base_driver.o
MODPOST /home/c-project/Module.symvers
CC [M] /home/c-project/base_driver.mod.o
LD [M] /home/c-project/base_driver.ko
BTF [M] /home/c-project/base_driver.ko
Skipping BTF generation for /home/c-project/base_driver.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.13.0-27-generic'
在当前目录加载驱动.ko,内核报错:
root@taylor-host:/home/c-project# insmod base_driver.
base_driver.ko base_driver.mod.o base_driver.o
root@taylor-host:/home/c-project# insmod base_driver.ko
root@taylor-host:/home/c-project# rmmod base_driver
[Sat Jan 22 15:08:13 2022] base_driver: module verification failed: signature and/or required key missing - tainting kernel
[Sat Jan 22 15:08:13 2022] load base driver successfully!
Ubuntu 在加载kernel驱动时,通过验证签名来提高安全性...这给我带来了新麻烦:
The kernel module signing facility cryptographically signs modules during installation and then checks the signature upon loading the module.
那就给驱动加上签名信息,如KERNEL MODULE SIGNING FACILITY:
root@taylor-host:/usr/src/linux-headers-5.13.0-27-generic# scripts/sign-file sha512 /root/.ssh/id_rsa /root/.ssh/id_rsa.pub /home/c-project/base_driver.ko
ok,基础完成了。
三、平台设备驱动
3.1 平台设备
首先提一个概念,即插即用设备。一旦插入就被内核处理。这些设备可以是USB设备、PCI Express设备,也可以是其他任何自动发现的设备。也存在不可热插拔的设备,内核在管理这些设备前需要了解设备类型,包括I2C、UART、SPI和其他没有连接到支持枚举总线的设备。
由此引出平台设备的概念:类似USB、I2S、I2C、UART、SPI、PCI、SATA等都是物理总线,实际上一类是名为控制器Controller的硬件设备。它们是SoC的一部分,因此无法删除、不可发现。
从SoC的角度来看,这些设备(总线)内部通过专用总线连接,而且大部分是专有的,专门针对特定制造商。从内核的角度看,这些是根设备,是内核虚拟总线,用于不在内核已知物理总线上的设备。也就是说,平台设备实际上是伪平台总线的设备。
平台设备通过结构体platform_device来表示:
struct platform_device {
*/ 必须与device_driver.driver.name保持相同 */
const char *name;
u32 id;
struct device dev;
/* 资源数组的大小 */
u32 num_resources;
/* 设备所需资源,包括IRQ/DMA/内存区域/IO端口 */
struct resource *resource;
}
由于内核不知道平台设备属于什么总线、能做什么、需要加载什么。由于没有自动协商的过程,所以将设备所需的资源和数据都通知给内核。这里引入新概念DTS,设备树。DTS的主要目标是从内核中删除特定且没有测试过的代码。设备树是硬件描述文件,其格式类似于树形结构,每个设备用一个节点表示,任何数据、资源或配置数据都表示为节点的属性。这样,对设备的修改只需要重新编译DTS即可,无需重构整个内核。
3.2 平台驱动
并非所有平台设备(伪平台设备)都是由平台驱动程序处理的
平台驱动专用于不基于传统总线的设备。I2C或SPI设备是平台设备,但分别依赖于I2C或SPI总线,而不是平台总线。因此,对于平台驱动程序,一切都需要手动完成。
开发平台设备驱动,必须声明主结构体platform_driver,并用专用函数把驱动程序注册到平台总线上:
static struct platform_driver mypdrv = {
/* `probe()`是设备匹配,声明驱动程序时所调用的函数 */
.probe = my_pdrv_probe;
/* 移除驱动程序时调用 `remove()` 函数 */
.remove = my_pdrv_remove;
/* `struct device_driver` 描述驱动程序本身,提供名称、所有者等 */
.driver = {
.name = "my_platform_driver",
.owner = THIS_MODULE;
}
}
在内核中注册平台驱动程序很简单,只需要在__init()函数中调用platform_driver_register()或platform_driver_probe(),也就是在模块加载时将设备注册到内核中。这两个函数时有区别的:
platform_driver_register():注册驱动程序并将其放入由内核维护的驱动程序列表中,以便每当发现新的匹配时就可以按需调用probe()函数。因此,为防止驱动程序在内核列表中插入和注册,不要使用该函数;platform_driver_probe():调用该函数后,内核立即运行匹配循环,检查是否有平台设备名称匹配,如果匹配就调用probe()函数,这意味着设备存在;否则忽略驱动程序。该方法可以防止延迟探测,因为它不会立即在系统上注册驱动程序。这里,probe()函数被放在__init部分,当内核启动完成后这个部分被释放,从而防止了延迟探测并减少驱动程序的内存占用。如果100%确定设备存在于系统中,就用该函数。
这里给出一个简单的向内核注册设备的平台设备驱动demo:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platfrom_device.h>
static int my_pdrv_probe (struct platform_device *pdev) {
pr_info("info device probed!\n");
return 0;
}
static void my_pdrv_remove(struct platform_deivce *pdev) {
pr_info("info device removed!\n");
}
static struct platform_driver mypdrv = {
.probe = my_pdrv_probe;
.remove = my_pdrv_remove;
.driver = {
.name = KBUILD_MODNAME;
.owner = THIS_MODULE;
};
};
static int __init my_pdrv_init(void) {
pr_info("info device init\n");
platform_driver_register(&mypdrv);
return 0;
}
static void __exit my_pdrv_exit (void) {
pr_info("info device remove\n");
platform_driver_unregister(&my_driver);
}
module_init(my_pdrv_init);
module_exit(my_pdrv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu");
每个总线都有特定的宏,用来注册对应驱动:
module_platform_driver(),用于平台驱动程序,专用于传统物理总线以外的设备;module_spi_driver(),用于SPI驱动程序;module_pci_driver(),用于PCI驱动程序;module_usb_driver(),用于USB驱动程序;- ...
3.2 设备和总线的匹配机制
在匹配发生前,Linux会调用platform_match()函数。平台设备通过字符串与驱动程序匹配。根据Linux设备模型,总线元素是最重要的部分。每个总线都会维护一个注册驱动程序和设备列表。总线驱动程序负责设备和驱动程序的匹配。每当链接新设备或向总线添加新的驱动程序时,总线都会启动匹配循环。
内核中定义了MODULE_DEVICE_TABLE宏,让驱动程序公开其ID表,该表描述驱动程序可以支持哪些设备。同时,如果驱动程序可以编译为模块,则driver.name字段应该与模块名称匹配;如果不匹配,则不会加载驱动程序。
四、网卡设备驱动
4.1 数据结构
处理网络硬件设备需要使用2种数据结构:
struct sk_buff-->include/linux/skbbuff.h,处理每个数据包的发送或接受struct net_device->include/linux/netdevice.h,用来表示网络设备
4.1.1 套接字缓冲区
//
// Created by taylor Tao on 2022/1/22.
//
#ifndef LINUX_SKBBUFF_H
#define LINUX_SKBBUFF_H
#endif //LINUX_SKBBUFF_H
// 套接字缓冲区socket buffer
struct sk_buff {
struct sk_buff *next; /* 下一个缓冲区 */
struct sk_buff *prev; /* 上一个缓冲区 */
ktime_t tstamp; /* 数据包到达/离开的时间 */
struct rb_node; /* for Netem and TCP */
struct sock * sk; /* 与数据包相关的套接字 */
struct net_device * dev; /* 数据包到达/离开的设备, 与input_dev和real_dev相关 */
unsigned int len; /* 数据包的总字节数 */
unsigned int data_len; /* 套接字缓冲区skb由线性数据缓冲区以及可选的一组称为室的区域组成,如果有室,data_len将保存数据区域的总字节数 */
_u16 mac_len; /* 保存MAC头的长度 */
_u16 hdr_len;
_32 priority; /* 表示QoS中包的优先级 */
dma_cookie_t dma_cookie;
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char * head; /* head、data和tail是指向套接字缓冲区不同区域(室)的指针 */
unsigned char * data; /* 指向套接字缓冲区的末尾 */
unsigned int truesize;
atomic_t users;
};
// 套接字缓冲区分配
/**
* 1. 整个内存分配应该用`netdev_alloc_skb()`函数实现
* 2. 用`skb_reserve()`函数增加和对齐头室
* 3. 用`skb_put()`函数拓展缓冲区的已用数据区
*/
/**
* 1 -> 分配缓冲区
* 调用`netdev_alloc_skb()`函数分配足够大的缓冲区,包含数据包和以太网头
* 由于以太网报头长度14字节,因此需要进行对齐,以便CPU在访问缓冲区时,
* 不会产生性能问题。
*/
struct sk_buff *netdev_alloc_skb(struct net_device *dev,
unsigned int length);
/**
* 2 --> 通过减少尾室来增加并且对齐头部
*/
void skb_reserve(struct sk_buff *skb, int len);
/**
* 3 -> 将缓冲区的已用数据区拓展为与包一样大。
* 该函数返回的指针,是指向数据区第一个字节的指针
* 分配的套接字缓冲区应该转发到内核网络层。这是套接字缓冲区生命周期的最后一步,调用`netif_rx_ni()`实现
*/
unsigned char *skb_put(struct sk_buff *skb, unsigned int len);
4.1.2 网络接口结构
//
// Created by taylor Tao on 2022/1/22.
//
#ifndef LINUX_NETDDVICE_H
#define LINUX_NETDDVICE_H
#endif //LINUX_NETDDVICE_H
// 网络设备net_device
struct net_device {
char name[IFNAMSIZ];
char *ifalias;
unsigned long mem_end;
unsigned long mem_start;
unsigned long base_addr;
int irq;
netdev_feature_t features;
netdev_features_t hw_features;
netdev_features_t wanted_features;
int ifindex;
struct net_device_stats stats;
atomic_long_t rx_dropped;
atomic_long-t tx_dropped;
const struct net_device_ops *netdev_ops;
const struct ethtool_ops *ethtool_ops;
unsigned int flags;
unsigned int priv_flags;
unsigned char link_mode;
unsigned char if_port;
unsigned char dma;
unsigned int mtu;
unsigned short type;
/* 接口地址信息 */
unsigned char perm_addr[MAC_ADDR_LEN];
unsigned char add_assign_type;
unsigned char addr_len;
unsigned short neigh_priv_len;
unsigned short dev_id;
unsigned short dev_port;
unsigned long last_rx;
unsigned char *dev_addr;
struct device dev;
struct phy_device *phydev;
};
// 1. 手动分配内存空间
struct net_device *alloc_etherdev(int sizeof_priv);
// 2. 返回设备的私有结构
void *netdev_priv(const struct net_device *dev);
// demo: 分配内存并获取私有结构
struct net_device *net_dev;
struct priv_struct *priv_net_struct;
net_dev = alloc_etherdev(sizeof(struct priv_struct));
my_priv_struct = netdev_priv(dev);
// 4. 设备从内核注销后,释放设备并释放内存
void free_netdev(struct net_device *dev);
// 5. 将设备注册到内核
int register_netdev(struct net_device *dev);
4.2 设备方法
4.2.1 打开和关闭
/**
* `ndo_open()`
* 网络控制器通常会在收到或完成数据包传输时引发中断。驱动程序需要注册中断处理程序,
* 只要控制器引发中断就会调用到。驱动程序可以在`init()/probe()例程/open()`函数
* 内注册终端处理程序。有些设备需要通过设置硬件中的特殊寄存器来启用中断,这种情况下
* 可以在`probe()`函数中请求中断,只需要在打开、关闭方法中设置/清除enable位。
*
* 总结open方法执行的操作:
* 1. 更新接口MAC地址(防止用户修改)
* 2. 必要时,复位硬件,让其退出低功耗模式
* 3. 请求所有资源(IO存储器、DMA通道、IRQ)
* 4. 映射IRQ,注册中断处理程序
* 5. 检查接口链路状态
* 6. 在设备上调用`net_if_start_queue()`,通知内核设备已准备好
*/
/* demo `ndo_open` */
static int my_net_open(struct net_device *dev) {
struct priv_net_struct *priv = netdev_priv(dev);
if (!is_valid_ether_addr(dev->dev_addr)) {
/* Maybe print a debug message here? */
return -EADDRNOTAVAIL;
}
/* Reset hardware, and wake it up from low-power mode */
my_netdev_lowpower(priv, false);
/* HW reset failed, let kernel known */
if(!my_netdev_hw_init(priv)) {
return -EINVAL;
}
/* Update MAC address */
set_hw_macaddr_registers(netdev, MAC_REGADDR_START,
netdev->addr_len,
netdev->dev_addr);
/* Enable interrupt */
my_netdev_hw_enable(priv);
/* Ready to receive request from network layer */
netif_start_queue(dev);
return 0;
}
/* demo `ndo_close` */
static int my_net_close(struct net_device *dev) {
struct priv_net_struct *priv = netdev_priv(dev);
my_netdev_hw_disable(priv);
my_netdev_lowpower(priv, true);
/* Stop transmit packet */
netif_stop_queue(dev);
return 0;
}
4.2.2 数据包处理
网络数据交换方式有2种方式,通过轮询或中断。轮询,如同定时器驱动的中断,轮询期间内核按指定时间间隔程序检查设备的变化。另外,在中断模式下内核不需要做任何事情,只需要监听IRQ线路,等待设备通知变化。中断的方式会在通过高流量时增加系统开销,所以一些驱动程序会混用两种方式。在高流量时使用轮询,在流量正常时使用中断IRQ。
/**
* 数据包接收
* 当数据包到达网络接口卡时,驱动程序必须为其建立新的套接字缓冲区sk_buff,并将数据
* 包复制到sk_ff->data字段内。这种复制并不重要,DMA也可以使用。
* NIC接收到数据包时,会引发中断,中断由驱动程序处理。先检查设备的中断状态寄存器,
* 检查中断产生的真正原因(也可能是RX错误),随后将引发中断事件对应的位保存在状态
* 寄存器中。
* 驱动程序接收到n个数据包,就执行n次sk_buff分配
*/
/*
* RX interrupt function
*/
static int my_rx_interrupt(struct net_device *ndev) {
struct priv_net_struct *priv = netdev_priv(ndev);
int pk_counter, ret;
/* Get data packet count received by ndev */
pk_counter = my_device_reg_read(priv, REG_PKT_CNT);
if (pk_counter > priv->max_pk_counter) {
/* update statics count */
priv->max_pk_counter = pk_counter;
}
ret = pk_counter;
/* Set receive buffer to start */
priv->next_pk_ptr = KNOWN_START_REGISTER;
whle(pk_counter-- > 0)
{
/* Capture data packet from ndev or transmitter */
my_hw_rx(ndev);
}
return ret;
}
/*
* Hardware receiver function
* To read buffer memory, and update FIFO pointer to free buffer
*/
static void my_hw_rx(struct net_device *ndev) {
struct priv_net_struct *priv = netdev_priv(ndev);
struct sk_buff *skb = NULL;
u16 erxrdpt, next_packet, rxstat;
u8 rsv[RSV_SIZE];
int packet_len;
packet_len = my_device_read_current_packet_size();
if ((priv->next_pk_ptr > RXEND_INIT)) {
/* packet address corrupted: reset RX logic */
/* Update RX error stats */
ndev->stats.rx_errors++;
return;
}
/* Read next packet pointer and RX stat */
my_device_reg_read(priv, priv->next_pk_ptr, sizeof(rsv), rsv);
/* Check RX error stat register */
if (an_error_is_detected_in_device_status_register()) {
/*
* will do:
* stats.rx_errors++;
* ndev->stats.rx_crc_errors++;
* ndev->stats.rx_frame_errors++;
* ndev->stats.rx_over_errors++
*/
} else {
skb = ndev_alloc_skb(ndev, len + NET_IP_ALIGN);
if (!skb) {
ndev->stats.rx_dropped++;
} else {
skb_reserve(skb, NET_IP_ALIGN);
/*
* Copy packet from receiver buffer to sk buffer
* `skb_put()` return pointer to head of buffer
*/
my_netdev_mem_read(priv,
rx_packet_tart(priv->next_pk_ptr),
len, skb_put(skb, len));
/* Set packet protocol ID */
skb->protocol = eth_type_trans(skb, ndev);
/* Update RX stats */
ndev->stats.rx_packets++;
ndev->stats.rx_bytes += len;
/* submit sk buffer to network layer */
netif_rx_ni(skb);
}
}
/* Move RX read pointer to head of next received packet */
priv->next_pk_ptr = my_netdev_update_reg_next_pkk();
}
/**
* 数据包发送
* 当内核将数据包从设备发送出去时,会调用驱动程序的`ndo_start_xmit()`方法。
* 成功返回NETDEV_TX_OK,失败返回NETDEV_TX_BUSY,并且在失败时无法对套接字
* 缓冲区执行任何操作,因为缓冲区仍然被网络队列层拥有。因此不能修改任何skb字段
* 或释放skb缓冲区。该函数通过自旋锁保护,避免并发调用。
* 数据包发送在大多数情况下都是异步完成的。传输数据包的sk_buff由上层方法传入。
* 其data字段包含要发送的数据包。驱动程序从sk_buff->data获取数据包并将其写入
* 设备硬件FIFO队列,或在将其写入设备硬件FIFO队列前放在临时TX缓冲区(比如设备
* 发送前需要某种大小的数据)。只有在FIFO达到阈值(通常由驱动程序定义,或在驱动
* 数据手册中提供),或通过设置特殊寄存器中的位(类似于触发器),有意让驱动程序
* 启动传输时,数据才能真正发送。
*
* 驱动程序需要通知内核在硬件准备好接收新数据之前不要开始任何传输。
* void netif_stop_queue(struct net_device *dev);
* 发送数据包后,NIC会引发中断。中断处理检查中断产生的真实原因。并更新统计信息,
* 通知内核该设备已经准备好可以发送新的数据包。
* void netif_wake_queue(struct net_device *dev);
*
* 总之,包的传输分为2部分:
* 1. `ndo_start_xmit`:通知内核,设备繁忙,重新配置,开始传输
* 2. `TX interrupt`:更新TX统计信息,通知内核,设备可再次使用
* `ndo_start_xmit`函数大致分为:
* 1. 在网络设备上调用`netif_stop_queue()`,通知内核设备忙于数据传输
* 2. 将sk_buff->data写入设备FIFO队列
* 3. 指示设备开始传输
* 4. 根据设备时内存映射型还是位于SPI总线上(在总线上的设备可能睡眠),将分别
* 在hwirq处理程序中执行,或在工作(线程化IRQ)中调度执行
* - 检查中断是否是传输中断
* - 读取传输描述符状态寄存器,查看数据包的状态
* - 如果传输中有任何问题,则增加错误统计
* - 递增成功传输数据包的统计数据
* - 调用`netif_wake_queue()`,启动传输队列,并允许内核再次调用驱动程
* 序的`ndo_start_xmit()`方法,开始下一个循环
*/
/* part of `ndo_start_xmit()` */
NIT_WORK(&priv->tx_work, my_netdev_hw_tx);
static netdev_tx_t my_netdev_start_xmit(struct sk_buff *skb,
struct net_device *dev) {
struct priv_net_struct *priv = netdev_priv(dev) ;
/* Inform device will be busy */
netif_stop_queue(dev);
/* Store sk_buffer to transmit */
priv->tx_skb = skb;
/* Copy sk_buff->data to HW FIFO queue */
schedule_work(&priv_tx_work);
/* successfully end */
return NETDEV_TX_OK:
}
/* Hardware transmit function */
static void my_netdev_hw_tx(struct priv_net_struct *priv) {
/* Write packet data to HW device TX buffer register */
my_netdev_packet_write(priv, priv->tx_skb->len, priv->tx_skb->data);
/* If this net device support write verification, then execute... */
/* Set TX request flag, so hardware will execute transmission */
my_netdev_reg_bitset(priv, ECON1, ECON1_TXRTS);
}
4.2.3 状态和控制
设备控制是指内核自行需要或响应用户操作,而更改接口属性这种情况。它可以使用struct net_device_ops结构公开的操作,也可以使用ethtool工具,但需要为驱动程序引入一组hook。
4.3 网卡驱动demo
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/ethtool.h>
#include <linux/skbuff.h>
#include <linux/slab.h>
#include <linux/of.h> /* For DT*/
#include <linux/platform_device.h> /* For platform devices */
struct eth_struct {
int bar;
int foo;
struct net_device *dummy_ndev;
};
static int fake_eth_open(struct net_device *dev) {
printk("fake_eth_open called\n");
/* We are now ready to accept transmit requests from
* the queueing layer of the networking.
*/
netif_start_queue(dev);
return 0;
}
static int fake_eth_release(struct net_device *dev) {
pr_info("fake_eth_release called\n");
netif_stop_queue(dev);
return 0;
}
static int fake_eth_xmit(struct sk_buff *skb, struct net_device *ndev) {
pr_info("dummy xmit called...\n");
ndev->stats.tx_bytes += skb->len;
ndev->stats.tx_packets++;
skb_tx_timestamp(skb);
dev_kfree_skb(skb);
return NETDEV_TX_OK;
}
static int fake_eth_init(struct net_device *dev)
{
pr_info("fake eth device initialized\n");
return 0;
};
static const struct net_device_ops my_netdev_ops = {
.ndo_init = fake_eth_init,
.ndo_open = fake_eth_open,
.ndo_stop = fake_eth_release,
.ndo_start_xmit = fake_eth_xmit,
.ndo_validate_addr = eth_validate_addr,
.ndo_validate_addr = eth_validate_addr,
};
static const struct of_device_id fake_eth_dt_ids[] = {
{ .compatible = "packt,fake-eth", },
{ /* sentinel */ }
};
static int fake_eth_probe(struct platform_device *pdev)
{
int ret;
struct eth_struct *priv;
struct net_device *dummy_ndev;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
dummy_ndev = alloc_etherdev(sizeof(struct eth_struct));
dummy_ndev->if_port = IF_PORT_10BASET;
dummy_ndev->netdev_ops = &my_netdev_ops;
/* If needed, dev->ethtool_ops = &fake_ethtool_ops; */
ret = register_netdev(dummy_ndev);
if(ret) {
pr_info("dummy net dev: Error %d initalizing card ...", ret);
return ret;
}
priv->dummy_ndev = dummy_ndev;
platform_set_drvdata(pdev, priv);
return 0;
}
static int fake_eth_remove(struct platform_device *pdev)
{
struct eth_struct *priv;
priv = platform_get_drvdata(pdev);
pr_info("Cleaning Up the Module\n");
unregister_netdev(priv->dummy_ndev);
free_netdev(priv->dummy_ndev);
return 0;
}
static struct platform_driver mypdrv = {
.probe = fake_eth_probe,
.remove = fake_eth_remove,
.driver = {
.name = "fake-eth",
.of_match_table = of_match_ptr(fake_eth_dt_ids),
.owner = THIS_MODULE,
},
};
module_platform_driver(mypdrv);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_DESCRIPTION("Fake ethernet driver");
测试:
root@taylor-host:/home/c-project/fake-eth# insmod fake-eth.ko
root@taylor-host:/home/c-project/fake-eth# insmod eth-ins.ko
[Sun Jan 23 16:34:03 2022] fake-eth added
[Sun Jan 23 16:34:16 2022] fake-fake removed
[Sun Jan 23 16:37:14 2022] fake eth device initialized
6: eth0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
4.4 驱动程序方法
驱动程序方法是probe函数和remove函数,负责向内核注册/移除网络设备。驱动必须通过struct net_device结构的设备方法,将其功能提供给内核,基础结构如:
static const struct net_device_opts my_netdev_ops {
.ndo_open = my_netdev_open;
.ndo_stop = my_netdev_close;
.ndo_start_xmit = my_netdev_start_xmit;
.ndo_set_rx_mode = my_netdev_set_multicast_list;
.ndo_set_mac_address = my_netdev_set_mac_address;
.ndo_tx_timeout = my_netdev_tx_timeout;
.ndo_change_mtu = eth_change_mtu;
.ndo_validate_addr = eth_validate_addr;
};
4.4.1 probe()函数
probe函数很基础,只需要执行设备的早期初始化(init),然后将网络设备注册到内核中,一般分为8个步骤:
- 使用
alloc_etherdev()函数,分配网络设备及其私有数据; - 初始化私有数据字段(互斥锁、自旋锁、工作队列等)。如果设备位于访问函数可能睡眠的总线上(如SPI),则应该使用工作队列和互斥锁。另一种场景是使用线程化IRQ;
- 初始化总线特定的参数和功能(SPI、USB、PCI等);
- 请求和映射资源(I/O内存、DMA通道、IRQ);
- 如有必要,生成随机MAC并分配给设备;
- 填写必须的netdev属性,包括
if_port、irq、netdev_ops、ethtool_ops等; - 将设备置于低功耗状态;
- 调用
register_netdev()函数,注册设备;
4.4.2 remove()函数
驱动程序的清理与释放函数。会从内核中删除网络设备,并清理NIC本身占用内存、私有数据内存以及网络设备内部分配的内存,类似于:
static int my_netdev_remove(struct spi_device *spi) {
struct priv_net_struct *priv = spi_get_drvdata(spi);
unregister_netdev(priv->netdev);
free_irq(spi->irq, priv);
free_netdev(priv->netdev);
return 0
}
参考
Linux设备驱动开发, John Madieu著, 袁鹏飞,刘寿永译

浙公网安备 33010602011771号