在上一章中通过一个helloworld把我们带入了linux内核驱动的世界,但是只是为了简单说明编写内核驱动的方法,helloworld驱动没有任何功能,而本章则是在helloworld的基础上增加一些“有用的”功能,(为了保持与书中例子一致,helloworld改名为scull,为了便于理解,scull只实现了一个设备,并且内存只有一块。)
- 首先要让内核“认识”到有一个helloworld这个驱动,这个是通过分配设备号来实现。
- 其次要给这个驱动分配一个存储空间,使得我们的数据可以存储在这个驱动中,就像一个磁盘、USB一样。
- 在用户态对此驱动操作,使得我们即可以往里面存入数据,也可以从里面读出数据。
linux中设备分为字符设备和块设备,设备也是一种文件,所以其对主要的三个数据结构为:file_operations、file和inode。所有的设备文件都组织在/dev文件系统中,以磁盘设备为例,如下:
[root@bogon ~]# ls -l /dev/sda*
brw-rw----. 1 root disk 8, 0 Jan 21 06:32 /dev/sda
brw-rw----. 1 root disk 8, 1 Jan 21 06:32 /dev/sda1
brw-rw----. 1 root disk 8, 2 Jan 21 06:32 /dev/sda2
brw-rw----. 1 root disk 8, 3 Jan 21 06:32 /dev/sda3
[root@bogon ~]#
linux是通过设备号来驱动每个设备的,每一个设备号的主设备号对应一种设备,如上图中第5列的“8”就表示磁盘设备的主设备号,而第6列的“0,1,2,3”称为次设备号,次设备号是对主设备号的一种细分,如上图中次设备号“0”表示sda磁盘设备,次设备号“1”表示sda磁盘的第一个分区设备等等。每一个设备想要被内核识别就必须在插入内核的时候主动传入一个设备号,或者让内核帮忙分配一个设备号。当然每种设备的设备号都不能重复。可以通过/proc/devices接口查看系统中已经分配了哪些设备号,如下(只是说明问题,没有列完):
[root@bogon ~]# cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
7 vcs
Block devices:
1 ramdisk
259 blkext
7 loop
8 sd
9 md
11 sr
65 sd
对于设备有了进一步的认识,我们就可以慢慢了往里面添加功能了,实现一个驱动,最主要步骤如下:
一,分配设备号
设备号相关的常用函数如下:
MAJOR(dev_t dev); //从设备号中获取主设备号
MINOR(dev_t dev); <span style="font-family: Arial, Helvetica, sans-serif;">//从设备号中获取次设备号</span>
MKDEV(int major, int minor); //通过主,次设备号计算出设备号
int register_chrdev_region(dev_t first, unsigned count, const char *name); //指定设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
<span style="white-space:pre"> </span>const char *name); //动态由内核分配设备号
void unregister_chrdev_region(dev_t from, unsigned count); //设备号也是一种宝贵的资源,<span style="font-family: Arial, Helvetica, sans-serif;">所以设备卸载的时候一定要记得调用此函数释放设备号</span>
scull设备的实现如下:
init函数中:
int result;
dev_t dev = 0;
result = alloc_chrdev_region(&dev, scull_minor, 1, "scull");
scull_major = MAJOR(dev);
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}
exit函数中:
dev_t devno = MKDEV(scull_major, scull_minor);
//1,移除驱动
/* 2,释放占用的设备号 */
unregister_chrdev_region(devno, 1);
二,注册设备
设备注册与移除主要是用以下几个接口实现
void cdev_init(struct cdev *cdev, const struct file_operations *fops); //设备初始化,主要是初始化设备的几个重要的结构休,后面会说明这几个结构休。
int cdev_add(struct cdev *p, dev_t dev, unsigned count);//将设备添加到内核中,一定要注意:此函数一返回,所添加的设备就已经添加到内核中了,随时可以被调用到,所以在驱动程序还没有完全准备好处理设备上的操作时,就不能调用此函数。所以一定要保证初始化设备时将所需要的操作都初始化好。
void cdev_del(struct cdev *p);//移除设备
scull设备的实现如下:
init函数中:
//1,分配一个设备结构体
scull_device = kmalloc(sizeof(struct scull_dev), GFP_KERNEL);
if (!scull_device) {
result = -ENOMEM;
goto fail; /* Make this more graceful */
}
memset(scull_device, 0, sizeof(struct scull_dev));
//2,初始化设备
init_MUTEX(&scull_device->sem);
cdev_init(&scull_device->cdev, &scull_fops);
scull_device->cdev.owner = THIS_MODULE;
scull_device->cdev.ops = &scull_fops;
//3,添加设备
result = cdev_add (&scull_device->cdev, dev, 1);
/* Fail gracefully if need be */
if (result)
printk(KERN_NOTICE "Error %d adding scull", result);
exit函数中:
dev_t devno = MKDEV(scull_major, scull_minor);
//1,移除驱动
if (scull_device) {
scull_trim(scull_device);
cdev_del(&scull_device->cdev);
kfree(scull_device);
}
/* 2,释放占用的设备号 */
三,实现设备的open,release,read,write函数,设备本来就是一种文件,所以这四个函数是最主要的函数,当然还有很多其他的函数,后面遇到再讲解。
在调用cdev_add之前一定要保证设备初始化好,也就是要仔细的初始化cdev_init函数。
每一个设备在内存中都会对应一个结构体数据以方便内核的操作,scull的结构体如下:
struct scull_dev {
char *data; /* Pointer to data */
unsigned long size; /* amount of data stored here */
struct semaphore sem; /* mutual exclusion semaphore */
struct cdev cdev; /* Char device structure,这个字段表示这个设备是个字符设备*/
};
这样cdev_init的第一个参数搞定了,另外一个参数是一个file_operations结构体变量,其实在前面已经提过,在linux中设备也是一种文件,所以用户态对设备的操作就也通过设备文件操作的,而file_operations结构体就包含了文件的所有操作(函数指针),系统中对于这些操作都有默认的实现,如果默认实现无法满足我们的要求,我们就可以通过复写来实现我们自己相应的功能(很像java中的函数覆盖)。在scull中主要实现了open,release,read,write四个操作,对应的结构体如下:
struct file_operations scull_fops = {
.owner = THIS_MODULE, //指向“拥有”该结构的模块指针,几乎在所有的情况下都被初始化成此值。
.read = scull_read,
.write = scull_write,
.open = scull_open,
.release = scull_release,
};
下面分别对四个函数分析
1,open函数提供给驱动程序以初始化的能力,从而为以后的操作完成初始化做准备,在大部分驱动程序中,open应完成以下工作:
- 检查设备特定的错误
- 如果设备是首次打开,对其进行初始化
- 如有必要,更新f_op指针
- 分配并填写置于filp->private_data里的数据结构
int scull_open(struct inode *inode, struct file *filp)
{
struct scull_dev *dev; /* device information */
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */
/* now trim to 0 the length of the device if open was write-only */
if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
scull_trim(dev); /* ignore errors */ //初始化scull设备的存储空间,这里是将空间置空,在write时再分配。
up(&dev->sem);
}
return 0; /* success */
}
2,release的作用正好与open相反,主要完成以下工作:
- 释放由open分配的,保存在filp->private_data中的所有内容。
- 在最后一次关闭操作时关闭设备
因为scull的基本模型很简单,与硬件无关,所以无需实现任何功能(不过个人觉得应该将filp->private_data置为空),如下:
int scull_release(struct inode *inode, struct file *filp)
{
return 0;
}
3,read方法实现文件读取的操作,返回值解释如下:
- 如果这个值等于传递给 read 系统调用的 count 参数, 请求的字节数已经被传送. 这是最好的情况.
- 如果是正数, 但是小于 count, 只有部分数据被传送. 这可能由于几个原因, 依赖于设备. 常常, 应用程序重新试着读取. 例如, 如果你使用fread 函数来读取, 库函数重新发出系统调用直到请求的数据传送完成.
- 如果值为 0, 到达了文件末尾(没有读取数据).
- 一个负值表示有一个错误. 这个值指出了什么错误, 根据<linux/errno.h>. 出错的典型返回值包括 -EINTR( 被打断的系统调用)或者 -EFAULT( 坏地址 ).
ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
ssize_t retval = 0;
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
if (*f_pos >= dev->size)
goto out;
if (*f_pos + count > dev->size)
count = dev->size - (*f_pos);
if (copy_to_user(buf, dev->data, count)) { //从内核态缓冲区拷贝数据到用户态缓冲区,read实现的核心
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
out:
up(&dev->sem);
return retval;
}
4,write实现文件写的操作,与read类似,返回值如下:
- 如果值等于 count, 要求的字节数已被传送.
- 如果正值, 但是小于 count, 只有部分数据被传送. 程序最可能重试写入剩下的数据.
- 如果值为 0, 什么没有写. 这个结果不是一个错误, 没有理由返回一个错误码. 再一次, 标准库重试写调用. 后面将详细说明.
- 一个负值表示发生一个错误; 如同对于读, 有效的错误值是定义于<linux/errno.h>中.
scull中write实现:
ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
ssize_t retval = -ENOMEM; /* value used in "goto out" statements */
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
if (!dev->data){
dev->data = kmalloc(SCLL_DEV_MEM_LEN * sizeof(char *), GFP_KERNEL);
if (!dev->data)
goto out;
memset(dev->data, 0, SCLL_DEV_MEM_LEN * sizeof(char *));
}
if (copy_from_user(dev->data, buf, count)) { //将数据从用户态缓冲区拷贝到内核态缓冲区,write实现的核心
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
/* update the size */
if (dev->size < *f_pos)
dev->size = *f_pos;
out:
up(&dev->sem);
return retval;
}
ok,一个最简单的字符设备就完成了,虽然功能很少,但至少有一个驱动的样子了,下面把详细的代码贴出,可以对比书中的例子
scull.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h> /* printk() */
#include <linux/slab.h> /* kmalloc() */
#include <linux/fs.h> /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/types.h> /* size_t */
#include <linux/cdev.h>
#include <asm/system.h> /* cli(), *_flags */
#include <asm/uaccess.h> /* copy_*_user */
#ifndef SCULL_MAJOR
#define SCULL_MAJOR 0 /* dynamic major by default */
#endif
#ifndef SCLL_DEV_MEM_LEN
#define SCLL_DEV_MEM_LEN 1024
#endif
MODULE_AUTHOR("Alessandro Rubini, Jonathan Corbet");
MODULE_LICENSE("Dual BSD/GPL");
struct scull_dev {
char *data; /* Pointer to first quantum set */
unsigned long size; /* amount of data stored here */
struct semaphore sem; /* mutual exclusion semaphore */
struct cdev cdev; /* Char device structure */
};
/*
* Our parameters which can be set at load time.
*/
int scull_major = SCULL_MAJOR;
int scull_minor = 0;
struct scull_dev *scull_device = NULL; /* allocated in scull_init_module */
/*
* Empty out the scull device; must be called with the device
* semaphore held.
*/
int scull_trim(struct scull_dev *dev)
{
if (!dev)
return -1;
if (dev->data)
kfree(dev->data);
dev->size = 0;
dev->data = NULL;
return 0;
}
/*
* Open and close
*/
int scull_open(struct inode *inode, struct file *filp)
{
struct scull_dev *dev; /* device information */
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */
/* now trim to 0 the length of the device if open was write-only */
if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
scull_trim(dev); /* ignore errors */
up(&dev->sem);
}
return 0; /* success */
}
int scull_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*
* Data management: read and write
*/
ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
ssize_t retval = 0;
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
if (*f_pos >= dev->size)
goto out;
if (*f_pos + count > dev->size)
count = dev->size - (*f_pos);
if (copy_to_user(buf, dev->data, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
out:
up(&dev->sem);
return retval;
}
ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
ssize_t retval = -ENOMEM; /* value used in "goto out" statements */
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
if (!dev->data){
dev->data = kmalloc(SCLL_DEV_MEM_LEN * sizeof(char *), GFP_KERNEL);
if (!dev->data)
goto out;
memset(dev->data, 0, SCLL_DEV_MEM_LEN * sizeof(char *));
}
if (copy_from_user(dev->data, buf, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
/* update the size */
if (dev->size < *f_pos)
dev->size = *f_pos;
out:
up(&dev->sem);
return retval;
}
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.read = scull_read,
.write = scull_write,
.open = scull_open,
.release = scull_release,
};
/*
* The cleanup function is used to handle initialization failures as well.
* Thefore, it must be careful to work correctly even if some of the items
* have not been initialized
*/
void scull_cleanup_module(void)
{
dev_t devno = MKDEV(scull_major, scull_minor);
/* Get rid of our char dev entries */
if (scull_device) {
scull_trim(scull_device);
cdev_del(&scull_device->cdev);
kfree(scull_device);
}
/* cleanup_module is never called if registering failed */
unregister_chrdev_region(devno, 1);
}
int scull_init_module(void)
{
int result;
dev_t dev = 0;
result = alloc_chrdev_region(&dev, scull_minor, 1, "scull");
scull_major = MAJOR(dev);
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}
/*
* allocate the devices -- we can't have them static, as the number
* can be specified at load time
*/
scull_device = kmalloc(sizeof(struct scull_dev), GFP_KERNEL);
if (!scull_device) {
result = -ENOMEM;
goto fail; /* Make this more graceful */
}
memset(scull_device, 0, sizeof(struct scull_dev));
/* Initialize each device. */
init_MUTEX(&scull_device->sem);
cdev_init(&scull_device->cdev, &scull_fops);
scull_device->cdev.owner = THIS_MODULE;
scull_device->cdev.ops = &scull_fops;
result = cdev_add (&scull_device->cdev, dev, 1);
/* Fail gracefully if need be */
if (result)
printk(KERN_NOTICE "Error %d adding scull", result);
return 0; /* succeed */
fail:
scull_cleanup_module();
return result;
}
module_init(scull_init_module);
module_exit(scull_cleanup_module);
Makefile(注意命令前是tab键,而非空格键)
ifneq ($(KERNELRELEASE),)
# call from kernel build system
obj-m := scull.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) LDDINC=$(PWD)/../include modules
endif
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions