Linux模块(2) - 创建设备节点

今天计划将之前写的模块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

下一章Linux模块(3) - 资源映射

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值