LINUX驱动编程

第三次看了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.   
  6. static int hello_init(void)   
  7. {   
  8.     printk(KERN_ALERT "hello module\n");   
  9.     return 0;   
  10. }   
  11.   
  12. static void hello_exit(void)   
  13. {   
  14.     printk(KERN_ALERT "hello module exit\n");   
  15. }   
  16.   
  17. module_init(hello_init);   
  18. 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.   
  8. else  
  9. #kernel PTAH   
  10. KERNELDIR ?= /lib/modules/$(shell uname -r)/build   
  11. PWD       := $(shell pwd)   
  12.   
  13. modules:   
  14.     $(MAKE) -C $(KERNELDIR) M=$(PWD) modules   
  15.   
  16. endif   
  17.   
  18. clean:   
  19.     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. /*  动态分配设备号    
  13.  *  parameter:        
  14.  *      dev : 用来存储分配的设备号值  
  15.  *      firstminor: 次设备号(一般填0)  
  16.  *      count: 分配的设备个数  
  17.  *      name : 设备名  
  18.  *  return value:  
  19.  *      0: success  
  20.  *      负值:出现错误,错误码  
  21. */  
  22. int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);   
  23.   
  24. /*  释放设备号  
  25.  *  parameter:        
  26.  *      first: 设备号  
  27.  *      count: 分配的设备个数  
  28. */  
  29. 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.   
  6. #define SIMPLE_DEBUG 1   
  7. #define DEV_COUNT 2   
  8. #define SIMPLE_NAME "simple_char"   
  9.   
  10. static int simple_major = 108;   
  11. static int simple_minor = 0;   
  12.   
  13. static __init int simple_init(void)   
  14. {   
  15.     dev_t dev;   
  16.     int err;   
  17. #if SIMPLE_DEBUG   
  18.     printk(KERN_INFO "In %s\n", __func__);   
  19. #endif   
  20.     dev = MKDEV(simple_major, simple_minor); //求取设备号  
  21.     if(dev > 0)//设备号有效  
  22.     {   
  23. #if SIMPLE_DEBUG   
  24.         printk(KERN_INFO "try to register static char dev  %d \n", dev);   
  25. #endif   
  26.         err = register_chrdev_region(dev,DEV_COUNT, SIMPLE_NAME); //静态分配设备号  
  27.         if(err < 0) //静态分配出错 尝试使用动态分配  
  28.         {   
  29.             printk(KERN_WARNING "register static char dev error\n");   
  30.             err = alloc_chrdev_region(&dev, 0, DEV_COUNT, SIMPLE_NAME); //动态分配设备号  
  31.             if(err < 0)   
  32.             {   
  33.                 printk(KERN_ERR "register char dev error in line %d\n",__LINE__);   
  34.                 goto error;   
  35.             }   
  36.             else  
  37.             {   
  38.                 simple_major = MAJOR(dev);//重新计算主设备号  
  39.                 simple_minor = MINOR(dev);//重新计算此设备号  
  40.             }   
  41.                
  42.         }   
  43.         else{   
  44.            
  45.         }   
  46.     }   
  47.     else //设备号无效使用动态分配  
  48.     {    
  49. #if SIMPLE_DEBUG   
  50.         printk(KERN_INFO "try to register alloc char dev  \n");   
  51. #endif   
  52.         err = alloc_chrdev_region(&dev, 0, DEV_COUNT, SIMPLE_NAME);   
  53.         if(err < 0)   
  54.         {   
  55.             printk(KERN_ERR "register char dev error in line %d\n\n",__LINE__);   
  56.             goto error;   
  57.         }   
  58.         else  
  59.         {   
  60.             simple_major = MAJOR(dev);   
  61.             simple_minor = MINOR(dev);   
  62.         }   
  63.   
  64.     }   
  65.   
  66. #if SIMPLE_DEBUG   
  67.     printk(KERN_INFO "register char dev success major = %d minor = %d \n", simple_major, simple_minor);   
  68. #endif   
  69.   
  70. error:   
  71.     return err;   
  72. }   
  73.   
  74. static __exit void simple_exit(void)   
  75. {   
  76.     dev_t dev;   
  77. #if SIMPLE_DEBUG   
  78.     printk(KERN_INFO "In %s\n", __func__);   
  79. #endif   
  80.     dev = MKDEV(simple_major, simple_minor);   
  81.     unregister_chrdev_region(dev, DEV_COUNT); //释放设备号  
  82. }   
  83.   
  84. module_init(simple_init);   
  85. module_exit(simple_exit);   
  86.   
  87. MODULE_LICENSE("GPL");   
  88. MODULE_AUTHOR("kai_zhang(jsha.zk@163.com)");   
  89. 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.   
  14.     err = cdev_add(simple_cdev, dev, DEV_COUNT); //向内核添加设备  
  15.     if(err < 0)   
  16.     {   
  17.         printk(KERN_ERR "add cdev err \n");   
  18.         goto error1;   
  19.     }   
  20.     else  
  21.     {   
  22. #if SIMPLE_DEBUG   
  23.         printk(KERN_INFO "add char dev OK!\n");   
  24. #endif   
  25.     }   

自动创建设备节点

  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.   
  14.     simple_dev = device_create(simple_class, NULL, dev, NULL, SIMPLE_NAME);   
  15.     if(simple_dev == NULL)   
  16.     {   
  17.         printk(KERN_ERR "create device error");   
  18.         goto error3;   
  19.     }   
  20.     else  
  21.     {   
  22. #if SIMPLE_DEBUG   
  23.         printk(KERN_INFO "create simple device  OK!\n");   
  24. #endif   
  25.     }  

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

  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‘来释放资源  

 

主要的文件操作方法实现

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

  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. };  

[cpp]  view plain copy
  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


 

结构体:

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

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

[cpp]  view plain copy
  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

  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.       
  17.     temp_dev = &char2_dev[minor];//获得真正操作的设备   
  18.     /* 进一步 初始化设备 因为是操作一个模拟的设备 故省去*/  
  19.     filp->private_data = temp_dev; //初始化 private_data   
  20.   
  21.     return 0;  
  22. }  

[cpp]  view plain copy
  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.       
  17.     temp_dev = &char2_dev[minor];//获得真正操作的设备  
  18.     /* 进一步 初始化设备 因为是操作一个模拟的设备 故省去*/  
  19.     filp->private_data = temp_dev; //初始化 private_data  
  20.   
  21.     return 0;  
  22. }  

release方法:

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

read方法:

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

  1. unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)  

[cpp]  view plain copy
  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的方法

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

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

write方法:

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

  1. long __must_check strncpy_from_user(char *dst, const char __user *src, long count)  

[cpp]  view plain copy
  1. long __must_check strncpy_from_user(char *dst, const char __user *src, long count)  
dst 驱动保存数据的地址
src 用户空间传入的数据
count 标识数据长度

  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.   
  10.     dev          = filp->private_data;  
  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  
  17.     {  
  18.         remain_space = MAX_SIMPLE_LEN - *loff;  
  19.         if(count > remain_space)  
  20.         {  
  21. #if SIMPLE_DEBUG   
  22.             printk(KERN_WARNING "the data is to long to write to the device\n");  
  23. #endif   
  24.             count = remain_space;  
  25.         }  
  26.         else  
  27.         {  
  28.               
  29.         }  
  30.     }  
  31.     err = copy_from_user((dev->data)+(*loff),userstr,count);//取得数据   
  32.     if(err != 0)  
  33.     {  
  34.         printk(KERN_ERR "an error occured when copy data from user\n");  
  35.         return err;  
  36.     }  
  37.     else  
  38.     {  
  39.       
  40. #if SIMPLE_DEBUG   
  41.         printk(KERN_INFO "data copy from user OK\n");  
  42. #endif   
  43.         *loff = *loff + count; //跳着偏移   
  44.         if(*loff > dev->count)  
  45.         {  
  46.             dev->count = *loff;  
  47.         }  
  48.         else  
  49.         {  
  50.           
  51.         }  
  52.         return count; //返回写入的数据量   
  53.     }  
  54. }  

[cpp]  view plain copy
  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.   
  10.     dev          = filp->private_data;  
  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  
  17.     {  
  18.         remain_space = MAX_SIMPLE_LEN - *loff;  
  19.         if(count > remain_space)  
  20.         {  
  21. #if SIMPLE_DEBUG  
  22.             printk(KERN_WARNING "the data is to long to write to the device\n");  
  23. #endif  
  24.             count = remain_space;  
  25.         }  
  26.         else  
  27.         {  
  28.               
  29.         }  
  30.     }  
  31.     err = copy_from_user((dev->data)+(*loff),userstr,count);//取得数据  
  32.     if(err != 0)  
  33.     {  
  34.         printk(KERN_ERR "an error occured when copy data from user\n");  
  35.         return err;  
  36.     }  
  37.     else  
  38.     {  
  39.       
  40. #if SIMPLE_DEBUG  
  41.         printk(KERN_INFO "data copy from user OK\n");  
  42. #endif  
  43.         *loff = *loff + count; //跳着偏移  
  44.         if(*loff > dev->count)  
  45.         {  
  46.             dev->count = *loff;  
  47.         }  
  48.         else  
  49.         {  
  50.           
  51.         }  
  52.         return count; //返回写入的数据量  
  53.     }  
  54. }  

lseek方法:

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

SEEK_SET从文件起始处开始偏移
SEEK_CUR从文件当前位置计算偏移
SEEK_END从文件末尾计算偏移
file结构的f_pos保存了文件的偏移量
在调整文件偏移后需要 更新file中得f_pos成员

  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.   
  9.     dev          = filp->private_data;  
  10.   
  11.     switch ( mode )  
  12.     {  
  13.         case SEEK_SET:  
  14.             if( loff < 0 )  
  15.             {  
  16.                 printk(KERN_ERR "can't move above file line %d \n", __LINE__);  
  17.                 return -1;  
  18.             }  
  19.             else if(loff > dev->count)  
  20.             {  
  21.                 printk(KERN_ERR "offset is too long line %d\n", __LINE__);  
  22.                 return -1;  
  23.             }  
  24.             else  
  25.             {  
  26.                 filp->f_pos = loff;  
  27.             }  
  28.             break;  
  29.         case SEEK_CUR:  
  30.             if((tmp_len = filp->f_pos+loff) < 0)  
  31.             {  
  32.                 printk(KERN_ERR "can't move above file line %d \n", __LINE__);  
  33.                 return -1;  
  34.             }  
  35.             else if(tmp_len > dev->count)  
  36.             {  
  37.                 printk(KERN_ERR "offset is too long line %d\n", __LINE__);  
  38.                 return -1;  
  39.             }  
  40.             else  
  41.             {  
  42.                 filp->f_pos = tmp_len;  
  43.             }  
  44.             break;  
  45.         case SEEK_END:  
  46.             if((tmp_len = dev->count+loff ) < 0)  
  47.             {  
  48.                 printk(KERN_ERR "can't move above file line %d \n", __LINE__);  
  49.                 return -1;  
  50.             }  
  51.             else if(tmp_len > dev->count)  
  52.             {  
  53.                 printk(KERN_ERR "offset is too long line %d\n", __LINE__);  
  54.                 return -1;  
  55.             }  
  56.             else  
  57.             {  
  58.                 filp->f_pos = tmp_len;  
  59.             }  
  60.             break;  
  61.         default :  
  62.             printk(KERN_INFO "illigal lseek mode! \n");  
  63.             return -1;  
  64.             break;  
  65.     }  
  66.     return filp->f_pos;  
  67. }  

 

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

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

上一节中提到在read write时候要检测用户空间传递的参数地址是否是有效地址,有的内核函数会自行检测,但是在调用轻量级的内核函数时候,就可能不去检测用户空间的地址是否有效,如果此时用户无传递一个无效地址,而内核函数去操作了它,这时棘手的问题出现了,轻则内核oops 关机重启就OK了,在特别严重的情况下,可能你的系统就崩溃了(我又遇到过),所以,我们在驱动程序中操作用户空间地址时候要小心加小心。如果电脑配置可以就在虚拟机中玩, 或者在开发板上试,当然这边的测试代码我都有试过,不至于让你系统崩溃的。
如何检测呢?
调用一个access_ok函数去检测
  1. #define access_ok(type,addr,size)  
[cpp]  view plain copy
  1. #define access_ok(type,addr,size)  
type 标识读写操作VERIFY_READ表示地址可读,VERIFY_WRITE表示地址可写
addr 用户传入的地址
size 读写的长度
此代码在有内存管理的芯片与无内存管理之间有区别

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

  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)  
[cpp]  view plain copy
  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函数

  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 plain copy
  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. }  
可以看到他在函数内部做了这种检测

而当我们调用

  1. __copy_to_user  
  2. __copy_from_user  
  3. get_user  
  4. __get_user  
  5. put_user  
  6. __put_user  
[cpp]  view plain copy
  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的时候 就不行了 因为你已经死机了 也就输不了这个命令 就看不到打印信息了)
  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  
[cpp]  view plain copy
  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可以发行时候,去掉这个定义。
在需要打印的地方我们就使用
  1. D(“print the log int func:%s line:%d”, __func__ ,__LINE__);  
[cpp]  view plain copy
  1. D(“print the log int func:%s line:%d”, __func__ ,__LINE__);  
当然要修改成有意义的debug信息

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

内核的container_of函数
在写字符设备驱动时候我们都会去自定义一个结构,其中包含了cdev结构
  1. struct simple_dev{  
  2.     char *data;  
  3.     loff_t count;  
  4.     struct cdev cdev;  
  5.     struct semaphore semp;  
  6. };  
[cpp]  view plain copy
  1. struct simple_dev{  
  2.     char *data;  
  3.     loff_t count;  
  4.     struct cdev cdev;  
  5.     struct semaphore semp;  
  6. };  
在open方法中除了上一节看到的那样用一个次设备好来获取结构(有时候会出错 如果我们次设备号不是从0开始分配)
所以需要寻求一种安全的方法
container_of为我们提供了这么一个很好的接口
  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)   
[cpp]  view plain copy
  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指向的成员在结构体中的名字
  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);  
[cpp]  view plain copy
  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 的实现基本原理是这样的:
知道了结构体中某个成员的地址,  又可以求的该成员在改结构体中得偏移,拿成员的地址减去这个偏移,就得到了整个结构的地址,太佩服写内核的人了
 
 
 

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

代码传至并发竞态控制

并发进程 导致竞态的一个例子

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

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

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

t1t2t3t4t5t6t7t8
RWWRWRRW
A12233D4
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>中

创建信号量

 
  1. void sema_init(struct semaphore *sem, int val)  
[cpp]  view plain copy
  1. void sema_init(struct semaphore *sem, int val)  
sem:   信号量结构体

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


  1. void init_MUTEX();  
  2. void init_MUTEX_LOCKED();  
[cpp]  view plain copy
  1. void init_MUTEX();  
  2. void init_MUTEX_LOCKED();  
两个定义互斥锁的函数 ( 最新的内核中已经没有这两个函数了,所以不要用

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

P操作
  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)  
[cpp]  view plain copy
  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操作

  1. void up(struct semaphore *sem)  
[cpp]  view plain copy
  1. void up(struct semaphore *sem)  
 
此操作 首先检测时候有其他进程等待此信号量,如果有则唤醒此进程,不改变信号量的值,如果没有则将信号量的值加1。
  1. static ssize_t simple_read(struct file *filp, char __user *userstr, size_t count, loff_t *loff)  
  2. {  
  3.       
  4.     struct simple_dev *dev = NULL;  
  5.     int data_remain = 0;  
  6.     int err;  
  7.     D("[%s:] In %s \n",current->comm, __func__);  
  8.       
  9.     dev         = filp->private_data;  
  10.       
  11.     err = down_interruptible(&dev->semp);  
  12.     if(err)  
  13.     {  
  14.         if(err == -EINTR)  
  15.             WAR("return by an interrupt signal\n");  
  16.         else  
  17.             printk(KERN_ERR "an error occured in down_interruptible\n");  
  18.         return err;  
  19.     }  
  20.       
  21.     else  
  22.     {  
  23.         D("have get the mutex %d\n", __LINE__);  
  24.     }  
  25.     /******************************************* 
  26.     *       临界区代码 
  27.     ********************************************/  
  28.       
  29.     up(&dev->semp);  
  30.     return count;  
  31. }  
[cpp]  view plain copy
  1. static ssize_t simple_read(struct file *filp, char __user *userstr, size_t count, loff_t *loff)  
  2. {  
  3.       
  4.     struct simple_dev *dev = NULL;  
  5.     int data_remain = 0;  
  6.     int err;  
  7.     D("[%s:] In %s \n",current->comm, __func__);  
  8.       
  9.     dev         = filp->private_data;  
  10.       
  11.     err = down_interruptible(&dev->semp);  
  12.     if(err)  
  13.     {  
  14.         if(err == -EINTR)  
  15.             WAR("return by an interrupt signal\n");  
  16.         else  
  17.             printk(KERN_ERR "an error occured in down_interruptible\n");  
  18.         return err;  
  19.     }  
  20.       
  21.     else  
  22.     {  
  23.         D("have get the mutex %d\n", __LINE__);  
  24.     }  
  25.     /******************************************* 
  26.     *       临界区代码 
  27.     ********************************************/  
  28.       
  29.     up(&dev->semp);  
  30.     return count;  
  31. }  

自旋锁

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

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

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

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

初始化

  1. spin_lock_init(_lock)  
[cpp]  view plain copy
  1. spin_lock_init(_lock)  

锁定

 
  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 plain copy
  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)  
第一个函数是 不去禁止中断 直接锁定

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

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

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

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

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

解锁

  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)  
[cpp]  view plain copy
  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值
在一定时间内不能获得锁,就放弃,并释放已占有的锁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了小程序应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值