文档章节

linux 字符驱动心得

quanwei9958
 quanwei9958
发布于 2014/09/17 20:24
字数 2342
阅读 577
收藏 26

  linux字符驱动框架相比初学还是比较难记的,在学了一阵子字符驱动的开发后对于框架的搭建总结出了几个字 。

  对于框架来讲主要要完成两步。

申请设备号,注册字符驱动

 其关键代码就两句

~
int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);//动态申请设备号

int cdev_add(struct cdev *, dev_t, unsigned);                      //注册字符驱动
~

执行完次就可以将我们的驱动程序加载到内核里了

首先我们搭建主程序,字符驱动的名字就叫做"main"

首先先写下将要用到的头文件,以及一个宏定义,指明了我们驱动的名称,当然名称可以任意这里就取"main" 作为名字  

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/coda.h>
#include <linux/slab.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define MUDULE_NAME "main"

驱动由于需要加载到内核里,所以我们需要声明一下我们驱动所遵循的协议,如果没有申明,那么加载内核的时候系统会提示一段信息。我们按照内核的风格来,就加一个GPL协议吧

MODULE_LICENSE("GPL");

我们要想将我们的驱动注册到内核里,就必须将我们的驱动本身作为一个抽象,抽象成一个struct cdev的结构体。因为我们系统内部有许多中字符驱动,为了将这些不同种类的驱动都能使用同一个函数进行注册,内核声明了一个结构体,不同的驱动通过这个结构体--变成了一个抽象的驱动供系统调用。这段有点罗嗦,我们来看一下cdev这个结构体吧。

//这段不属于主程序
struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

这个结构体就包含了一个驱动所应有的东西其中 kobj 不需要管它,我也没有仔细研究,owner指向模块的所有者,常常使用THIS_MODULE这个宏来赋值,ops是我们主要做的工作,其中定义了各种操作的接口。

下面我们定义了我们程序的抽象体mydev,以及他所需要的接口

struct cdev mydev;
struct file_operations ops;

struct file_operations这个结构有点庞大。

//不属于本程序
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 *);
    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 *, loff_t, loff_t, 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 **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
};

 上面看到,这个结构内部都是一些函数指针,相当与这个结构本身就是一个接口,在c语言中没有接口这个概念,使用这种方式来定义也是一种巧妙的用法。不过有所不同的是我们可以不完全实现其中的接口。

 应用程序在使用驱动的时候常常需要open,write,read,close这几种操作,也就对应了file_operations结构中的open,write,read,release这几个函数指针。下面我们开始实现我们自己的函数体。注意:我们自己实现的函数必须满足接口函数所定义的形式。

static int main_open(struct inode* inode,struct file* filp)
{
	return 0;
}

这个教程里面的程序,我们就让驱动只能往里面写一个字符为例,读取也是只能读取一个字符。

 我们定义一个静态的字符类型的变量来当作我们的存储空间,通过copy_from_user来将用户空间的数据拷贝到我们驱动  所在的内核空间。原型是:  

                static inline long copy_from_user(void *to, const void __user * from, unsigned long n)

 类似地,我们使用copy_to_user来完成内核空间到用户空间的数据拷贝。

                static inline long copy_to_user(void __user *to,const void *from, unsigned long n)

static char main_buffer;
static ssize_t main_write(struct file* filp,const char __user* buffer,size_t length,loff_t * l)
{
	if(length!=1)	return -1;
	copy_from_user(&main_buffer,buffer,length);
	return 1;
}

下面是读的实现

static ssize_t main_read(struct file* filp,char __user * buffer,size_t length,loff_t*l)
{
	if(length!=1) return -1;
	copy_to_user(buffer,&main_buffer,length);
	return 1;
}

再稍稍实现一下close

static int main_close(struct inode* inode,struct file* filp)
{
	return 0;
}

我们所需要的内容都已经填写完毕,我们在驱动初始化的时候调用cdev_add驱动注册到系统就行了,不过在注册之前我们要申请设备号。

static dev_t dev;
static int __init main_init(void)
{

首先我们使用动态申请的方式申请设备号

       	int result=alloc_chrdev_region(&dev,0,1,MODULE_NAME);

dev就是我们申请的设备号,其中dev_t其实就是一个无符号的long型,通过调用alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *) 将申请到的设备号写入到dev中,第二个参数是子设备号从几开始,第三个参数是申请几个设备号,因为申请多个设备号是连续的,所以我们只需要知道第一个就行了。第四个参数代表我们驱动的名称。

返回值如果小于0则表示我们申请失败,通过printk打印错误信息。我测试的在动态加载的时候printk都不能打印其信息,如果在ubuntu下可以查看/var/log/kern.log,如果是centos下可以查看/var/log/mssages来查看printk打印的信息,一般查看后10条就能足够了。

        if(result<0)
	{
		printk(KERN_ALERT"device load error");
		return -1;
	}

    然后我们再构造一下我们的接口结构体

        ops.owner=THIS_MODULE;
        ops.open=main_open;
	ops.release=main_close;
	ops.write=main_write;
	ops.read=main_read;

  构造完之后,我们就只剩下我们最重要的一步了,就是向系统注册我们的驱动。

  不过,先别急,我们注册前得先把我们的抽象驱动mydev给构造了,mydev的定义在最上面。

        cdev_init(&mydev,&ops);
	mydev.owner=THIS_MODULE;

这样,我们就使用了我们的ops构造了我们的抽象驱动,当我们把这个驱动添加到我们的内核里面的时候,假如内核想对这个驱动进行写的操作,就会从mydev->ops->main_write这样找到我们自己实现的写函数了。

接下来就注册我们的驱动。

        cdev_add(&mydev,dev,1);
        printk(KERN_ALERT"device load success\n");
        return 0;
}

至此,我们的驱动就算完成了,不过有一点,就是我们的驱动有了初始化函数了就一定还有一个清理的函数了,该函数主要在我们卸载驱动的时候会调用,module_init及module_exit主要用于声明这个驱动的入口与出口,是必须要做的一步。

static void __exit main_exit(void)
{
        unregister_chrdev_region(dev,1);
	cdev_del(&mydev);
}
module_init(main_init);
module_exit(main_exit);

我的Makefile是下面这样

ifeq ($(KERNELRELEASE),)
KERNELDIR?=/lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules -Wall
modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install -Wall
clean:
	rm -rf *.0 *~ core .depend .*.cmd
	sudo rmmod main
	sudo rm /dev/main
install:
	sudo insmod main.ko
	sudo mknod /dev/main c 250 0
message:
	tail -n 5 /var/log/kern.log
.PHONY: modules modules_install clean
else
	obj-m :=main.o
endif

因为我的机器是使用ubuntu,所以在message标签下是tail -n 5 /var/log/kern.log 如果的/var/log目录下没有kern.log,那么就替换成/var/log/messages

编译

$make

因为这个Makefile的install 都是针对我自己电脑而写的,所以并不能在你电脑上保证执行make install 的正确性。还是在命令行中敲吧

sudo insmod main.ko

安装模块,然后查看/proc/devices里面main这个模块对应的设备号是多少,我的是250,所以创建设备节点时这样创建

sudo mknod /dev/main c 250 0

这样就成功把我们的驱动安装到内核里了

    执行make message可以看到如下信息

tail -n 5 /var/log/kern.log
Sep 17 20:05:57 quanweiC kernel: [23536.688371] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=2558 DF PROTO=UDP SPT=11818 DPT=26724 LEN=43 
Sep 17 20:06:02 quanweiC kernel: [23541.691748] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=3335 DF PROTO=UDP SPT=11818 DPT=26724 LEN=43 
Sep 17 20:06:51 quanweiC kernel: [23590.610275] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=10708 PROTO=UDP SPT=11818 DPT=10948 LEN=43 
Sep 17 20:07:04 quanweiC kernel: [23603.815562] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=12523 PROTO=UDP SPT=11818 DPT=10104 LEN=43 
Sep 17 20:07:04 quanweiC kernel: [23603.930248] device load success

最后一行显示我们加载驱动成功了。这样一个简单的字符驱动就写成功了。

© 著作权归作者所有

quanwei9958

quanwei9958

粉丝 29
博文 79
码字总数 36250
作品 0
程序员
私信 提问
再谈UNIX流机制和tty驱动

忍不住再次说一下unix的流机制。周末在家调试linux的终端驱动,发现linux并没有按照unix流机制的建议来实现tty驱动,虽然我对windows内核理解不如对linux深刻,可是还是略知一二,windows的分...

晨曦之光
2012/04/10
282
0
Linux设备驱动开发学习(2):Linux设备驱动简介

2.1 设备驱动的角色 设备驱动是介于应用软件和硬件设备(或其他虚拟设备)之间的程序,驱动完成对硬件设备(或其他虚 拟设备)的管理,应用软件对硬件的访问通过驱动程序来完成。当环境变得复...

lengxujun
2018/06/26
0
0
Linux 字符设备驱动简单总结

看完宋宝华的《Linux设备驱动开发详解》及其有关博客,对字符设备驱动做一个小总结。 一、字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系。 如图,在Linux内核中使用cdev结...

Zha_GW
2014/01/09
0
0
Linux操作系统下安装显卡驱动的方法步骤

Linux下安装显卡驱动 第一步:下载一个for Linux版的显卡驱动,我下的NVIDIA-Linux-x86-173.08-pkg1.run我的内核是2.6.18-53.el5 第二步:如果查出你的内核中存在xen字样,说时你正处在虚拟机...

zt371
2009/05/08
329
0
Linux操作系统下安装显卡驱动的方法步骤

Linux下安装显卡驱动 第一步:下载一个for Linux版的显卡驱动,我下的NVIDIA-Linux-x86-173.08-pkg1.run我的内核是2.6.18-53.el5 第二步:如果查出你的内核中存在xen字样,说时你正处在虚拟机...

zt371
2009/05/08
249
0

没有更多内容

加载失败,请刷新页面

加载更多

1.webstorm安装,配置

官网地址:http://www.jetbrains.com/webstorm/ 激活 http://idea.lanyus.com/ 一、安装node.js(https://nodejs.org/zh-cn/download/) 下载完毕后,可以安装node,建议不要安装在系统盘(如...

江戸川
31分钟前
1
0
共享Session

分布式系统中,Session 共享有很多的解决方案,其中托管到缓存中应该是最常用的方案之一。 spring官方说明: Spring Session 提供了一套创建和管理 Servlet HttpSession 的方案。Spring Sess...

贾峰uk
50分钟前
2
0
秒杀

少年已不再年少
51分钟前
1
0
解决SecureCRT中文版“数据库里没找到防火墙‘无’”

用户初巡检,进入系统,登陆对方系统的CRT,是一个中文的crt,但是每次打开都会有个防火墙的错误提示,“数据库里没找到防火墙“无”。此会话将尝试不通过防火墙进行连接。”如下图: 出现这...

突突突酱
52分钟前
2
0
UltraEdit使用正则表达式

正则表达式 (UltraEdit Syntax): % 匹配行首 - 表明要搜索的字符串一定在行首. $ 匹配行尾 - 表明要搜索的字符串一定在行尾 ? 匹配除换行符外的任一单个字符. * 匹配任意个数的字符出现任意次...

阿锋zxf
今天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部