linux驱动简单实例

基础知识:

       不同于windows驱动程序,Linux设备驱动程序在与硬件设备之间建立了标准的抽象接口。通过这个接口,用户可以像处理普通文件一样,通过open,close,read,write等系统调用对设备进行操作,如此一来也大大简化了linux驱动程序的开发,后面会看到,通过file_operations这个结构体(实际上是一个函数指针的集合),把驱动的操作和设备号联系起来,程序员所要做的工作只是通过file_operations挂接自己的系统调用函数。

       linux中的设备可大致分为字符设备、块设备、网络设备等。本文关注的是字符设备驱动的编写。

(右图为linux驱动工作的基本示意图)

====================================================================================================

动手编写:

       linux内核源码中驱动程序占了相当大的比重,这些驱动程序可以被编译进内核,也可以编译为模块以供动态加载,为了便于调试以及一些列不可告人的目的,猪的第一个驱动程序将以模块的形式出现(废话,难道为了一个什么都不做的驱动重新编译内核?)

首先列出这个驱动的源码:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/xiaobenzhu/]
/* hello.c */
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>

#include <linux/kernel.h>	/* printk() */
#include <linux/fs.h>	   
#include <linux/errno.h>	/* error codes */
#include <linux/types.h>	/* size_t */
#include <linux/cdev.h>
#include <asm/uaccess.h>	/* copy_to_user() */

/* function prototypes */
static int hello_open( struct inode *inode, struct file *filp );
static int hello_release( struct inode *inode, struct file *filp );
ssize_t hello_read( struct file *flip, char __user *buf, size_t count,loff_t
					*f_pos);
ssize_t hello_write( struct file *filp, const char __user *buf, size_t count,
					 loff_t *f_pos );
static int hello_major = 0;		/* major device number */
MODULE_AUTHOR( "xiaobenzhu" );
MODULE_LICENSE( "Dual BSD/GPL" );


static struct cdev helloDev;	/* hello device structure */

/* file operations for hello device */
static struct file_operations hello_ops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.read = hello_read,
	.write = hello_write,
	.release = hello_release,
};
/* 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;
}

/* 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;
	dev->ops = fops;				 /* why not do it in cdev_init ? */
	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 );

	/* 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: %d, %d\n",hello_major,0 );
	//printk( KERN_ALERT"hello init: %d, %d\n",hello_major,0 );
	hello_setup_cdev(&helloDev, 0 , &hello_ops );
	
	return 0;
	
}

/* Exit routine */
static void hello_exit(void){
    /* remove the cdev from kernel */
	cdev_del(&helloDev );
	/* 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 );
}

/* 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;
}

/* register the init and exit routine of the module */
module_init( hello_init );
module_exit( hello_exit );

注意事项:

1. MODULE_LICENSE( "Dual BSD/GPL" );  指定模块使用的许可证

能被内核识别的许可证有GPL、GPL v2、 Dual BSD/GPL、 Dual MPL/GPL、Proprietary(专有)等,如果模块没有显式标记许可证,则会被认定为“专有”,内核加载这样的模块会被“污染”。

2.最后两句module_init( hello_init ); module_exit( hello_exit );指定了模块初始化和关闭函数

3.因为驱动工作在内核空间,不能使用用户空间的libc函数,所以程序中打印语句为内核提供的printk,而非printf,KERN_NOTICE宏其实标记的是日志级别(共有八个)不同级别的消息会记录到不同的地方。如果你运行本模块,可能会发现printk语句并没有输出到控制台,这是正常的,控制台只显示一定级别的消息。当日志级别小于console_loglevel时,消息才能显示出来。你可以通过dmsg命令看到这些信息,也可以通过修改日志级别使之输出到你的虚拟终端。方法详见:http://blog.sina.com.cn/s/blog_4a4832fe0100cvrk.html###

猪还有一个更方便的方法,就是另开一个终端,输入命令

# cat /proc/kmsg

然后这个终端就会变成你的内核日志记录器。

===============================================================================================================================

驱动详解:

1.模块被加载伊始,进入hello_init 函数,在这个函数中,动态的为我们的驱动申请了主设备号。设备号是干什么吃的?据LDD记载,对字符设备的访问是通过文件系统内的设备名称进行的。那些被称为特殊文件、设备文件的节点,通常位于/dev目录,如果ls -l 查看该目录,第一列中带有c标志的即为字符设备,有b标志的为块设备。而第5、6列所示的两个数字分别为设备的主、次设备号。通常,主设备号标识设备所用的驱动程序(现在大多设备仍然采用“一个主设备号对应一个驱动程序”的规则),次设备号用于确定设备,比如你有两块网卡,使用同一驱动,主设备号相同,那么他们将由次设备号区分。

        这里主设备号的分配由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);

2.然后进入hello_setup_cdev函数,对设备进行初始化

这里cdev结构体是内核内部使用来表示字符设备的。在内核调用设备操作之前,必须分配并注册一个或多个这样的结构。猪为了方便,没有动态使用cdev_alloc函数分配空间,而是定义了一个全局静态cdev变量。通常你可以将你的cdev嵌入到自定义的结构体中(猪这个驱动很naive,没有这么做),通过cdev_init 函数初始化。这里注意第二个参数,这就是传说中的file_operations, 用来挂载你的设备操作,他是一个函数指针集合,结构大致为

 

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/xiaobenzhu/]
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);
     ...  

};

 

猪实现了其中的open,read,write,close,因此在初始化ops变量时
div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/xiaobenzhu/]
static struct file_operations hello_ops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.read = hello_read,
	.write = hello_write,
	.release = hello_release,
};

.owner为所有者字段,防止在使用时模块被卸载。一边都设为THIS_MODULE

在cdev_init 之后,还需要对cdev中的类似的owner字段进行初始化。之后调用cdev_add函数在内核注册设备。(第二个参数为该设备对应的第一个设备编号,第三个参数是关联设备编号数量),注册之后设备的操作就可以被内核调用了。在模块卸载时,调用cdev_del注销设备。

相关函数原型:

void cdev_init(struct cdev *dev, struct file_operations *fops);

int cdev_add(struct cdev *dev, dev_t num, unsigned int count);

void cdev_del(struct cdev *dev);

3.接下来就是实现open,close,read,write操作了,这个驱动什么都没干,所以很好理解,用户请求read系统调用时,这个虚拟设备反回相应长度的“A”字符串,用户write时,将内容显示到日志中。

这里要注意的是,内核空间中不能使用用户态的malloc,而是使用kmalloc/kfree。而且,用户read/write提供的buf地址也是用户态的,内核自然不能直接访问,需要通过copy_to_user/copy_from_user 进行数据拷贝。

4.你可能会注意到open和release函数头中的file和inode结构体,inode是内核内部文件的表示,当其指向一个字符设备时,其中的i_cdev成员既包含了指向cdev结构的指针。而file表示打开的文件描述符,对一个文件,若打开多次,则会有多个file结构,但只有一个inode与之对应。

===============================================================================================================================

编译模块:

这就是这个驱动的全部内容了,下面猪来编译它。

makefile这样来写:

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/xiaobenzhu/]
#DEBUG = y

# Add your debugging flag (or not) to EXTRA_CFLAGS
ifeq ($(DEBUG),y)
  DEBFLAGS = -O -g # "-O" is needed to expand inlines
else
  DEBFLAGS = -O2
endif

EXTRA_CFLAGS += $(DEBFLAGS)
# 如果已经定义KERNELRELEASE,则说明是从内核构造系统调用的
# 因此可以利用其内建语句
ifneq ($(KERNELRELEASE),)
# call from kernel build system

obj-m	:= hello.o
# 否则,是直接从命令行调用的
# 这时要调用内核构造系统
else

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD       := $(shell pwd)

default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD)  modules

endif



clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

depend .depend dep:
	$(CC) $(EXTRA_CFLAGS) -M *.c > .depend


TEST_SOURCES = test_hello.c
.PHONY: test
test:$(TEST_SOURCES)
	$(CC) $< -o $@

ifeq (.depend,$(wildcard .depend))
include .depend
endif

2.6内核的模块构造方法与2.4不同,新构造系统使用更加简单,了解细节请阅读内核源码Documentation/kbuild目录下的文件,猪没有研究过

这里真正有用的只有一句,obj-m := hello.o ,若你的模块有多个.o文件组成,应该写成

obj-m    := hello.o
hello-objs := file1.o file2.o

上面的makefile之所以看上去比较麻烦,是因为他应对在内核树之外构造模块,当makefilel从命令行调用时,KERNELRELEASE尚未设置,那么设定KERNELDIR 为 当前模块目录中的build,这其实是一个符号链接,指向对应的内核构造树,这样一来就可以定位内核源码目录,然后调用default:目标,这使得make命令第二次被调用,这时,会检测KERNELRELEASE已然设置,那么设置obj-m, 然后内核的makefile真正负责构造模块。 具体过程请阅读LDD第二章内容

还有,在新版本内核中,过去的CFLAGS标志要改为EXTRA_CFLAGS

===============================================================================================================================

模块加载&设备文件节点构造:

1. 编译成功后,会得到hello.ko, 这时你就可以通过insmod命令加载模块

# insmod hello.ko

这时你的日志控制台中会出现hello_init中的打印信息,如果你使用lsmod列出当前已加载模块,会发现hello模块赫然在目

2.要想使用驱动,你需要在/dev 目录下建立设备文件节点,语法是

mknod [options] name {bc} major minor

这里需要知道设备的主、次设备号,何以知之?使用cat /proc/devices | grep hello 你就会得到其主设备号

比如猪得知hello的主设备号为250

那么就用下面的命令:

# mknod /dev/hello0 c 250 0

c表示字符设备,这样就可以通过该设备文件操作设备了。

这里mknod所创建的文件必定是属于root用户的,为了能让广大人民都能使用,使用chgrp和chmod命令更改其所属组和权限位

# chmod 664 /dev/hello0
# chgrp benzhu  /dev/hello0

上述一系列动作可以写一个shell脚本来简化工作

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/xiaobenzhu/]
#!/bin/sh
module="hello"
device="hello"
mode="664"

# Group: since distributions do it differently, look for wheel or use staff
if grep '^staff:' /etc/group > /dev/null; then
    group="staff"
else
    group="benzhu"
fi

# invoke insmod with all arguments we got
# and use a pathname, as newer modutils don't look in . by default
/sbin/insmod -f ./$module.ko $* || exit 1

major=$(awk "\$2==\"$module\" {print \$1}" /proc/devices)
# Remove stale nodes and replace them, then give gid and perms
# Usually the script is shorter, it's simple that has several devices in it.

echo $major
rm -f /dev/${device}0
mknod /dev/${device}0 c $major 0
chgrp $group /dev/${device}0
chmod $mode /dev/${device}0

其中查找主设备号通过awk完成

===============================================================================================================================

测试驱动:

       现在就可以通过系统调用操作设备了。猪写了C和Python的两个测试。

1.C测试

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/xiaobenzhu/]
/* test_hello.c */
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define READ_SIZE 10

int main(int argc, char **argv){
	int fd,count;
	char buf[READ_SIZE+1];
	if( argc<2 ){
		printf( "[Usage: test device_name ]\n" );
		exit(0);
	}
	if(( fd = open(argv[1],O_RDWR ))<0){
		printf( "Error:can not open the device: %s\n",argv[1] );
		exit(1);
	}
	printf("%s has been opened: (fd:%d).\n",argv[1],fd );
	if( (count = read(fd,buf,READ_SIZE ))<0 ){
		perror("read error.\n");
		exit(1);
	}
	printf( "read %d bytes from %s:%s\n",count,argv[1],buf );
	memcpy( buf,"Hello",6 );
	if( (count = write( fd, buf ,6 ))<0 ){
		perror("write error.\n");
		exit(1);		
	}
	printf( "write %d bytes to %s:%s\n",count,argv[1],buf );
	close(fd);
	printf("close device %s\n",argv[1] );
	return 0;
}

 

2.Python 测试

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/xiaobenzhu/]
#!/usr/bin/python
import sys,getopt
def usage():
    print "[Usage: python_test dev_name]"
    
def getopts():
    try:
        opts,args = getopt.getopt(sys.argv[1:],"ho:",["help","output="])
    except getopt.GetoptError:
        print "Bad options."
        usage()
        sys.exit()
    for o,a in opts:
        if o in ("-h","--help"):
            usage()
            sys.exit()
        if o in ("-o","--output"):
            output = a

def test_read(filename):
    #open the given file
    try:
        file = open(filename,"r")
    except IOError,message:
        print >> sys.stderr, "File could not be opened",message
        sys.exit(1)
    print "file opened."

    msg = file.read(10);
    print "get message from:",filename,":",msg 

    file.close()
    print "file closed."

def test_write(filename):
    #open the given file
    try:
        file = open(filename,"w")
    except IOError,message:
        print >> sys.stderr, "File could not be opened",message
        sys.exit(1)
    print "file opened."

    msg = "Hello!I am tomsheep!\0"
    file.write(msg)
    print "wrote to device:",filename , ":",msg
    
    file.close()
    print "file closed."

def main():
    if len(sys.argv) < 2:
        usage()
        sys.exit()
    #deal with the options
    getopts()
    
    filename = sys.argv[1]

    #test I/O
    test_read(filename)
    test_write(filename)



#main routine
main()

通过控制台和系统日志中的信息,就可以知道驱动是不是工作正常了

===============================================================================================================================

卸载模块&移除设备

通过rmmod卸载模块, rm移除设备文件节点

同样可以写一个一劳永逸的脚本

div css xhtml xml Example Source Code Example Source Code [http://www.cnblogs.com/xiaobenzhu/]
#!/bin/sh
module="hello"
device="hello"

# invoke rmmod with all arguments we got
/sbin/rmmod $module $* || exit 1

# Remove stale nodes
rm -f /dev/${device}0

===============================================================================================================================

你可能感兴趣的:(linux,驱动)