Linux字符设备驱动(设备文件,用户空间与内核空间进行数据交互,ioctl接口)

这篇具有很好参考价值的文章主要介绍了Linux字符设备驱动(设备文件,用户空间与内核空间进行数据交互,ioctl接口)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

在Linux系统中“一切皆文件”,上一篇讲述了cdev结构体就描述了一个字符设备驱动,主要包括设备号和操作函数集合。但是要怎么操作这个驱动呢?例如,使用open()该打开谁,read()该从哪读取数据等等。所以就需要创建一个设备文件来代表设备驱动。

应用程序要操纵外部硬件设备,需要像和普通文件一样,使用open(),read(),write()(初始化cdev时实现的操作函数)等系统调用来操作设备文件间接实现控制外部硬件设备。注册设备驱动后想要创建相对应的设备文件有两种方式:手动创建自动创建

手动创建:

加载驱动模块之后,使用mknod命令在/dev目录下创建设备文件。

mknod 设备文件路径 文件类型 主设备号 次设备号

设备文件路径:/dev/xxx

文件类型:c代表字符设备,b代表块设备

设备文件代表设备驱动,用主次设备号来关联。

字符设备文件,Linux,Linux驱动,linux,驱动开发,硬件工程

自动创建:

每次新添加一个驱动都手动创建感觉非常麻烦,所以比较推荐自动创建设备文件。

新添加一个头文件。

#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,不用管和功能没有太大的关系

//返回设备类指针

销毁设备类

有创建自然有销毁。

void class_destroy(struct class *cls);

 创建设备文件也叫设备节点

创建好一个设备类,调用 device_create函数就可以在/dev目录下创建相应的设备节点。

struct device *device_create(struct class *class, struct device *parent,
                                              dev_t devt, void *drvdata, const char *fmt, ...)

参数依次对应:设备类指针, 父设备指针,设备号, 额外数据, "设备文件名"

//父设备指针:如果有些设备之间有依赖关系,就可以传入父设备指针,没有就不需要

销毁设备文件

void device_destroy(struct class *class, dev_t devt);

驱动错误处理

接下来说说怎么判断这些函数是否创建成功 ,有人可能觉得返回类型不都是指针吗,直接判断指针是不是NULL就好了。想一想如果都是返回NULL那你能判断是哪一步出现了错误,错误的原因是什么吗。事实上有很多函数的返回类型是指针,结果有三种分别是合法指针、NULL指针和非法指针。

那怎么判断函数返回的指针是否为有效地址呢?使用IS_ERR宏去检查函数的返回值,如果地址落在0xfffffffffffff000~0xffffffffffffffff范围(64位系统),表示函数执行失败,IS_ERR宏返回真。在Linux中函数执行错误返回的非法地址对应着一个错误码,使用PTR_ERR宏把相应的非法地址转换成错误码,每个错误码都用不一样的含义,感兴趣可以去errno.h中查看。

IS_ERR(指针)                      //返回真,表示指针出错

IS_ERR_OR_NULL(指针)   //返回真,表示指针出错(可判断空指针)

PTR_ERR(指针)                  //将出错的指针转换成错误码

ERR_PTR(错误码)              //将错误码转成指针

 说了那么多我们来写一个测试一下。

#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!");//描述信息

字符设备文件,Linux,Linux驱动,linux,驱动开发,硬件工程

从上图可以看出这样就不需要手动创建设备文件了。 

这里需要说明一下我们现在编写字符设备驱动的流程是: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, ...) ;

参数:

fd - 文件描述符

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 修饰。

字符设备文件,Linux,Linux驱动,linux,驱动开发,硬件工程

应用测试模块cdd_test 

 字符设备文件,Linux,Linux驱动,linux,驱动开发,硬件工程

这大概可以算字符设备的基本框架吧,里面有些东西没有细说,如果感兴趣可自行百度。

好了,如果对以上内容有什么疑问或建议欢迎在评论区里提出来^-^。文章来源地址https://www.toymoban.com/news/detail-662722.html

到了这里,关于Linux字符设备驱动(设备文件,用户空间与内核空间进行数据交互,ioctl接口)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux 驱动之字符设备

    什么是设备号 Linux 规定每一个字符设备或者块设备都必须有一个专属的设备号。一个设备号由主设备号和次设备号组成。主设备号用来表示某一类驱动,如鼠标,键盘都可以归类到 USB 驱动中。而次设备号是用来表示这个驱动下的各个设备。比如第几个鼠标,第几个键盘等。

    2024年02月16日
    浏览(39)
  • Linux 驱动之高级字符设备

    什么是IO呢? IO 的英文全称是 input 和output,翻译过来就是输入和输出。 在冯.诺依曼结构中,将计算机分成分为5个部分: 运算器、控制器、存储器、输入设备、输出设备 。其中输入设备指的是向计算机输入数据或者信息,如鼠标,键盘都是输入设备。输出设备指的是用于接收

    2023年04月14日
    浏览(41)
  • Linux -- 字符设备驱动--LED的驱动开发(初级框架)

    看原理图确定引脚,确定引脚输出什么电平才能点亮 / 熄灭 LED 看主芯片手册,确定寄存器操作方法:哪些寄存器?哪些位?地址是? 编写驱动:先写框架,再写硬件操作的代码 注意 :在芯片手册中确定的寄存器地址被称为 物理地址 ,在 Linux 内核中无法直接使用。 需要使

    2024年04月28日
    浏览(37)
  • LDD学习笔记 -- Linux字符设备驱动

    字符驱动程序用于与Linux内核中的设备进行交互; 字符设备指的是像内存区域这样的硬件组件,通常称为伪设备; 用户空间应用程序通常使用 open read write 等系统调用与这些设备通信; 把用户空间的系统调用连接到设备驱动的系统调用实现方法上。 内核的虚拟文件系统 vir

    2024年02月02日
    浏览(45)
  • Linux下字符设备驱动开发以及流程介绍

    首先我们介绍一下什么是字符设备,然后讲解一下字符设备开发的具体的流程,分别详细介绍每一个流程中涉及到的结构体以及知识点,最后我们编写代码实现字符设备的开发以及测试。 Linux内核设计哲学是把所有的东西都抽象成文件进行访问,这样对设备的访问都是通过文

    2024年02月01日
    浏览(41)
  • Linux设备驱动开发学习笔记(等待队列,锁,字符驱动程序,设备树,i2C...)

    container_of函数可以通过结构体的成员变量检索出整个结构体 函数原型: 内核开发者只实现了循环双链表,因为这个结构能够实现FIFO和LIFO,并且内核开发者要保持最少代码。 为了支持链表,代码中要添加的头文件是linux/list.h。内核中链表实现核心部分的数据结构 是struct li

    2024年01月22日
    浏览(55)
  • 【linux驱动开发】在linux内核中注册一个杂项设备与字符设备以及内核传参的详细教程

    开发环境: windows + ubuntu18.04 + 迅为rk3568开发板 相较于字符设备,杂项设备有以下两个优点: 节省主设备号:杂项设备的主设备号固定为 10,在系统中注册多个 misc 设备驱动时,只需使用子设备号进行区分即可。 使用简单:相比如普通的字符设备驱动, misc驱动只需要将基本信

    2024年01月21日
    浏览(51)
  • 嵌入式培训机构四个月实训课程笔记(完整版)-Linux ARM驱动编程第五天-ARM Linux编程之字符设备驱动(物联技术666)

    链接:https://pan.baidu.com/s/1V0E9IHSoLbpiWJsncmFgdA?pwd=1688 提取码:1688 教学内容: 1 、内核模块的简单框架: __init __exit 执行完后就释放空间 简单框架:包含三个部分 1)模块初始化和模块退出函数 2)注册模块函数 3)模块许可 //*************************************************** #include linux

    2024年02月21日
    浏览(47)
  • 驱动开发--字符驱动设备2

    字符设备驱动 1.定义 以字节流的形式进行访问,且只能顺序访问的设备,针对字符设备编写的驱动叫做字符设备驱动 2.字符设备框架 用户空间通过IO函数如open、read、write、close等函数接口,调用内核空间中的字符设备驱动函数中的用户自定义的open、read、write、close等函数,通

    2024年02月15日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包