什么是字符驱动?
所谓驱动程序,本质上讲是硬件接口,因为操作系统不可能实现每种硬件的接口,所以只对厂商提供接口,只要厂商实现这些接口,就可被操作系统调用,Linux系统驱动程序分为字符设备驱动和块设备驱动,所谓字符设备驱动就是例如键盘驱动,只能顺次读取数据,块设备驱动入硬盘等,可以随机分块读取。而有些程序虽然符合驱动程序规范,但却不真正驱动硬件,而是对操作系统功能的扩充,也称作内核模块。所以驱动程序和内核模块本质上讲属于同一种类别。
如何实现?
操作系统对字符设备驱动提供 file_operations 结构,该结构成员大部分都是回调函数(以下代码摘自Linux 2.6.34内核源码):
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 **);
};
作为驱动程序,只需要实现部分回调函数,注册该结构体,用户态进程便可通过系统调用open,read等调用相应回调函数指针。
首先,系统提供若干宏声明驱动程序的属性,如,入口,作者,描述信息等等。
初始化程序原型为 static int __init initialization(void);
若初始化成功则返回0,否则返回错误码。
清理函数原型为static void __exit cleanup(void);
其中__init __exit
是指定代码段属性的宏,当然也可不指定此属性。
另外宏MODULE_AUTHOR指明作者等等
实现该函数后便可通过宏指明入口点:
module_init(initialization);
module_exit(cleanup);
最简单的驱动程序就是仅仅实现这两个函数,文件simple.c如下:
#include<linux/init.h>
#include<linux/module.h>
static int __init initialization(void)
{
printk(KERN_INFO " init simple\n");
return 0;
}
static void __exit cleanup(void)
{
printk(KERN_INFO " cleanup simple\n");
}
module_init(initialization);
module_exit(cleanup);
MODULE_AUTHOR("alloc cppbreak@gmail.com");
MODULE_DESCRIPTION("A simple linux kernel module");
MODULE_VERSION("V0.1");
MODULE_LICENSE("Dual BSD/GPL");
驱动的编译需要写Makefile文件,内容如下
obj-m := simple.o
编译时需指定Linux内核源码所处位置:
make -C /usr/src/linux M=$PWD modules
其中/usr/src/linux为当前内核源码目录,$PWD
为驱动程序所处目录,PWD
为当前目录。
执行成功后,会生成simple.ko
文件,此文件即为驱动程序。
加载驱动程序可执行 insmod simple.ko 卸载驱动执行 rmmod simple 命令 lsmod 可以查看目前系统加载的驱动程序,modinfo simple.ko 查看程序的基本信息,输出即为程序声明信息:
filename: simple.ko
license: Dual BSD/GPL
version: V0.1
description: A simple linux kernel module
author: alloc cppbreak@gmail.com
srcversion: 95E3CE3AB899900656E9CAD
depends:
vermagic: 2.6.33.3-85.fc13.x86_64 SMP mod_unload
在程序中调用了两次printk,为内核输出函数,这里的输出不会显示到控制台,只会输出到内核,可以通过读取/proc/kmsg
文件查看信息,或者调用dmesg命令查看,此为内核跟踪错误的重要手段。KERN_INFO宏只是一个数字字符串,含义为日志级别,可以通过echo num > /proc/sys/kernel/printk
来控制输出信息的级别。
当然,只有此两个函数只能正常加载卸载驱动程序,并没有任何意义,下面通过注册回调函数来实现字符设备的功能,只举一个简单的例子,实现open
,read
,close
函数。
根据file_operations结构成员的原型,这里我们需要实现如下回调:
int (*open) (struct inode *, struct file *);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
int (*release) (struct inode *, struct file *);
open和close函数为了简单起见不做任何处理,只是简单的输出kernel信息:
int simple_open(struct inode * pnode, struct file * pfile)
{
printk(KERN_INFO "open simple\n");
return 0;
}
int simple_release(struct inode * pnode, struct file * pfile)
{
printk(KERN_INFO "close simple\n");
return 0;
}
对于read函数,因为需要把内核态的数据返回到用户态,而二者是具有堆栈隔离的,需要借助几个函数:
long copy_from_user(void *to, const void __user *from, long n);
long copy_to_user(void __user *to, const void *from, long n);
copy_from_user 是把用户态数据from拷贝至内核态内存to中,coyp_to_user与之相反,返回结果为不能被复制的字节数,如果成功,则返回0,对于内核编程,错误处理非常重要,因为一微不足道的错误就可能导致系统崩溃,所以对于函数调用,要异常小心,需要区分每一个传入参数,以及判断每一个函数返回值,read函数可以实现如下:
ssize_t simple_read(struct file * pfile,
char __user * buf, size_t size, loff_t * ppos)
{
if (copy_to_user(buf, "test data\n", 10))
return -EFAULT;
else
return 10;
}
到这里回调函数都已实现,但用户态进程如何才能访问驱动呢?这还得从初始化内核时说起,要访问一个设备,需要进行三个基本操作: 第一,对于一个设备驱动,需要分配一个设备号来唯一标识; 第二,需要注册回调函数至文件系统,以便上层回调; 第三,需要创建一个设备节点,挂接至/dev/下,这样用户层就可类似打开文件驱动方式打开设备驱动了。
由以上三点,重写设备初始化函数如下:
static int __init initialization(void)
{
int result;
/* 分配设备号 */
result = alloc_chrdev_region(&devno, 0, 1, "simple");
if (result < 0)
return result;
/* 注册回调函数 */
cdev_init(&cdev, &simple_op);
result = cdev_add(&cdev, devno, 1);
/* 创建设备节点 */
simple_class = class_create(THIS_MODULE, "simple");
device_create(simple_class, NULL, devno, NULL, "simple");
printk(KERN_INFO " init simple\n");
return result;
}
当然,在exit函数中,需要对分配的资源逐一释放,整理之后代码如下:
#include<linux/init.h>
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/types.h>
#include<linux/cdev.h>
#include<linux/mm.h>
#include<linux/sched.h>
#include<asm/io.h>
#include<asm/uaccess.h>
#include<asm/system.h>
#include<linux/device.h>
dev_t devno;
struct class * simple_class;
static struct cdev cdev;
ssize_t simple_read(struct file * pfile,
char __user * buf, size_t size, loff_t * ppos)
{
if (copy_to_user(buf, "test data\n", 10))
return -EFAULT;
else
return 10;
}
int simple_open(struct inode * pnode, struct file * pfile)
{
printk(KERN_INFO "open simple\n");
return 0;
}
int simple_release(struct inode * pnode, struct file * pfile)
{
printk(KERN_INFO "close simple\n");
return 0;
}
static struct file_operations simple_op =
{
.owner = THIS_MODULE,
.read = simple_read,
.open = simple_open,
.release = simple_release,
};
static int __init initialization(void)
{
int result;
result = alloc_chrdev_region(&devno, 0, 1, "simple");
if (result < 0)
return result;
cdev_init(&cdev, &simple_op);
result = cdev_add(&cdev, devno, 1);
simple_class = class_create(THIS_MODULE, "simple");
device_create(simple_class, NULL, devno, NULL, "simple");
printk(KERN_INFO " init simple\n");
return result;
}
static void __exit cleanup(void)
{
device_destroy(simple_class, devno);
class_destroy(simple_class);
cdev_del(&cdev);
unregister_chrdev_region(devno, 1);
printk(KERN_INFO " cleanup simple\n");
}
module_init(initialization);
module_exit(cleanup);
MODULE_AUTHOR("alloc cppbreak@gmail.com");
MODULE_DESCRIPTION("A simple linux kernel module");
MODULE_VERSION("V0.1");
MODULE_LICENSE("Dual BSD/GPL");
编译加载驱动后,ls /dev/
可查看到simple节点,调用 cat /dev/simple
命令,发现会不停输出 test data
,原因是cat命令会输出文件所有内容,但此时的驱动只要调用read就会返回数据,不会到达文件结尾,会无休止输出,毕竟这是一个很简单的驱动,去除了影响理解框架的逻辑信息。