功能
实现简单的字符操作(从用户空间向内核空间写入一串字符;从内核空间读一个字符到内核空间)
众所周知,字符设备是linux下最基本,也是最常用到的设备,它是学习Linux驱动入门最好的选择,计算机的东西很多都是相通的,掌握了其中一块,其他就可以触类旁通了。在写驱动前,必须先搞清楚字符设备的框架大概是怎样的,弄清楚了流程,才开始动手,不要一开始就动手写代码!
这里内核是基于Linux 2.6,3.0以上的有些地方会不一样(主要是file_operations中的ioctl修改了),但基本上适用,因为我就是在3.0的内核上实现的!字符设备驱动的初始化流程大概如下所示:
定义相关的设备文件结构体(如file_operation()中的相关成员函数的定义)->
向内核申请主设备号(建议采用动态方式) ->
申请成功后,调用MAJOR()获取主设备号 ->
初始化cdev的结构体,调用cdev_init() ->
调用cdev_add(),注册cdev到kernel ->
注册设备模块:module_init()、module_exit()。
首先定义两个全局变量(主设备号和字符设备hellow):
static int hello_major = 0; /* major device number */
static struct cdev hellow; /* hello device structure */
然后来看看file_operations(),它的定义可以在…/include/linux/fs.h下找到,这里只用到了其中的几个成员函数:
/* file operations for hello device */
static struct file_operations hello_ops = {
.owner = THIS_MODULE, /*owner为所有者字段,防止在使用时模块被卸载。一边都设为THIS_MODULE*/
.open = hello_open,
.read = hello_read,
.write = hello_write,
.release = hello_release,
};
不同于windows驱动程序,Linux设备驱动程序在与硬件设备之间建立了标准的抽象接口。通过这个接口,用户可以像处理普通文件一样,通过open,close,read,write等系统调用对设备进行操作,如此一来也大大简化了linux驱动程序的开发。
通过file_operations这个结构体(实际上是一个函数指针的集合),把驱动的操作和设备号联系起来,程序员所要做的工作只是通过file_operations挂接自己的系统调用函数。
接下来就是实现open,close,read,write操作了,这个驱动什么都没干,所以很好理解,用户请求read系统调用时,这个虚拟设备反回相应长度的“A”字符串,用户write时,将内容显示到日志中。
这里要注意的是,内核空间中不能使用用户态的malloc,而是使用kmalloc/kfree。而且,用户read/write提供的buf地址也是用户态的,内核自然不能直接访问,需要通过copy_to_user/copy_from_user 进行数据拷贝,具体如下:
/* Open the device */
static int hello_open(struct inode *inode, struct file *filp)
{
printk(KERN_NOTICE"Hello device open!\n");
return 0;
}
/* Close hello_device */
static int hello_release(struct inode *inode, struct file *filp)
{
printk(KERN_NOTICE"Hello device close!\n");
return 0;
}
/* user read from hello device*/
ssize_t hello_read(struct file *flip, char __user *buf, size_t count, loff_t *f_pos)
{
ssize_t retval = 0;
char *bank;
bank = kmalloc(count+1, GFP_KERNEL);
if(bank == NULL)
return -1;
memset(bank, 'A',count);
if(copy_to_user(buf, bank, count))
{
retval = -EFAULT;
goto out;
}
retval += count;
*(bank + count) = 0;
printk( KERN_NOTICE"hello: user read %d bytes from me. %s\n",count,bank );
out:
kfree(bank);
return retval;
}
/* write to hello device */
ssize_t hello_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
ssize_t retval = 0;
char *bank = kmalloc(count ,GFP_KERNEL);
if(bank == NULL)
return retval;
if(copy_from_user(bank, buf, count))
{
retval = -EFAULT;
printk(KERN_NOTICE"hello: write error\n");
goto out;
}
retval += count;
printk(KERN_NOTICE"hello: user has written %d bytes to me: %s\n",count, bank);
out:
kfree(bank );
return retval;
}
你可能会注意到open和release函数头中的file和inode结构体,inode是内核内部文件的表示,当其指向一个字符设备时,其中的i_cdev成员既包含了指向cdev结构的指针。而file表示打开的文件描述符,对一个文件,若打开多次,则会有多个file结构,但只有一个inode与之对应。
因为驱动工作在内核空间,不能使用用户空间的libc函数,所以程序中打印语句为内核提供的printk,而非printf,KERN_NOTICE宏其实标记的是日志级别(共有八个)不同级别的消息会记录到不同的地方。如果你运行本模块,可能会发现printk语句并没有输出到控制台,这是正常的,控制台只显示一定级别的消息。当日志级别小于console_loglevel时,消息才能显示出来。你可以通过dmsg命令看到这些信息,也可以通过修改日志级别使之输出到你的虚拟终端。
作好以上准备工作后,接下来就可以开始进行向内核申请主设备号了。设备号是干什么吃的?据LDD记载,对字符设备的访问是通过文件系统内的设备名称进行的。那些被称为特殊文件、设备文件的节点,通常位于/dev目录,如果ls -l 查看该目录,第一列中带有c标志的即为字符设备,有b标志的为块设备。而第5、6列所示的两个数字分别为设备的主、次设备号。通常,主设备号标识设备所用的驱动程序(现在大多设备仍然采用“一个主设备号对应一个驱动程序”的规则),次设备号用于确定设备,比如你有两块网卡,使用同一驱动,主设备号相同,那么他们将由次设备号区分。
/* Module housekeeping */
static int hello_init(void)
{
int result;
dev_t dev = MKDEV(hello_major, 0); /*to transfer major as dev_t type ,将主设备号转换为设备类型 */
/* alloc the major device number dynamicly */
result = alloc_chrdev_region(&dev, 0 ,1, "hello"); /* 向内核申请动态主设备号 */
if(result < 0)
{
printk(KERN_NOTICE"Hello: unable to get major %d\n",hello_major);
return result;
}
hello_major = MAJOR(dev); /* 获取主设备号 */
/* set up devices, in this case, there is only one device */
printk( KERN_NOTICE"hello init. major:%d, minor:%d\n",hello_major,0 );
//printk( KERN_ALERT"hello init: %d, %d\n",hello_major,0 );
hello_setup_cdev(&hellow, 0 , &hello_ops ); /* 设置字符设备的结构,主要是一些函数指针 */
return 0;
}
/* Exit routine */
static void hello_exit(void)
{
/* remove the cdev from kernel */
cdev_del(&hellow );
/* release the device numble alloced earlier */
unregister_chrdev_region(MKDEV( hello_major, 0 ), 1);
printk(KERN_NOTICE"hello exit. major:%d,minor %d\n",hello_major,0);
}
这里主设备号的分配由alloc_chrdev_region(第一个参数为dev_t 指针,用来存放设备编号,第二个参数为要使用的第一个次设备号,通常为0,第三个参数为请求的连续设备编号个数)动态分配,当然也可以静态指定一个未被使用的主设备号,相应函数为register_chrdev_region,但不推荐这样做。在模块被卸载时(hello_exit),通过unregister_chrdev_region释放设备号。MKDEV宏将给出的主、次设备号转换成dev_t类型,MAJOR,MINOR分别从dev_t中析取主次设备号。
这里几个函数的原型为:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
void unregister_chrdev_region(dev_t first, unsigned int count);
然后进入hello_setup_cdev函数,对设备进行初始化这里cdev结构体是内核内部使用来表示字符设备的。在内核调用设备操作之前,必须分配并注册一个或多个这样的结构。为了方便,没有动态使用cdev_alloc函数分配空间,而是定义了一个全局静态cdev变量。通常你可以将你的cdev嵌入到自定义的结构体中(这个驱动很naive,没有这么做),通过cdev_init 函数初始化。最后调用cdev_add(),注册cdev到内核。
/* set up the cdev stucture for a device */
static void hello_setup_cdev(struct cdev *dev, int minor, struct file_operations *fops)
{
int err;
int devno = MKDEV(hello_major, minor);
/* initialize the cdev struct */
cdev_init(dev,fops);
dev->owner = THIS_MODULE;
err = cdev_add(dev, devno, 1); /* register the cdev in the kernel */
if(err)
printk( KERN_NOTICE"Error %d adding hello%d\n",err ,minor );
}
最后module_init( hello_init ); module_exit( hello_exit );指定了模块初始化和关闭函数。MODULE_LICENSE( “Dual BSD/GPL” ); 指定模块使用的许可证能被内核识别的许可证有GPL、GPL v2、 Dual BSD/GPL、 Dual MPL/GPL、Proprietary(专有)等,如果模块没有显式标记许可证,则会被认定为“专有”,内核加载这样的模块会被“污染”。
/* register the init and exit routine of the module */
module_init(hello_init);
module_exit(hello_exit);
MODULE_AUTHOR("root");
MODULE_LICENSE("Dual BSD/GPL");
到这里,这个字符设备驱动已经完成,接下来就是编译它。
这个是我写的makefile文件,在我台机上我没把这个模块加入到内核源码的字符设备目录下,而是放在了用户目录下面。但这个makefile文件对以上两种情况都支持:
Makefile:
obj-m += hellow.o
KER_APP = hellow
USER_APP = test_char_dev
USER_OBJ = test_char_dev.o
KERNEL_PATH = /lib/modules/$(shell uname -r)/build/
all: $(KER_APP) $(USER_APP)
$(USER_APP): $(USER_OBJ)
gcc $(USER_OBJ) -o $(USER_APP)
$(KER_APP):
make -C $(KERNEL_PATH) M=$(PWD) modules
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
rm -f $(USER_APP) $(USER_OBJ)
- 编译成功后,会得到hellow.ko, 这时你就可以通过insmod命令加载模块
insmod hellow.ko
这时你的日志控制台中会出现hello_init中的打印信息,如果你使用lsmod列出当前已加载模块,会发现hellow模块赫然在目:
[root@ charDevice]# lsmod | grep hello
hellow 16384 0
[root@ charDevice]# dmesg | tail -10
[159219.257355] pcnet32 0000:02:01.0 ens33: link up
[159239.257064] pcnet32 0000:02:01.0 ens33: link down
[159249.257119] pcnet32 0000:02:01.0 ens33: link up
[159793.242238] pcnet32 0000:02:01.0 ens33: link down
[159803.241294] pcnet32 0000:02:01.0 ens33: link up
[167121.200708] pcnet32 0000:02:01.0 ens33: link down
[167131.199587] pcnet32 0000:02:01.0 ens33: link up
[168569.168015] pcnet32 0000:02:01.0 ens33: link down
[168579.167902] pcnet32 0000:02:01.0 ens33: link up
[190154.539959] hello init. major:246, minor:0
内核完整代码:
#include <linux/proc_fs.h>
#include <linux/module.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
#include <linux/cdev.h>
static int hello_major = 0; /* major device number */
static struct cdev hellow; /* hello device structure */
/* Open the device */
static int hello_open(struct inode *inode, struct file *filp)
{
printk(KERN_NOTICE"Hello device open!\n");
return 0;
}
/* Close hello_device */
static int hello_release(struct inode *inode, struct file *filp)
{
printk(KERN_NOTICE"Hello device close!\n");
return 0;
}
/* user read from hello device*/
ssize_t hello_read(struct file *flip, char __user *buf, size_t count, loff_t *f_pos)
{
ssize_t retval = 0;
char *bank;
bank = kmalloc(count+1, GFP_KERNEL);
if(bank == NULL)
return -1;
memset(bank, 'A',count);
if(copy_to_user(buf, bank, count))
{
retval = -EFAULT;
goto out;
}
retval += count;
*(bank + count) = 0;
printk( KERN_NOTICE"hello: user read %d bytes from me. %s\n",count,bank );
out:
kfree(bank);
return retval;
}
/* write to hello device */
ssize_t hello_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
ssize_t retval = 0;
char *bank = kmalloc(count ,GFP_KERNEL);
if(bank == NULL)
return retval;
if(copy_from_user(bank, buf, count))
{
retval = -EFAULT;
printk(KERN_NOTICE"hello: write error\n");
goto out;
}
retval += count;
printk(KERN_NOTICE"hello: user has written %d bytes to me: %s\n",count, bank);
out:
kfree(bank );
return retval;
}
static struct file_operations hello_ops = {
.owner = THIS_MODULE, /*owner为所有者字段,防止在使用时模块被卸载。一边都设为THIS_MODULE*/
.open = hello_open,
.read = hello_read,
.write = hello_write,
.release = hello_release,
};
/* set up the cdev stucture for a device */
static void hello_setup_cdev(struct cdev *dev, int minor, struct file_operations *fops)
{
int err;
int devno = MKDEV(hello_major, minor);
/* initialize the cdev struct */
cdev_init(dev,fops);
dev->owner = THIS_MODULE;
err = cdev_add(dev, devno, 1); /* register the cdev in the kernel */
if(err)
printk( KERN_NOTICE"Error %d adding hello%d\n",err ,minor );
}
/* Module housekeeping */
static int hello_init(void)
{
int result;
dev_t dev = MKDEV(hello_major, 0); /*to transfer major as dev_t type ,将主设备号转换为设备类型 */
/* alloc the major device number dynamicly */
result = alloc_chrdev_region(&dev, 0 ,1, "hello"); /* 向内核申请动态主设备号 */
if(result < 0)
{
printk(KERN_NOTICE"Hello: unable to get major %d\n",hello_major);
return result;
}
hello_major = MAJOR(dev); /* 获取主设备号 */
/* set up devices, in this case, there is only one device */
printk( KERN_NOTICE"hello init. major:%d, minor:%d\n",hello_major,0 );
//printk( KERN_ALERT"hello init: %d, %d\n",hello_major,0 );
hello_setup_cdev(&hellow, 0 , &hello_ops ); /* 设置字符设备的结构,主要是一些函数指针 */
return 0;
}
/* Exit routine */
static void hello_exit(void)
{
/* remove the cdev from kernel */
cdev_del(&hellow );
/* release the device numble alloced earlier */
unregister_chrdev_region(MKDEV( hello_major, 0 ), 1);
printk(KERN_NOTICE"hello exit. major:%d,minor %d\n",hello_major,0);
}
/* register the init and exit routine of the module */
module_init(hello_init);
module_exit(hello_exit);
MODULE_AUTHOR("root");
MODULE_LICENSE("Dual BSD/GPL");
2.要想使用驱动,你需要在/dev 目录下建立设备文件节点,语法是
[root@ charDevice]# mknod /dev/hellow c 246 0
这里需要知道设备的主、次设备号,何以知之?使用cat /proc/devices | grep hello 你就会得到其主设备号
比如我这里得知hellow的主设备号为246
那么就用下面的命令:
root@ubuntu:~/share/hellow# mknod /dev/hellow c 246 0
c表示字符设备,这样就可以通过该设备文件操作设备了。
测试程序:
/* test_char_dev.c */
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main(void)
{
int fd, ret;
char *buf = "Hello world!";
char temp[10] = "0";
fd = open ("/dev/hellow" , O_RDWR);
if ( fd == -1 )
{
perror("open");
exit(0);
}
ret = write(fd, buf,strlen(buf));
if (ret == -1)
{
perror("write");
exit(0);
}
ret = read (fd ,temp, strlen(temp));
if (ret == -1)
{
perror("read");
exit(0);
}
close(fd);
return 0;
}
运行结果:
[root@ charDevice]# ./test_char_dev
[root@ charDevice]# dmesg | tail -10
[159803.241294] pcnet32 0000:02:01.0 ens33: link up
[167121.200708] pcnet32 0000:02:01.0 ens33: link down
[167131.199587] pcnet32 0000:02:01.0 ens33: link up
[168569.168015] pcnet32 0000:02:01.0 ens33: link down
[168579.167902] pcnet32 0000:02:01.0 ens33: link up
[190154.539959] hello init. major:246, minor:0
[190794.508520] Hello device open!
[190794.508525] hello: user has written 14 bytes to me: Hello world!
[190794.508527] hello: user read 1 bytes from me. A
[190794.508528] Hello device close!