Linux驱动编程 step-by-step
来源:互联网 发布:举牌照软件下载 编辑:程序博客网 时间:2024/05/16 15:59
第三次看了LDD3了(虽然现在已经是kernel3.0但从这本书商还是能学到很多) 每次都有一些收获 现在终于能够写一写代码了
驱动程序的作用:
简单来说 驱动程序就是使计算机与设备通信的特殊的代码,在作单片机时候(无OS)我们自己定义接口及自定义的结构来操作相关硬件,而在有OS的模式下我们操作的硬件是去实现对应的接口(这些接口是已定义好的,我们需要实现这些接口)而无需自己定义接口,这样既能正确的控制设备。又能很好的维护(如果需要升级驱动,上边的应用程序不需要改变)
编写驱动考虑的因素
- 提供给用户更多的选项
- 保持用户操作的简单性
- 编写驱动的时间
驱动分类:
- 字符设备:能够像字节流(类似文件)一样被访问的设备(至少实现open, close, read ,write等功能)
- 快设备: 用户空间接口与字符设备相同, 内部实与字符设备完全不同(可以被随即访问,一般在类UNIX系统中快设备的读取每次只能读一整块在linux可以操作任意字节)
- 网络设备:网络通路通过网络设备形成,能够与主机交换数据的设备
内核功能划分:
- 进程管理(PM):进程的创建与撤销,在单个或者多个CPU上实现多进程的抽象
- 内存管理(MM):管理内存分配及回收的策略
- 文件系统(FS/VFS): Linux 非常依赖于文件系统,内核在没有结构的硬件系统上构造结文件系统,而文件抽象在整个系统中会广泛使用,Linux支持多种文件系统类型
- 设备控制:驱动程序,操控硬件以及相应总线设备
- 网络(NET): 在网络接口于应用程序之间传输数据。
好了 理论行得东西介绍的差不多了,下边说点有用的,内核的驱动可以做成模块在需要的时候装载,在不需要的时候卸载
我们在编写用户程序的时候总喜欢从编写hello world 写起 , 在内核驱动模块也是一样,下边是一个hello_world的一个模块
- //hello_world.c
- #include <linux/init.h>
- #include <linux/module.h>
- MODULE_LICENSE("GPL");
- static int hello_init(void)
- {
- printk(KERN_ALERT "hello module\n");
- return 0;
- }
- static void hello_exit(void)
- {
- printk(KERN_ALERT "hello module exit\n");
- }
- module_init(hello_init);
- module_exit(hello_exit);
以及对应的Makefile
- ifneq ($(KERNELRELEASE),)
- # call from kernel build system
- obj-m := hello_world.o
- #if we need more than one source code to build the module
- #we should use the variable below: example: modules-objs := file1.o file2.o
- modules-objs :=
- else
- #kernel PTAH
- KERNELDIR ?= /lib/modules/$(shell uname -r)/build
- PWD := $(shell pwd)
- modules:
- $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
- endif
- clean:
- rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
有几点与用户空间程序不同的地方
- 模块程序没有main函数(那么程序入口在哪里?)
- 打印函数使用的printk 而不是用户空间的printf 而且使用方式不一样
- 模块的编译不是通常的方式
- 头文件不是常见的那些头文件
- 以及编译之后不会产生可执行文件,而是 .ko 文件
...
模块没有main函数,在装载模块 insmod 时会调用module_init注册的函数 此处为hello_init
在模块卸载remod时 会调用module_exit注册的函数 此处为hello_exit
在module_init 注册的函数主要是进行初始化,分配内存, 注册设备等
而module_exit中注册的函数与之相反, 设备注销, 释放内存等
具体的编译模块的Makefile我在另一篇文章中有说到 此处不再赘述
内核的打印函数使用printk去打印信息, printk不支持浮点类型, 在printk中可以加入信息级别有7中
- #define KERN_EMERG "<0>" /* system is unusable */
- #define KERN_ALERT "<1>" /* action must be taken immediately */
- #define KERN_CRIT "<2>" /* critical conditions */
- #define KERN_ERR "<3>" /* error conditions */
- #define KERN_WARNING "<4>" /* warning conditions */
- #define KERN_NOTICE "<5>" /* normal but significant */
- #define KERN_INFO "<6>" /* informational */
- #define KERN_DEBUG "<7>" /* debug-level messages */
编译内核的头文件是在/lib/modules/$(shell uname -r)/build/include下得,而不是用户模式下得/usr/include
编译后不会生产可执行文件,会生成一个.ko的文件
使用insmod xxx.ko去装载模块
使用lsmod去查看已装载的模块
使用rmmod xxx 去卸载相应模块(卸载是不带.ko)
======================================================================================================
主设备号标识设备连接的的驱动,此设备好由内核使用,标识在相应驱动下得对应的设备
在linux中设备号是一个32位的dev_t类型
typedef __kernel_dev_t dev_t;
crw------- 1 root root 4, 1 Oct 2803:04 tty1
crw-rw-rw- 1 root tty 4, 64 Apr 11 2011 ttys0
crw-rw---- 1 root uucp 4, 65 Apr 11 2011 ttyS
前12位标识主设备号MAJOR(dev_t dev)获得主设备号后20位标识此设备号MINOR(dev_t dev)获得此设备号
由主次设备号生成设备号
可以使用宏MKDEV
dev_t dev_num = MKDEV(dev_t major, dev_t minor);
2、分配与释放设备号
在linux2.6的字符设备中(kernel3.0也是)首先做的事就是申请一个或者多个设备号
- /* 静态分配设备号
- * parameter:
- * first : 分配的第一个设备号
- * count: 分配的设备个数
- * name : 设备名
- * return value:
- * 0: success
- * 负值:出现错误,错误码
- */
- int register_chrdev_region(dev_t first, unsigned int count, char *name);
- /* 动态分配设备号
- * parameter:
- * dev : 用来存储分配的设备号值
- * firstminor: 次设备号(一般填0)
- * count: 分配的设备个数
- * name : 设备名
- * return value:
- * 0: success
- * 负值:出现错误,错误码
- */
- int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
- /* 释放设备号
- * parameter:
- * first: 设备号
- * count: 分配的设备个数
- */
- void unregister_chrdev_region(dev_t first, unsigned int count);
静态分配设备号,是在已经知道一个可用设备号的时候使用,而程序员在编写程序之前大多并知道设备号是否可用,或者现在可用,不能确保在系统升级时候次设备还是可用的
所以linux社区极力推荐使用动态分配,它会去寻找可用的设备号,而不会产生冲突。在次设备卸载的时候需要释放次设备号。
3、一个没有作用的字符设备驱动
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/types.h>
- #include <linux/fs.h>
- #define SIMPLE_DEBUG 1
- #define DEV_COUNT 2
- #define SIMPLE_NAME "simple_char"
- static int simple_major = 108;
- static int simple_minor = 0;
- static __init int simple_init(void)
- {
- dev_t dev;
- int err;
- #if SIMPLE_DEBUG
- printk(KERN_INFO "In %s\n", __func__);
- #endif
- dev = MKDEV(simple_major, simple_minor); //求取设备号
- if(dev > 0)//设备号有效
- {
- #if SIMPLE_DEBUG
- printk(KERN_INFO "try to register static char dev %d \n", dev);
- #endif
- err = register_chrdev_region(dev,DEV_COUNT, SIMPLE_NAME); //静态分配设备号
- if(err < 0) //静态分配出错 尝试使用动态分配
- {
- printk(KERN_WARNING "register static char dev error\n");
- err = alloc_chrdev_region(&dev, 0, DEV_COUNT, SIMPLE_NAME); //动态分配设备号
- if(err < 0)
- {
- printk(KERN_ERR "register char dev error in line %d\n",__LINE__);
- goto error;
- }
- else
- {
- simple_major = MAJOR(dev);//重新计算主设备号
- simple_minor = MINOR(dev);//重新计算此设备号
- }
- }
- else{
- }
- }
- else //设备号无效使用动态分配
- {
- #if SIMPLE_DEBUG
- printk(KERN_INFO "try to register alloc char dev \n");
- #endif
- err = alloc_chrdev_region(&dev, 0, DEV_COUNT, SIMPLE_NAME);
- if(err < 0)
- {
- printk(KERN_ERR "register char dev error in line %d\n\n",__LINE__);
- goto error;
- }
- else
- {
- simple_major = MAJOR(dev);
- simple_minor = MINOR(dev);
- }
- }
- #if SIMPLE_DEBUG
- printk(KERN_INFO "register char dev success major = %d minor = %d \n", simple_major, simple_minor);
- #endif
- error:
- return err;
- }
- static __exit void simple_exit(void)
- {
- dev_t dev;
- #if SIMPLE_DEBUG
- printk(KERN_INFO "In %s\n", __func__);
- #endif
- dev = MKDEV(simple_major, simple_minor);
- unregister_chrdev_region(dev, DEV_COUNT); //释放设备号
- }
- module_init(simple_init);
- module_exit(simple_exit);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("kai_zhang(jsha.zk@163.com)");
- MODULE_DESCRIPTION("simple char driver!");
这里只在模块初始化的时候去分配设备号,在模块注销的时候去释放次驱动拥有的设备号
在函数里边我们看到用到了在应用编程里边声名狼藉的goto函数,在linux驱动编程时 goto 函数可以让我们的编程更加有条理性,在出现错误时候能更快的去处理。
如果在调用函数检查返回者都去做错误处理则模块函数就显得臃肿,庞大。所以还是建议合理使用goto函数的。
加载次模块后
运行 $cat /proc/devices可以看到 simple_char 的设备以及主设备号。
这里我们看到原来假设的主设备号是不可用的,所以使用的动态分配设备号,由此我们申请到主设备号为249,我们可以在上边添加我们的设备,具体操作下一节会讲到。呵呵留点悬念先。
======================================================================================================
<linux/fs.h>
struct file
struct inode
一、文件操作
在之前的一篇文章中已经有介绍了如何去生情字符设备设备号,但是没有做任何的工作,也就只能写一个不能工作的字符设备;
struct file_operations 结构域用来连接设备与操作,实现系统调用。
重要字段介绍:
struct module *owner;//表示拥有这个结构模块的指针,几乎所有的驱动都会设置为THIS_MODULE<linux/module.h>
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 *);//项设备写入数据
unsigned int (*poll) (struct file *, struct poll_table_struct *);//查询文件描述符上的读取写入是否被阻塞
int (*mmap) (struct file *, struct vm_area_struct *);//将设备内存映射到进程空间
int (*open) (struct inode *, struct file *);//对应于打开设备
int (*release) (struct inode *, struct file *);//对应于关闭一个设备
.
.
.
};
当open 与release置为NULL时 就以内核默认的方式进行打开或者关闭,并且不会出错,
当其他函数没有被定义时候,应用程序调用会出错。
下边是一个最重要的几个设备操作方法的定义。
.owner = THIS_MODULE,
.open = simple_open,
.release = simple_close,
.read = simple_read,
.write = simple_write,
.llseek = simple_llseek,
.poll = simple_poll,
.mmap = simple_mmap,
};
二、file结构
这里介绍的file结构并不是C语言中的FILE结构,两者没有任何关联,struct file只是一个内核的结构,每一个打开的文件,都会对应一个struct file结构,一个文件可以对应于不同的struct file结构
struct path f_path; //文件位置
const struct file_operations *f_op;.//文件操作符
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;//文件标识(O_NONBLOCK, O_RDONLY等由应用程序传入)
fmode_t f_mode;//文件模式可读可写
loff_t f_pos;//文件读写位置
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
.
.
.
void *private_data; //most important!! 私有数据,驱动可以使用它指向任何的数据结构
};
linux内核使用inode结构表示一个文件,与file不同,file可以理解为用来表示文件描述符的结构,一个文件可以对应很多的文件描述符,而最后只会指向同一个inode结构
struct inode {
...
dev_t i_rdev; //保存设备号
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev; //指向了struct cdev结构
};
...
};
四、file 结构 与inode结构图解
======================================================================================================
似乎每一章介绍的内容比较少,但学习是一个循序渐进的过程,不在于一天学多少,重要的一天能真正的学懂多少,所以我主张一步一步来,从多个渠道去学习知识,实现互补。
本节测试代码传到此处了:char_step1 大家可以下载测试一下。
字符设备的注册与设备节点的自动创建
cdev 结构
内核内部使用struct cdev<linux/cdev.h>来表示一个字符设备
struct kobject kobj; //kobj设备模型以后介绍
struct module *owner;
const struct file_operations *ops;//文件操作
struct list_head list;
dev_t dev;//设备号
unsigned int count; //设备个数
};
注册字符设备
动态初始化:
my_cdev->ops = &my_ops;
静态初始化:
cdev_init(&my_dev, &my_ops);
向内核注册(添加设备):
num: 设备号
count: 所要添加的设备个数
返回值:
成功返回0 失败返回错误码,设备被注册时候可能立即会呗调用,所以在驱动程序还没有准备好处理设备操作时候不要先调用cdev_add
注销字符设备:
在不需要用西设备时候需要注销设备早期的方法
在2.6的内核之前注册字符设备有一个简单易用的方法,现在也可以使用但是不建议使用,因为到最后这个方法会从内核消失注册字符设备:
const char *name, const struct file_operations *fops);
name: 驱动名
fops: 文件操作
缺点: 不能同时注册多个设备, 他是用一种默认的方式去建立一个cdev结构,而这样的话它不能使用大于255的主次设备号
注销设备:
设备节点的自动创建
在上一个程序中,虽然能够争取的得到一个设备号,但是并不会在/dev/目录下生成一个设备节点。使用手动创建
c: 表述创建字符设备节点(同理b 表示创建块设备文件节点)
major: 主设备号
minor: 次设备号
在做测试时候这样做事可以理解的,而在作为一个模块要发行时候,这样就太不方面了,①要先查看设备是否成功创建,②查看注册设备的主设备号,③输入命令创建文件节点
当然可以写一个shell脚本来完成这些事情。
自动创建设备节点
需要的纤体内核空间支持udev,在内核配置,及busybox配置时候需要指定内核中定义了struct class 与 struct devic结构<linux/device.h>
创建一个class结构
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
name 表示设备名
调用此函数后 创建了一个struct class结构放在sysfs下边,而后调用
struct device *parent,
dev_t devt, void *drvdata,
const char *fmt, ...)
cls : 是上边函数的创建的struct class结构指针
parent: 指向设备的父节点设备(如果没有填NULL
devt: 被添加设备的设备号
drvdata: 回调函数参数
fmt ,args:一个或者多个设备名
至此我们就自动创建了一个设备节点
删除设备节点
在设备不需要使用时候我们需要删除设备节点:void class_destroy(struct class *cls);//删除sysfs下得struct class结构
实例:
初始化 添加设备到内核
- simple_cdev = cdev_alloc(); //动态初始化字符设备
- if(simple_cdev != NULL)
- {
- simple_cdev->ops = &simple_fops; //文件操作
- simple_cdev->owner = THIS_MODULE;
- }
- else
- {
- printk(KERN_ERR "alloc cdev err no memory");
- unregister_chrdev_region(dev, DEV_COUNT); //如果分配资源出错则释放设备号
- return -ENOMEM;
- }
- err = cdev_add(simple_cdev, dev, DEV_COUNT); //向内核添加设备
- if(err < 0)
- {
- printk(KERN_ERR "add cdev err \n");
- goto error1;
- }
- else
- {
- #if SIMPLE_DEBUG
- printk(KERN_INFO "add char dev OK!\n");
- #endif
- }
自动创建设备节点
- simple_class = class_create(THIS_MODULE, SIMPLE_NAME);
- if(simple_class == NULL)
- {
- printk(KERN_ERR "create simple class error\n");
- goto error2;
- }
- else
- {
- #if SIMPLE_DEBUG
- printk(KERN_INFO "create simple class OK!\n");
- #endif
- }
- simple_dev = device_create(simple_class, NULL, dev, NULL, SIMPLE_NAME);
- if(simple_dev == NULL)
- {
- printk(KERN_ERR "create device error");
- goto error3;
- }
- else
- {
- #if SIMPLE_DEBUG
- printk(KERN_INFO "create simple device OK!\n");
- #endif
- }
删除设备节点,注销字符设备
- dev = MKDEV(simple_major, simple_minor); //计算出设备号
- device_destroy(simple_class, dev); //删除设备节点
- class_destroy(simple_class);//删除并释放class结构
- cdev_del(simple_cdev);//注销设备
- cdev_put(simple_cdev);//对动态的设备需要使用cdev_put‘来释放资源
================================================================================================================
主要的文件操作方法实现
文件操作函数有很多的操作接口,驱动编程需要实现这些接口,在用户编程时候系统调用时候会调用到这些操作- struct file_operations {
- ...
- 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 *);
- int (*open) (struct inode *, struct file *);
- int (*release) (struct inode *, struct file *);
- ...
- };
以上只列出了主要的操作,下面会依次介绍:
本次的测试代码上传在:char_step2
结构体:
首先 我们会模拟写一个不操作任何设备,而仅仅是存储的一个驱动。定义自己的一个结构体为:
- struct simple_dev{
- char data[MAX_SIMPLE_LEN];
- loff_t count;
- struct semaphore semp;
- };
之后的程序中结构体也会做相应的变化,以适应linux编写驱动的习惯
open方法:
打开设备并进一步初始化工作,在没有定义open方法时内核以一种默认的方式打开设备,保证每次都能正确打开。
open方法中有有struct inode参数,包含了设备号,程序中可以使用次设备号得到正操作的设备
在struct file中主要的操作是private_data指针,他可以传递任何自己创建的结构。
总得说来open方法的作用有3
1、获得操作的设备(通过设备号)
2、进一步的初始化设备
3、初始化file结构体的private_data
- static int simple_open(struct inode *inodp, struct file *filp)
- {
- struct simple_dev *temp_dev = NULL;
- int minor = 0;
- #if SIMPLE_DEBUG
- printk(KERN_INFO "In %s \n", __func__);
- #endif
- minor = iminor(inodp);//获得操作的设备的次设备号
- if(minor > DEV_COUNT-1){
- printk(KERN_ERR "the char dev in invalid \n");
- return -ENODEV;
- }
- #if SIMPLE_DEBUG
- printk(KERN_INFO "the minor is %d \n", minor);
- #endif
- temp_dev = &char2_dev[minor];//获得真正操作的设备
- /* 进一步 初始化设备 因为是操作一个模拟的设备 故省去*/
- filp->private_data = temp_dev; //初始化 private_data
- return 0;
- }
release方法:
主要是对open进一步初始化的操作的反操作
比如open时候分配了内存,在release时就需要释放它等
例子中因为操作内存设备,故在release时无需做什么事
read方法:
read 是把设备中的数据传递给调用者
主要步骤
1、检测偏移量有效(有些设备驱动不需要检测)
2、检测用户空间地址有效
3、将数据传给用户(在此步骤中调用的函数可能会自己检测步骤2)
4、调整偏移量
5、返回读到的数据长度
(read write 用法相对灵活,不要依赖上边的步骤,设备驱动程序要根据设备特性去设计此方法)
这里先介绍一个会检测用户空间地址是否有效的copy函数
用户调用read读设备,而在内核空间就是将数据传给用户,是一个to的操作
- unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
to是用户空间指针 也就是read函数传入的用户空间的指针,
from指向设备要传送的数据
n标识传入长度
上图是 摘自LDD3上的经典视图, 应该比较能说明read的方法
- static ssize_t simple_read(struct file *filp, char __user *userstr, size_t count, loff_t *loff)
- {
- struct simple_dev *dev = NULL;
- int data_remain = 0;
- int err;
- #if SIMPLE_DEBUG
- printk(KERN_INFO "In %s \n", __func__);
- #endif
- dev = filp->private_data;
- data_remain = dev->count - *loff;
- if(MAX_SIMPLE_LEN < *loff)//检测偏移量
- {
- printk(KERN_ERR "the offset is illegal in func %s \n",__func__ );
- return -EINVAL;
- }
- else if(data_remain <= 0)
- {
- printk(KERN_WARNING "there was not much data in the device\n");
- return 0;
- }
- else
- {
- if(count > data_remain)
- {
- #if SIMPLE_DEBUG
- printk(KERN_INFO "the data is less than the user want to read\n");
- #endif
- count = data_remain;
- }
- else
- {
- }
- }
- err = copy_to_user(userstr, (dev->data)+(*loff), count); //调用内核函数进行数据拷贝,它会检测用户地址是否有效
- if(err != 0)
- {
- printk(KERN_ERR "an error occured when copy data to user\n");
- return err;
- }
- else
- {
- #if SIMPLE_DEBUG
- printk(KERN_INFO "data copy to user OK\n");
- #endif
- *loff = *loff + count; //调整偏移量
- return count; //返回写入的数据量
- }
- }
write方法:
与read类似 它是从用户传数据给设备驱动
从内核空间看就是一个从用户空间取数据 是一个from操作
- long __must_check strncpy_from_user(char *dst, const char __user *src, long count)
src 用户空间传入的数据
count 标识数据长度
- static ssize_t simple_write(struct file *filp, const char __user *userstr, size_t count, loff_t *loff)
- {
- struct simple_dev *dev = NULL;
- int err;
- int remain_space = 0;
- #if SIMPLE_DEBUG
- printk(KERN_INFO "In %s\n",__func__);
- #endif
- dev = filp->private_data;
- if(MAX_SIMPLE_LEN <= *loff) //检测偏移量
- {
- printk(KERN_ERR "the offset is illegal in func %s\n", __func__);
- return -EINVAL;
- }
- else
- {
- remain_space = MAX_SIMPLE_LEN - *loff;
- if(count > remain_space)
- {
- #if SIMPLE_DEBUG
- printk(KERN_WARNING "the data is to long to write to the device\n");
- #endif
- count = remain_space;
- }
- else
- {
- }
- }
- err = copy_from_user((dev->data)+(*loff),userstr,count);//取得数据
- if(err != 0)
- {
- printk(KERN_ERR "an error occured when copy data from user\n");
- return err;
- }
- else
- {
- #if SIMPLE_DEBUG
- printk(KERN_INFO "data copy from user OK\n");
- #endif
- *loff = *loff + count; //跳着偏移
- if(*loff > dev->count)
- {
- dev->count = *loff;
- }
- else
- {
- }
- return count; //返回写入的数据量
- }
- }
lseek方法:
根据用户传入的参数调整文件偏移
mode
在调整文件偏移后需要 更新file中得f_pos成员
- static loff_t simple_llseek(struct file *filp, loff_t loff, int mode)
- {
- struct simple_dev *dev = NULL;
- loff_t tmp_len;
- #if SIMPLE_DEBUG
- printk(KERN_INFO "In %s\n",__func__);
- #endif
- dev = filp->private_data;
- switch ( mode )
- {
- case SEEK_SET:
- if( loff < 0 )
- {
- printk(KERN_ERR "can't move above file line %d \n", __LINE__);
- return -1;
- }
- else if(loff > dev->count)
- {
- printk(KERN_ERR "offset is too long line %d\n", __LINE__);
- return -1;
- }
- else
- {
- filp->f_pos = loff;
- }
- break;
- case SEEK_CUR:
- if((tmp_len = filp->f_pos+loff) < 0)
- {
- printk(KERN_ERR "can't move above file line %d \n", __LINE__);
- return -1;
- }
- else if(tmp_len > dev->count)
- {
- printk(KERN_ERR "offset is too long line %d\n", __LINE__);
- return -1;
- }
- else
- {
- filp->f_pos = tmp_len;
- }
- break;
- case SEEK_END:
- if((tmp_len = dev->count+loff ) < 0)
- {
- printk(KERN_ERR "can't move above file line %d \n", __LINE__);
- return -1;
- }
- else if(tmp_len > dev->count)
- {
- printk(KERN_ERR "offset is too long line %d\n", __LINE__);
- return -1;
- }
- else
- {
- filp->f_pos = tmp_len;
- }
- break;
- default :
- printk(KERN_INFO "illigal lseek mode! \n");
- return -1;
- break;
- }
- return filp->f_pos;
- }
======================================================================================================
说点上节没有讲完的话题
检测用户空间地址的有效性
上一节中提到在read write时候要检测用户空间传递的参数地址是否是有效地址,有的内核函数会自行检测,但是在调用轻量级的内核函数时候,就可能不去检测用户空间的地址是否有效,如果此时用户无传递一个无效地址,而内核函数去操作了它,这时棘手的问题出现了,轻则内核oops 关机重启就OK了,在特别严重的情况下,可能你的系统就崩溃了(我又遇到过),所以,我们在驱动程序中操作用户空间地址时候要小心加小心。如果电脑配置可以就在虚拟机中玩, 或者在开发板上试,当然这边的测试代码我都有试过,不至于让你系统崩溃的。如何检测呢?
调用一个access_ok函数去检测
- #define access_ok(type,addr,size)
addr 用户传入的地址
size 读写的长度
此代码在有内存管理的芯片与无内存管理之间有区别
我们 看一段内核代码 (path : arch/arm/include/asm/uaccess.h)
- #define access_ok(type,addr,size) (__range_ok(addr,size) == 0)
- #ifdef CONFIG_MMU
- ...
- #define __range_ok(addr,size) ({ \
- unsigned long flag, roksum; \
- __chk_user_ptr(addr); \
- __asm__("adds %1, %2, %3; sbcccs %1, %1, %0; movcc %0, #0" \
- : "=&r" (flag), "=&r" (roksum) \
- : "r" (addr), "Ir" (size), "0" (current_thread_info()->addr_limit) \
- : "cc"); \
- flag; })
- #else
- ...
- #define __range_ok(addr,size)<span style="white-space:pre"> </span>(0)
即在有内存管理并配置了内存管理的芯片内调用次函数会执行检测操作,而在没有配置内存管理的芯片中此函数总是返回真,而做驱动的不应该做这些假设,所以传入的参数在有必要的情况下还是要自行检测再看看copy_to_user函数
- static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
- {
- if (access_ok(VERIFY_WRITE, to, n))
- n = __copy_to_user(to, from, n);
- return n;
- }
而当我们调用
- __copy_to_user
- __copy_from_user
- get_user
- __get_user
- put_user
- __put_user
简单模块调试技术
为什么要加简单呢? 因为这边只介绍了用打印来调试程序。看了LDD3上边介绍的很多调试技术 查询调试 观察调试之类
我觉得 打印调试来的最简单最直接 虽然他有一些限制
1、大量的使用printk会使系统变慢
2、没次打印一行都会引起磁盘操作
...
在printk中有7中 消息的选项 表示着不同的消息等级
KERN_GMERG<0>用于紧急消息, 常常是那些崩溃前的消息.KERN_ALERT<1>需要立刻动作的情形.KERN_CRIT<2>严重情况, 常常与严重的硬件或者软件失效有关.KERN_ERR<3>用来报告错误情况; 设备驱动常常使用 来报告硬件故障.KERN_WARNING<4>有问题的情况的警告, 这些情况自己不会引起系统的严重问题KERN_NOTICE<5>正常情况, 但是仍然值得注意. 在这个级别一些安全相关的情况会报告.KERN_INFO<6>信息型消息. 比如 :打印它们发现的硬件的信息.KERN_DEBUG<7>用作调试消息.内核中定义了DEFAULT_MESSAGE_LOGLEVEL(在printk.c中)默认数值小于它的消息类型才会被答应到终端,我们可以把他设置为8则所有的信息都会被终端打印出来。
在系统中我们 可以使用 echo 8 > /proc/sys/kernel/printk 来调整这个数值(要root权限) 使信息全部被打印出来。
当然我们 也可以通过dmesg来查看所有的打印信息(有一点不适用,就是当系统出现oops的时候 就不行了 因为你已经死机了 也就输不了这个命令 就看不到打印信息了)。
- #if SIMPLE_DEBUG
- #define D(...) printk(KERN_DEBUG __VA_ARGS__)
- #define WAR(...) printk(KERN_WARNING __VA_ARGS__)
- #else
- #define D(...) ((void)0)
- #define WAR(...) ((void)0)
- #endif
在需要打印的地方我们就使用
- D(“print the log int func:%s line:%d”, __func__ ,__LINE__);
打印当前进程信息
内核模块不像应用程序一样顺序执行,只用应用进程调用到想关联的函数才会到内核模块中call这个接口,那可不可以 打印调用进程的信息呢?
答案是肯定的,linux中定义了current这个变量(<linux/sched.h>)current指向了当前的进程,他是一个task_struct类型
其中有两个重要的成员
comm 表示了 当前的命令名名
pid 表示了当前进程号
- D("[process: %s] [pid: %d] xxx\n" , current->comm, current->pid);
内核的container_of函数
在写字符设备驱动时候我们都会去自定义一个结构,其中包含了cdev结构
- struct simple_dev{
- char *data;
- loff_t count;
- struct cdev cdev;
- struct semaphore semp;
- };
所以需要寻求一种安全的方法
container_of为我们提供了这么一个很好的接口
- /**
- * container_of - cast a member of a structure out to the containing structure
- * @ptr: the pointer to the member.
- * @type: the type of the container struct this is embedded in.
- * @member: the name of the member within the struct.
- *
- */
- #define container_of(ptr, type, member)
ptr是指结构体一个成员的地址
type 指要获得的结构体
member ptr指向的成员在结构体中的名字
- /*container_of(pointer, container_type, container_fild)
- we can get the container_type with one number in the container
- by this function. container_fild is the one of the number in the
- container_type , pointer point to confain_field type
- */
- temp_dev = container_of(inodp->i_cdev, struct simple_dev, cdev);
container_of 的实现基本原理是这样的:
知道了结构体中某个成员的地址, 又可以求的该成员在改结构体中得偏移,拿成员的地址减去这个偏移,就得到了整个结构的地址,太佩服写内核的人了
- #define container_of(ptr, type, member) ({ \
- const typeof(((type *)0)->member) * __mptr = (ptr); \
- (type *)((char *)__mptr - offsetof(type, member)); })
并发 竞态 (信号量与自旋锁)
前面所述的字符驱动都是没有考虑并发竟态的情况,想象一下
一个进程去读一个字符设备,另一个进程在同一时间向这个设备写入(完全有这种情况)
原来设备中存有 A B C D 要想设备写入1 2 3 4 每次读写一个字节
t1t2t3t4t5t6t7t8RWWRWRRWA12233D4R: read
W:write
所以最后读出了A23D不是原来的ABCD
而如果两个进程同时写入一个设备则写入的值可能既不是A进程想要的又不是B进程想要的。
并发与竞态
并发是指 多个进程同时访问相同一段代码(不仅限于内核空间的代码)
竞态 是对共享资源访问的结果, 并发进程访问了相同的数据或结构(硬件资源)等。
解决竞态的思想 主要有两种
1、所有的代码都不使用全局变量
2、使用锁机制确保一次只有一个进程使用共享资源
显然第一种方式是不可能的,我们只能尽量少的使用全局变量,而不能完全避免它
下边介绍 两种锁机制
信号量
这个概念对我们来说应该不是很陌生,至少听说过,它有两种操作:通常叫做P操作与V操作
希望进入临界区的进程调用P操作,检测当前信号量,如果信号量大于0,则信号量减1,进入临界区,否则进程等待其他进程释放此信号量,信号量的释放通过一个V操作增加信号量的值,在某些情况下会去唤醒等待此信号量的进程。
以上说到了一个 临界区这个名词 简单来讲就是内部操作了共享数据的代码
Linux 内核中信号量的实现
因为从LLD3(2.6.10)版本出来到现在(3.1)内核函数有了很多的变化,许多书上介绍的函数都已经不存在了,也能几版新kernel之后现在我说的函数也会有变化了。
信号量结构以及相关函数的定义在<linux/semaphore.h>中
创建信号量
- void sema_init(struct semaphore *sem, int val)
val : 初始化信号量的值(代表了资源数)【val 为1时 表示代码在同一时间只允许一个进程访问 称之为互斥锁】
- void init_MUTEX();
- void init_MUTEX_LOCKED();
- struct simple_dev{
- char *data;
- loff_t count;
- struct cdev cdev;
- struct semaphore semp;
- };
- static __init int simple_init(void)
- {
- ...
- for( index = 0 ; index < DEV_COUNT ; ++index )
- {
- sema_init(&char5_dev[index].semp,1);
- //init_MUTEX(&(char5_dev[index].semp));
- init_waitqueue_head(&(char5_dev[index].queue));
- }
- ...
- }
P操作
- void down(struct semaphore *sem);
- int __must_check down_interruptible(struct semaphore *sem);
- int __must_check down_killable(struct semaphore *sem);
- int __must_check down_trylock(struct semaphore *sem);
- int __must_check down_timeout(struct semaphore *sem, long jiffies)
第二个函数 在第一个操作的基础上,如果进程因为没有得到信号量睡眠,在别的进程释放信号量或者发成中断的情况下都会被唤醒,在被中断信号唤醒时候返回-EINTR,成功返回0
第三个函数 如果没有另外的任务会获取此信号量,则可以调用此函数,在收到中断信号时会返回-EINTR
第四个函数 试图去获取信号量,在获取不到的时候不会睡眠,而是继续运行,返回0值表示得到了此信号量, 返回1 表示没能获取到。
第五个函数 可以去设置最长睡眠时间, 但是 此函数不可中断
V操作
- void up(struct semaphore *sem)
- static ssize_t simple_read(struct file *filp, char __user *userstr, size_t count, loff_t *loff)
- {
- struct simple_dev *dev = NULL;
- int data_remain = 0;
- int err;
- D("[%s:] In %s \n",current->comm, __func__);
- dev = filp->private_data;
- err = down_interruptible(&dev->semp);
- if(err)
- {
- if(err == -EINTR)
- WAR("return by an interrupt signal\n");
- else
- printk(KERN_ERR "an error occured in down_interruptible\n");
- return err;
- }
- else
- {
- D("have get the mutex %d\n", __LINE__);
- }
- /*******************************************
- * 临界区代码
- ********************************************/
- up(&dev->semp);
- return count;
- }
自旋锁
自旋锁是另一种锁机制,信号量会引起进程的休眠,而在不能睡眠的代码中我们就需要使用自旋锁。
自旋锁也是一个互斥的概念,有“锁定”与“解锁”两个操作,当程序需要锁定时候,则先检测锁是否可用,如果可用则获得锁,程序进入临界区,否则进入忙等待重复检测这个锁是否可用(这就是自旋),临界区代码操作完成则解锁。
信号量实现中其实也用到了自旋锁机制(有兴趣的刻一看内核源码,这边不展开,等以后写内核时候再介绍)
因为自旋锁在得不到锁的时候会“自旋” 即不会让出CPU ,所以我们的临界区执行速度应该尽量的快,最好使用原子操作(不会睡眠)。这也是使用自旋锁的核心规则,在多数情况下我们做不到这一点,所以自旋锁在驱动程序中使用的不如信号量频繁。
初始化
- spin_lock_init(_lock)
锁定
- static inline void spin_lock(spinlock_t *lock)
- static inline int spin_trylock(spinlock_t *lock)
- static inline void spin_lock_irq(spinlock_t *lock)
- spin_lock_irqsave(lock, flags)
- static inline void spin_lock_bh(spinlock_t *lock)
第二个函数 会尝试加锁,检测返回之判断是否锁定,在不能锁定时候程序也继续运行
第三个函数 禁止中断,不保存保存原先的中断状态
第四个函数 在禁止中断之前,保存原先的中断状态,
第五个函数 表示只禁止软件中断而保持硬件中断的打开
因为自旋锁本质上要不会被中断,所以调用时候建议使用包含有禁止中断的函数
解锁
- static inline void spin_unlock(spinlock_t *lock)
- //static inline void spin_unlock(spinlock_t *lock)
- static inline void spin_unlock_irq(spinlock_t *lock)
- static inline void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)
- static inline void spin_unlock_bh(spinlock_t *lock)
死锁
上边只提到了使用锁的好处,以及如何使用锁,但是引入锁机制也会带来风险,那就是死锁,进程死锁最明显的表现就是死机。
如上图所示,A进程占有锁1,并在持有锁1的时候需要得到锁2程序才能继续进行
B进程占有锁2, 并在保持锁2的同时去获取锁1程序才能继续运行,这样A, B 进程就卡在这里,互不相让,这就导致了死锁。
亦或 在一个进程内同时试图获取同样的锁
死锁的名词解释
是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁
解决死锁的方法:
1、加解锁顺序一致
在必须使用多个锁的时候,应该始终以相同的顺序获取,也最好以获取锁顺序逆序解锁。
2、使用原子变量
原子变量不会被多个进程并发操作,内核提供了一种原子类型(atomic_t <asm/stomic.h>中)
在一定时间内不能获得锁,就放弃,并释放已占有的锁
=======================================================================================
阻塞型字符设备驱动
前面说到了 如何实现read write 等操作,但如果设备缓冲已满,现在想而用户此时又想写入设备,此请求就无法立即执行,那怎么办呢?第一种情况是:驱动程序想用户返回请求失败的信息。
第二种情况是:使调用进程阻塞等待设备可以被操作。
而用户更希望自己选择在请求无法满足时候如何操作,所以在用户空间有了O_NONBLOCK标志
在打开设备的时候如果用户指定了此标志(会保存到filp->f_flags中),表示用户希望以非阻塞的形式打开设备,读写时如果设备不能满足要求,则返回错误码(-EAGAIN).
如果用户不指定此标志位,则默认是阻塞型打开,在设备不能满足请求时候需要就阻塞直到设备可以被操作。
休眠的介绍
进程被阻塞即意味着释放CPU,直到另一个进程修改了某个状态,使调度器再次去调度他,所以进程休眠后不知道自己在休眠期间做了什么事情,所以在被唤醒之后还需要检查当前状态,看等待的条件是否为真在linux中等待队列通过等待队列头来管理,(wait_queue_head_t)
初始化队列头
- init_waitqueue_head(wait_queue_head_t * queue);
还有一种简单的方式去初始化队列头(静态初始化)
- DECLARE_WAIT_QUEUE_HEAD(name)
使进程休眠
让一个进程进入睡眠,在醒来时进程仍需要检测一个条件是否为真,正如上边提到的一样- #define wait_event(wq, condition)
- #define wait_event_timeout(wq, condition, timeout)
- #define wait_event_interruptible(wq, condition)
- #define wait_event_interruptible_timeout(wq, condition, timeout)
condition 表示检测的的条件,在condition为真时,睡眠终止。
含有 interruptible 的函数 表示睡眠可以被信号中断,当睡眠被信号中断事,函数返回负值(-ERESTARTSYS)
含有timeout 的函数 timeout 参数表示最长的等待时间,
进程唤醒
有睡眠就有唤醒
在其他进程操作了设备 使等待进程的 等待条件(condition)为真,则需要在此进程中唤醒睡眠的进程
- #define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
- #define wake_up_all(x) __wake_up(x, TASK_NORMAL, 0, NULL)
- #define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
- #define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
- #define wake_up_interruptible_all(x) __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
wake_up_all 唤醒等待队列上的所有进程,不管独占 还是 非独占
wake_up_interruptible跟wake_up 类似, 不过他只会唤醒 队列中可中断的睡眠进程
wake_up_interruptible_all 会唤醒等待队列上的所有可中断睡眠,
wake_up_interruptible_nr 回唤醒等待队列上的nr个独占进程独占等待进程 : 因为在一个睡眠队列上,睡眠的进程可能会有很多,而每次唤醒之后只有一个进程唤醒执行,其他进程继续在等待队列上睡眠,这就导致了有些=重要的进程迟迟不呢个得到调用,为改善这个情况,有了独占进程的概念,
在睡眠时候我们可以指定一个参数WQ_FLAG_EXCLUSEVE标志会加到队列的尾部(不加此标志则默认加到队列头部), 在调用wake_up_xx 对应的函数时候会首先唤醒这些进程。
- while(dev->datawr == dev->datard)
- {
- while(dev->datard == dev->datawr) //循环检测等待条件是否真正的满足
- {
- up(&dev->semp)
- if(filp->f_flags & O_NONBLOCK) //判断是否是非阻塞打开
- {
- return -EAGAIN;
- }
- D("[%s] reading going to sleep!", current->comm);
- if(wait_event_interruptible(dev->rdque, dev->datard != dev->datawr)) //使当前进程睡眠在读睡眠队列
- {
- return -ERESTARTSYS;
- }
- D("waked up %d\n", __LINE__);
- ...
- if(down_interruptible(&dev->semp) < 0)//获取锁
- {
- printk(KERN_ERR "[%s]get the mutex lock error %d, %s",
- current->comm, __LINE__, __func__);
- return -ERESTARTSYS;
- }
- else
- {
- D("have get the mutex %d\n", __LINE__);
- }
- }
- /*read data*/
- wake_up_interruptible(dev->wrque)//读出数据 唤醒写睡眠队列上的进程
- }
poll 函数实现
- unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait);
这些系统调用都是通过poll函数驱动实现
当调用此函数时候内核会分配一个 poll_table_struct 结构,我们 多需要的动作有两步
1、在等待队列上调用poll_wait
- static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
- #define POLLIN 0x0001 //设备可读
- #define POLLPRI 0x0002 //无阻塞读取高优先级数据
- #define POLLOUT 0x0004 //设备可写入
- #define POLLERR 0x0008 //设备发生错误
- #define POLLHUP 0x0010 //设备挂起
- #define POLLRDNORM 0x0040 //normal data is ready now
- #define POLLWRNORM 0x0100 //normal data can be writen to device
- static int simple_poll(struct file *filp, struct poll_table_struct *wait)
- {
- struct simple_dev *dev = filp->private_data;
- unsigned int mask = 0;
- char *next_ptr;
- if(down_interruptible(&dev->semp))
- {
- printk(KERN_ERR "get the semphore err %d \n",__LINE__);
- return -ERESTARTSYS;
- }
- D("have get the semphore %d\n", __LINE__);
- poll_wait(filp, &dev->inq, wait);
- poll_wait(filp, &dev->outq, wait);
- if(dev->datard != dev->datawr)
- {
- mask |= POLLIN | POLLRDNORM; //can be read
- }
- if(dev->datawr+1 == dev->dataend)
- next_ptr = dev->data;
- else
- next_ptr = dev->datawr+1;
- if(next_ptr != dev->datard)
- {
- mask |= POLLOUT | POLLWRNORM; //can be write
- }
- up(&dev->semp);
- return mask;
- }
下一节 会以一个驱动程序 模拟管道 结束基于内存的模拟字符设备驱动程序 , 同时介绍 驱动测试的相关东东
========================================================================================
字符设备模拟pipe的驱动程序
让我们用一个”pipe“的设备驱动去结束简单字符设备吧(这里所说的pipe并非标准的pipe只是模拟了一个从一端写入从另一端写入的设备)
测试代码1 测试代码2
设计思路
用一个图来说明(可是画了很久哟)简单说来就是一个进程写入缓冲区,另一个进程可以读出,读出后原buffer中的数据被置为无效值,
自定义一个结构
- #define MAX_SIMPLE_LEN 1024 //buffer 数据长度
- struct simple_dev{
- char *data; //指向数据的头部
- char *datard; //读指针
- char *datawr; //写指针
- char *dataend; //指向缓冲区的结尾
- wait_queue_head_t inq; //读取等待队列头
- wait_queue_head_t outq; //写入等待队列头
- struct cdev cdev; //字符设备结构
- struct semaphore semp; //结构体信号量
- };
申请设备号
之前有介绍,不再赘述为自定义结构体分配区内存
- D("alloc simple_dev struct \n");
- char7_dev = kmalloc(DEV_COUNT*sizeof(struct simple_dev), GFP_KERNEL);
- if( char7_dev == NULL)
- {
- printk(KERN_ERR "kmalloc simple_dev err no memory");
- unregister_chrdev_region(dev, DEV_COUNT);
- return -ENOMEM;
- }
使用kmalloc 给设备结构他分配内存,因为系统可能因为暂时不能分配到内存(虽然很少见),所以在分配到内存后需要检测是否分配成功,在检测到成功分配内存后还需要将所分配的内存区清零,以免以后出现奇怪的错误难以调试(一定要记住)
- memset(char7_dev, 0, DEV_COUNT*sizeof(struct simple_dev));
为缓冲区申请内存
在为自定义结构他申请好内存后,我们需要为每个结构体内的buffer申请内存,使用相同的方法,分配并清零
- for( index = 0 ; index < DEV_COUNT ; ++index )
- {
- //char7_dev[index].count = 0
- if(( char7_dev[index].data = kmalloc(MAX_SIMPLE_LEN, GFP_KERNEL) )!= NULL )
- {
- memset(char7_dev[index].data, 0, MAX_SIMPLE_LEN);
- D("kmalloc the data space OK! \n");
- }
- else{
- for( --index ; index >= 0 ; --index )
- {
- kfree(char7_dev[index].data);
- }
- printk(KERN_ERR "kmalloc simple_dev data number err no memory");
- kfree(char7_dev);
- unregister_chrdev_region(dev, DEV_COUNT);
- return -ENOMEM;
- }
- }
这里需要注意的一点是 在申请某个buffer缓冲区失败时候,需要将已经成功申请的内存释放掉(else中做了这个工作)
初始化结构体中的各数据指针、信号量、等待队列头
在申请好设备及各设备buffer内存后,我们需要对结构中的一些变量进行初始化,
初始化数据指针
将data指向buffer起始位置, datawr,datard 初始化也指向起始位置(表示空buffer),dataend 指向buffer末尾一个无效位置,用于判断读写位置是否合法
初始化信号量以及读写队列头
- for( index = 0 ; index < DEV_COUNT ; ++index )
- {
- /*init the data ptr*/
- char7_dev[index].datard = char7_dev[index].data;
- char7_dev[index].datawr = char7_dev[index].data;
- char7_dev[index].dataend = char7_dev[index].data + MAX_SIMPLE_LEN;
- /*init semaphore, waitqueue_head and so on*/
- sema_init(&(char7_dev[index].semp), 1);
- init_waitqueue_head(&(char7_dev[index].inq));
- init_waitqueue_head(&(char7_dev[index].outq));
- }
初始化字符设备添加字符设备
逐个设备初始化(注册)并添加(告知内核)- for( index = 0 ; index < DEV_COUNT ; ++index )
- {
- cdev_init(&(char7_dev[index].cdev), &simple_fops);
- char7_dev[index].cdev.owner = THIS_MODULE;
- err = cdev_add(&(char7_dev[index].cdev), dev, 1);
- if(err < 0)
- {
- printk(KERN_ERR "add cdev err \n");
- goto error1;
- }
- else
- {
- D( "add %d char dev OK!\n", index+1);
- }
- }
字符设备操作
- struct file_operations simple_fops={
- .owner = THIS_MODULE,
- .open = simple_open,
- .release = simple_close,
- .read = simple_read,
- .write = simple_write,
- .llseek = simple_llseek,
- // .ioctl = simple_ioctl,
- .poll = simple_poll,
- .mmap = simple_mmap,
- };
因为2.6.35之后文件操作已经没有ioctl方法,所以不在介绍了
打开及关闭操作
打开关闭函数于之前的设备驱动没有差异,故不叙述读写操作
首先获取信号量(读写操作都一样)
- if(down_interruptible(&dev->semp) < 0)
- {
- printk(KERN_ERR "[%s]get the mutex lock error %d, %s",
- current->comm, __LINE__, __func__);
- return -ERESTARTSYS;
- }
- else
- {
- D("have get the mutex %d\n", __LINE__);
- }
读操作
对于读操作需要检测buffer中是否有数据
- if(dev->datawr == dev->datard) //empty buffer
- {
- while(dev->datawr == dev->datard)//循环检测是否buffer中是否已经有数据
- {
- up(&dev->semp);//释放信号量
- if(filp->f_flags & O_NONBLOCK) //检测用户是否是非阻塞打开
- {
- D("set NONBLOCK mask %d\n", __LINE__);
- return -EAGAIN;
- }
- D("[%s] reading going to sleep!", current->comm);
- /*将当调用进程加到写等待队列*/
- if(wait_event_interruptible(dev->inq, dev->datard != dev->datawr))
- {
- return -ERESTARTSYS;
- }
- if(down_interruptible(&dev->semp) < 0)//wait_wvent_interrupt返回,获取信号量
- {
- printk(KERN_ERR "[%s]get the mutex lock error %d, %s",
- current->comm, __LINE__, __func__);
- return -ERESTARTSYS;
- }
- else
- {
- D("have get the mutex %d\n", __LINE__);
- }
- }
- }
看到这里你可能已经知道上边的流程图有一些错误(up 和 down 操作应该在while循环中去做,而不是在整个if 中)由于时间问题上图就不做修改了
计算buffer 剩余的数据
如果读指针在写指针之后(datard > datawr)则buffer中的数据就从读位置到buffer结尾,又buffer开头转到写位置
- if(dev->datawr < dev->datard)
- {
- data_remain = (dev->dataend - dev->datard)
- + (dev->datawr-dev->data);
- }
- else
- {
- data_remain = dev->datawr - dev->datard;
- }
判断数据长度的合法性并计算能够写入用户空间的长度
- if(data_remain < 0)
- {
- printk(KERN_ERR "the remain data we calculate is wrong check! %d \n", __LINE__);
- }
- else if(count > data_remain)
- {
- WAR("the data is less than the user want to read\n");
- D("we can only copy %d bytes to user\n", data_remain);
- count = data_remain;
- }
- else
- {
- }
向用户空间传入数据
1、当读取操作不会读到buffer尾部时候,直接将数据copy给用户,调整读指针, 唤醒睡眠在写队列上的进程,释放信号量,相用户返回已经读取的数据长度
- if(( dev->datawr > dev->datard ) || (dev->datard + count <= dev->dataend))
- {
- err = copy_to_user(userstr, dev->datard, count);
- if(err != 0)
- {
- printk(KERN_ERR "an error occured when copy data to user:%d\n", __LINE__);
- up(&dev->semp);
- return err;
- }
- else
- {
- D("data copy to user OK\n");
- dev->datard = dev->datard + count;
- if(dev->datard == dataend)
- dev->datard = dev->data;
- wake_up_interruptible(&dev->outq);
- up(&dev->semp);
- return count;
- }
- }
2、如果读到buffer末尾还需要绕回来从数据头部再读取
则先读取read 指针到buffer末尾的数据
然后再从头部读取相应长度的数据
同样在成功读取后需要唤醒写等待队列, 调整读指针, 释放信号量
- else
- {
- data_remain= (dev->dataend -dev->datard );
- /*读取从当前位置到buffer结尾的数据长度*/
- err = copy_to_user(userstr, dev->datard+1, data_remain);
- if(err != 0)
- {
- printk(KERN_ERR "an error occured when copy data to user:%d\n", __LINE__);
- up(&dev->semp);
- return err;
- }
- else
- {
- D("data copy to user OK\n");
- // up(&dev->semp);
- }
- /*从buffer头部读取剩余的长度*/
- err = copy_to_user(userstr+data_remain, dev->data, count-data_remain);
- if(err != 0)
- {
- printk(KERN_ERR "an error occured when copy data to user:%d\n", __LINE__);
- up(&dev->semp);
- return err;
- }
- else
- {
- D("data copy to user OK\n");
- dev->datard = dev->data+(count-data_remain);
- wake_up_interruptible(&dev->outq);
- up(&dev->semp);
- return count;
- }
- }
写操作
与读操作类似
获取即将写入的位置
检测buffer是否已经满需要检测下即将写入的地址是否有效(是否已经到了尾部位置),如果数据已经写到buffer的结尾则需要调整写
- if (dev->datawr+1 == dev->dataend)//即将写入的位置是buffer尾部
- next_ptr = dev->data; //调整写入指针的指向
- else
- next_ptr = dev->datawr + 1;
判断buffer是否已满
当即将写入的位置 正好是读指针指向的位置则表示buffer已满需要等待读进程- if( next_ptr == dev->datard )
- {
- while(next_ptr == dev->datard)
- {
- up(&dev->semp);
- if(filp->f_flags & O_NONBLOCK)
- {
- D("set NONBLOCK mask %d\n", __LINE__);
- return -EAGAIN;
- }
- D("[%s] writing going to sleep!", current->comm);
- if(wait_event_interruptible(dev->outq, next_ptr != dev->datard))
- {
- return -ERESTARTSYS;
- }
- if(down_interruptible(&dev->semp) < 0)
- {
- printk(KERN_ERR "[%s]get the mutex lock error %d, %s",
- current->comm, __LINE__, __func__);
- return -ERESTARTSYS;
- }
- else
- {
- D("have get the mutex %d\n", __LINE__);
- }
- }
- }
计算buffer剩余长度(可以写入的数据的长度)
同样需要分两种情况,可以参照上图- if(dev->datawr >= dev->datard)
- {
- remain_space = (dev->dataend - dev->datawr-1)
- + (dev->datard - dev->data);
- }
- else
- {
- remain_space = dev->datard - dev->datawr - 1;
- }
向buffer写入数据调整读写指针
- if( (dev->datawr < dev->datard) || (dev->datawr + count < dev->dataend) )
- {
- err = copy_from_user(dev->datawr, userstr, count);
- if(err != 0)
- {
- printk(KERN_ERR "error occured when copy data from user %d\n", __LINE__);
- up(&dev->semp);
- return err;
- }
- else
- {
- D("data copy from user OK\n");
- dev->datawr = dev->datawr + count ;
- wake_up_interruptible(&dev->inq);
- up(&dev->semp);
- return count;
- }
- }
- else
- {
- remain_space = dev->dataend - dev->datawr ;
- err = copy_from_user(dev->datawr, userstr, remain_space);
- if(err != 0)
- {
- printk(KERN_ERR "error occured when copy data from user %d\n", __LINE__);
- up(&dev->semp);
- return err;
- }
- else
- {
- D("copy part of the data from user\n");
- }
- err = copy_from_user(dev->data, userstr+remain_space, count-remain_space);
- if(err != 0)
- {
- printk(KERN_ERR "error occured when copy data from user %d\n", __LINE__);
- up(&dev->semp);
- return err;
- }
- else{
- D("data copy from user OK\n");
- dev->datawr = dev->data + (count-remain_space);
- wake_up_interruptible(&dev->inq);
- up(&dev->semp);
- return count;
- }
- }
poll方法
在等待队列上调用poll_wait
- poll_wait(filp, &dev->inq, wait);
- poll_wait(filp, &dev->outq, wait);
检测文件是否可读或者可写
- if(dev->datard != dev->datawr)
- {
- mask |= POLLIN | POLLRDNORM; //can be read
- }
- if(dev->datawr+1 == dev->dataend)
- next_ptr = dev->data;
- else
- next_ptr = dev->datawr+1;
- if(next_ptr != dev->datard)
- {
- mask |= POLLOUT | POLLWRNORM; //can be write
- }
- Linux驱动编程 step-by-step
- Linux驱动编程 step-by-step (一)
- Linux驱动编程 step-by-step (二)
- Linux驱动编程 step-by-step (三)
- Linux驱动编程 step-by-step (四)
- Linux驱动编程 step-by-step (五)
- Linux驱动编程 step-by-step (一)
- Linux驱动编程 step-by-step (六)
- Linux驱动编程 step-by-step (七)
- Linux驱动编程 step-by-step (一)
- Linux驱动编程 step-by-step (二)
- Linux驱动编程 step-by-step (三) .
- Linux驱动编程 step-by-step (四)
- Linux驱动编程 step-by-step (五)
- Linux驱动编程 step-by-step (六) .
- Linux驱动编程 step-by-step (八)
- Linux驱动编程 step-by-step (七)
- Linux驱动编程 step-by-step (一)
- Cisco CCNP课程
- WP7基本技巧 新手必看
- (2)Camera框架 --- 窥一斑而见全豹
- jquery小结
- 删除Linux后如何找回Windows启动菜单
- Linux驱动编程 step-by-step
- 思科认证:让黑客远离DNS和SMTP攻击
- 写几个带继承关系的类,使用直接强转的方式,以及使用as转换, 并且练习is的类型判断
- 全面认识Android手机(MIUI ROM适配之旅第二天——准备工作)
- 淘宝现“职业追评人” 恶意追评为了“讨”个三五百
- 编写strcpy函数
- 全面认识Android手机(MIUI ROM适配之旅第三天——反编译)
- 组策略如何禁止用户存储windows凭证
- 我的空间EverET.org