今天计划将之前写的模块Linux模块(1) - 加载与卸载填充一下,达到设备节点创建的功能。
1. 全局数据
模块中有许多数据结构是要动态申请和释放的,就少不了一些全局数据来标记这些信息,这里通过宏MODULE_MISC_DEVICE
来区分下杂项设备
和普通设备的注册流程。
#include <linux/misdevice.h>
#include <linux/cdev.h>
struct driver_info { // 自定义结构体类型
#ifdef MODULE_MISC_DEVICE // 将模块作为杂项设备自动注册
struct miscdevice miscdev; // 杂项设备结构体,会自动创建设备节点
#else // 将模块作为普通设备自动注册
struct cdev cdev; // 设备结构体,可以使用动态内存申请,也可以静态定义
struct class *class; // 用于自动创建设备节点
#endif
}
atomic_t mac_instance_num = ATOMIC_INIT(0); // 设备实例化的个数,用于创建设备文件时的数字后缀,避免多核驱动加载时产生竞争,采用原子类型
1.1 设备节点
linux设备文件都有一个主设备号
和次设备号
,主设备号
相同的设备使用相同的驱动,次设备号
决定了设备实例化的个数,部分主设备号
保留给特定的设备,其他值可以由用户自由使用。设备号可以在编写模块时指定,指定的设备号不能与系统中现有的所有设备冲突,也可以动态申请。
// include/linux/kdev_t.h --- 定义了`dev_t`的结构是一个`uint32_t`的数
#define MINORBITS 20 // 次设备号使用低20位,主设备号使用高12位
#define MINORMASK ((1U<<MINORIBITS)-1)
#define MAJOR(dev) ((unsigned int)((dev))>>MINORBITS) // 提取主设备号
#define MINOR(dev) ((unsigned int)((dev)&MINORMASK)) // 提取次设备号
#define MKDEV(ma, mi) (((ma)<<MINORBITS)|(mi)) // 合成设备号
1.2 misdevice
杂项设备
是指主设备号
固定为10
的设备,内核为杂项设备提供了一套统一的驱动,在驱动内部再根据次设备号
调用模块中用户定义的接口。
// drivers/char/misc.c
// 向内核注册设备信息,同时创建设备节点,设备节点的名称即misc->name
int misc_register(struct misdevice *misc)
{
...
bool is_dynamic = (misc->minor == MISC_DYNAMIC_MINOR); // 杂项设备的次设备号支持指定和动态申请
...
}
// 注销设备,删除设备节点
void misc_deregister(struct miscdevice *misc)
{
...
}
1.3 cdev
字符设备
是Linux常用设备中的一种,内核对此抽象了一个结构体struct cdev
。
// fs/char_dev.c
// 动态申请一个结构体
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
// ktype_cdev_dynamic中定义了一个release()函数,该函数会用于内存的释放
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
// 静态定义的初始化
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof(*cdev));
INIT_LIST_HEAD(&cdev->list);
// ktype_cdev_default中定义了一个release()函数,该函数仅用于从内核中移除相关信息
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops; // 比cdev_alloc()多了一步
}
// 填充设备信息,并添加到内核中
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
...
p->dev = dev; // 赋值设备号
p->count = count; // 赋值设备编号,即第几个设备
...
}
// 移除设备信息,无论动态申请的还是静态初始化的都使用该函数,动态的内存会自动释放
void cdev_del(struct cdev *p)
{
...
}
1.4 class
这一部分暂未做详细了解,大致意思是通过struct class
在文件系统中创建/sys/class/mac
文件夹,里面记录了设备节点的信息,再由udev
来创建对应的设备文件。
// include/linux/device.h
// 动态申请一个class数据结构并注册,class_create本身是一个宏定义
struct class *class_create(struct module *ower, const char *name)
{
...
}
// drivers/base/class.c
// 注销并释放
void class_destroy(struct class *class)
{
...
}
创建了class
之后还需要根据class
创建一个设备并注册到文件系统中。
// drivers/base/core.c
// 创建一个设备,`parent`和`drvdata`可以为`NULL`,`fmt`表示设备文件的名称
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
{
...
// 返回值可能是负值,也可能是正常的指针,应当使用`IS_ERR()`进行判断,使用`PTR_ERR()`转换结果为错误值
}
// 注销设备文件
void device_destroy(struct class *class, dev_t devt)
{
...
}
// include/linux/err.h
static inline bool IS_ERR(void *ptr)
{
return unlikely((unsigned long)(void*)ptr >= (unsigned long)-4095);
}
static inline long PTR_ERR(void *ptr)
{
return (long)ptr;
}
1.5 设备号
// fs/char_dev.c
// 动态申请一个主设备号,和一段连续的次设备号,并完成注册
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, uinsigned count, const char *name)
{
...
__register_chrdev_region(0, baseminor, count, name); // 0表示申请新的主设备号,非0表示沿用主设备号
{
find_dynamic_major(); // 查找空闲主设备号
{
// fs/char_dev.c
#define CHRDEV_MAJOR_HASH_SIZE 255 // 静态分配
// include/linux/fs.h
#define CHRDEV_MAJOR_MAX 512 // 设定最大主设备号为512
#define CHRDEV_MAJOR_DYN_END 234 // 第一区域,明显第一区域被静态分配了无法申请
#define CHRDEV_MAJOR_DYN_EXT_START 511
#define CHRDEV_MAJOR_DYN_EXT_END 384 // 第二区域倒着分配
}
...
*dev = MKDEV(cd->major, cd->baseminor); 返回申请到的起始设备号
return 0; // 0成功
}
// 静态注册一段连续的设备号
int register_chrdev_region(dev_t form, unsigned count, const char *name)
{
...
__register_chrdev_region(MAJOR(n), MINOR(n), next-n, name); // 按主设备号进行注册
...
}
// 取消注册,对于动态申请的会给予释放
void unregister_chrdev_region(dev_t form, unsigned count)
{
...
}
1.6 设备计数
一份驱动可能会适配到多个硬件设备,则设备名需要递增,就需要一个静态数据进行累加,该数据需要原子属性,该属性通常由汇编实现,与CPU架构有关。
#include <linux/atomic.h>
typedef struct {
int counter; // 就是个普通的整形,只是在执行读写操作时要采用专用函数,由函数确保原子操作
} atomic_t;
// arch/csky/include/asm/atomic.h
int atomic_add_return(int i, atomic_t *v); // 加i并返回最新值
int atomic_sub_return(int i, atomic_t *v); // 减i并返回最新值
...
2. probe
模块加载后,若驱动和设备的名称匹配,则会调用struct platform_driver->probe()
函数,则我们可以再probe()
中完成设备的初始化和注册动作。
int mac_probe(struct platform_device *pdev)
{
struct driver_info *mac;
int ret, instance_num;
struct device *device;
// 使用devm_前缀的函数,能够自动进行内存的释放,当然也可以手动释放
// 申请一个结构体用于标记我们申请的各种内存,以便最后释放
mac = mdev_kzalloc(&pdev->dev, sizeof(struct driver_info), GFP_KERNEL); // 错误返回NULL
if (unlikely(mac == NULL)) { // 预期不会出现
de_err(&pdev->dev, "alloc mac failed\n"); // 内核日志函数,具备打印等级控制的功能
ret = -ENOMEM; // 内存不足
goto go_ret; // 驱动加载过程较多故需要采用goto语句,逐层返回
}
instance_num = atomic_add_return(1, &mac_instance_num) - 1; // 获取当前已注册设备的数量,从0开始,后续若注册失败也不再回减,以免发生冲突
#ifdef MODULE_MISC_DEVICE // 杂项设备注册流程
mac->miscdev.minor = MISC_DYNAMIC_MINOR; // 设置次设备号进行动态分配,当然也可以手动指定
mac->miscdev.fops = &mac_file_operations; // 设备文件操作接口,我们稍后定义
mac->miscdev.parent = &pdev_dev; // 暂不清楚作用
// C库中也有一个asprintf()函数作用类似
mac->miscdev.name = devm_kasprintf(&pdev->dev, GFP_KERNEL, "%s%d", "mac", instance_num); // 申请一块内存,用于存放格式化的字符串,该字符串用于设备文件名,错误返回NULL
if (unlikely(mac->miscdev.name == NULL)) {
de_err(&pdev->dev, "alloc miscdev name failed\n");
ret = -ENOMEM; // 内存不足
goto go_kfree_mac;
}
ret = misc_register(&mac->miscdev); // 杂项设备注册,返回值使用IS_ERR()检查
if (IS_ERR(ret)) {
de_err(&pdev->dev, "misc register[%s] failed\n", mac->miscdev.name);
goto go_kfree_name;
}
#else // 以下是普通设备的注册流程
cdev_init(&mac->cdev, &mac_file_operations); // 静态初始化cdev,设备文件操作接口我们稍后定义
mac->cdev.owner = THIS_MODULE; // 指定当前模块为cdev拥有者,THIS_MODULE,是一个结构体指针
ret = alloc_chrdev_region(&mac->cdev.dev, instance_num, 1, "mac"); // 申请设备号,当然也可以不用该函数,手动指定
if (IS_ERR(ret)) {
de_err(&pdev->dev, "alloc chrdev failed\n");
goto go_kfree_mac;
}
ret = cdev_add(&mac->cdev, mac->cdev.dev, 1); // 注册cdev
if (IS_ERR(ret)) {
de_err(&pdev->dev, "cdev add failed\n");
goto go_unregister_chrdev;
}
mac->class = class_create(THIS_MODULE, "mac"); // 创建class,用于设备文件的创建
if (IS_ERR(mac->class)) {
de_err(&pdev->dev, "class create failed\n");
goto go_cdev_del;
}
// 创建设备文件,文件名为mac%u格式化后的字符串
device = device_create(mac->class, NULL, mac->cdev.dev, NULL, "mac%u", instance_num);
if (IS_ERR(device)) {
de_err(&pdev->dev, "device create failed\n");
ret = PTR_ERR(device);
goto go_class_destroy;
}
#endif
platform_set_drvdata(pdev, mac); // 等同于pdev->dev.driver_data = mac;
dev_info(&pdev->dev, "mac");
return 0;
#ifdef MODULE_MISC_DEVICE
goto go_misc_deregister; // 不会运行到这
go_misc_deregister:
misc_deregister(&mac->miscdev);
go_kfree_name:
devm_kfree(&pdev->dev, (void *)mac->miscdev.name);
#else
goto go_device_destory; // 不会运行到这
go_device_destory:
device_destory(mac->class, mac->cdev.dev); // mac->cdev.dev是设备号
go_class_destroy:
class_destroy(mac->class);
go_cdev_del:
cdev_del(&mac->cdev);
go_unregister_chrdev:
unregister_chrdev_region(mac->cdev.dev, 1); // mac->cdev.dev是设备号
#endif
go_kfree_mac:
devm_kfree(&pdev->dev, mac);
go_ret:
return ret;
}
3. remove
有加载就有卸载,我们将probe()
函数goto
的内容拷贝过来即可。
int mac_remove(struct platform_device *pdev)
{
struct driver_info *mac = platform_get_drvdata(pdev); // 等同于pdev->dev.driver_data
#ifdef MODULE_MISC_DEVICE
misc_deregister(&mac->miscdev);
devm_kfree(&pdev->dev, (void *)mac->miscdev.name);
#else
device_destory(mac->class, mac->cdev.dev); // mac->cdev.dev是设备号
class_destroy(mac->class);
cdev_del(&mac->cdev);
unregister_chrdev_region(mac->cdev.dev, 1); // mac->cdev.dev是设备号
#endif
devm_kfree(&pdev->dev, mac);
}
4. 文件操作接口
Linux下一切皆文件,内核对文件的操作也做了抽象。
// include/linux/fs.h
struct file_operations {
struct module *owner;
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
int (*open)(struct inode *, struct file *);
int (*release)(struct *inode *, struct file *); // 实现close的动作
...
}
暂时先实现几个空函数用于测试。
static int mac_open(struct inode *inode, struct file *file)
{
printk("%s\n", __func__);
return 0;
}
static int mac_close(struct *inode *, struct file *)
{
printk("%s\n", __func__);
return 0;
}
static ssize_t mac_read(struct file *file, char __user dst*, size_t len, loff_t *off)
{
printk("%s\n", __func__);
return 0;
}
static ssize_t mac_write(struct file *file, const char __user *src, size_t len, loff_t *off)
{
printk("%s\n", __func__);
return 0;
}
static struct file_operations mac_file_operations = {
.owner = THIS_MODULE,
.open = mac_open;
.release = mac_close,
.read = mac_read,
.write = mac_write,
};
5. 用户程序
目前模块已经有设备文件了,就可以通过用户程序打开或关闭设备文件了。
main.c
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main(void) { int fd; fd = open("/dev/mac0", O_RDWR); if (fd < 0) { perror("/dev/mac0"); } close(fd); return 0; }
编译、下载(略)、运行
# 主机上交叉编译 $ csky-abiv2-linux-gcc main.c -mcpu=ck860 -O2 -W -Wall -o test # 板卡运行 $ insmod mac.ko [77401.056269] mac mac.0: mac # dev_info(&pdev->dev, "mac");打印的 $ ./test # 执行用户程序 [77405.099229] mac_open # mac_open()函数正常调用了 [77405.101611] mac_close $ ls /dev/mac0 -l crw-rw---- 1 root root 250, 0 Jan 1 21:29 /dev/mac0 # 字符设备文件,设备号250,0 $ cat /proc/devices # 这里记录了系统中所有的设备号信息 Character devices: ... 250 mac ... $ cat /sys/class/mac/mac0/dev # 查看/sys/class中的文件 250:0 $ rmmod [77912.745661] mac_release