第三次看了LDD3了(虽然现在已经是kernel3.0但从这本书商还是能学到很多) 每次都有一些收获 现在终于能够写一写代码了

驱动程序的作用:

简单来说 驱动程序就是使计算机与设备通信的特殊的代码,在作单片机时候(无OS)我们自己定义接口及自定义的结构来操作相关硬件,而在有OS的模式下我们操作的硬件是去实现对应的接口(这些接口是已定义好的,我们需要实现这些接口)而无需自己定义接口,这样既能正确的控制设备。又能很好的维护(如果需要升级驱动,上边的应用程序不需要改变)

编写驱动考虑的因素

  1. 提供给用户更多的选项
  2. 保持用户操作的简单性
  3. 编写驱动的时间

驱动分类:

  1. 字符设备:能够像字节流(类似文件)一样被访问的设备(至少实现open, close, read ,write等功能)
  2. 快设备:    用户空间接口与字符设备相同, 内部实与字符设备完全不同(可以被随即访问,一般在类UNIX系统中快设备的读取每次只能读一整块在linux可以操作任意字节)
  3. 网络设备:网络通路通过网络设备形成,能够与主机交换数据的设备

内核功能划分:

  1. 进程管理(PM):进程的创建与撤销,在单个或者多个CPU上实现多进程的抽象
  2. 内存管理(MM):管理内存分配及回收的策略
  3. 文件系统(FS/VFS): Linux 非常依赖于文件系统,内核在没有结构的硬件系统上构造结文件系统,而文件抽象在整个系统中会广泛使用,Linux支持多种文件系统类型
  4. 设备控制:驱动程序,操控硬件以及相应总线设备
  5. 网络(NET): 在网络接口于应用程序之间传输数据。

好了 理论行得东西介绍的差不多了,下边说点有用的,内核的驱动可以做成模块在需要的时候装载,在不需要的时候卸载
我们在编写用户程序的时候总喜欢从编写hello world 写起 , 在内核驱动模块也是一样,下边是一个hello_world的一个模块

  1. //hello_world.c
  2. #include <linux/init.h>
  3. #include <linux/module.h>
  4. MODULE_LICENSE("GPL");
  5. static int hello_init(void)
  6. {
  7. printk(KERN_ALERT "hello module\n");
  8. return 0;
  9. }
  10. static void hello_exit(void)
  11. {
  12. printk(KERN_ALERT "hello module exit\n");
  13. }
  14. module_init(hello_init);
  15. module_exit(hello_exit);

以及对应的Makefile

  1. ifneq ($(KERNELRELEASE),)
  2. # call from kernel build system
  3. obj-m   := hello_world.o
  4. #if we need more than one source code to build the module
  5. #we should use the variable below:  example: modules-objs := file1.o file2.o
  6. modules-objs :=
  7. else
  8. #kernel PTAH
  9. KERNELDIR ?= /lib/modules/$(shell uname -r)/build
  10. PWD       := $(shell pwd)
  11. modules:
  12. $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
  13. endif
  14. clean:
  15. rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

有几点与用户空间程序不同的地方

  1. 模块程序没有main函数(那么程序入口在哪里?)
  2. 打印函数使用的printk 而不是用户空间的printf 而且使用方式不一样
  3. 模块的编译不是通常的方式
  4. 头文件不是常见的那些头文件
  5. 以及编译之后不会产生可执行文件,而是 .ko 文件
    ...

模块没有main函数,在装载模块 insmod 时会调用module_init注册的函数 此处为hello_init
在模块卸载remod时 会调用module_exit注册的函数 此处为hello_exit
在module_init 注册的函数主要是进行初始化,分配内存, 注册设备等
而module_exit中注册的函数与之相反, 设备注销, 释放内存等
具体的编译模块的Makefile我在另一篇文章中有说到 此处不再赘述
内核的打印函数使用printk去打印信息, printk不支持浮点类型, 在printk中可以加入信息级别有7中

  1. #define KERN_EMERG    "<0>"    /* system is unusable */
  2. #define KERN_ALERT    "<1>"    /* action must be taken immediately */
  3. #define KERN_CRIT     "<2>"    /* critical conditions */
  4. #define KERN_ERR      "<3>"    /* error conditions */
  5. #define KERN_WARNING  "<4>"    /* warning conditions */
  6. #define KERN_NOTICE   "<5>"    /* normal but significant */
  7. #define KERN_INFO     "<6>"    /* informational */
  8. #define KERN_DEBUG    "<7>"    /* debug-level messages */

对应与不同的错误等级 选择不懂的option, 并做不同的处理, 小于一定等级的信息会直接打印到终端(非X-window下的终端),可以使用 dmesg来查看全部的打印信息
编译内核的头文件是在/lib/modules/$(shell uname -r)/build/include下得,而不是用户模式下得/usr/include
编译后不会生产可执行文件,会生成一个.ko的文件
使用 insmod xxx.ko去装载模块
使用 lsmod去查看已装载的模块

使用rmmod xxx去卸载相应模块(卸载是不带.ko)

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

简单字符设备驱动

1、主次设备号
主设备号标识设备连接的的驱动,此设备好由内核使用,标识在相应驱动下得对应的设备
在linux中设备号是一个32位的dev_t类型

typedef __u32    __kernel_dev_t;
typedef __kernel_dev_t    dev_t;
crw------- 1 root  root  101 Apr 11  2011 psaux 
crw------- 1 root  root  4,  1 Oct 2803:04 tty1 
crw-rw-rw- 1 root  tty   464 Apr 11  2011 ttys0 
crw-rw---- 1 root  uucp  465 Apr 11  2011 ttyS

上图是再/dev目录下用$ls -l 命令显示的部分结果可以看到tty driver的主设备号都为4(各个系统版本有差别),次设备号不同

前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也是)首先做的事就是申请一个或者多个设备号

  1. /*  静态分配设备号
  2. *  parameter:
  3. *      first : 分配的第一个设备号
  4. *      count: 分配的设备个数
  5. *      name : 设备名
  6. *  return value:
  7. *      0: success
  8. *      负值:出现错误,错误码
  9. */
  10. int register_chrdev_region(dev_t first, unsigned int count, char *name);
  11. /*  动态分配设备号
  12. *  parameter:
  13. *      dev : 用来存储分配的设备号值
  14. *      firstminor: 次设备号(一般填0)
  15. *      count: 分配的设备个数
  16. *      name : 设备名
  17. *  return value:
  18. *      0: success
  19. *      负值:出现错误,错误码
  20. */
  21. int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
  22. /*  释放设备号
  23. *  parameter:
  24. *      first: 设备号
  25. *      count: 分配的设备个数
  26. */
  27. void unregister_chrdev_region(dev_t first, unsigned int count);

静态分配设备号,是在已经知道一个可用设备号的时候使用,而程序员在编写程序之前大多并知道设备号是否可用,或者现在可用,不能确保在系统升级时候次设备还是可用的
所以linux社区极力推荐使用动态分配,它会去寻找可用的设备号,而不会产生冲突。在次设备卸载的时候需要释放次设备号。

3、一个没有作用的字符设备驱动

  1. #include <linux/init.h>
  2. #include <linux/module.h>
  3. #include <linux/types.h>
  4. #include <linux/fs.h>
  5. #define SIMPLE_DEBUG 1
  6. #define DEV_COUNT 2
  7. #define SIMPLE_NAME "simple_char"
  8. static int simple_major = 108;
  9. static int simple_minor = 0;
  10. static __init int simple_init(void)
  11. {
  12. dev_t dev;
  13. int err;
  14. #if SIMPLE_DEBUG
  15. printk(KERN_INFO "In %s\n", __func__);
  16. #endif
  17. dev = MKDEV(simple_major, simple_minor); //求取设备号
  18. if(dev > 0)//设备号有效
  19. {
  20. #if SIMPLE_DEBUG
  21. printk(KERN_INFO "try to register static char dev  %d \n", dev);
  22. #endif
  23. err = register_chrdev_region(dev,DEV_COUNT, SIMPLE_NAME); //静态分配设备号
  24. if(err < 0) //静态分配出错 尝试使用动态分配
  25. {
  26. printk(KERN_WARNING "register static char dev error\n");
  27. err = alloc_chrdev_region(&dev, 0, DEV_COUNT, SIMPLE_NAME); //动态分配设备号
  28. if(err < 0)
  29. {
  30. printk(KERN_ERR "register char dev error in line %d\n",__LINE__);
  31. goto error;
  32. }
  33. else
  34. {
  35. simple_major = MAJOR(dev);//重新计算主设备号
  36. simple_minor = MINOR(dev);//重新计算此设备号
  37. }
  38. }
  39. else{
  40. }
  41. }
  42. else //设备号无效使用动态分配
  43. {
  44. #if SIMPLE_DEBUG
  45. printk(KERN_INFO "try to register alloc char dev  \n");
  46. #endif
  47. err = alloc_chrdev_region(&dev, 0, DEV_COUNT, SIMPLE_NAME);
  48. if(err < 0)
  49. {
  50. printk(KERN_ERR "register char dev error in line %d\n\n",__LINE__);
  51. goto error;
  52. }
  53. else
  54. {
  55. simple_major = MAJOR(dev);
  56. simple_minor = MINOR(dev);
  57. }
  58. }
  59. #if SIMPLE_DEBUG
  60. printk(KERN_INFO "register char dev success major = %d minor = %d \n", simple_major, simple_minor);
  61. #endif
  62. error:
  63. return err;
  64. }
  65. static __exit void simple_exit(void)
  66. {
  67. dev_t dev;
  68. #if SIMPLE_DEBUG
  69. printk(KERN_INFO "In %s\n", __func__);
  70. #endif
  71. dev = MKDEV(simple_major, simple_minor);
  72. unregister_chrdev_region(dev, DEV_COUNT); //释放设备号
  73. }
  74. module_init(simple_init);
  75. module_exit(simple_exit);
  76. MODULE_LICENSE("GPL");
  77. MODULE_AUTHOR("kai_zhang(jsha.zk@163.com)");
  78. MODULE_DESCRIPTION("simple char driver!");

这里只在模块初始化的时候去分配设备号,在模块注销的时候去释放次驱动拥有的设备号
在函数里边我们看到用到了在应用编程里边声名狼藉的goto函数,在linux驱动编程时 goto 函数可以让我们的编程更加有条理性,在出现错误时候能更快的去处理。
如果在调用函数检查返回者都去做错误处理则模块函数就显得臃肿,庞大。所以还是建议合理使用goto函数的。
加载次模块后 
运行 $cat /proc/devices可以看到 simple_char 的设备以及主设备号。

这里我们看到原来假设的主设备号是不可用的,所以使用的动态分配设备号,由此我们申请到主设备号为249,我们可以在上边添加我们的设备,具体操作下一节会讲到。呵呵留点悬念先。

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

字符设备中 重要的数据结构

大部分字符驱动设计三个重要的数据结构
<linux/fs.h>

struct file_operations
struct file
struct inode

一、文件操作
           在之前的一篇文章中已经有介绍了如何去生情字符设备设备号,但是没有做任何的工作,也就只能写一个不能工作的字符设备;
struct file_operations 结构域用来连接设备与操作,实现系统调用。
重要字段介绍:

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 *);//对应于关闭一个设备     .
    .
    .
};

结构还有很多的操作因为还没有学会,就没有多做介绍,(ioctl函数操作在2.6.35之后就变成了unlocked_ioctl、compat_ioctl其实作用也不是很大 也没有做介绍,以后有机会再做介绍)
当open 与release置为NULL时 就以内核默认的方式进行打开或者关闭,并且不会出错,
当其他函数没有被定义时候,应用程序调用会出错。

下边是一个最重要的几个设备操作方法的定义。

struct file_operations simple_fops={
    .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 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!! 私有数据,驱动可以使用它指向任何的数据结构 };

三、inode结构
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 cdev {
    struct kobject kobj; //kobj设备模型以后介绍     struct module *owner;
    const struct file_operations *ops;//文件操作     struct list_head list;
    dev_t dev;//设备号     unsigned int count; //设备个数 };

注册字符设备

动态初始化:

struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_ops;

静态初始化:

struct cdev my_cdev;
cdev_init(&my_dev, &my_ops);

向内核注册(添加设备):

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

dev: 指向以初始化的字符设备结构
num: 设备号
count: 所要添加的设备个数
返回值:
成功返回0 失败返回错误码,设备被注册时候可能立即会呗调用,所以在驱动程序还没有准备好处理设备操作时候不要先调用cdev_add

注销字符设备:

在不需要用西设备时候需要注销设备

void cdev_del(struct cdev *);

早期的方法

在2.6的内核之前注册字符设备有一个简单易用的方法,现在也可以使用但是不建议使用,因为到最后这个方法会从内核消失

注册字符设备:

static inline int register_chrdev(unsigned int major, 
    const char *name, const struct file_operations *fops);

major: 主设备号 (当major为0时表示有内核分配一个可用的主设备号并返回)
name: 驱动名
fops: 文件操作
缺点: 不能同时注册多个设备, 他是用一种默认的方式去建立一个cdev结构,而这样的话它不能使用大于255的主次设备号

注销设备:

static inline void unregister_chrdev(unsigned int major, const char *name)

设备节点的自动创建

在上一个程序中,虽然能够争取的得到一个设备号,但是并不会在/dev/目录下生成一个设备节点。

使用手动创建

mknod /dev/device_name c major minor

device_name: 指设备名,
c: 表述创建字符设备节点(同理b 表示创建块设备文件节点)
major: 主设备号
minor: 次设备号
在做测试时候这样做事可以理解的,而在作为一个模块要发行时候,这样就太不方面了,①要先查看设备是否成功创建,②查看注册设备的主设备号,③输入命令创建文件节点
当然可以写一个shell脚本来完成这些事情。

自动创建设备节点

需要的纤体内核空间支持udev,在内核配置,及busybox配置时候需要指定
内核中定义了struct class 与 struct devic结构<linux/device.h>
创建一个class结构

#define class_create(owner, name)    \
({            \
    static struct lock_class_key __key;   \
    __class_create(owner, name, &__key); \
})

owner表示拥有这个模块的指针
name 表示设备名
调用此函数后 创建了一个struct class结构放在sysfs下边,而后调用

struct device *device_create(struct class *cls, 
struct device *parent,
                    dev_t devt, void *drvdata,
                    const char *fmt, ...)

在/dev目录下创建设备节点
cls : 是上边函数的创建的struct class结构指针
parent: 指向设备的父节点设备(如果没有填NULL
devt: 被添加设备的设备号
drvdata: 回调函数参数
fmt ,args:一个或者多个设备名
至此我们就自动创建了一个设备节点

删除设备节点

在设备不需要使用时候我们需要删除设备节点:

void device_destroy(struct class *class, dev_t devt);//删除设备节点 void class_destroy(struct class *cls);//删除sysfs下得struct class结构

至于深层的一些实现机制,没有细细研究,大家干兴趣可以参考相关资料

实例:

初始化 添加设备到内核

  1. simple_cdev = cdev_alloc(); //动态初始化字符设备
  2. if(simple_cdev != NULL)
  3. {
  4. simple_cdev->ops   = &simple_fops; //文件操作
  5. simple_cdev->owner = THIS_MODULE;
  6. }
  7. else
  8. {
  9. printk(KERN_ERR "alloc cdev err no memory");
  10. unregister_chrdev_region(dev, DEV_COUNT); //如果分配资源出错则释放设备号
  11. return -ENOMEM;
  12. }
  13. err = cdev_add(simple_cdev, dev, DEV_COUNT); //向内核添加设备
  14. if(err < 0)
  15. {
  16. printk(KERN_ERR "add cdev err \n");
  17. goto error1;
  18. }
  19. else
  20. {
  21. #if SIMPLE_DEBUG
  22. printk(KERN_INFO "add char dev OK!\n");
  23. #endif
  24. }

自动创建设备节点

  1. simple_class = class_create(THIS_MODULE, SIMPLE_NAME);
  2. if(simple_class == NULL)
  3. {
  4. printk(KERN_ERR "create simple class error\n");
  5. goto error2;
  6. }
  7. else
  8. {
  9. #if SIMPLE_DEBUG
  10. printk(KERN_INFO "create simple class  OK!\n");
  11. #endif
  12. }
  13. simple_dev = device_create(simple_class, NULL, dev, NULL, SIMPLE_NAME);
  14. if(simple_dev == NULL)
  15. {
  16. printk(KERN_ERR "create device error");
  17. goto error3;
  18. }
  19. else
  20. {
  21. #if SIMPLE_DEBUG
  22. printk(KERN_INFO "create simple device  OK!\n");
  23. #endif
  24. }

删除设备节点,注销字符设备

  1. dev = MKDEV(simple_major, simple_minor); //计算出设备号
  2. device_destroy(simple_class, dev); //删除设备节点
  3. class_destroy(simple_class);//删除并释放class结构
  4. cdev_del(simple_cdev);//注销设备
  5. cdev_put(simple_cdev);//对动态的设备需要使用cdev_put‘来释放资源

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

主要的文件操作方法实现

文件操作函数有很多的操作接口,驱动编程需要实现这些接口,在用户编程时候系统调用时候会调用到这些操作

[cpp] view plaincopyprint?
  1. struct file_operations {
  2. ...
  3. loff_t (*llseek) (struct file *, loff_t, int);
  4. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
  5. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
  6. int (*open) (struct inode *, struct file *);
  7. int (*release) (struct inode *, struct file *);
  8. ...
  9. };

以上只列出了主要的操作,下面会依次介绍:
本次的测试代码上传在:char_step2

结构体:

首先 我们会模拟写一个不操作任何设备,而仅仅是存储的一个驱动。
定义自己的一个结构体为:

[cpp] view plaincopyprint?
  1. struct simple_dev{
  2. char data[MAX_SIMPLE_LEN];
  3. loff_t count;
  4. struct semaphore semp;
  5. };

data 保存数据, count表示文件的数据有效的位置, semp是一个信号量锁,在以后的编程中使用,
之后的程序中结构体也会做相应的变化,以适应linux编写驱动的习惯

open方法:

打开设备并进一步初始化工作,在没有定义open方法时内核以一种默认的方式打开设备,保证每次都能正确打开。
open方法中有有struct inode参数,包含了设备号,程序中可以使用次设备号得到正操作的设备
在struct file中主要的操作是private_data指针,他可以传递任何自己创建的结构。
总得说来open方法的作用有3
1、获得操作的设备(通过设备号)
2、进一步的初始化设备
3、初始化file结构体的private_data

[cpp] view plaincopyprint?
  1. static int simple_open(struct inode *inodp, struct file *filp)
  2. {
  3. struct simple_dev *temp_dev = NULL;
  4. int minor = 0;
  5. #if SIMPLE_DEBUG
  6. printk(KERN_INFO "In %s \n", __func__);
  7. #endif
  8. minor = iminor(inodp);//获得操作的设备的次设备号
  9. if(minor > DEV_COUNT-1){
  10. printk(KERN_ERR "the char dev in invalid \n");
  11. return -ENODEV;
  12. }
  13. #if SIMPLE_DEBUG
  14. printk(KERN_INFO "the minor is  %d \n", minor);
  15. #endif
  16. temp_dev = &char2_dev[minor];//获得真正操作的设备
  17. /* 进一步 初始化设备 因为是操作一个模拟的设备 故省去*/
  18. filp->private_data = temp_dev; //初始化 private_data
  19. return 0;
  20. }


release方法:

主要是对open进一步初始化的操作的反操作
比如open时候分配了内存,在release时就需要释放它等
例子中因为操作内存设备,故在release时无需做什么事

read方法:

read 是把设备中的数据传递给调用者
主要步骤
1、检测偏移量有效(有些设备驱动不需要检测)
2、检测用户空间地址有效
3、将数据传给用户(在此步骤中调用的函数可能会自己检测步骤2)
4、调整偏移量
5、返回读到的数据长度
(read write 用法相对灵活,不要依赖上边的步骤,设备驱动程序要根据设备特性去设计此方法)
这里先介绍一个会检测用户空间地址是否有效的copy函数
用户调用read读设备,而在内核空间就是将数据传给用户,是一个to的操作

[cpp] view plaincopyprint?
  1. unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)

__must_check表述必须检测其返回值,操作成功返回0,不成功返回负的错误码
to是用户空间指针 也就是read函数传入的用户空间的指针,
from指向设备要传送的数据

n标识传入长度

上图是 摘自LDD3上的经典视图, 应该比较能说明read的方法

[cpp] view plaincopyprint?
  1. static ssize_t simple_read(struct file *filp, char __user *userstr, size_t count, loff_t *loff)
  2. {
  3. struct simple_dev *dev = NULL;
  4. int data_remain = 0;
  5. int err;
  6. #if SIMPLE_DEBUG
  7. printk(KERN_INFO "In %s \n", __func__);
  8. #endif
  9. dev         = filp->private_data;
  10. data_remain = dev->count - *loff;
  11. if(MAX_SIMPLE_LEN < *loff)//检测偏移量
  12. {
  13. printk(KERN_ERR "the offset is illegal in func %s \n",__func__ );
  14. return -EINVAL;
  15. }
  16. else if(data_remain <= 0)
  17. {
  18. printk(KERN_WARNING "there was not much data in the device\n");
  19. return 0;
  20. }
  21. else
  22. {
  23. if(count > data_remain)
  24. {
  25. #if SIMPLE_DEBUG
  26. printk(KERN_INFO "the data is less than the user want to read\n");
  27. #endif
  28. count = data_remain;
  29. }
  30. else
  31. {
  32. }
  33. }
  34. err = copy_to_user(userstr, (dev->data)+(*loff), count); //调用内核函数进行数据拷贝,它会检测用户地址是否有效
  35. if(err != 0)
  36. {
  37. printk(KERN_ERR "an error occured when copy data to user\n");
  38. return err;
  39. }
  40. else
  41. {
  42. #if SIMPLE_DEBUG
  43. printk(KERN_INFO "data copy to user OK\n");
  44. #endif
  45. *loff = *loff + count; //调整偏移量
  46. return count; //返回写入的数据量
  47. }
  48. }


write方法:

与read类似 它是从用户传数据给设备驱动
从内核空间看就是一个从用户空间取数据 是一个from操作

[cpp] view plaincopyprint?
  1. long __must_check strncpy_from_user(char *dst, const char __user *src, long count)

dst 驱动保存数据的地址
src 用户空间传入的数据
count 标识数据长度

[cpp] view plaincopyprint?
  1. static ssize_t simple_write(struct file *filp, const char __user *userstr, size_t count, loff_t *loff)
  2. {
  3. struct simple_dev *dev = NULL;
  4. int err;
  5. int remain_space = 0;
  6. #if SIMPLE_DEBUG
  7. printk(KERN_INFO "In %s\n",__func__);
  8. #endif
  9. dev          = filp->private_data;
  10. if(MAX_SIMPLE_LEN <= *loff) //检测偏移量
  11. {
  12. printk(KERN_ERR "the offset is illegal in func %s\n", __func__);
  13. return -EINVAL;
  14. }
  15. else
  16. {
  17. remain_space = MAX_SIMPLE_LEN - *loff;
  18. if(count > remain_space)
  19. {
  20. #if SIMPLE_DEBUG
  21. printk(KERN_WARNING "the data is to long to write to the device\n");
  22. #endif
  23. count = remain_space;
  24. }
  25. else
  26. {
  27. }
  28. }
  29. err = copy_from_user((dev->data)+(*loff),userstr,count);//取得数据
  30. if(err != 0)
  31. {
  32. printk(KERN_ERR "an error occured when copy data from user\n");
  33. return err;
  34. }
  35. else
  36. {
  37. #if SIMPLE_DEBUG
  38. printk(KERN_INFO "data copy from user OK\n");
  39. #endif
  40. *loff = *loff + count; //跳着偏移
  41. if(*loff > dev->count)
  42. {
  43. dev->count = *loff;
  44. }
  45. else
  46. {
  47. }
  48. return count; //返回写入的数据量
  49. }
  50. }


lseek方法:

根据用户传入的参数调整文件偏移
mode

SEEK_SET 从文件起始处开始偏移
SEEK_CUR 从文件当前位置计算偏移
SEEK_END 从文件末尾计算偏移

file结构的f_pos保存了文件的偏移量
在调整文件偏移后需要 更新file中得f_pos成员

[cpp] view plaincopyprint?
  1. static loff_t simple_llseek(struct file *filp, loff_t loff, int mode)
  2. {
  3. struct simple_dev *dev = NULL;
  4. loff_t tmp_len;
  5. #if SIMPLE_DEBUG
  6. printk(KERN_INFO "In %s\n",__func__);
  7. #endif
  8. dev          = filp->private_data;
  9. switch ( mode )
  10. {
  11. case SEEK_SET:
  12. if( loff < 0 )
  13. {
  14. printk(KERN_ERR "can't move above file line %d \n", __LINE__);
  15. return -1;
  16. }
  17. else if(loff > dev->count)
  18. {
  19. printk(KERN_ERR "offset is too long line %d\n", __LINE__);
  20. return -1;
  21. }
  22. else
  23. {
  24. filp->f_pos = loff;
  25. }
  26. break;
  27. case SEEK_CUR:
  28. if((tmp_len = filp->f_pos+loff) < 0)
  29. {
  30. printk(KERN_ERR "can't move above file line %d \n", __LINE__);
  31. return -1;
  32. }
  33. else if(tmp_len > dev->count)
  34. {
  35. printk(KERN_ERR "offset is too long line %d\n", __LINE__);
  36. return -1;
  37. }
  38. else
  39. {
  40. filp->f_pos = tmp_len;
  41. }
  42. break;
  43. case SEEK_END:
  44. if((tmp_len = dev->count+loff ) < 0)
  45. {
  46. printk(KERN_ERR "can't move above file line %d \n", __LINE__);
  47. return -1;
  48. }
  49. else if(tmp_len > dev->count)
  50. {
  51. printk(KERN_ERR "offset is too long line %d\n", __LINE__);
  52. return -1;
  53. }
  54. else
  55. {
  56. filp->f_pos = tmp_len;
  57. }
  58. break;
  59. default :
  60. printk(KERN_INFO "illigal lseek mode! \n");
  61. return -1;
  62. break;
  63. }
  64. return filp->f_pos;
  65. }

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

说点上节没有讲完的话题

用户地址检测 简单模块调试 以及一些杂项

检测用户空间地址的有效性

上一节中提到在read write时候要检测用户空间传递的参数地址是否是有效地址,有的内核函数会自行检测,但是在调用轻量级的内核函数时候,就可能不去检测用户空间的地址是否有效,如果此时用户无传递一个无效地址,而内核函数去操作了它,这时棘手的问题出现了,轻则内核oops 关机重启就OK了,在特别严重的情况下,可能你的系统就崩溃了(我又遇到过),所以,我们在驱动程序中操作用户空间地址时候要小心加小心。如果电脑配置可以就在虚拟机中玩, 或者在开发板上试,当然这边的测试代码我都有试过,不至于让你系统崩溃的。
如何检测呢?
调用一个access_ok函数去检测

[cpp] view plaincopyprint?
  1. #define access_ok(type,addr,size)

type 标识读写操作VERIFY_READ表示地址可读,VERIFY_WRITE表示地址可写
addr 用户传入的地址
size 读写的长度
此代码在有内存管理的芯片与无内存管理之间有区别

我们 看一段内核代码 (path : arch/arm/include/asm/uaccess.h)

[cpp] view plaincopyprint?
  1. #define access_ok(type,addr,size)   (__range_ok(addr,size) == 0)
  2. #ifdef CONFIG_MMU
  3. ...
  4. #define __range_ok(addr,size) ({ \
  5. unsigned long flag, roksum; \
  6. __chk_user_ptr(addr);   \
  7. __asm__("adds %1, %2, %3; sbcccs %1, %1, %0; movcc %0, #0" \
  8. "=&r" (flag), "=&r" (roksum) \
  9. "r" (addr), "Ir" (size), "0" (current_thread_info()->addr_limit) \
  10. "cc"); \
  11. flag; })
  12. #else
  13. ...
  14. #define __range_ok(addr,size)<span style="white-space:pre">   </span>(0)


即在有内存管理并配置了内存管理的芯片内调用次函数会执行检测操作,而在没有配置内存管理的芯片中此函数总是返回真,而做驱动的不应该做这些假设,所以传入的参数在有必要的情况下还是要自行检测再看看copy_to_user函数

[cpp] view plaincopyprint?
  1. static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
  2. {
  3. if (access_ok(VERIFY_WRITE, to, n))
  4. n = __copy_to_user(to, from, n);
  5. return n;
  6. }

可以看到他在函数内部做了这种检测

而当我们调用

[cpp] view plaincopyprint?
  1. __copy_to_user
  2. __copy_from_user
  3. get_user
  4. __get_user
  5. put_user
  6. __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的时候 就不行了 因为你已经死机了 也就输不了这个命令 就看不到打印信息了)

[cpp] view plaincopyprint?
  1. #if SIMPLE_DEBUG
  2. #define D(...) printk(KERN_DEBUG __VA_ARGS__)
  3. #define WAR(...) printk(KERN_WARNING __VA_ARGS__)
  4. #else
  5. #define D(...) ((void)0)
  6. #define WAR(...) ((void)0)
  7. #endif

在需要调试的时候,我们去定义SIMPLE_DEBUG这个宏,在驱动代码测试都OK可以发行时候,去掉这个定义。
在需要打印的地方我们就使用

[cpp] view plaincopyprint?
  1. D(“print the log int func:%s line:%d”, __func__ ,__LINE__);

当然要修改成有意义的debug信息

打印当前进程信息
内核模块不像应用程序一样顺序执行,只用应用进程调用到想关联的函数才会到内核模块中call这个接口,那可不可以 打印调用进程的信息呢?
答案是肯定的,linux中定义了 current这个变量(<linux/sched.h>)current指向了当前的进程,他是一个 task_struct类型
其中有两个重要的成员
comm 表示了 当前的命令名名
pid 表示了当前进程号

[cpp] view plaincopyprint?
  1. D("[process: %s] [pid: %d] xxx\n" , current->comm, current->pid);


内核的container_of函数
在写字符设备驱动时候我们都会去自定义一个结构,其中包含了cdev结构

[cpp] view plaincopyprint?
  1. struct simple_dev{
  2. char *data;
  3. loff_t count;
  4. struct cdev cdev;
  5. struct semaphore semp;
  6. };

在open方法中除了上一节看到的那样用一个次设备好来获取结构(有时候会出错 如果我们次设备号不是从0开始分配)
所以需要寻求一种安全的方法
container_of为我们提供了这么一个很好的接口

[cpp] view plaincopyprint?
  1. /**
  2. * container_of - cast a member of a structure out to the containing structure
  3. * @ptr:    the pointer to the member.
  4. * @type:   the type of the container struct this is embedded in.
  5. * @member: the name of the member within the struct.
  6. *
  7. */
  8. #define container_of(ptr, type, member)

再解释一下 
ptr是指结构体一个成员的地址
type 指要获得的结构体
member ptr指向的成员在结构体中的名字

[cpp] view plaincopyprint?
  1. /*container_of(pointer, container_type, container_fild)
  2. we can get the container_type with one number in the container
  3. by this function. container_fild is the one of the number in the
  4. container_type , pointer point to confain_field type
  5. */
  6. temp_dev = container_of(inodp->i_cdev, struct simple_dev, cdev);

上边的E文 是我写的 poor English

container_of 的实现基本原理是这样的:
知道了结构体中某个成员的地址,  又可以求的该成员在改结构体中得偏移,拿成员的地址减去这个偏移,就得到了整个结构的地址,太佩服写内核的人了

[cpp] view plaincopyprint?
  1. #define container_of(ptr, type, member) ({          \
  2. const typeof(((type *)0)->member) * __mptr = (ptr);  \
  3. (type *)((char *)__mptr - offsetof(type, member)); })

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

并发 竞态 (信号量与自旋锁)

前面所述的字符驱动都是没有考虑并发竟态的情况,想象一下

一个进程去读一个字符设备,另一个进程在同一时间向这个设备写入(完全有这种情况)

原来设备中存有 A B C D 要想设备写入1 2 3 4 每次读写一个字节

t1 t2 t3 t4 t5 t6 t7 t8
R W W R W R R W
A 1 2 2 3 3 D 4

R: 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>中

创建信号量

[cpp] view plaincopyprint?
  1. void sema_init(struct semaphore *sem, int val)

sem:   信号量结构体

val :    初始化信号量的值(代表了资源数)【val 为1时 表示代码在同一时间只允许一个进程访问 称之为互斥锁】

[cpp] view plaincopyprint?
  1. void init_MUTEX();
  2. void init_MUTEX_LOCKED();

两个定义互斥锁的函数 ( 最新的内核中已经没有这两个函数了,所以不要用

[cpp] view plaincopyprint?
  1. struct simple_dev{
  2. char *data;
  3. loff_t count;
  4. struct cdev cdev;
  5. struct semaphore semp;
  6. };
  7. static __init int simple_init(void)
  8. {
  9. ...
  10. for( index = 0 ; index < DEV_COUNT ; ++index )
  11. {
  12. sema_init(&char5_dev[index].semp,1);
  13. //init_MUTEX(&(char5_dev[index].semp));
  14. init_waitqueue_head(&(char5_dev[index].queue));
  15. }
  16. ...
  17. }


P操作

[cpp] view plaincopyprint?
  1. void down(struct semaphore *sem);
  2. int __must_check down_interruptible(struct semaphore *sem);
  3. int __must_check down_killable(struct semaphore *sem);
  4. int __must_check down_trylock(struct semaphore *sem);
  5. int __must_check down_timeout(struct semaphore *sem, long jiffies)

第一个函数在信号量大于0时直接将信号量的值减一程序继续运行,在信号量为0时进程睡眠只有等到有其他进程释放信号量时候才被唤醒( 不推荐使用

第二个函数 在第一个操作的基础上,如果进程因为没有得到信号量睡眠,在别的进程释放信号量或者发成中断的情况下都会被唤醒,在被中断信号唤醒时候返回-EINTR,成功返回0

第三个函数 如果没有另外的任务会获取此信号量,则可以调用此函数,在收到中断信号时会返回-EINTR

第四个函数 试图去获取信号量,在获取不到的时候不会睡眠,而是继续运行,返回0值表示得到了此信号量, 返回1 表示没能获取到。

第五个函数 可以去设置最长睡眠时间, 但是 此函数不可中断

V操作

[cpp] view plaincopyprint?
  1. void up(struct semaphore *sem)

此操作 首先检测时候有其他进程等待此信号量,如果有则唤醒此进程,不改变信号量的值,如果没有则将信号量的值加1。

[cpp] view plaincopyprint?
  1. static ssize_t simple_read(struct file *filp, char __user *userstr, size_t count, loff_t *loff)
  2. {
  3. struct simple_dev *dev = NULL;
  4. int data_remain = 0;
  5. int err;
  6. D("[%s:] In %s \n",current->comm, __func__);
  7. dev         = filp->private_data;
  8. err = down_interruptible(&dev->semp);
  9. if(err)
  10. {
  11. if(err == -EINTR)
  12. WAR("return by an interrupt signal\n");
  13. else
  14. printk(KERN_ERR "an error occured in down_interruptible\n");
  15. return err;
  16. }
  17. else
  18. {
  19. D("have get the mutex %d\n", __LINE__);
  20. }
  21. /*******************************************
  22. *       临界区代码
  23. ********************************************/
  24. up(&dev->semp);
  25. return count;
  26. }


自旋锁

自旋锁是另一种锁机制,信号量会引起进程的休眠,而在不能睡眠的代码中我们就需要使用自旋锁

自旋锁也是一个互斥的概念,有“锁定”与“解锁”两个操作,当程序需要锁定时候,则先检测锁是否可用,如果可用则获得锁,程序进入临界区,否则进入忙等待重复检测这个锁是否可用(这就是自旋),临界区代码操作完成则解锁。

信号量实现中其实也用到了自旋锁机制(有兴趣的刻一看内核源码,这边不展开,等以后写内核时候再介绍)

因为自旋锁在得不到锁的时候会“自旋” 即不会让出CPU ,所以我们的临界区执行速度应该尽量的快,最好使用原子操作(不会睡眠)。这也是使用自旋锁的核心规则,在多数情况下我们做不到这一点,所以自旋锁在驱动程序中使用的不如信号量频繁。

初始化

[cpp] view plaincopyprint?
  1. spin_lock_init(_lock)

锁定

[cpp] view plaincopyprint?
  1. static inline void spin_lock(spinlock_t *lock)
  2. static inline int spin_trylock(spinlock_t *lock)
  3. static inline void spin_lock_irq(spinlock_t *lock)
  4. spin_lock_irqsave(lock, flags)
  5. static inline void spin_lock_bh(spinlock_t *lock)

第一个函数是 不去禁止中断 直接锁定

第二个函数 会尝试加锁,检测返回之判断是否锁定,在不能锁定时候程序也继续运行

第三个函数 禁止中断,不保存保存原先的中断状态

第四个函数 在禁止中断之前,保存原先的中断状态,

第五个函数 表示只禁止软件中断而保持硬件中断的打开

因为自旋锁本质上要不会被中断,所以调用时候建议使用包含有禁止中断的函数

解锁

[cpp] view plaincopyprint?
  1. static inline void spin_unlock(spinlock_t *lock)
  2. //static inline void spin_unlock(spinlock_t *lock)
  3. static inline void spin_unlock_irq(spinlock_t *lock)
  4. static inline void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)
  5. static inline void spin_unlock_bh(spinlock_t *lock)

对应于锁定的五个函数,分别解锁,(对应于锁定的第二个操作,在加锁成功时候需要使用spin_unlock来解锁)

死锁

上边只提到了使用锁的好处,以及如何使用锁,但是引入锁机制也会带来风险,那就是死锁,进程死锁最明显的表现就是死机。

如上图所示,A进程占有锁1,并在持有锁1的时候需要得到锁2程序才能继续进行

B进程占有锁2, 并在保持锁2的同时去获取锁1程序才能继续运行,这样A, B 进程就卡在这里,互不相让,这就导致了死锁。

亦或 在一个进程内同时试图获取同样的锁

死锁的名词解释

是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁

解决死锁的方法:

1、加解锁顺序一致

在必须使用多个锁的时候,应该始终以相同的顺序获取,也最好以获取锁顺序逆序解锁。

2、使用原子变量
原子变量不会被多个进程并发操作,内核提供了一种原子类型(atomic_t <asm/stomic.h>中)

3、设置加锁timeout值
在一定时间内不能获得锁,就放弃,并释放已占有的锁

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

阻塞型字符设备驱动

前面说到了 如何实现read write 等操作,但如果设备缓冲已满,现在想而用户此时又想写入设备,此请求就无法立即执行,那怎么办呢?
第一种情况是:驱动程序想用户返回请求失败的信息。
第二种情况是:使调用进程阻塞等待设备可以被操作。

而用户更希望自己选择在请求无法满足时候如何操作,所以在用户空间有了O_NONBLOCK标志
在打开设备的时候如果用户指定了此标志(会保存到filp->f_flags中),表示用户希望以非阻塞的形式打开设备,读写时如果设备不能满足要求,则返回错误码(-EAGAIN).
如果用户不指定此标志位,则默认是阻塞型打开,在设备不能满足请求时候需要就阻塞直到设备可以被操作。

休眠的介绍

进程被阻塞即意味着释放CPU,直到另一个进程修改了某个状态,使调度器再次去调度他,所以进程休眠后不知道自己在休眠期间做了什么事情,所以在被唤醒之后还需要检查当前状态,看等待的条件是否为真
在linux中等待队列通过等待队列头来管理,(wait_queue_head_t)
初始化队列头

[cpp] view plaincopyprint?
  1. init_waitqueue_head(wait_queue_head_t * queue);

还有一种简单的方式去初始化队列头(静态初始化)

[cpp] view plaincopyprint?
  1. DECLARE_WAIT_QUEUE_HEAD(name)

它会将name 定义为一个 struct wait_queue_head_t 结构,并初始化

使进程休眠

让一个进程进入睡眠,在醒来时进程仍需要检测一个条件是否为真,正如上边提到的一样

[cpp] view plaincopyprint?
  1. #define wait_event(wq, condition)
  2. #define wait_event_timeout(wq, condition, timeout)
  3. #define wait_event_interruptible(wq, condition)
  4. #define wait_event_interruptible_timeout(wq, condition, timeout)

wq: 指对应的等待队列头

condition 表示检测的的条件,在condition为真时,睡眠终止。

含有 interruptible 的函数 表示睡眠可以被信号中断,当睡眠被信号中断事,函数返回负值(-ERESTARTSYS)

含有timeout 的函数 timeout 参数表示最长的等待时间,

进程唤醒

有睡眠就有唤醒

在其他进程操作了设备 使等待进程的 等待条件(condition)为真,则需要在此进程中唤醒睡眠的进程

[cpp] view plaincopyprint?
  1. #define wake_up(x)          __wake_up(x, TASK_NORMAL, 1, NULL)
  2. #define wake_up_all(x)          __wake_up(x, TASK_NORMAL, 0, NULL)
  3. #define wake_up_interruptible(x)    __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
  4. #define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
  5. #define wake_up_interruptible_all(x)    __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)

wake_up 会首先唤醒队列上1个 独占等待的进程(下边有简单的解释),如果没有则去唤醒其他所有的进程

wake_up_all 唤醒等待队列上的所有进程,不管独占 还是 非独占

wake_up_interruptible跟wake_up 类似, 不过他只会唤醒 队列中可中断的睡眠进程

wake_up_interruptible_all 会唤醒等待队列上的所有可中断睡眠,

wake_up_interruptible_nr 回唤醒等待队列上的nr个独占进程独占等待进程 : 因为在一个睡眠队列上,睡眠的进程可能会有很多,而每次唤醒之后只有一个进程唤醒执行,其他进程继续在等待队列上睡眠,这就导致了有些=重要的进程迟迟不呢个得到调用,为改善这个情况,有了独占进程的概念,
在睡眠时候我们可以指定一个参数WQ_FLAG_EXCLUSEVE标志会加到队列的尾部(不加此标志则默认加到队列头部), 在调用wake_up_xx 对应的函数时候会首先唤醒这些进程。

[cpp] view plaincopyprint?
  1. while(dev->datawr == dev->datard)
  2. {
  3. while(dev->datard == dev->datawr) //循环检测等待条件是否真正的满足
  4. {
  5. up(&dev->semp)
  6. if(filp->f_flags & O_NONBLOCK) //判断是否是非阻塞打开
  7. {
  8. return -EAGAIN;
  9. }
  10. D("[%s] reading going to sleep!", current->comm);
  11. if(wait_event_interruptible(dev->rdque, dev->datard != dev->datawr)) //使当前进程睡眠在读睡眠队列
  12. {
  13. return -ERESTARTSYS;
  14. }
  15. D("waked up %d\n", __LINE__);
  16. ...
  17. if(down_interruptible(&dev->semp) < 0)//获取锁
  18. {
  19. printk(KERN_ERR "[%s]get the mutex lock error %d, %s",
  20. current->comm, __LINE__, __func__);
  21. return -ERESTARTSYS;
  22. }
  23. else
  24. {
  25. D("have get the mutex %d\n", __LINE__);
  26. }
  27. }
  28. /*read data*/
  29. wake_up_interruptible(dev->wrque)//读出数据 唤醒写睡眠队列上的进程
  30. }

poll 函数实现

[cpp] view plaincopyprint?
  1. unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait);

在系统编程时候我们经常会只用到poll 或者 select函数去监听某个文件描述符是否可写或者可读等
这些系统调用都是通过poll函数驱动实现
当调用此函数时候内核会分配一个 poll_table_struct 结构,我们 多需要的动作有两步
1、在等待队列上调用poll_wait

[cpp] view plaincopyprint?
  1. static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)

2、返回一个描述设备可读可写的掩码

[cpp] view plaincopyprint?
  1. #define POLLIN      0x0001 //设备可读
  2. #define POLLPRI     0x0002 //无阻塞读取高优先级数据
  3. #define POLLOUT     0x0004 //设备可写入
  4. #define POLLERR     0x0008 //设备发生错误
  5. #define POLLHUP     0x0010 //设备挂起
  6. #define POLLRDNORM      0x0040 //normal data is ready now
  7. #define POLLWRNORM  0x0100 //normal data can be writen to device

example:

[cpp] view plaincopyprint?
  1. static int simple_poll(struct file *filp, struct poll_table_struct *wait)
  2. {
  3. struct simple_dev *dev = filp->private_data;
  4. unsigned int mask = 0;
  5. char *next_ptr;
  6. if(down_interruptible(&dev->semp))
  7. {
  8. printk(KERN_ERR "get the semphore err %d \n",__LINE__);
  9. return -ERESTARTSYS;
  10. }
  11. D("have get the semphore %d\n", __LINE__);
  12. poll_wait(filp, &dev->inq, wait);
  13. poll_wait(filp, &dev->outq, wait);
  14. if(dev->datard != dev->datawr)
  15. {
  16. mask |= POLLIN | POLLRDNORM;   //can be read
  17. }
  18. if(dev->datawr+1 == dev->dataend)
  19. next_ptr = dev->data;
  20. else
  21. next_ptr = dev->datawr+1;
  22. if(next_ptr != dev->datard)
  23. {
  24. mask |= POLLOUT | POLLWRNORM; //can be write
  25. }
  26. up(&dev->semp);
  27. return mask;
  28. }


下一节 会以一个驱动程序 模拟管道 结束基于内存的模拟字符设备驱动程序 ,  同时介绍 驱动测试的相关东东

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

字符设备模拟pipe的驱动程序

让我们用一个”pipe“的设备驱动去结束简单字符设备吧(这里所说的pipe并非标准的pipe只是模拟了一个从一端写入从另一端写入的设备)

测试代码1      测试代码2

设计思路

用一个图来说明(可是画了很久哟)

简单说来就是一个进程写入缓冲区,另一个进程可以读出,读出后原buffer中的数据被置为无效值,

自定义一个结构

[cpp] view plaincopyprint?
  1. #define MAX_SIMPLE_LEN 1024 //buffer 数据长度
  2. struct simple_dev{
  3. char *data;         //指向数据的头部
  4. char *datard;       //读指针
  5. char *datawr;       //写指针
  6. char *dataend;      //指向缓冲区的结尾
  7. wait_queue_head_t inq;  //读取等待队列头
  8. wait_queue_head_t outq; //写入等待队列头
  9. struct cdev cdev;       //字符设备结构
  10. struct semaphore semp;  //结构体信号量
  11. };

申请设备号

之前有介绍,不再赘述

为自定义结构体分配区内存

[cpp] view plaincopyprint?
  1. D("alloc simple_dev struct \n");
  2. char7_dev = kmalloc(DEV_COUNT*sizeof(struct simple_dev), GFP_KERNEL);
  3. if( char7_dev == NULL)
  4. {
  5. printk(KERN_ERR "kmalloc simple_dev err no memory");
  6. unregister_chrdev_region(dev, DEV_COUNT);
  7. return -ENOMEM;
  8. }

使用kmalloc 给设备结构他分配内存,因为系统可能因为暂时不能分配到内存(虽然很少见),所以在分配到内存后需要检测是否分配成功,在检测到成功分配内存后还需要将所分配的内存区清零,以免以后出现奇怪的错误难以调试(一定要记住)

[cpp] view plaincopyprint?
  1. memset(char7_dev, 0, DEV_COUNT*sizeof(struct simple_dev));

为缓冲区申请内存

在为自定义结构他申请好内存后,我们需要为每个结构体内的buffer申请内存,使用相同的方法,分配并清零

[cpp] view plaincopyprint?
  1. for( index = 0 ; index < DEV_COUNT ; ++index )
  2. {
  3. //char7_dev[index].count = 0
  4. if(( char7_dev[index].data = kmalloc(MAX_SIMPLE_LEN, GFP_KERNEL) )!= NULL )
  5. {
  6. memset(char7_dev[index].data, 0, MAX_SIMPLE_LEN);
  7. D("kmalloc the data space OK! \n");
  8. }
  9. else{
  10. for( --index ; index >= 0 ; --index )
  11. {
  12. kfree(char7_dev[index].data);
  13. }
  14. printk(KERN_ERR "kmalloc simple_dev data number err no memory");
  15. kfree(char7_dev);
  16. unregister_chrdev_region(dev, DEV_COUNT);
  17. return -ENOMEM;
  18. }
  19. }

这里需要注意的一点是 在申请某个buffer缓冲区失败时候,需要将已经成功申请的内存释放掉(else中做了这个工作)

初始化结构体中的各数据指针、信号量、等待队列头

在申请好设备及各设备buffer内存后,我们需要对结构中的一些变量进行初始化,

初始化数据指针

将data指向buffer起始位置, datawr,datard 初始化也指向起始位置(表示空buffer),dataend 指向buffer末尾一个无效位置,用于判断读写位置是否合法

初始化信号量以及读写队列头

[cpp] view plaincopyprint?
  1. for( index = 0 ; index < DEV_COUNT ; ++index )
  2. {
  3. /*init the data ptr*/
  4. char7_dev[index].datard = char7_dev[index].data;
  5. char7_dev[index].datawr = char7_dev[index].data;
  6. char7_dev[index].dataend = char7_dev[index].data + MAX_SIMPLE_LEN;
  7. /*init semaphore, waitqueue_head and so on*/
  8. sema_init(&(char7_dev[index].semp), 1);
  9. init_waitqueue_head(&(char7_dev[index].inq));
  10. init_waitqueue_head(&(char7_dev[index].outq));
  11. }

初始化字符设备添加字符设备

逐个设备初始化(注册)并添加(告知内核)

[cpp] view plaincopyprint?
  1. for( index = 0 ; index < DEV_COUNT ; ++index )
  2. {
  3. cdev_init(&(char7_dev[index].cdev), &simple_fops);
  4. char7_dev[index].cdev.owner = THIS_MODULE;
  5. err = cdev_add(&(char7_dev[index].cdev), dev, 1);
  6. if(err < 0)
  7. {
  8. printk(KERN_ERR "add cdev err \n");
  9. goto error1;
  10. }
  11. else
  12. {
  13. D( "add %d char dev OK!\n", index+1);
  14. }
  15. }

字符设备操作

[cpp] view plaincopyprint?
  1. struct file_operations simple_fops={
  2. .owner   = THIS_MODULE,
  3. .open    = simple_open,
  4. .release = simple_close,
  5. .read    = simple_read,
  6. .write   = simple_write,
  7. .llseek  = simple_llseek,
  8. //  .ioctl   = simple_ioctl,
  9. .poll    = simple_poll,
  10. .mmap    = simple_mmap,
  11. };

因为2.6.35之后文件操作已经没有ioctl方法,所以不在介绍了

打开及关闭操作

打开关闭函数于之前的设备驱动没有差异,故不叙述

读写操作

首先获取信号量(读写操作都一样)

[cpp] view plaincopyprint?
  1. if(down_interruptible(&dev->semp) < 0)
  2. {
  3. printk(KERN_ERR "[%s]get the mutex lock error %d, %s",
  4. current->comm, __LINE__, __func__);
  5. return -ERESTARTSYS;
  6. }
  7. else
  8. {
  9. D("have get the mutex %d\n", __LINE__);
  10. }

读操作

对于读操作需要检测buffer中是否有数据

[cpp] view plaincopyprint?
  1. if(dev->datawr == dev->datard) //empty buffer
  2. {
  3. while(dev->datawr == dev->datard)//循环检测是否buffer中是否已经有数据
  4. {
  5. up(&dev->semp);//释放信号量
  6. if(filp->f_flags & O_NONBLOCK) //检测用户是否是非阻塞打开
  7. {
  8. D("set NONBLOCK mask %d\n", __LINE__);
  9. return -EAGAIN;
  10. }
  11. D("[%s] reading going to sleep!", current->comm);
  12. /*将当调用进程加到写等待队列*/
  13. if(wait_event_interruptible(dev->inq, dev->datard != dev->datawr))
  14. {
  15. return -ERESTARTSYS;
  16. }
  17. if(down_interruptible(&dev->semp) < 0)//wait_wvent_interrupt返回,获取信号量
  18. {
  19. printk(KERN_ERR "[%s]get the mutex lock error %d, %s",
  20. current->comm, __LINE__, __func__);
  21. return -ERESTARTSYS;
  22. }
  23. else
  24. {
  25. D("have get the mutex %d\n", __LINE__);
  26. }
  27. }
  28. }

看到这里你可能已经知道上边的流程图有一些错误(up 和 down 操作应该在while循环中去做,而不是在整个if 中)由于时间问题上图就不做修改了

计算buffer 剩余的数据

如果读指针在写指针之后(datard > datawr)则buffer中的数据就从读位置到buffer结尾,又buffer开头转到写位置

[cpp] view plaincopyprint?
  1. if(dev->datawr < dev->datard)
  2. {
  3. data_remain = (dev->dataend - dev->datard)
  4. + (dev->datawr-dev->data);
  5. }
  6. else
  7. {
  8. data_remain = dev->datawr - dev->datard;
  9. }

如果有一些不理解 你可以参照下边的示意图,应该很容易就理解上述计算buffer内数据长度的

判断数据长度的合法性并计算能够写入用户空间的长度

[cpp] view plaincopyprint?
  1. if(data_remain < 0)
  2. {
  3. printk(KERN_ERR "the remain data we calculate is wrong check! %d \n", __LINE__);
  4. }
  5. else if(count > data_remain)
  6. {
  7. WAR("the data is less than the user want to read\n");
  8. D("we can only copy %d bytes to user\n", data_remain);
  9. count = data_remain;
  10. }
  11. else
  12. {
  13. }

向用户空间传入数据

1、当读取操作不会读到buffer尾部时候,直接将数据copy给用户,调整读指针, 唤醒睡眠在写队列上的进程,释放信号量,相用户返回已经读取的数据长度

[cpp] view plaincopyprint?
  1. if(( dev->datawr > dev->datard ) || (dev->datard + count <= dev->dataend))
  2. {
  3. err = copy_to_user(userstr, dev->datard, count);
  4. if(err != 0)
  5. {
  6. printk(KERN_ERR "an error occured when copy data to user:%d\n", __LINE__);
  7. up(&dev->semp);
  8. return err;
  9. }
  10. else
  11. {
  12. D("data copy to user OK\n");
  13. dev->datard = dev->datard + count;
  14. if(dev->datard == dataend)
  15. dev->datard = dev->data;
  16. wake_up_interruptible(&dev->outq);
  17. up(&dev->semp);
  18. return count;
  19. }
  20. }

2、如果读到buffer末尾还需要绕回来从数据头部再读取

则先读取read 指针到buffer末尾的数据

然后再从头部读取相应长度的数据

同样在成功读取后需要唤醒写等待队列, 调整读指针, 释放信号量

[cpp] view plaincopyprint?
  1. else
  2. {
  3. data_remain= (dev->dataend -dev->datard );
  4. /*读取从当前位置到buffer结尾的数据长度*/
  5. err = copy_to_user(userstr, dev->datard+1, data_remain);
  6. if(err != 0)
  7. {
  8. printk(KERN_ERR "an error occured when copy data to user:%d\n", __LINE__);
  9. up(&dev->semp);
  10. return err;
  11. }
  12. else
  13. {
  14. D("data copy to user OK\n");
  15. //  up(&dev->semp);
  16. }
  17. /*从buffer头部读取剩余的长度*/
  18. err = copy_to_user(userstr+data_remain, dev->data, count-data_remain);
  19. if(err != 0)
  20. {
  21. printk(KERN_ERR "an error occured when copy data to user:%d\n", __LINE__);
  22. up(&dev->semp);
  23. return err;
  24. }
  25. else
  26. {
  27. D("data copy to user OK\n");
  28. dev->datard = dev->data+(count-data_remain);
  29. wake_up_interruptible(&dev->outq);
  30. up(&dev->semp);
  31. return count;
  32. }
  33. }

写操作

与读操作类似

获取即将写入的位置

检测buffer是否已经满需要检测下即将写入的地址是否有效(是否已经到了尾部位置),如果数据已经写到buffer的结尾则需要调整写

[cpp] view plaincopyprint?
  1. if (dev->datawr+1 == dev->dataend)//即将写入的位置是buffer尾部
  2. next_ptr = dev->data;         //调整写入指针的指向
  3. else
  4. next_ptr = dev->datawr + 1;

判断buffer是否已满

当即将写入的位置 正好是读指针指向的位置则表示buffer已满需要等待读进程

[cpp] view plaincopyprint?
  1. if( next_ptr == dev->datard )
  2. {
  3. while(next_ptr == dev->datard)
  4. {
  5. up(&dev->semp);
  6. if(filp->f_flags & O_NONBLOCK)
  7. {
  8. D("set NONBLOCK mask %d\n", __LINE__);
  9. return -EAGAIN;
  10. }
  11. D("[%s] writing going to sleep!", current->comm);
  12. if(wait_event_interruptible(dev->outq, next_ptr != dev->datard))
  13. {
  14. return -ERESTARTSYS;
  15. }
  16. if(down_interruptible(&dev->semp) < 0)
  17. {
  18. printk(KERN_ERR "[%s]get the mutex lock error %d, %s",
  19. current->comm, __LINE__, __func__);
  20. return -ERESTARTSYS;
  21. }
  22. else
  23. {
  24. D("have get the mutex %d\n", __LINE__);
  25. }
  26. }
  27. }

计算buffer剩余长度(可以写入的数据的长度)

同样需要分两种情况,可以参照上图

[cpp] view plaincopyprint?
  1. if(dev->datawr >= dev->datard)
  2. {
  3. remain_space = (dev->dataend - dev->datawr-1)
  4. + (dev->datard - dev->data);
  5. }
  6. else
  7. {
  8. remain_space = dev->datard - dev->datawr - 1;
  9. }

向buffer写入数据调整读写指针

[cpp] view plaincopyprint?
  1. if( (dev->datawr < dev->datard) || (dev->datawr + count < dev->dataend) )
  2. {
  3. err = copy_from_user(dev->datawr, userstr, count);
  4. if(err != 0)
  5. {
  6. printk(KERN_ERR "error occured when copy data from user %d\n", __LINE__);
  7. up(&dev->semp);
  8. return err;
  9. }
  10. else
  11. {
  12. D("data copy from user OK\n");
  13. dev->datawr = dev->datawr + count ;
  14. wake_up_interruptible(&dev->inq);
  15. up(&dev->semp);
  16. return count;
  17. }
  18. }
  19. else
  20. {
  21. remain_space = dev->dataend - dev->datawr ;
  22. err = copy_from_user(dev->datawr, userstr, remain_space);
  23. if(err != 0)
  24. {
  25. printk(KERN_ERR "error occured when copy data from user %d\n", __LINE__);
  26. up(&dev->semp);
  27. return err;
  28. }
  29. else
  30. {
  31. D("copy part of the data from user\n");
  32. }
  33. err = copy_from_user(dev->data, userstr+remain_space, count-remain_space);
  34. if(err != 0)
  35. {
  36. printk(KERN_ERR "error occured when copy data from user %d\n", __LINE__);
  37. up(&dev->semp);
  38. return err;
  39. }
  40. else{
  41. D("data copy from user OK\n");
  42. dev->datawr = dev->data + (count-remain_space);
  43. wake_up_interruptible(&dev->inq);
  44. up(&dev->semp);
  45. return count;
  46. }
  47. }

poll方法

在等待队列上调用poll_wait

[cpp] view plaincopyprint?
  1. poll_wait(filp, &dev->inq, wait);
  2. poll_wait(filp, &dev->outq, wait);

检测文件是否可读或者可写

[cpp] view plaincopyprint?
  1. if(dev->datard != dev->datawr)
  2. {
  3. mask |= POLLIN | POLLRDNORM;   //can be read
  4. }
  5. if(dev->datawr+1 == dev->dataend)
  6. next_ptr = dev->data;
  7. else
  8. next_ptr = dev->datawr+1;
  9. if(next_ptr != dev->datard)
  10. {
  11. mask |= POLLOUT | POLLWRNORM; //can be write
  12. }

Linux驱动编程 step-by-step相关推荐

  1. Linux驱动编程 step-by-step (二) 简单字符设备驱动

    简单字符设备驱动 1.主次设备号 主设备号标识设备连接的的驱动,此设备好由内核使用,标识在相应驱动下得对应的设备 在linux中设备号是一个32位的dev_t类型 typedef __u32    _ ...

  2. Linux驱动编程 step-by-step (二)

    简单字符设备驱动 1.主次设备号 主设备号标识设备连接的的驱动,此设备好由内核使用,标识在相应驱动下得对应的设备 在linux中设备号是一个32位的dev_t类型 typedef __u32    _ ...

  3. linux驱动编程——ch340x驱动移植

    Linux驱动编程--ch340x驱动移植 主要概念: ​ ch340x驱动移植 ​ 作为通用器件,厂商都有提供适配各种平台的驱动.linux一般会提供驱动源码. ​ 一般所谓驱动移植,就是将厂商的驱 ...

  4. Linux驱动编程视频教程

    本视频教程主要介绍字符驱动.杂项设备.中断.调试驱动的基本方法以及驱动的移植等. Linux驱动编程视频教程: 链接:https://pan.baidu.com/s/1Yn5d4w9uudb4tGDT ...

  5. Linux驱动编程 step-by-step (五)主要的文件操作方法实现

    主要的文件操作方法实现 文件操作函数有很多的操作接口,驱动编程需要实现这些接口,在用户编程时候系统调用时候会调用到这些操作 [cpp] view plaincopy struct file_opera ...

  6. linux驱动read函数 copytouser,Linux驱动编程 step-by-step (五)主要的文件操作方法实现...

    主要的文件操作方法实现 文件操作函数有很多的操作接口,驱动编程需要实现这些接口,在用户编程时候系统调用时候会调用到这些操作 structfile_operations { ... loff_t (*l ...

  7. Linux驱动编程(驱动程序基石)(下)

    一.中断的线程化处理 复杂.耗时的事情,尽量使用内核线程来处理.上节视频介绍的工作队列用起来挺简单,但是它有一个缺点:工作队列中有多个 work,前一个 work 没处理完会影响后面的 work.解决 ...

  8. Linux驱动编程(驱动程序基石)(上)

    一.休眠与唤醒 要休眠的线程,放在 wq 队列里,中断处理函数从 wq 队列里把它取出来唤醒.所以,我们要做这几件事: ① 初始化 wq 队列 ② 在驱动的 read 函数中,调用 wait_even ...

  9. Linux驱动编程 step-by-step (八) 阻塞型字符设备驱动

    阻塞型字符设备驱动 前面说到了 如何实现read write 等操作,但如果设备缓冲已满,现在想而用户此时又想写入设备,此请求就无法立即执行,那怎么办呢? 第一种情况是:驱动程序想用户返回请求失败的信 ...

最新文章

  1. 《如何高效学习》读书笔记(一)——整体性学习策略
  2. 节省 58% IT 成本,调用函数计算超过 30 亿次,石墨文档的 Serverless 实践
  3. HDU 1754 I Hate It(线段树单点更改、区间查找最大值)
  4. 8 种有趣的用于 Web 品牌的动物
  5. win7 怎么干净删除php,window_win7系统如何删除注册表里面的垃圾文件?,win7系统如何删除注册表里面的 - phpStudy...
  6. 计算机视觉哪个方向最火?来看这篇综述《中国图像工程:2020》
  7. 属性绑定与双向数据绑定
  8. Iterables和迭代器
  9. 企业库2.0培训系列课程大纲[意见征询]
  10. 王道考研操作系统笔记(第四章)附:王道考研408所有PPT和思维导图
  11. mamp安装php扩展,mac版mamp下php安装pcntl扩展
  12. wireshark抓取手机app包
  13. css加号图标_excel单元格加号展开折叠
  14. php 安装扩展管理工具 pecl
  15. 这个儿童节,我们和小时候有什么区别?
  16. python缺少位置参数_缺少1个必需的位置参数
  17. 多种方法进行数组从小到大排序
  18. 钣金cad插件_ug,CAD,SolidWorks,钣金??????非标等设计一站式学习,
  19. 高中数学有哪些好的学习方法?
  20. NBUT - 1457 Sona (莫队)

热门文章

  1. 游戏更新一直卡在计算机,win10电脑玩战地5游戏一直卡屏卡顿如何解决
  2. 如何避免搜索引擎爬虫产生的流量过大以及搜索引擎设置优化
  3. 王者苹果服务器转安卓系统,王者荣耀苹果用户可以转?王者荣耀苹果用户转安卓的方法教程...
  4. 农作物病害识别_FarmEasy:向农民推荐农作物变得容易
  5. wps 2019个人版 创建布尔运算
  6. WPS广告投放的优势!WPS广告投放的展现形式!
  7. python 打包命令以及使用方法
  8. 机器学习实例--预测美国人口收入状况
  9. 计算机自动安装程序,如何防止软件自动安装软件?防止电脑自动安装软件的方法...
  10. TED演讲:404找不到页面的故事