Linux内核驱动之字符驱动

之前写过hello world驱动,在它的基础上我们进行扩展,写一个字符驱动。

字符驱动的编写流程大致如下图所示:


先看代码,在hello目录下新增hello.h文件:

#ifndef _HELLO_ANDROID_H_
#define _HELLO_ANDROID_H_

#include <linux/cdev.h>
#include <linux/semaphore.h>

#define HELLO_DEVICE_NODE_NAME "hello"
#define HELLO_DEVICE_FILE_NAME "hello"

struct hello_android_dev {
	int val;
	struct semaphore sem;
	struct cdev dev;
};

#endif

该头文件中定义了一些字符串常量宏,还定义了一个字符设备结构体hello_android_dev,这个就是我们虚拟的硬件设备。val成员变量就代表设备里面的寄存器,它的类型是int。sem成员变量是一个信号量,是用于同步访问寄存器val的。dev成员变量是一个内嵌的字符设备。这个是Linux驱动程序自定义字符设备结构体的标准写法。

接下来看驱动程序的实现部分。驱动程序的功能主要是向上层提供访问设备寄存器的值,包括读和写。这里先看通过传统的设备文件的方法来访问的代码写法:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>

#include "hello.h"

/*主设备和从设备编号变量*/
static int hello_major = 0;
static int hello_minor = 0;

/*设备变量*/
static struct hello_android_dev* hello_dev = NULL;

/*传统的设备文件操作方法*/
static int hello_open(struct inode* inode, struct file* filp);
static int hello_release(struct inode* inode, struct file* filp);
static ssize_t hello_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos);
static ssize_t hello_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos);

/*设备文件操作方法表*/
static struct file_operations hello_fops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.release = hello_release,
	.read = hello_read,
	.write = hello_write,
};

/*打开设备的方法*/
static int hello_open(struct inode* inode, struct file* filp) {
	struct hello_android_dev* dev;
	
	/*将自定义设备结构体保存在文件指针的私有数据域中,以便访问设备时拿来用*/
	dev = container_of(inode->i_cdev, struct hello_android_dev, dev);
	filp->private_data = dev;
	
	return 0;
}

/*设备文件释放时调用,空实现*/
static int hello_release(struct inode* inode, struct file* filp) {
	return 0;
}

/*读取设备寄存器val的值*/
static ssize_t hello_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos) {
	ssize_t err = 0;
	struct hello_android_dev* dev = filp->private_data;
	
	/*同步访问*/
	if (down_interruptible(&(dev->sem))) {
		return -ERESTARTSYS;
	}
	
	if (count < sizeof(dev->val)) {
		goto out;
	}
	
	/*将寄存器val的值拷贝到用户提供的缓冲区*/
	if (copy_to_user(buf, &(dev->val), sizeof(dev->val))) {
		err = -EFAULT;
		goto out;
	}
	
	err = sizeof(dev->val);
	
out:
	up(&(dev->sem));
	return err;
}

/*写值到设备寄存器val*/
static ssize_t hello_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos) {
	struct hello_android_dev* dev = filp->private_data;
	ssize_t err = 0;
	
	/*同步访问*/
	if (down_interruptible(&(dev->sem))) {
		return -ERESTARTSYS;
	}
	
	if (count != sizeof(dev->val)) {
		goto out;
	}
	
	/*将用户提供的缓冲区的值写到设备寄存器中*/
	if (copy_from_user(&(dev->val), buf, count)) {
		err = -EFAULT;
		goto out;
	}
	
	err = sizeof(dev->val);
	
out:
	up(&(dev->sem));
	return err;
}

/*初始化设备*/
static int __hello_setup_dev(struct hello_android_dev* dev) {
	int err;
	dev_t devno = MKDEV(hello_major, hello_minor);
	
	memset(dev, 0, sizeof(struct hello_android_dev));
	
	cdev_init(&(dev->dev), &hello_fops);
	dev->dev.owner = THIS_MODULE;
	dev->dev.ops = &hello_fops;
	
	/*注册字符设备*/
	err = cdev_add(&(dev->dev), devno, 1);
	if (err) {
		return err;
	}
	
	/*初始化信号量和寄存器val的值*/
	sema_init(&(dev->sem), 1);
	dev->val = 0;
	
	return 0;
}

/*模块加载方法*/
static int __init hello_init(void) {
    printk(KERN_ALERT "Hello, world\n");
	int err = -1;
	dev_t dev = 0;
	struct device* temp = NULL;
	
	/*动态分配主设备和从设备编号*/
	err = alloc_chrdev_region(&dev, 0, 1, HELLO_DEVICE_NODE_NAME);
	if (err < 0) {
		printk(KERN_ALERT "Failed to alloc char dev region.\n");
		goto fail;
	}
	
	hello_major = MAJOR(dev);
	hello_minor = MINOR(dev);
	
	/*分配hello设备结构体变量*/
	hello_dev = kmalloc(sizeof(struct hello_android_dev), GFP_KERNEL);
	if (!hello_dev) {
		err = -ENOMEM;
		printk(KERN_ALERT "Failed to alloc hello_dev.\n");
		goto unregister;
	}
	
	/*初始化设备*/
	err = __hello_setup_dev(hello_dev);
	if (err) {
		printk(KERN_ALERT "Failed to setup dev: %d.\n", err);
		goto cleanup;
	}
    return 0;
	
cleanup:
	kfree(hello_dev);
	
unregister:
	unregister_chrdev_region(MKDEV(hello_major, hello_minor), 1);
	
fail:
	return err;
}

static void __exit hello_exit(void) {
    printk(KERN_ALERT "Goodbye, cruel world\n");
	dev_t devno = MKDEV(hello_major, hello_minor);
	
	/*删除字符设备、释放设备内存*/
	if (hello_dev) {
		cdev_del(&(hello_dev->dev));
		kfree(hello_dev);
	}
	
	/*释放设备编号*/
	unregister_chrdev_region(devno, 1);
}

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("First Driver");

module_init(hello_init);
module_exit(hello_exit);

大部分基础性的驱动操作都包括3个重要的内核数据结构:file_operations,file和inode。

文件操作

file_operations结构就是告诉我们怎么把一个字符驱动跟设备建立连接的。它定义在<linux/fs.h>中,是一个函数指针的集合。结构中的每个成员必须指向驱动中的函数,这些函数实现一个特别的操作或者对于不支持的操作留置为NULL。下面逐一分析file_operations结构体中的各个成员:

struct module *owner:它是一个指向拥有这个结构的模块的指针。一般都初始化为定义在<linux/module.h>中的THIS_MODULE宏。
int(*open) (struct inode*, struct file*):打开设备的方法。如果设为NULL,则打开会一直成功。
int(*release) (struct inode*, struct file*):释放文件结构的方法。
ssize_t(*read) (struct file*, char __user*, size_t, loff_t*):读取设备中数据的方法。返回值是成功读取的字节数,类型是“signed size”。
ssize_t(*write) (struct file*, const char __user*, size_t, loff_t*):写数据到设备上。
再看下其他成员:
loff_t(*llseek) (struct file*, loff_t, int):用于改变文件中的当前读/写位置,返回改变后的位置。
ssize_t(*aio_read) (struct kiocb*, char __user*, size_t, loff_t):初始化一个异步读,如果该方法是NULL,则所有的操作会由read代替进行(同步读)。
ssize_t(*aio_write) (struct kiocb*, const char __user*, size_t, loff_t*):初始化一个异步写。
int(*readdir) (struct file*, void*, filldir_t):读取目录的方法,仅对文件系统有用。对于设备文件该成员应该为NULL。
unsigned int(*poll) (struct file*, struct poll_table_struct*):用作查询对一个或多个文件描述符的读或写是否会阻塞。
int(*ioctl) (struct inode*, struct file*, unsigned int, unsigned long):执行特定操作的方法(如格式化软盘的一个磁道,这不是读写操作)。
int(*mmap) (struct file*, struct vm_area_struct*):将设备内存映射到进程的地址空间的方法。
int(*flush) (struct file*):刷新操作。
int(*fsync) (struct file*, struct dentry*, int):刷新操作。
int(*fasync) (int, struct file*, int):通知设备它的FASYNC标志改变了。
int(*lock) (struct file*, int, struct file_lock*):用于实现文件加锁。加锁对常规文件是必不可少的,但是设备驱动几乎从不实现它。
ssize_t(*readv) (struct file*, const struct iovec*, unsigned long, loff_t*);
ssize_t(*writev) (struct file*, const struct iovec*, unsigned long, loff_t*);

上面两个方法用于实现发散/汇聚读写操作应用程序偶尔需要做一个包含多个内存区的单个读或写操作。
ssize_t(*sendfile) (struct file*, loff_t*, size_t, read_actor_t, void*):实现sendfile系统调用的读,使用最少的拷贝从一个文件描述符搬移数据到另一个。
ssize_t(*sendpage) (struct file*, struct page*, int, size_t, loff_t*, int):sendpage是sendfile的另一半,它由内核调用来发送数据,一次一页,到对应的文件,设备驱动实际上不实现sendpage。

文件结构

struct file,定义在<linux/fs.h>中,是设备驱动中第二个最重要的数据结构。文件结构代表一个打开的文件,系统每个打开的文件在内核空间都有一个关联的struct file,它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后关闭。在文件的所有实例都关闭后,内核释放这个数据结构。
下面看下它的成员:
void *private_data:可以使用该成员指向分配给它的数据,在内核销毁文件结构之前,必须在release方法释放那个内存。
mode_t f_mode:文件模式,通过位FMODE_READ和FMODE_WRITE来确定文件是可读或者可写或者可读可写的。
loff_t f_pos:当前读写位置。
unsigned int f_flgs:文件标志,如O_RDONLY,O_NONBLOCK和O_SYNC。驱动应该检查O_NONBLOCK标志来看请求是否为非阻塞操作。标志定义在<linux/fcntl.h>中。
struct file_operations *f_op:和文件关联的操作。
struct dentry *f_dentry:关联到文件的目录入口(dentry)结构。除了用filp->f_dentry->d_inode存取inode结构外几乎不用。

inode结构

inode结构由内核在内部用来表示文件。因此它和代表打开文件描述符的文件结构是不同的。可能有代表单个文件的多个打开描述符的许多文件结构,但是它们都指向一个单个inode结构。
inode结构包含大量关于文件的信息。编写驱动代码相关的成员只有两个:
dev_t i_rdev:设备文件的节点,该成员包含实际的设备编号。
struct cdev *i_cdev:struct cdev是内核的内部结构,代表字符设备。该成员包含一个指针,当节点指的是一个字符设备文件时,它就指向这个字符设备。
上面hello_open方法中的inode->i_cdev就指向自定义的字符设备结构体hello_android_dev中的dev。

看完几个重要的数据结构后,从头分析上面代码,先看设备编号:

后面注册/注销设备时都需要用到设备编号。

字符设备通过文件系统中的名字来存取,它位于/dev目录下,可以通过ls -l查看,第一列以“c”开头的就是字符设备,以“b”开头的是块设备。如下:


图中设备文件列有2个用逗号分隔的数,分别是主设备编号和从设备编号。主设备编号标识与设备相连的驱动。如/dev/null和/dev/zero都是由驱动1来管理。内核根据从设备编号来决定引用哪个设备。

分配和释放设备编号

建立一个字符驱动时首先就要获取一个或多个设备编号,需要使用alloc_chrdev_region方法让内核动态地分配设备编号:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
其中dev是函数执行成功时Linux为驱动分配范围内的第一个数。firstminor是驱动请求的第一个要用的从设备编号,一般都是0.count是驱动请求的连续设备编号的总数。name是设备的名字,它会出现在/proc/devices和sysfs中。

不再需要使用驱动的设备编号时(如注册发生异常或卸载模块时)执行释放操作:
void unregister_chrdev_region(dev_t first, unsigned int count);


在内核中,设备编号的类型是dev_t,定义在<linux/types.h>中。获取主从设备编号使用:
MAJOR(dev_t dev);
MINOR(dev_t dev);

如果已经获取到主从设备编号,则需要将它们转换成一个dev_t:
MKDEV(int major, int minor);

如上面代码中所示,至此驱动的主设备编号和从设备编号已经动态分配完毕。另:所有驱动中编号分配代码都类似。
接下来看字符设备的注册过程

字符设备的注册

前面介绍过内核在内部使用struct cdev类型的结构体来代表字符设备。在内核操作设备前,需要分配和注册这些结构。内核中有2个核心函数来管理内存,分别是:
void *kmalloc(size_t size, int flags);
void kfree(void *ptr);
kmalloc方法用于分配size字节的内存,返回值是指向所分配内存的指针,失败时返回NULL。flags用来描述如何分配内存。分配的内存在释放时应该调用kfree方法。
分配:
如代码中所示使用
hello_dev = kmalloc(sizeof(struct hello_android_dev), GFP_KERNEL);

因为这里的cdev结构嵌入在了自定义的设备hello_android_dev中,所以需要初始化已经分配的结构,如代码中所示使用:
cdev_init(&(dev->dev), &hello_fops);
dev->dev.owner = THIS_MODULE;
dev->dev.ops = &hello_fops;


初始化成功后需要把它注册到内核:
err = cdev_add(&(dev->dev), devno, 1);
该方法的第一个参数是cdev结构,第二个参数是设备的编号,最后一个参数是应当关联到设备的设备编号的总数,通常是1.注意:该方法可能会调用失败,如果失败则会返回一个负的错误码。如果执行成功,则内核就可以操作该设备了。

从系统中移除字符设备的方法:
cdev_del(&(hello_dev->dev));

到这里,设备就注册完了。下面再详细看下操作设备的方法:
open:在大部分驱动中,open应当进行如下的工作:
检查设备特定的错误(如设备没准备好,或者硬件错误)
如果第一次打开,初始化设备
如果需要,更新f_op指针
分配并填充要放进filp->private_data的任何数据结构
open方法中的inode参数包含我们之前建立的cdev结构,但是我们想要得到的是包含cdev的自定义的hello_android_dev结构。所以我们需要使用定义在<linux/kernel.h>中的container_of方法进行查找:
struct hello_android_dev* dev;
dev = container_of(inode->i_cdev, struct hello_android_dev, dev);
filp->private_data = dev;
一旦找到hello_android_dev结构,就将在文件结构中的private_data成员中存储一个它的指针,以便后续的存取。

release:该方法应当进行如下的工作:
释放open分配在filp->private_data中的任何东西
关闭设备
如hello驱动没有任何硬件需要关闭,故该方法直接return 0了。

read和write:主要是从应用程序拷贝数据或拷贝数据到应用程序。
方法中的filp是文件指针。buf指向持有被写入数据的缓存,或者放入新数据的空缓存。count是请求的传输数据大小。f_pos指示用户正在存取的文件位置。
如代码中所示读和写分别通过下面两个方法实现:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
执行这两个方法时,还会检查用户空间指针是否有效,如果无效就不进行拷贝。返回值是还要拷贝的数据量,如果不是0,就返回-EFAULT给用户。

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


Makefile文件跟之前hello world模块一样。加载模块后会在/dev目录下创建hello文件。

读写操作:

cat /dev/hello            [读设备]
echo "test">/dev/hello    [写设备]

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值