【linux驱动开发】在linux内核中注册一个杂项设备与字符设备以及内核传参的详细教程

文章目录

开发环境: windows + ubuntu18.04 + 讯为rk3568开发板

注册杂项设备

相较于字符设备,杂项设备有以下两个优点:

  • 节省主设备号:杂项设备的主设备号固定为 10,在系统中注册多个 misc 设备驱动时,只需使用子设备号进行区分即可。
  • 使用简单:相比如普通的字符设备驱动, misc驱动只需要将基本信息通过结构体传递给相应处理函数即可。

在linxu系统中可使用cat /proc/misc命令查看系统中的杂项设备。注册杂项设备的步骤:

  • 1.填充设备操作集结构体struct file_operations

  • 2.填充杂项设备结构体struct miscdevice;

  • 3.使用函数misc_register注册杂项设备;

  • 4.使用函数misc_deregister卸载杂项设备;

上面三步可使用下面函数直观用表达,即:

c 复制代码
static struct file_operations xxx_fops{
	.owner = THIS_MODULE, 
	.read = xxx_read, 
	....
};
struct miscdevice xxx_dev{
	.minor = MISC_DYNAMIC_MINOR, 
	.name = "xxx", 
	.fops = &xxx_fops
};
static int __init xxx_init(void) //驱动入口函数
{
	int ret;
	printk(KERN_EMERG "xxx_init\r\n");
	ret = misc_register(&xxx_dev);//注册杂项设备
	if(ret<0)
	{
		printk( "misc_register failed\r\n");
		return -1;
	}
	printk( "misc_register ok\r\n");
	return 0;
}
static void __exit xxx_exit(void) //驱动出口函数
{
	printk(KERN_EMERG "xxx_exit\r\n");
	misc_deregister(&xxx_dev); //卸载杂项设备
}
module_init(xxx_init); //注册入口函数
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxx");

具体实现注册一个杂项设备的示例代码如下:

c 复制代码
#include <linux/kernel.h>
#include <linux/init.h>              //初始化头文件
#include <linux/module.h>            //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h>        //注册杂项设备头文件
#include <linux/fs.h>                //注册设备节点的文件结构体
#include <linux/uaccess.h>

// 打开杂项设备
int _open(struct inode *inode,struct file*file)
{
	printk(KERN_EMERG"hello misc");
	return 0;
}

// 关闭杂项设备
int close(struct inode * inode, struct file *file)
{
	printk(KERN_EMERG"close");
	return 0;
}

// 读取杂项设备中的数据
ssize_t misc_read (struct file *file, char __user *buff, size_t size, loff_t *loff)
{
	char kbuff[32] = "kernel";
	if(copy_to_user(buff,kbuff,strlen(kbuff)) != 0) // 将内核中的数据给应用
	{
		printk("copy_to_user error\r\n");
		return -1;
	}	
	printk(KERN_EMERG"copy_to_user is successful\r\n");
	
	return size;
}

// 写入数据到杂项设备中
ssize_t misc_write (struct file *file, const char __user *buff, size_t size, loff_t *loff)
{
	char kbuff[32] ;
	if(copy_from_user(kbuff,buff,size)!= 0) // 从应用那儿获取数据
	{
		printk("copy_from_user error\r\n");
		return -1;
	}	
	printk(KERN_EMERG"copy_from_user data:%s\r\n",kbuff);

	return size;
}

// 设备文件描述集  
struct file_operations misc_fops ={
	.owner = THIS_MODULE,
	.open = misc_open,
	.release = close,
	.read = misc_read,
	.write = misc_write
};

struct miscdevice misc_dev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "hello_misc", // 杂项设备名   注册成功后会在 /dev目录下显示
	.fops = &misc_fops
};

// 驱动的入口函数
static int __init misc_init(void)
{
	int ret = 0;
	ret = misc_register(&misc_dev);
	if(ret < 0)
		printk(KERN_EMERG"misc register is error\r\n.");
	else
		printk(KERN_EMERG"misc register is seccussful\r\n.");
	return 0;
}

//驱动的出口函数
static void __exit misc_exit(void)
{
	misc_deregister(&misc_dev);
	printk(KERN_EMERG"baibai\r\n");
}

module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zhouxianjie0716@qq.com");

编译传送到开发板上后,先试用insmod +驱动名.ko挂载驱动,其结果为:

上述驱动代码的测试代码如下:

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc,char *argv[])
{
	char filePath[] = "/dev/hello_misc";
	// 打开文件
	int fd = open(filePath,O_RDWR);
	if(fd < 0)
	{
		printf("opening is failed.\n");
		return -1;
	}
	else
		printf("opening is successful.\n");

	// 读取
	char buff1[32],buff2[32] = "hello this is app";
	read(fd,buff1,sizeof(buff1));
	printf("buff1 is %s.\n",buff1);
	
	// 写入
	write(fd,buff2,sizeof(buff2));	

	close(fd);
	return 0;
}

使用./+程序名运行测试代码后,得结果如下:


驱动模块传参

总所周知,应用程序传参是通过shell终端传,只要将main函数按照下面格式书写即可完成传参操作

c 复制代码
int main(int argc ,char *argv[])
{
	return 0;
}

相比之下,驱动模块传递参数需要借助其他函数完成传参操作:

1. 传递单个参数给内核

c 复制代码
module_param(name, type, perm)

参数解释:

  • name:参数名,既是外部参数名,又是内部参数名。

  • type:参数的数据类型,可取int、charp等。

  • perm:访问权限。八进制,如:0777。0表示该参数在文件系统中不可见。

注意:传递字符作为参数时数据类为charp,而不是char.

2.传递数组给内核

c 复制代码
module_param_array(name, type, nump, perm)
  • module_param_array(name,type,nump,perm)
  • name:数组参数名,既是外部参数,又是内部参数
  • type:参数的数据类型
  • nump:终端传给数组的实际元素个数(指针变量)
  • perm:访问权限,0644。0表示该参数在文件系统中不可见

3.传递字符串给内核

c 复制代码
module_param_string(name, string, len, perm)
  • name:参数名,外部参数名
  • string:内部参数名(内部字符数组名)
  • len:数组长度
  • perm:访问权限,0644。0表示该参数在文件系统中不可见

传递参给内核的作用:

  • 1.设置驱动的相关参数,如:设备数量、设备缓冲区大小等等。
  • 2.可进行安全校验,放置驱动被他人盗用。

说明

参数传递的适用时机为 加载驱动到内核时,命令形式为:insmod +驱动名.ko +参数1名=参数值 参数2名=参数值 ......。例如下面示例中使用insmod file.ko date=12传递参数:

c 复制代码
#include <linux/module.h>
#include <linux/init.h>

// 保存参数
static int date;
static int date1;
static int data[5];
static int count;
static char str[32];
static char *strData;

// 传递单个参数
module_param(date,int,S_IRUGO|S_IWUSR); // 可读可写
module_param(date1,int,S_IRUGO); // 可读可写
module_param(strData,charp,S_IRUGO); // 可读

// 传递多个参数
module_param_array(data,int,&count,S_IRUGO); // 可读
module_param_string(str,str,sizeof(str),S_IRUGO); // 可读

// 驱动的入口函数
static int __init dev_init(void)
{
	int i=0;
	if(strcmp(str,"myTest")!=0)
	{
		printk("dev_init error\r\n");
		return -1;
	}
	printk("---------------------------------------\r\n");
	printk(KERN_EMERG"dev_init is successful!\r\n");
	for(i=0;i<count;++i)
		printk("data[%d] = %d \r\n",i,data[i]);
	printk("str:%s  strData:%s\r\n",str,strData);
	printk("date:%d  count:%d\r\n",date,count);
	
	data[1] = 111;
	date = 10;
	date1 = 111;
	
	return 0;
}

//驱动的出口函数
static void __exit dev_exit(void)
{
	int i=0;
	for( i=0;i<count;++i)
		printk("data[%d] = %d \r\n",i,data[i]);
	printk("date:%d  count:%d\r\n",date,count);
	
	printk(KERN_EMERG"dev_exit is successful\r\n");
}

module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zhouxianjie0716@qq.com");

注册字符设备

步骤一:驱动初始化,需要申请设备号,初始化并且注册cdev结构体,初始化硬件;

可使用动态申请或静态申请设备号,其中动态申请设备号一般在235-255,静态申请则一般由用户手动输入。

静态申请的函数原型为:

c 复制代码
 int register_chrdev_region(dev_t, unsigned, const char *);

参数含义:

  • from: 自定义的 dev_t 类型设备号。
  • count: 申请设备的数量。
  • name: 申请的设备名称。
    函数返回值:申请成功返回 0,申请失败返回负数。

动态申请的函数原型为:

c 复制代码
 int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);

参数含义:

  • dev : 会将申请完成的设备号保存在 dev 变量中。
  • baseminor: 次设备号可申请的最小值。
  • count: 申请设备的数量。
  • name: 申请的设备名称。
    函数返回值:申请成功返回 0,申请失败返回负

Linux 内核中将字符设备抽象成一个具体的数据结构 (struct cdev), 我们可以理解为字符设备对象,cdev 记录了字符设备号、内核对象、文件操作 file_operations 结构体(设备的打开、读写、关闭等操作接口)等信息:

c 复制代码
struct cdev {
	struct kobject kobj; //内嵌的内核对象.
	struct module *owner; //该字符设备所在的内核模块的对象指针. 
	const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结
	构体.struct list_head list; //用来将已经向内核注册的所有字符设备形成链表. 
	dev_t dev; //字符设备的设备号,由主设备号和次设备号构成. 
	unsigned int count; //隶属于同一主设备号的次设备号的个数. 
};

初始化设备描述集cdev结构体的函数原型为:

c 复制代码
void cdev_init(struct cdev *, const struct file_operations *);

参数含义:

  • 参数1:表示是抽象设备结构体;
  • 参数2:表示文件操作集;

注册设备到内驱使用下面函数

c 复制代码
int cdev_add(struct cdev *, dev_t,  unsigned);

参数含义:

  • 参数1:为要添加的 struct cdev 类型的结构体
  • 参数2:为申请的字符设备号
  • 参数3:为和该设备关联的设备编号的数量
    若函数在内核中添加成功返回 0,添加失败返回负数。

步骤二:构建设备文件操作描述集file_operations

也就是read、write、open、close等函数。

步骤三:生成并且添加设备节点

  • 手动添加设备节点 :也就是在加载驱动到内核时添加,即mknod + 路径/设备名 +设备类型 + 主设备号 + 次设备号
  • 自动添加设备节点 :初始化内核时,使用函数自动添加。即先试用函数class_create创建一个类,在使用函数device_create创建并且添加设备节点。

class_create函数说明:

c 复制代码
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \ __class_create(owner, name, &__key); \
})

函数作用:

用于动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加进 Linux 内核系统。

参数含义:

  • owner:指向函数即将创建的这个 struct class 的模块。一般为 THIS_MODULE。
  • name:代表即将创建的 struct class 变量的名字。
    返回值:struct class * 类型的类。

device_create函数说明:

c 复制代码
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt,...);

函数作用:

用来在 class 类中下创建一个设备属性文件,udev 会自动识别从而进行设备节点的创建。

参数含义:

  • cls:指定所要创建的设备所从属的类。
  • parent:指定该设备的父设备,如果没有就指定为 NULL。
  • devt:指定创建设备的设备号。
  • drvdata:被添加到该设备回调的数据,没有则指定为 NULL。
  • fmt:添加到系统的设备节点名称。

返回值:struct device * 类型结构体的设备

步骤四:注销字符设备驱动,

  • 步骤一:使用函数unregister_chrdev_region释放设备号
c 复制代码
void unregister_chrdev_region(dev_t, unsigned)

该函数只有一个参数,为要删除设备的设备号,并且函数无返回值。

  • 步骤二:使用函数cdev_del步骤二:删除设备操作集卸载cdev
c 复制代码
void cdev_del(struct cdev *);

该函数只有一个参数,为要删除的 struct cdev 类型的结构体,并且函数无返回值。

  • 步骤三:使用函数device_destroy卸载设备
c 复制代码
void device_destroy(struct class *cls, dev_t devt);

用来删除 cls 类中的devt设备属性文件,udev 会自动识别从而进行设备节点的删除。

  • 步骤四:使用函数class_destroy删除类class
c 复制代码
 void class_destroy(struct class *cls);

该函数只有一个参数,为要删除的类,并且函数无返回值。

示例代码

c 复制代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

#define DEVICE_NUMBER 1 // 设备数量
#define DEVICE_SNAME "schrdev" // 静态申请时设备名
#define DEVICE_ANAME "achrdev" // 动态申请时设备名
#define DEVICE_MINOR_NUM 0 // 次设备号起始地址
#define DEVICE_CLASS_NAME "myTestClass" // 类名
#define DEVICE_MYNAME "mytest"

// 打开设备
int chrdev_open(struct inode*inode,struct file*file)
{
	printk("chrdev_open is opened\r\n");

	return 0;
}

// 保存设备号 其中前12位为主设备号 后20位为次设备号
static dev_t dev_num;

// 定义主设备号 次设备号
static int major_num,minor_num; 

// 设备信息描述集
struct cdev cdev;

// 类描述集
struct class *cls;

// 设备描述集
struct device*device;

// 传递单个参数
module_param(major_num,int,S_IRUGO); // 可读
module_param(minor_num,int,S_IRUGO); // 可读

// 文件操作集
struct file_operations chrdev_opr = {
	.owner = THIS_MODULE,
	.open = chrdev_open
};

// 驱动的入口函数
static int __init dev_init(void)
{
	int ret;
	/* 步骤一:申请设备号 */
	// 有传递主设备号就静态申请
	if(major_num)
	{	
		dev_num = MKDEV(major_num, minor_num);//将主设备号 次设备号合并成设备号
		//参数分别表示 设备号 设备数量 设备名称
		ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME); 
		if(ret < 0)
		{
			printk("dev_init error\r\n");
			return -1;
		}
	}	
	// 否则就动态申请
	else
	{
		// 参数:设备号 次设备号起始地址 设备数量 设备名称
		ret = alloc_chrdev_region(&dev_num , DEVICE_MINOR_NUM, DEVICE_NUMBER, DEVICE_ANAME);
		if(ret < 0)
		{
			printk("dev_init error\r\n");
			return -1;
		}
		// 获取主设备号 次设备号
		major_num = MAJOR(dev_num);
		minor_num = MINOR(dev_num);
	}
	printk("---------------------------------------\r\n");
	printk("dev_num:%d major_num:%d minor_num:%d\r\n",dev_num,major_num,minor_num);

	/* 步骤二:初始化设备 */
	// 初始化cdev
	cdev.owner = THIS_MODULE;
	// 初始化设备  
	cdev_init(&cdev, &chrdev_opr);

	/* 步骤三:注册设备到内核 */
	//添加(注册)到内核 参数: 设备   设备号  设备数量
	cdev_add(&cdev, dev_num, DEVICE_NUMBER);

	/* 步骤四:先创建类再自动创建添加设备名称*/
	// 创建类 参数1: 类的归属   参数2: 类名
	cls = class_create(THIS_MODULE,DEVICE_CLASS_NAME);
	// 创建设备 参数1: 归属到类   参数2:设备的父设备 参数3:设备号 参数4:添加到设备的回调数据 参数5:设备名
	device = device_create(cls,NULL,dev_num,NULL,DEVICE_MYNAME);
	printk("auto add device name\r\n");
	return 0;
}

//驱动的出口函数
static void __exit dev_exit(void)
{
	/* 步骤一:注销设备号 参数:设备号   设备数量 */ 
	unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER);
	/* 步骤二:删除设备操作集 */
	cdev_del(&cdev);
	/* 步骤三:删除设备*/
	device_destroy(cls,dev_num);
	/* 步骤四: 删除类*/
	class_destroy( cls);
	
	printk(KERN_EMERG"dev_exit is successful\r\n");
}

module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zhouxianjie0716@qq.com");

使用加载驱动到内核insmod file.ko命令,得结果

在测试驱动前需要使用mknod /dev/mytest c 236 0创建设备文件,命令格式为:mknod + /路径/设备名称 +设备类型+主设备号 + 次设备号

然后就可写一个应用程序来测试我们的字符驱动是否成功注册,测试源码如下:

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc,char* argv[])
{
	char name[] = "/dev/mytest";

	int fd = open(name,O_RDONLY); // 仅读
	if(fd < 0)
	{
		printf("open is failed\n");
		return -1;
	}

	close(fd);

	return 0;
}

执行测试程序后结果如下:

注意:在运行测试程序时,一定要先创建驱动文件,否则就会报段错误,具体如下:


杂项设备与字符设备的比较

  • 杂项设备的主设备号固定为10,而字符设备的主设备号需要创建。
  • 杂项设备的创建相对简单,只需要填充设备操作集结构体、杂项设备结构体再注册即可。而字符设备需要经过 申请设备号、初始化并且注册cdev结构体、初始化硬件、构建设备文件操作描述集、生成并且添加设备节点。
  • 杂项设备与字符设备都需要构建文件操作描述集。(应用层调用驱动的核心)
相关推荐
跃渊Yuey9 分钟前
【Linux】线程同步与互斥
linux·笔记
杨江10 分钟前
seafile docker安装说明
运维
舰长11512 分钟前
linux 实现文件共享的实现方式比较
linux·服务器·网络
好好沉淀17 分钟前
Docker开发笔记(详解)
运维·docker·容器
zmjjdank1ng26 分钟前
Linux 输出重定向
linux·运维
路由侠内网穿透.28 分钟前
本地部署智能家居集成解决方案 ESPHome 并实现外部访问( Linux 版本)
linux·运维·服务器·网络协议·智能家居
树℡独31 分钟前
ns-3仿真之应用层(三)
运维·服务器·ns3
VekiSon1 小时前
Linux内核驱动——基础概念与开发环境搭建
linux·运维·服务器·c语言·arm开发
zl_dfq1 小时前
Linux 之 【进程信号】(signal、kill、raise、abort、alarm、Core Dump核心转储机制)
linux
Ankie Wan1 小时前
cgroup(Control Group)是 Linux 内核提供的一种机制,用来“控制、限制、隔离、统计”进程对系统资源的使用。
linux·容器·cgroup·lxc