Linux驱动入门(五)——简单的字符设备驱动程序

news2024/11/15 23:57:39

文章目录

  • 前言
  • 字符设备驱动程序框架
    • 字符设备和块设备
    • 主设备号和次设备号
    • 申请和释放设备号
  • 初识cdev结构
    • cdev结构体
    • file_operations结构体
    • cdev和file_operation结构体的关系
    • inode结构体
  • 字符设备驱动的组成
    • 字符设备加载和卸载函数
    • file_operations结构体和其他成员函数
    • 驱动程序与应用程序的数据交换
    • 字符设备驱动程序组成小结
  • VirtualDisk字符设备驱动
    • VirtualDisk的头文件、宏和设备结构体
    • 加载和卸载驱动程序
    • cdev的初始化和注册
    • 打开和释放函数
    • 读写函数
    • seek函数
    • ioctl()函数
  • 小结


前言

在Linux设备驱动程序的家族中,字符设备驱动程序是较为简单的驱动程序,同时也是应用非常广泛的驱动程序。所以学习字符设备驱动程序,对构建Linux设备驱动程序的知识结构非常重要。本篇博客将带领读者编写一个完整的字符设备驱动程序。

字符设备驱动程序框架

本节对字符设备驱动程序框架进行了简要的分析。字符设备驱动程序中有许多非常重要的概念,下面将从最简单的概念讲起:字符设备和块设备。

字符设备和块设备

Linux系统将设备分为3类:字符设备、块设备和网络接口设备。其中字符设备可块设备难以区分,下面对其进行重要讲解。
1、字符设备
字符设备是指那些只能一个字节一个字节读写数据的设备,不能随机读取设备内存中的某一数据。其读取数据需要按照先后顺序,从这点来看,字符设备是面向数据流的设备。常见的字符有鼠标、键盘、串口、控制台和LED等设备。
2、块设备
块设备是指那些可以从设备的任意位置读取一定长度数据的设备。其读取数据不必按照先后顺序,可以定位到设备的某一具体位置,读取数据。常见的块设备有硬盘、磁盘、U盘、SD卡等。
3、字符设备和块设备的区分
每个字符设备或块设备都在/dev目录下对应一个设备文件。读者可以通过查看/dev目录下的文件的属性,来区分设备是字符设备还是块设备。使用cd命令进入/dev目录,并执行ls -l命令就可以查看设备的属性。
在这里插入图片描述
在这里插入图片描述
ls -l命令的第一字段中的第一字符c表示设备是字符设备,b表示设备是块设备。第234字段对驱动程序开发来说没有关系。第5,6字段分别表示设备的主设备号和次设备号,将在后面讲解。第7字段表示文件的最后修改时间。第8字段表示设备的名字。
由第1和8字段可知,adsp是字符设备,dm-0是块设备。其中adsp设备的主设备号是14,次设备号是12。

主设备号和次设备号

一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。
1、主设备号和次设备的表示
在Linux内核中,dev_t类型用来表示设备号。在Linux2.6.29.4中,dev_t定义为一个无符号长整型变量,如下,:

typedef u_long dev_t

u_long在32位机中是4字节,在64位机中是8字节。以32位机为例,其中高12位表示主设备号,低20位表示次设备号,如下如所示:
在这里插入图片描述
2、主设备号和次设备号的获取
为了写出可移植的驱动程序,不能假定主设备号和次设备号的位数。不同机型中,主设备号和次设备号的位数可能是不同的。应该使用MAJOR宏得到主设备号,使用MINOR宏来得到次设备号。下面是两个宏的定义:

#define MINORBITS 20 /*次设备号位数*/
#define MINORMASK ((1U << MINORBITES) - 1) /*次设备号掩码*/
#define MAJOR(dev) ((unsigned int)((dev) >> MINORBITS)) /*dev右移20位得到主设备号*/
#define mINOR(dev) ((unsigned int)((dev) & MINORMASK))  /*与次设备掩码与,得到次设备号*/

MAJOR宏将dev_t向右移动20位,得到主设备号;MAJOR宏将dev_t的高12位清零,得到次设备号。相反,可以将主设备号和次设备转换成设备类型(dev_t),使用宏MKDEV可以完成这个功能。

#define MKDEV(ma, mi)  (((ma) << MINORBITS) | (mi))

MKDEV宏将主设备号(ma)左移20位,然后与次设备号(mi)相与,得到设备号。
3、静态分配设备号
静态分配设备号,就是驱动程序开发者,静态地指定一个设备号。对于一部分常用的设备,内核开发者已经为其分配了设备号。这些设备号可以在内核源码documentation/devices.txt文件中找到。如果只有开发者自己使用这些设备驱动程序,那么其可以选择一个尚未使用过的设备号。在不添加新硬件的时候,这种方式不会产生设备号冲突。但是当添加新硬件时,则很可能造成设备冲突,影响设备的使用。
4、动态分配设备号
由于静态分配设备号存在冲突的问题,所以内核社区建议开发者使用动态分配设备号的方法。动态分配设备号的函数是alloc_chrdev_region(),该函数将在"申请和释放设备号"一节讲述。
5、查看设备号
当静态分配设备号时,需要查看系统中已经存在的设备号,从而决定使用那个新设备号。可以读取/proc/devices文件获得设备的设备号。/proc/devices文件包含字符设备和块设备的设备号。如下所示。
在这里插入图片描述

申请和释放设备号

内核维护着一个特殊的数据结构,用来存放设备号与设备的关系。在安装设备时,应该给设备申请一个设备号,使系统可以明确设备对应的设备号。设备驱动程序中的很多功能,是通过设备号来操作设备的。下面,首先对申请设备号进行简述。
1、申请设备号
在构建字符设备之前,首先要向系统申请一个或者多个设备号。完成该工作的函数是register_chrdev_region(),该函数在<fs/char_dev.c>中定义:

int register_chrdev_region(dev_t from, unsigned count, const char *name)

其中,from是要分配的设备号范围的起始值。一般只提供from主设备号,from的次设备号通常被设置成0。count是需要申请的连续设备号的个数。最后的name是和该范围编号相关联的设备名称,该名称不能超过64字节
和大多数内核函数一样,register_chrdev_region()函数成功时返回0。错误时,返回一个负的错误码,并且不能为字符设备分配设备号。下面是一个例子代码,其申请了CS5535_GPIO_COUNT个设备号。

retval = register_chrdev_region(dev_id, CS5535_GPIO_COUNT, NAME);

在Linux中有非常多的字符设备,在人为的为字符设备分配设备号时,很可能发生冲突。Linux内核开发者一直在努力将设备号变成动态的。可以使用alloc_chrdev_region()函数达到这个目的。

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

上面的函数中,dev作为输出参数,在函数返回成功后将保存已经分配的设备号。函数有可能申请一段连续的设备号,这是dev返回的第一个设备号。baseminor表示要申请的第一个次设备号,其通常设为0。countnameregister_chrdev_region()函数的对应参数一样。count表示要申请的连续设备号个数,name表示设备的名字。下面是一个例子代码,其申请了CS5535_CPIO_COUNT个设备号。

retval = alloc_chrdev_region(&dev_id, 0, CS5535_GPIO_COUNT, NAME);

2、释放设备号
使用上面两种方式申请的设备号,都应该在不使用设备时,释放设备号。设备号的释放统一使用下面的函数:

void unregister_chrdev_region(dev_t from, unsigned count);

在上面这个函数中,from表示要释放的设备号,count表示从from开始要释放的设备号个数。通常,在模块的卸载函数中调用unregister_chrdev_region()函数。

初识cdev结构

当申请字符设备的设备号后,这时,需要将字符设备注册到系统中,才能使用字符设备。为了理解这个实现过程,首先解释一下cdev结构体。

cdev结构体

在Linux内核中使用cdev结构体描述字符设备。该结构体是所有字符设备的抽象,其包含了大量字符设备所共有的特征。cdev结构体定义如下:

struct cdev{
	struct kobject kobj; /*内嵌的kobject结构,用于内核设备驱动模型的管理*/
	struct module *owner; /*指向包含该结构的模块的指针,用于引用计数*/
	const struct file_operations *ops; /*指向字符设备操作函数集的指针*/
	struct list_head list; /*该结构将使用该驱动的字符设备连接成一个链表*/
	dev_t dev;  /*该字符设备的起始设备号,一个设备可能有多个设备号*/
	unsigned int count; /*使用该字符设备驱动的设备数量*/
};

cdev结构中的kobj结构用于内核管理字符设备,驱动开发人员一般不使用该成员。ops是指向file_operations结构的指针,该结构定义了操作字符设备的函数。由于此结构体较为复杂,所以将在后面一节进行讲解。
dev就是用来存储字符设备所申请的设备号。count表示目前有多少个字符设备在使用该驱动程序。当使用rmmod卸载模块时,如果count成员不为0,那么系统不允许卸模块。
list结构是一个双向链表,用于将其他结构体连接成一个双向链表。该结构在Linux内核中广泛使用1,需要读者掌握。

struct list_head{
	struct list_head *next, *prev;	
};

在这里插入图片描述
如上图所示,cdev结构体的list成员连接到了inode结构体i_devices成员。其中i_devices也是一个list_head结构。这样,使cdev结构与inode结点组成了一个双向链表。inode结构体表示/dev目录下的设备文件,该结构体较为复杂,所辖将在下面讲述。
每一个字符设备在/dev目录下都有一个设备文件,打开设备文件就相当于打开相应的字符设备。例如应用程序打开设备文件A,那么系统会产生一个inode结点。这样可以通过inode结点的i_cdev字段找到c_dev字符结构体。通过cdevops指针,就能找到设备A的操作函数。对操作函数的讲解,将放在后面的内容中。

file_operations结构体

file_operations是一个对设备进行操作的抽象结构体。Linux内核的设计非常巧妙。内核允许为设备建立一个设备文件,对设备文件的所有操作,就相当于对设备的操作。这样的好处是,用户程序可以使用访问普通文件的方法访问设备文件,进而访问设备。这样的方法,极大地减轻了程序员的编程负担,程序员不必去熟悉新的驱动接口,就能访问设备。
对普通文件的访问常常使用open()、read()、write()、close()、ioctl()等方法。同样对设备文件的访问,也可以使用这些方法。这些调用最终会引起对file_operations结构体中对应函数的调用。对于程序员来说,只要为不同的设备编写不同的操作函数就可以了。
为了增加file_operations的功能,所以将很多函数集中在了该结构中。该结构的定义目前已经比较庞大了,其定义如下:

struct file_operations{
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const  struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	int (*ioctl) (struct inode *, struct file *, unsigned int , unsigned long);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, struct dentry *, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int , size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock)(struct file *, int , struct file_lock *);
	ssize_t (*splice_write) (struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read) (struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease) (struct file *, long, struct file_lock **);
};

下面对file_operations结构体的重要成员进行讲解。

  • owner成员根本不是一个函数;它是一个指向拥有这个结构模块的指针。这个成员用来维持模块的引用计数,当模块还在使用时,不能用rmmod卸载模块。几乎所有时刻,它被简单初始化为THIS_MODULE,一个在<linux/module.h>中定义的宏。
  • llseek()函数用来改变文件中的当前读/写位置,并将新位置返回。loff_t参数是一个"long long"类型,“long long”类型即使在32位机上也是64位宽。这是为了与64位机兼容而定的,因为64位机的文件大小完全可以突破4G。
  • read()函数用来从设备获取数据,成功时函数返回读取的字节数,失败时返回一个错误码。
  • write()函数用来写数据到设备中。成功时该函数返回写入的字节数,失败时返回一个负的错误码。
  • ioctl函数提供了一个执行设备特定命令的方法。例如使设备复位,这既不是读操作也不是写操作,不适合用read()write()方法来实现。如果在应用程序中给ioctl传入没有定义的命令,那么将返回-ENOTTY的错误,表示该设备不支持这个命令。
  • open()函数用来打开一个设备,在该函数中可以对设备进行初始化。如果这个函数被复制NULL,那么设备打开永远成功,并不会对设备产生影响。
  • release()函数用来释放open()函数中申请的资源,将在文件引用计数为0时,被系统调用。其对应应用程序的close()方法,但并不是每次调用close()方法,都会触发release()函数,在对设备文件的所有都释放后,才会被调用。

cdev和file_operation结构体的关系

一般来说,驱动开发人员会将特定设备的特定数据放到cdev结构体后,组成一个新的结构体。如下图,“自定义字符设备”中就包含特定设备的数据。该“自定义设备”中有一个cdev结构体。cdev结构体中有一个指向file_operations的指针。这里,file_operations中的函数就可以用来操作硬件,或者“自定义字符设备”中的其他数据,从而起到控制设备的作用。
在这里插入图片描述

inode结构体

内核使用inode结构在内部表示文件。inode一般作为file_operation结构中函数的参数传递过来。例如,open()函数将传递一个inode指针进来,表示目前打开的文件节点。需要注意的是,inode成员已经被系统赋予了合适的值,驱动程序只需要使用该结点中的信息,而不用更改。open()函数为:

int (*open) (struct inode *, struct file *);

inode结构中包含大量的有关文件的信息。这里,只对编写驱动程序有用的字段进行介绍,对于该结构更多的信息,可以参考内核源码。

  • dev_t i_rdev,表示设备文件对应的设备号。
  • struct list_head i_devices,该成员使设备文件连接到对应的cdev结构,从而对应到自己的驱动程序。
  • struct cdev *i_cdev该成员也指向cdev设备。
    除了从dev_t得到主设备号和次设备号外,这里还可以使用imajor()iminor()函数从i_rdev中得到主设备号和次设备号。
    imajor()函数在内部调用MAJOR宏,如下代码所示。
static inline unsigned imajor(const struct inode *inode)
{
	return MAJOR(inode->i_rdev); /*从inode->i_rdev中提取主设备号*/
}

同样,iminor()函数在内部调用MINOR宏,如下代码所示。

static inline unsigned iminor(const struct inode *inode)
{
	return MINOR(inode->i_rdev); /*从inode->i_rdev中提取次设备号*/
}

字符设备驱动的组成

了解字符设备驱动程序的组成,对编写驱动程序非常有用。因为字符设备在结构上都有很多相似的地方,所以只要会编写一个字符设备驱动程序,那么相似的字符设备驱动程序的编写,就不难了。在Linux系统中,字符设备驱动程序由以下几个部分组成。

字符设备加载和卸载函数

在字符设备的加载函数中,应该实现字符设备号的申请和cdev的注册。相反,在字符设备的卸载函数中应该实现字符设备号的释放和cdev的注销。
cdev是内核开发者对字符设备的一个抽象。除了cdev中的信息外,特定的字符设备还需要特定的信息,常常将特定的信息放在cdev之后,形成一个设备结构体,如代码中的xxx_dev
常见的设备结构体、加载函数和卸载函数如下面的代码:

struct xxx_dev    /*自定义设备结构体*/
{
	struct cdev dev;   /*cdev结构体*/
	...         /*特定设备的特定数据*/
};
static int __init xxx_init(void)
{
	...
	/*申请设备号,当xxx_major不为0时,表示静态指定;当为0时,表示动态申请*/
	if(xxx_major)
		result = register_chrdev_region(xxx_devno, 1, "DEV_NAME"); /*静态申请*/
	else
		result = alloc_chrdev_region(&xxx_devno, 0, 1, "DEV_NAME"); /*动态申请*/
		xxx_major = MAJOR(xxx_devno); /*获取申请的主设备号*/
	/*初始化cdev结构,并传递file_operations结构指针*/
	cdev_init(&xxx_dev.cdev, &xxx_fops);
	dev->cdev.owner = THIS_MODULE;         /*指定所属模块*/ 
	err = cdev_add(&xxx_dev.cdev, xxx_devno, 1); /*注册设备*/
}
static void __exit xxx_exit(void)   /*模块卸载函数*/
{
	cdev_del(&xxx_dev.cdev);  /*注销cdev*/
	unregister_chrdev_region(xxx_devno, 1); /*释放设备号*/
}

file_operations结构体和其他成员函数

file_operations结构体中的成员函数都对应着驱动程序的接口,用户程序可以通过内核来调用这些接口,从而控制设备。大多数字符设备驱动都会实现read()、write()和ioctl()函数,这三个函数的常见写法如下面代码所示。

/*文件操作结构体*/
static const struct file_operations xxx_fops =
{
	.owner = THIS_MODULE,  /*模块引用,任何时候都赋值THIS_MODULE*/
	.read = xxx_read,      /*指定设备的读函数*/
	.write = xxx_write,    /*指定设备的写函数*/
	.ioctl = xxx_ioctl     /*指定设备的控制函数*/
};
/*读函数*/
static ssize_t xxx_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
	...
	if(size>8)
		copy_to_user(buf,...,...); /*当数据较大时,使用copy_to_user(),效率较高*/
	else
		put_user(....,buf);    /*当数据较小时,使用put_user(),效率较高*/
	....
}
/*写函数*/
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
	...
	if(size>8)
		copy_from_user(...,buf,...); /*当数据较大时,使用copy_to_user(),效率较高*/
	else
		get_user(...,buf);      /*当数据较小时,使用put_user(),效率较高*/
	....
}
/*ioctl设备控制函数*/
static long xxx_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	...
	switch(cmd)
	{
		case xxx_cmd1:
			...  /*命令1执行的操作*/
			break;
		case xxx_cmd2:
			... /*命令2执行的操作*/
			break;
		default:
			return -EINVAL; /*内核和驱动程序都不支持该命令时,返回无效的命令*/
	}
	return 0;
}

文件操作结构体xxx_fops中保存了操作函数的指针。对于没有实现的函数,被赋值为NULLxxx_fops结构体在字符设备加载函数中,作为cdev_init()的参数,与cdev建立了关联。
设备驱动程序的read()和write()函数有同样的参数。filp是文件结构体的指针,指向打开的文件。buf是来自用户空间的数据地址,该地址不能再驱动程序中直接读取。size是要读的字节。ppos是读写的位置,其相当于文件的开头。
xxx_ioctl控制函数的cmd参数是事先定义的I/O控制命令,arg对应该命令的参数。

驱动程序与应用程序的数据交换

驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据破坏。
安全的方法是使用内核提供的专用函数,完成数据在应用程序和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有:

unsigned long copy_to_user(void __user *to, const void *from, unsigned long);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long);
put_user(local, user);
get_user(local, user);

字符设备驱动程序组成小结

字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operation结构体中的read()、write()、ioctl()等重要函数。如下图所示cdev结构体、file_operations和用户空间调用驱动的关系。
在这里插入图片描述

VirtualDisk字符设备驱动

后面部分都将以一个VirtualDisk设备为蓝本进行讲解。VirtualDisk是一个虚拟磁盘设备,在这个虚拟磁盘设备中分配了8K的连续内存空间,并定义了两个端口数据(port1和port2)。驱动程序可以对设备进行读写、控制和定位操作,用户空间的程序可以通过Linux系统调用访问VirtualDisk设备中的数据。

VirtualDisk的头文件、宏和设备结构体

VirtualDisk驱动程序应该包含必要的头文件和宏信息,并定义一个与实际设备相对应的设备结构体,相关的定义如下面的代码所示。

#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#define VIRTUALDISK_SIZE     0x2000  /*全局内存最大8K字节*/
#define MEM_CLEAR    0x1             /*全局内存清零*/
#define PORT1_SET    0x2             /*将port1端口清零*/
#define PORT2_SET    0x3             /*将port2端口清零*/
#define VIRTUALDISK_MAJOR  200        /*预设的VitrualDisk的主设备号为200*/

static int VirtualDisk_major = VIRTUALDISK_MAJOR;
/*VirtualDisk设备结构体*/
struct VirtualDisk
{
	struct cdev cdev;     /*cdev结构体*/
	unsigned char mem[VIRTUALDISK_SIZE]; /*全局内存8K*/
	int port1;      /*两个不同类型的端口*/
	long port2;
	long count;      /*记录设备目前被多少设备打开*/
};
  • 1~11行列出了必要的头文件,这些头文件中包含了驱动程序可能使用的函数。
  • 19~26行代码,定义了VirtualDisk设备结构体。其中包含了cdev字符设备结构体,和一块连续的8K的设备内存。另外定义了两个端口port1port2,用来模拟实际设备的端口。count表示设备被打开的次数。在驱动程序中,可以不将这些成员放在一个结构中,但放在一起的好处是借助了面向对象的封装思想,将设备相关的成员封装成了一个整体。
  • 22行定义了一个8K的内存块,驱动程序中一般不静态的分配内存,因为静态分配的内存的生命周期非常长,随着驱动程序生和死。而驱动程序一般运行咋系统的整个开机状态中,所以驱动程序分配的内存,一直不会得到释放。所以,编写驱动程序,应避免申请大块内存和静态分配内存。这里,只是为了方便演示,所以分配了静态内存。

加载和卸载驱动程序

前面已经对字符设备驱动程序的加载和卸载模板进行了介绍。VirtualDisk的加载和卸载函数也和前面介绍的相似,其实现如下:

/*设备驱动模型加载函数*/
int VirtualDisk_init(void)
{
	int result;
	dev_t devno = MKDEV(VirtualDisk_major, 0); /*构建设备号*/
	/*申请设备号*/
	if(VirtualDisk_major)
		result = register_chardev_region(devno, 1, "VirtualDisk");
	else
		result = alloc_chrdev_region(&devno, 0, 1, "VirtualDisk");
		VirtualDisk_major = MAJOR(devno); /*从申请设备号中得到主设备号*/
	if(result < 0)
		return result;
	/*动态申请设备结构体的内存*/
	struct VirtualDisk *Virtualdisk_devp = kmalloc(sizeof(struct VitualDisk), GFP_KERNEL);
	if(!Vitualdisk_devp) /*申请失败*/
	{
		result = -ENOMEM;
		goto fail_kmalloc;
	} 
	memset(Virtualdisk_devp, 0, sizeof(struct VirtualDisk));/*清零*/
	/*初始化并且添加cdev结构体*/
	VirtualDisk_setup_cdev(Virtualdisk_devp, 0);
	return 0;
fail_kmalloc:
	unregister_chrdev_region(devno, 1);
	return result;
}
/*模块卸载函数*/
void VirtualDisk_exit(void)
{
	cdev_del(&Vitualdisk_devp->cdev);  /*注销cdev*/
	kfree(Virtualdisk_devp);   /*释放设备结构体内存*/
	unregister_chrdev_region(MKDEV(VirtualDisk_major, 0), 1); /*释放设备号*/
}
  • 7~13行,使用两种方式申请设备号。VirtualDisk_major变量被静态定义为200。当加载模块时不使VirtualDisk_major等于0。那么就执行register_chrdev_region()函数静态分配一个设备号;如果VirtualDisk_major等于0,那么就使用alloc_chrdev_region()函数动态分配一个设备号,并由参数devno返回。12行,使用MAJOR宏返回得到的主设备号。
  • 17~22行,分配一个VirtualDisk设备结构体。
  • 23行,将分配的VirtualDisk设备结构体清零。
  • 25行,调用自定义的VirtualDisk_setup_cdev()函数初始化cdev结构体,并加入内核中。该函数将在下面讲到。
  • 32~37行是卸载函数,该函数中注销了cdev结构体,释放了VirtualDisk设备所占的内存,并且释放了设备占用的设备号。

cdev的初始化和注册

前面代码中调用的VirtualDisk_setup_cdev()函数完成了cdev的初始化和注册,其代码如下:

/*初始化并注册cdev*/
static void VirtualDisk_setup_cdev(struct VirutalDisk *dev, int minor)
{
	int err;
	devno = MKDEV(VirtualDisk_major, minor);  /*构造设备号*/
	cdev_init(&dev->cdev, &VirtualDisk_fops); /*初始化cdev设备*/
	dev->cdev.owner = THIS_MODULE;  /*使驱动程序属于该模块*/
	dev->cdev.ops = &VirtualDisk_fops; /*cdev连接file_operations指针*/
	err = cdev_add(&dev->cdev, devno, 1); /*将cdev注册到系统中*/
	if(err)
		printk(KERNEL_NOTICE "Error in cdev_add()\n");
}

下面对该函数进行简要的解释:

  • 5行,使用MKDEV宏构造一个主设备号为VirtualDisk_major,次设备号为minor的设备号
  • 6行,调用cdev_init()函数,将设备结构体cdevfile_operations指针相关联。这个文件操作指针定义如下代码所示。
/*文件操作结构体*/
static const struct file_operation VirtualDisk_fops =
{
	.owner = THIS_MODULE,
	.llseek = VirtualDisk_llseek,  /*定位偏移量函数*/
	.read = VirtualDisk_read,     /*读设备函数*/
	.write = VirtualDisk_write,   /*写设备函数*/
	.ioctl = VirtualDisk_ioctl,   /*控制函数*/
	.open = VirtualDisk_open,     /*打开设备函数*/
	.release = VirtualDisk_release, /*释放设备函数*/
}
  • 8行,指定VirtualDisk_fops为字符设备的文件操作函数指针。
  • 9行,调用cdev_add函数将字符设备加入到内核中。
  • 10、11行,如果注册字符设备失败,则返回。

打开和释放函数

当用户程序调用open()函数打开设备文件时,内核会最终调用VirtualDisk_open()函数。该函数的代码如下:

/*文件打开函数*/
int VirutalDisk_open(struct inode *inode, struct file *filp)
{
	/*将设备结构体指针赋值给文件私有数据指针*/
	filp->private_data = Virtualdisk_devp;
	struct VirtualDisk *devp = filp->private_data; /*获得设备结构体指针*/
	devp->count++;     /*增加设备打开次数*/
	return 0; 
}

下面对该函数进行简要解释:

  • 5、6行,将Virtualdisk_devp赋给私有数据指针,在后面将用到这个指针。
  • 7行,将设备打开计数增加1。
    当用户程序调用close()函数关闭设备文件时,内核会最终调用VirtualDisk_release()函数。这个函数主要是讲计数器减1。该函数代码如下。
/*文件释放函数*/
int VirtualDisk_release(struct inode *inode, struct file *filp)
{
	struct VirtualDisk *devp = filp->private_data; /*获得设备结构体指针*/
	devp->count--;   /*减少设备打开次数*/
	return 0;
}

读写函数

当用户程序调用read()函数读设备文件中的数据时,内核会最终调用VirtualDisk_read()函数。该函数的代码如下:

/*读函数*/
static ssize_t VirtualDisk_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
	unsigned long p = *ppos;  /*记录文件指针偏移位置*/
	unsigned int count = size; /*记录需要读取的字节数*/
	int ret = 0; /*返回值*/
	struct VirtualDisk *devp = filp->private_data; /*获得设备结构体指针*/
	/*分析和获得有效的读长度*/
	if(p >= VIRTUALDISK_SIZE)  /*需要读取的偏移大于设备内存空间*/
		return count? -ENXIO: 0;  /*读取地址错误*/
	if(count > VIRTUALSIZE - p) /*要读取的字节大于设备的内存空间*/
		count = VIRTUALSIZE - p;   /*将要读取的字节数设为剩余的字节数*/
	/*内核空间->用户空间交换数据*/
	if(copy_to_user(buf, (void *)(devp->mem + p), count))
		ret = -EFAULT;
	else
		*ppos += count;
		ret = count;
		printk(KERNEL_INFO "read %d bytes(s) from %d\n",count, p);
	return ret;
}

下面对函数进行简要的分析

  • 5~7行,定义了一些局部变量
  • 8行,从文件指针中获得设备结构体指针。
  • 10行,如果要读取的位置大于设备的大小,则出错。
  • 12行,如果要读的数据的位置大于设备的大小,则只读到设备的末尾。
  • 15~24行,从用户空间复制数据到设备中。如果复制数据成功,就将文件的偏移位置加上读出的数据个数。
    当用户程序调用write()函数向设备文件写入数据时,内核会最终调用VirtualDisk_write()函数。该函数的代码如下:
/*写函数*/
static ssize_t VirtualDisk_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
	unsigned long p = *ppos;  /*记录文件指针偏移位置*/
	int ret = 0;    /*返回值*/
	unsigned int count = size;  /*记录需要写入的字节数*/
	struct VirtualDisk *devp = file ->private_data; /*获得设备结构体指针*/
	/*分析和获取有效的写长度*/
	if(p >= VIRTUALDISK_SIZE)   /*要写入的偏移大于设备的内存空间*/
		return count ? -ENXIO: 0;  /*写入地址错误*/
	if(count > VIRTUALDISK - p) /*要写入的字节大于设备的内存空间*/
		count = VIRTUALDISK_SIZE - p;  /*将要写入的字节数设为剩余的字节数*/
	/*用户空间->内核空间*/
	if(copy_from_user(devp->mem + p , buf, count))
		ret = -EFAULT;
	else
		*ppos += count;   /*增加偏移位置*/
		ret = count;   /*返回实际的写入字节数*/
		printk(KERNEL_INFO "write %d bytes(s) from %d\n",count, p);
	return ret;
}

下面对该函数进行简要的介绍

  • 5~7行,定义了一些局部变量
  • 8行,从文件指针中获得设备结构体指针。
  • 10行,如果要读取的位置大于设备的大小,则错误。
  • 12行,如果要读的数据的位置大于设备的大小,则只读到设备的末尾。
  • 15~24行,从设备中复制数据到用户空间中。如果复制数据成功,就将文件的偏移位置加上写入的数据个数。

seek函数

当用户程序调用fssek()函数在设备文件中移动文件指针时,内核会最终调用VirtualDisk_llseek()函数。该函数的代码如下:

/*seek文件定位函数*/
static loff_t VirtualDisk_llseek(struct file *filp, loff_t offset, int orig)
{
	loff_t ret = 0;   /*返回的位置偏移*/
	switch(orig)
	{
		case SEEK_SET:
			if(offset <  0)
			{
				ret = - EINVAL
				break;
			}
			if((unsigned int )offset > VIRTUALDISK_SIZE)
			{
				ret = - EINVAL;
				break;
			}
			filp->f_pos = (unsigned int) offset;
			ret = filp->f_pos;
			break;
		case SEEK_CUR:
			if((filp->f_pos + offset) > VIRTUALDISK_SIZE)
			{
				ret = - EINVAL;
				break;
			}
			if((filp->f_pos + offset) < 0)
			{
				ret = - EINVAL
				break;
			}
			filp->f_pos += offset;
			ret = filp->f_pos;
			break;
		default:
			ret = - EINVAL;
			break;
	}
	return ret;
}

下面对该函数进行简要介绍:

  • 4行,定义了一个返回值,用来表示文件指针现在的偏移量。
  • 5行,用来选择文件指针移动方向。
  • 7~20行,表示文件指针移动的类型是SEEK_SET,表示相对于文件的开始移动指针offset个位置。
  • 8~12行,如果偏移小于0,则返回错误。
  • 13~17行,如果偏移大于文件的长度,则返回错误。
  • 18行,设置文件的偏移值到filp->f_pos,这个指针表示文件的当前位置。
  • 21~34行,表示文件指针移动的类型是SEEK_CUR,表示相对于文件的当前位置移动指针offset个位置。
  • 22~26行,如果偏移值大于文件的长度,则返回错误。
  • 27~31行,表示指针小于0的情况,这种情况指针是不合法的。
  • 32行,将文件的偏移值filp->f_pos加上offset个偏移。
  • 35、36行,表示命令不是SEEK_SET或者SEEK_CUR,这种情况下表示传入了非法命令,直接返回。

ioctl()函数

当用户程序调用ioctl()函数改变设备的功能时,内核会最终调用VirtualDisk_ioctl()函数。该函数的代码如下:

/*ioctl设备控制函数*/
static int VirtualDisk_ioctl(struct inode *inodep, struct file *filp, unsigned int cmd, unsigned long arg)
{
	struct VirtualDisk *devp = filp->private_date;
	switch(cmd)
	{
		case MEM_CLEAR:  /*设备内存清零*/
			memset(devp->mem, 0, VIRTUALDISK_SIZE);
			printk(KERNEL_INFO "VirtualDisk is set to zero\n");
			break;
		case PORT1_SET:  /*将端口1置为0*/
			devp->port1 = 0;
			break;
		case PORT2_SET: /*将端口2置0*/
			devp->port2 = 0;
			break;
		default:
			return -EINVAL;
	}
	return 0;
}

下面对该函数进行简要介绍:

  • 5行,得到文件的私有数据,私有数据中存放的是VirtualDisk设备的指针。
  • 6~20行,根据ioctl()函数传进来的参数判断将要执行的操作。这里的字符设备支持3个操作,第一个操作是将字符设备的内存全部清零,第二个是将端口1设置为0,第三个是将端口2设置成0。

小结

讲解了字符设备驱动程序。字符设备是Linux中三大设备之一,很多设备都可以看成是字符设备,所以学习字符设备驱动程序的编程是很有用的。本章先从整体上介绍了字符设备的框架结构,然后介绍了字符设备结构体struct cdev。接着介绍了字符设备的组成,最后详细讲解了一个VirtualDisk字符设备驱动程序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/735072.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Tomcat面试题

江湖无名 安心练剑 关注微信公众号&#xff1a;程序三两行 获取最新版面试资料、java学习路线 1、Tomcat 的缺省端口是多少&#xff0c;怎么修改&#xff1f; 1&#xff09;找到 Tomcat 目录下的 conf 文件夹 2&#xff09;进入 conf 文件夹里面找到 server.xml 文件 3&…

Elasticsearch】文档操作

目录 3.文档操作 3.1.新增文档 3.2.查询文档 3.3.删除文档 3.4.修改文档 3.4.1.全量修改 3.4.2.增量修改 3.5.总结 3.文档操作 3.1.新增文档 语法&#xff1a; POST /索引库名/_doc/文档id {"字段1": "值1","字段2": "值2"…

AUTOSAR CP标准的RTE和BSW各模块的设计及开发工作

AUTOSAR&#xff08;Automotive Open System Architecture&#xff09;是一种开放的汽车电子系统架构标准&#xff0c;旨在提供一种统一的软件架构&#xff0c;以实现汽车电子系统的模块化和可重用性。 AUTOSAR标准中的两个重要模块是RTE&#xff08;Runtime Environment&…

YOLOV5/V7动态多Batch推理,GPU预处理、后处理C++源码

YOLO系列多batch处理后处理代码&#xff0c;C代码&#xff0c;结构清晰&#xff0c;自己后期会继续发布真实场景项目&#xff1b;关注公众号欢迎下载。 一、获取链接 所有源码获取方法&#xff0c;链接&#xff1a;https://www.cnblogs.com/shenduxuexizahuopu/p/17538815.htm…

【LeetCode】HOT 100(22)

题单介绍&#xff1a; 精选 100 道力扣&#xff08;LeetCode&#xff09;上最热门的题目&#xff0c;适合初识算法与数据结构的新手和想要在短时间内高效提升的人&#xff0c;熟练掌握这 100 道题&#xff0c;你就已经具备了在代码世界通行的基本能力。 目录 题单介绍&#…

【Python】Python实现串口通信(Python+Stm32)

&#x1f389;欢迎来到Python专栏~Python实现串口通信 ☆* o(≧▽≦)o *☆嗨~我是小夏与酒&#x1f379; ✨博客主页&#xff1a;小夏与酒的博客 &#x1f388;该系列文章专栏&#xff1a;Python学习专栏 文章作者技术和水平有限&#xff0c;如果文中出现错误&#xff0c;希望…

在linux中安装HAProxy

使用xfrp将压缩包上传到linux的opt目录下 1.解压HAProxy安装包 tar -zxvf haproxy-1.8.12.tar.gz 2. 查看点钱的内核及版本: uname -r 3. 根据内核版本选择编译参数 cd haproxy-1.8.12 cat README 4. 编译安装HAProxy make TARGETlinux2628 ARCHx86_64 PREFIX/usr/local…

测试,写用例!真有必要?

今天的文章是想跟所有小伙伴讨论软件测试工作中必不可少的一项工作&#xff1a;写测试用例&#xff0c;但目前还有很多公司并不重视写测试用例&#xff0c;觉得写测试用例是浪费时间&#xff0c;还不如拿这些时间来执行测试&#xff0c;那我们真的有必要写测试用例么&#xff1…

算法与数据结构-递归

文章目录 什么是递归递归需要满足的三个条件递归可能存在的问题堆栈溢出重复计算 总结 什么是递归 递归是一种直接或者间接调用自身函数或者方法的算法&#xff08;或者编程技巧&#xff09;&#xff0c;应用非常广泛。我们举个例子来说明什么是递归&#xff1a; 推荐注册返佣金…

join的作用【A中调用B.join表示 B先完成后A再继续】【b.join 表示b先完成】

★ 2.5 等待一个线程-join() ★★★A中调用B.join表示 B先完成后A再继续 有时&#xff0c;我们需要等待一个线程完成它的工作后&#xff0c;才能进行自己的下一步工作。例如&#xff0c;张三只有等李四转账成功&#xff0c;才决定是否存钱&#xff0c;这时我们需要一个方法明确…

maya粒子碰撞(碰撞几何体索引)

全部下滑了 nParticleShape1.rgbPP<<1,0,0>>; 碰撞层 int $yase nParticleShape1.collisionGeometryIndex; if ($yase 0 ) nParticleShape1.rgbPP<<0,1,0>>; int $yase nParticleShape1.collisionGeometryIndex; if ($yase 0 ) nParticleShape1.…

【Linux】网络基础和网络套接字的概念

文章目录 前言一、网络的发展及其网络中的概念总结 一、网络的发展及其网络中的概念 刚开始的计算机大多处于独立模式&#xff0c;也就是说计算机之间相互独立。等到网络出现的时候就出现了网络互联&#xff0c;多台计算机连接在一起完成数据共享。随着发展计算机越多越多就出…

选购云主机

目录 一、购买云主机 二、SSH连接云主机 三、在云主机上面开放端口 一、购买云主机 云服务商有很多&#xff0c;但是我推荐大家使用腾讯云。之前我讲过阿里云超卖的事情&#xff0c;云主机IO性能非常差劲&#xff0c;我们要在云主机上面安装MySQL、MongoDB这样的数据库&…

TortoiseGit 入门指南01:环境搭建和软件设置

在我的博文Keil MDK环境下Git入门指南的最后&#xff0c;我这样写道&#xff1a; 目前使用 TortoiseGit 管理工程&#xff0c;用 Gitee 作为远程仓库。 命令行 Git 已经不再使用。 当时我并没有介绍软件 TortoiseGit 的使用方法&#xff0c;这个系列补上。如果你还没有看过《Ke…

Linux开发工具【gcc/g++】

Linux开发工具之【gcc/g】 上文我们已经学习了vim编辑器的相关操作和使用&#xff0c;已经可以在Linux下编写代码了&#xff0c;有了代码就需要编译运行&#xff0c;此时就需要用到Linux中的编译工具gcc/g了&#xff0c;其中gcc是C语言的编译器&#xff0c;g是C的编译器&#…

RabbitMQ系列(29)--RabbitMQ搭建Shovel

前言&#xff1a; Federation具备的数据转发功能类似&#xff0c;Shovel能够可靠、持续地从一个Broker中的队列(作为源端&#xff0c;即source)拉取数据并转发至另一个Broker中的交换器(作为目的端&#xff0c;即destination)。作为源端的队列和作为目的端的交换器可以同时位于…

SGM58031与单片机驱动实现

SGM58031与单片机驱动实现 文章目录 SGM58031与单片机驱动实现CUBEIDE设置I2C通讯封装SGM58031通讯实现 CUBEIDE设置 使用硬件I2C与sgm芯片通讯&#xff0c;上面即配置硬件I2C,其他参数默认即可。 I2C通讯封装 封装实现 /*** brief Manages error callback by re-initializ…

【JVM调优】JVM调优工具之Arthas

Arthas的作用 Arthas是一款线上监控诊断产品&#xff0c;通过全局视角实时查看应用 load、内存、gc、线程的状态信息&#xff0c;并能在不修改应用代码的情况下&#xff0c;对业务问题进行诊断&#xff0c;包括查看方法调用的出入参、异常&#xff0c;监测方法执行耗时&#x…

【Elasticsearch】索引库操作

目录 2.索引库操作 2.1.mapping映射属性 2.2.索引库的CRUD 2.2.1.创建索引库和映射 基本语法&#xff1a; 示例&#xff1a; 2.2.2.查询索引库 2.2.3.修改索引库 2.2.4.删除索引库 2.2.5.总结 2.索引库操作 索引库就类似数据库表&#xff0c;mapping映射就类似表的…

[神经网络]Anchor_Free网络(YoloX,CenterNet)

Anchor_Free网络不同于传统的目标检测网络需要先生成很多先验框再从中筛选回归生成预测框。其可以直接从目标的中心点向周围发散一个预测框。这样做有两个好处&#xff1a;①省略了生成大量先验框的过程&#xff0c;可以一定程度增加预测速度&#xff1b;②预测框没有预设长宽比…