在Linux系统中“一切皆文件”,上一篇讲述了cdev结构体就描述了一个字符设备驱动,主要包括设备号和操作函数集合。但是要怎么操作这个驱动呢?例如,使用open()该打开谁,read()该从哪读取数据等等。所以就需要创建一个设备文件来代表设备驱动。
应用程序要操纵外部硬件设备,需要像和普通文件一样,使用open(),read(),write()(初始化cdev时实现的操作函数)等系统调用来操作设备文件间接实现控制外部硬件设备。注册设备驱动后想要创建相对应的设备文件有两种方式:手动创建和自动创建。
手动创建:
加载驱动模块之后,使用mknod命令在/dev目录下创建设备文件。
mknod 设备文件路径 文件类型 主设备号 次设备号
设备文件路径:/dev/xxx
文件类型:c代表字符设备,b代表块设备
设备文件代表设备驱动,用主次设备号来关联。
自动创建:
每次新添加一个驱动都手动创建感觉非常麻烦,所以比较推荐自动创建设备文件。
新添加一个头文件。
#include <linux/device.h>
创建设备类
内核中定义了struct class结构体,一个struct class结构体类型变量对应一个类, 内核同时提供了class_create函数,可以用它来创建一个类,这个类存放于/sys/class下面。
//原型是一个宏,主要使用里面__class_create函数。
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
owner:类的所有者, 固定是 THIS_MODULE
name:类名,可随意起名struct class * __class_create(模块所有者, 设备类名);
//参数中还有一个key,不用管和功能没有太大的关系
//返回设备类指针
有创建自然有销毁。
创建设备文件也叫设备节点
创建好一个设备类,调用 device_create函数就可以在/dev目录下创建相应的设备节点。
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)参数依次对应:设备类指针, 父设备指针,设备号, 额外数据, "设备文件名"
//父设备指针:如果有些设备之间有依赖关系,就可以传入父设备指针,没有就不需要
销毁设备文件
驱动错误处理
接下来说说怎么判断这些函数是否创建成功 ,有人可能觉得返回类型不都是指针吗,直接判断指针是不是NULL就好了。想一想如果都是返回NULL那你能判断是哪一步出现了错误,错误的原因是什么吗。事实上有很多函数的返回类型是指针,结果有三种分别是合法指针、NULL指针和非法指针。
那怎么判断函数返回的指针是否为有效地址呢?使用IS_ERR宏去检查函数的返回值,如果地址落在0xfffffffffffff000~0xffffffffffffffff范围(64位系统),表示函数执行失败,IS_ERR宏返回真。在Linux中函数执行错误返回的非法地址对应着一个错误码,使用PTR_ERR宏把相应的非法地址转换成错误码,每个错误码都用不一样的含义,感兴趣可以去errno.h中查看。
IS_ERR(指针) //返回真,表示指针出错
IS_ERR_OR_NULL(指针) //返回真,表示指针出错(可判断空指针)
PTR_ERR(指针) //将出错的指针转换成错误码
说了那么多我们来写一个测试一下。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>
//起始次设备号
#define CDD_MINOR 0
//设备号个数
#define CDD_COUNT 1
//设备号
dev_t dev;
//声明cdev
struct cdev cdd_cdev;
//设备类指针
struct class *cdd_class;
//设备指针
struct device *cdd_device;
int cdd_open(struct inode *inode, struct file *filp)
{
printk("enter cdd_open!\n");
return 0;
}
ssize_t cdd_read(struct file *filp, char __user *buf, size_t size, loff_t *offset)
{
printk("enter cdd_read!\n");
return 0;
}
ssize_t cdd_write(struct file *filp, const char __user *buf, size_t size, loff_t *offset)
{
printk("enter cdd_write!\n");
return 0;
}
long cdd_ioctl(struct file *filp, unsigned int cmd, unsigned long data)
{
printk("enter cdd_ioctl!\n");
return 0;
}
int cdd_release(struct inode *inode, struct file *filp)
{
printk("enter cdd_release!\n");
return 0;
}
//声明操作函数集合
struct file_operations cdd_fops = {
.owner = THIS_MODULE,
.open = cdd_open,
.read = cdd_read,
.write = cdd_write,
.unlocked_ioctl = cdd_ioctl,//ioctl接口
.release = cdd_release,//对应用户close接口
};
//加载函数
int cdd_init(void)
{
int ret;
//1.动态申请设备号
ret = alloc_chrdev_region(&dev, CDD_MINOR, CDD_COUNT, "cdd_demo");
if(ret<0){
printk("alloc_chrdev_region failed!\n");
goto failure_register_chrdev;
}
// 2.注册cdev
//初始化
cdev_init(&cdd_cdev, &cdd_fops);
//将cdev添加到内核
ret = cdev_add(&cdd_cdev, dev, CDD_COUNT);
if(ret<0){
printk("cdev_add failed!\n");
goto failure_cdev_add;
}
// 3.注册设备类
/*成功会在/sys/class目录下出现cdd_class子目录*/
cdd_class = class_create(THIS_MODULE, "cdd_class");
if(IS_ERR(cdd_class)){
printk("class_create failed!\n");
ret = PTR_ERR(cdd_class);
goto failure_class_create;
}
// 4.创建设备文件
cdd_device = device_create(cdd_class, NULL, dev,NULL, "cdd");
if(IS_ERR(cdd_device)){
printk("device_create failed!\n");
ret = PTR_ERR(cdd_device);
goto failure_device_create;
}
return 0;
failure_device_create:
class_destroy(cdd_class);
failure_class_create:
cdev_del(&cdd_cdev);
failure_cdev_add:
unregister_chrdev_region(dev, CDD_COUNT);
failure_register_chrdev:
return ret;
}
//卸载函数
void cdd_exit(void)
{
//销毁设备文件
device_destroy(cdd_class, dev);
//注销设备类
class_destroy(cdd_class);
//销毁cdev
cdev_del(&cdd_cdev);
//注销设备号
unregister_chrdev_region(dev, CDD_COUNT);
}
//声明为模块的入口和出口
module_init(cdd_init);
module_exit(cdd_exit);
MODULE_LICENSE("GPL");//GPL模块许可证
MODULE_AUTHOR("xin");//作者
MODULE_VERSION("2.0");//版本
MODULE_DESCRIPTION("charactor driver!");//描述信息
从上图可以看出这样就不需要手动创建设备文件了。
这里需要说明一下我们现在编写字符设备驱动的流程是:1、注册设备号 2、注册 添加cdev 3、创建设备类 4、创建设备文件。如果我们在创建设备设备文件时出现了错误,那我们不仅需要返回错误码,还需要把之前的设备号注销、销毁cdev和注销设备类,就是哪一步出错了,就需要把之前的复原。这就体现了goto语句用来处理多步骤错误处理的好处了。
注册设备号,注册cdev合二为一
字符设备驱动的流程有4步,内核中提供了一个register_chrdev函数来把第一步和第二部合并。
int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops);
参数:
major - 主设备号
name - 设备号名字
fops - 操作函数集合
静态申请成功返回0,动态申请成功返回主设备号,失败返回负数
注意:该函数的第一个参数不为0,就静态申请设备号,第一个参数为0,就动态申请设备号,该函数会自动初始化好cdev,并添加到内核中
该函数是调用内核级__register_chrdev函数实现其功能,多添加了2个参数起始此设备号和次设备号的范围。
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}该函数的调用将为给定的主设备号注册0~255作为次设备号,并为每个设备建立一个对应的默认cdev结构。使用这一接口的驱动程序必须能够处理所有256个次设备号上的open调用(无论它们是否真正对应于实际的设备),而且不能使用大于255的主设备号和次设备号。
将使用上面方法创建的设备从系统中移除使用以下函数。
int unregister_chrdev(unsigned int major, const char *name);
major和name必须与传递给register_chrdev函数的值保持一致,否则该调用会失败。
注意这种方式是早期内核驱动程序注册字符设备驱动程序的方法,现在不推荐使用。
访问字符设备文件=使用字符设备驱动
字符设备文件都有设备号,当我们操作字符设备文件时,内核会通过设备号去找到相同设备号cdev然后把cdev中的file_operations赋值给file结构中的file_operations,最后调用file里file_operations中我们写好的操作函数。所以我们访问字符设备文件就相当于使用了字符设备驱动。
内核和用户空间进行数据交互
需要的头文件:#include <asm/uaccess.h>
Linux中内核空间和用户空间是隔离的,互相之间不能直接访问,地址空间也相互独立。内核中提供用户空间到内核空间之间数据拷贝的方法。
copy_to_user(用户地址,内核地址,大小) // 从内核空间--->用户空间
copy_from_user(内核地址,用户地址,大小) //从用户空间--->内核空间
//在内核中属于用户空间的地址需要用 __user 修饰
注:copy_to_user和copy_from_user调用时可能导致睡眠,某些禁止睡眠的场合不能使用。
ioctl接口
ioctl是Linux专门为用户层控制设备设计的系统调用接口,这个接口具有极大的灵活性,我们的设备打算让用户通过哪些命令实现哪些功能。
用户空间使用ioctl
需要的头文件:#include <sys/ioctl.h>
int ioctl(int fd, int cmd, ...) ;
参数:
返回值:执行成功时返回 0,失败则返回 -1 并设置全局变量 errorno 值
驱动程序使用ioctl
需要的头文件:#include <linux/ioctl.h>
long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long data);
ioctl 命令(cmd)的统一格式
将一个32位int型划分为4个部分
设备类型 序列号 方向 数据尺寸
8bit 8bit 2bit 14bit
//设备类型,可以是0~0xff之间的数称为幻数,其主要作用是使 ioctl 命令有唯一的设备标识
//序列号,表示当前命令是整个ioctl命令中的第几个,从0开始计数
//方向,表示数据的传输方向,可以为_IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,代表四种访问模式:无数据、读数据、写数据、读写数据
//数据尺寸,表示涉及的用户数据的大小
构造ioctl命令还是比较繁琐的,内核提供了宏来方便用户构造ioctl命令。
_IO(设备类型,序列号) //没有参数的命令
_IOR(设备类型,序列号,数据尺寸) //该命令是从驱动读取数据
_IOW(设备类型,序列号,数据尺寸) //该命令是从驱动写入数据
_IOWR(设备类型,序列号,数据尺寸) //双向数据传输的命令
有生成命令的宏,也有拆分命令的宏。
_IOC_DIR(cmd) //从命令中提取方向
_IOC_TYPE(cmd) //从命令中提取幻数
_IOC_NR(cmd) //从命令中提取序数
_IOC_SIZE(cmd) //从命令中提取数据大小
说了这么多,还是用一个简单的示例来演示一下 ,把注册设备号,注册cdev合二为一,内核和用户空间进行数据交互,ioctl接口都用上。
用户和内核空间共用的头文件,里面是ioctl命令的构成和头文件。
#ifndef __IOTEST_H
#define __IOTEST_H
#include <linux/ioctl.h>
//定义设备类型(幻数)
#define IOC_MAGIC 'x'
#define HELLO_DEMO _IO(IOC_MAGIC,0)
#define HELLO_READ _IOR(IOC_MAGIC,1,int)
#define HELLO_WRITE _IOW(IOC_MAGIC,2,int)
#endif
驱动模块
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include "iotest.h"
#define CDD_MINOR 0
//设备号
dev_t dev;
//声明cdev
struct cdev cdd_cdev;
//设备类指针
struct class *cdd_class;
//设备指针
struct device *cdd_device;
//内核缓冲区
char arr[128] = {0};
int data = 1;
int cdd_open(struct inode *inode, struct file *filp)
{
printk("enter cdd_open!\n");
return 0;
}
//在内核中属于用户空间的地址需要用 __user 修饰
ssize_t cdd_read(struct file *filp, char __user *buf, size_t size, loff_t *offset)
{
int ret;
printk("enter cdd_read!\n");
if(size>127)
size = 127;//数据不够长,取最长的数据
ret = copy_to_user(buf, arr, size);
if(ret)
return -EFAULT;
return size;
}
//在内核中属于用户空间的地址需要用 __user 修饰
ssize_t cdd_write(struct file *filp, const char __user *buf, size_t size, loff_t *offset)
{
int ret = 0;
printk("enter cdd_write!\n");
if(size>127)
return -ENOMEM;//越界
//拷贝数据
ret = copy_from_user(arr, buf, size);
if(ret)
return -EFAULT;
printk("arr = %s\n",arr);
return size;
}
long cdd_ioctl(struct file *filp, unsigned int cmd, unsigned long val)
{
int ret = 0;
printk("enter cdd_ioctl!\n");
//不同的命令对应不同的操作
switch(cmd){
case HELLO_DEMO:
printk("HELLO_DEMO!\n");
break;
case HELLO_READ:
{
ret = copy_to_user((int __user *)val, \
&data, sizeof(int));
}
printk("HELLO_READ!\n");
break;
case HELLO_WRITE:
{
ret = copy_from_user(&data, \
(int __user *)val, sizeof(int));
printk("HELLO_WRITE data = %d\n",data);
}
break;
default:
return -EINVAL;
}
return 0;
}
int cdd_release(struct inode *inode, struct file *filp)
{
printk("enter cdd_release!\n");
return 0;
}
//声明操作函数集合
struct file_operations cdd_fops = {
.owner = THIS_MODULE,
.open = cdd_open,
.read = cdd_read,
.write = cdd_write,
.unlocked_ioctl = cdd_ioctl,//ioctl接口
.release = cdd_release,//对应用户close接口
};
//加载函数
int cdd_init(void)
{
int ret;
// 1.注册字符设备驱动
ret = register_chrdev(0, "cdd_demo", &cdd_fops);
if(ret<0){
printk("register_chrdev failed!\n");
goto failure_register_chrdev;
}
//构建设备号
dev = MKDEV(ret,CDD_MINOR);
printk("register_chrdev success!\n");
// 2.注册设备类
/*成功会在/sys/class目录下出现cdd_class子目录*/
cdd_class = class_create(THIS_MODULE, "cdd_class");
if(IS_ERR(cdd_class)){
printk("class_create failed!\n");
ret = PTR_ERR(cdd_class);
goto failure_class_create;
}
// 3.创建设备文件
cdd_device = device_create(cdd_class, NULL, dev,NULL, "cdd");
if(IS_ERR(cdd_device)){
printk("device_create failed!\n");
ret = PTR_ERR(cdd_device);
goto failure_device_create;
}
return 0;
failure_device_create:
class_destroy(cdd_class);
failure_class_create:
unregister_chrdev(MAJOR(dev), "cdd_demo");
failure_register_chrdev:
return ret;
}
//卸载函数
void cdd_exit(void)
{
//销毁设备文件
device_destroy(cdd_class, dev);
//注销设备类
class_destroy(cdd_class);
//注销字符设备驱动
unregister_chrdev(MAJOR(dev), "cdd_demo");
}
//声明为模块的入口和出口
module_init(cdd_init);
module_exit(cdd_exit);
MODULE_LICENSE("GPL");//GPL模块许可证
MODULE_AUTHOR("xin");//作者
MODULE_VERSION("3.0");//版本
MODULE_DESCRIPTION("charactor driver!");//描述信息
测试模块
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <unistd.h>
#include "iotest.h"
int main()
{
char ch = 0;
char w_buf[10] = "welcome";
char r_buf[10] = {0};
int data = 5;
int fd = open("/dev/cdd",O_RDWR);
if(fd==-1){
perror("open");
exit(-1);
}
printf("open successed!fd = %d\n",fd);
while(1){
ch = getchar();
getchar();
if(ch=='q')
break;
switch(ch){
case 'r':
read(fd,r_buf,sizeof(r_buf));
printf("r_buf = %s\n",r_buf);
break;
case 'w':
write(fd,w_buf,sizeof(r_buf));
break;
case 'd':
ioctl(fd,HELLO_DEMO);
break;
case 'i':
{
ioctl(fd,HELLO_READ,&data);
printf("ioread data=%d\n",data);
}
break;
case 'o':
ioctl(fd,HELLO_WRITE,&data);
break;
default:
printf("error input!\n");
break;
}
sleep(1);
}
close(fd);
return 0;
}
在驱动模块中使用两种不同的方式进行用户空间和内核空间的数据交互,一种是在read()和write()中交换字符串数据,一种是在ioctl命令中交换int型数据。需要注意在内核中属于用户空间的地址需要用 __user 修饰。
应用测试模块cdd_test
这大概可以算字符设备的基本框架吧,里面有些东西没有细说,如果感兴趣可自行百度。
好了,如果对以上内容有什么疑问或建议欢迎在评论区里提出来^-^。