Linux设备驱动程序学习之基础篇

来源:互联网 发布:范玮琪和张韶涵 知乎 编辑:程序博客网 时间:2024/05/22 13:06

文章转载自:http://blog.csdn.net/yangdelong/article/details/5497173


Linux设备驱动程序学习(0)-Hello, world!模块

文章来源:http://blog.chinaunix.net/u2/62910/showart_492083.html

一个学习Linux设备驱动程序都会碰到的第一个例程:

#include <linux/init.h>
#include <linux/module.h
MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
    printk(KERN_ALERT "Hello, Tekkaman Ninja !/n");
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_ALERT "Goodbye, Tekkaman Ninja !/n Love Linux !Love ARM ! Love KeKe !
/n"
);
}

module_init(hello_init);
module_exit(hello_exit);


我将其复制到我的工作目录,并编写了一个简单的Makefile文件:

KERNELDIR = /home/tekkaman/working/SBC2440/linux-2.6.22.2
    # The current directory is passed to sub-makes as argument
PWD := $(shell pwd)
INSTALLDIR = /home/tekkaman/working/rootfs/lib/modules

CROSS_COMPILE    =/home/tekkaman/working/crosstool-gcc410-k26222/gcc-4.1.0-glibc-2.3.2/arm-9tdmi-linux-gnu/bin/arm-9tdmi-linux-gnu-
CC    = $(CROSS_COMPILE)gcc

obj-:= hello.

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

modules_install:
    cp hello.ko $(INSTALLDIR)

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

.PHONY: modules modules_install clean


说实话,以上是我参考了《Linux设备驱动程序(第3版)》的Makefile源码修改得来的。我对Makefile不是很了解,是该好好学习学习了!

然后就是make  modules 、 make  modules_install 。

[root@Tekkaman-Ninja Helloworld]# make modules
make -C /home/tekkaman/working/SBC2440/linux-2.6.22.2 M=/home/tekkaman/working/Linuxdriver/Helloworld modules
make[1]: Entering directory `/home/tekkaman/working/SBC2440/linux-2.6.22.2'
  CC [M]  /home/tekkaman/working/Linuxdriver/Helloworld/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/tekkaman/working/Linuxdriver/Helloworld/hello.mod.o
  LD [M]  /home/tekkaman/working/Linuxdriver/Helloworld/hello.ko
make[1]: Leaving directory `/home/tekkaman/working/SBC2440/linux-2.6.22.2'
[root@Tekkaman-Ninja Helloworld]# make modules_install
cp hello.ko /home/tekkaman/working/rootfs/lib/modules
[root@Tekkaman-Ninja Helloworld]# 

在我的开发板上的操作:

[Tekkaman2440@SBC2440V4]#cd /lib/modules/

[Tekkaman2440@SBC2440V4]#ls

cs89x0.ko hello.ko p80211.ko prism2_usb.ko

[Tekkaman2440@SBC2440V4]#insmod hello.ko

Hello, Tekkaman Ninja !

[Tekkaman2440@SBC2440V4]#lsmod

Module Size Used by Not tainted

hello 1376 0

[Tekkaman2440@SBC2440V4]#rmmod hello

Goodbye, Tekkaman Ninja !

Love Linux !Love ARM ! Love KeKe !

[Tekkaman2440@SBC2440V4]#lsmod

Module Size Used by Not tainted

[Tekkaman2440@SBC2440V4]#



学习心得:
(1)驱动模块运行在内核空间,运行时不能依赖于任何函数库和模块连接,所以在写驱动时所调用的函数只能是作为内核一部分的函数。
(2)驱动模块和应用程序的一个重要不同是:应用程序退出时可不管资源释放或者其他的清除工作,但模块的退出函数必须仔细撤销初始化函数所作的一切,否则,在系统重新引导之前某些东西就会残留在系统中。

(3)处理器的多种工作模式(级别)其实就是为了操作系统的用户空间和内核空间设计的。在Unix类的操作系统中只用到了两个级别:最高和最低级别。
(4)要十分注意驱动程序的并发处理。
(5)内核API中具有双下划线(_ _)的函数,通常是接口的底层组件,应慎用。
(6)
内核代码不能实现浮点书运算。

(7)Makefile文件分析:
obj-:= hello.o  代表了我们要构造的模块名为hell.ko,make 会在该目录下自动找到hell.c文件进行编译。如果 hello.o是由其他的源文件生成(比如file1.c和file2.c)的,则在下面加上(注意红色字体的对应关系):
hello-objs := file1.o file2.o ......

    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
其中 
-C $(KERNELDIR指定了内核源代码的位置,其中保存有内核的顶层makefile文件。
    
M=$(PWD指定了模块源代码的位置
    
modules目标指向obj-m变量中设定的模块。

(8)insmod使用公共内核符号表来解析模块中未定义的符号。公共内核符号表中包含了所有的全局内核项(即函数和变量的地址),这是实现模块化驱动程序所必须的。
(9)Linux使用模块层叠技术,我们可以将模块划分为多个层,通过简化每个层可缩短开发周期。如果一个模块需要向其他模块到处符号,则使用下面的宏:

EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);

符号必须在模块文件的全局变量部分导出,因为这两个宏将被扩展为一个特殊变量的声明,而该变量必须是全局的。

(10)所有模块代码中都包含一下两个头文件:

#include <linux/init.h>
#include <linux/module.h>


(11)所有模块代码都应该指定所使用的许可证:

MODULE_LICENSE("Dual BSD/GPL");


此外还有可选的其他描述性定义:

MODULE_AUTHOR("");
MODULE_DESCRIPTION("");
MODULE_VERSION("");
MODULE_ALIAS("");
MODULE_DEVICE_TABLE("");


上述
MODULE_声明习惯上放在文件最后。

(12)初始化和关闭
初始化的实际定义通常如下:

static int _ _init initialization_function(void)
{
/*初始化代码*/
}

module_init(initialization_function)


清除函数的实际定义通常如下:

static int _ _exit cleanup_function(void)
{
/*清除代码*/
}

module_exit(cleanup_function)


(13) Linux内核模块的初始化出错处理一般使用“goto”语句。通常情况下很少使用“goto”,但在出错处理是(可能是唯一的情况),它却非常有用。在大二学习C语言时,老师就建议不要使用“goto”,并说很少会用到。在这里也是我碰到的第一个建议使用“goto”的地方。在追求效率的代码中使用goto语句仍是最好的错误恢复机制。”--《Linux设备驱动程序(第3版)》以下是初始化出错处理的推荐代码示例:

struct something *item1;
struct somethingelse *item2;
int stuff_ok;


void my_cleanup(void)
{
    if (item1)

        release_thing(item1);
    if (item2)
        release_thing2(item2);
    if (stuff_ok)
        unregister_stuff();
    return;
}
int __init my_init(void)
{
    int err = -ENOMEM;
    item1 = allocate_thing(arguments);
    item2 = allocate_thing2(arguments2);
    if (!item2 || !item2)
        goto fail;
    err = register_stuff(item1, item2);
    if (!err)
        stuff_ok = 1;
    else
        goto fail;
    return 0; /* success */

 fail:
        my_cleanup( );
        return err;
}


(14)模块参数:内核允许对驱动程序指定参数,而这些参数可在装载驱动程序模块时改变
以下是我的实验程序:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>

MODULE_LICENSE("Dual BSD/GPL");

static char *whom = "Tekkaman Ninja";
static int howmany = 1;
static int TNparam[] = {1,2,3,4};
static int TNparam_nr = 4;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);
module_param_array(TNparam , int , &TNparam_nr , S_IRUGO);

static int hello_init(void)
{
    int i;
    for (= 0; i < howmany; i++)
        printk(KERN_ALERT "(%d) Hello, %s !/n", i, whom);
    for (= 0; i < 8; i++)
        printk(KERN_ALERT "TNparam[%d] : %d /n", i, TNparam[i]);
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_ALERT "Goodbye, Tekkaman Ninja !/n Love Linux !Love ARM ! Love KeKe !/n");
}

module_init(hello_init);
module_exit(hello_exit);


实验结果是 :

[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#ls
cs89x0.ko hello.ko prism2_usb.ko
hello-param.ko p80211.ko
[Tekkaman2440@SBC2440V4]#insmod hello-param.ko howmany=2 whom="KeKe" TNparam=4,3,2,1
(0) Hello, KeKe !
(1) Hello, KeKe !
TNparam[0] : 4
TNparam[1] : 3
TNparam[2] : 2
TNparam[3] : 1
TNparam[4] : 1836543848
TNparam[5] : 7958113
TNparam[6] : 1836017783
TNparam[7] : 0
[Tekkaman2440@SBC2440V4]#insmod hello-param.ko howmany=2 whom="KeKe"  TNparam=4,3,2,1,5,6,7,8
TNparam: can only take 4 arguments
hello_param: `4' invalid for parameter `TNparam'
insmod: cannot insert 'hello-param.ko': Invalid parameters (-1): Invalid argument
[Tekkaman2440@SBC2440V4]# 


我这个实验除了对参数的改变进行实验外,我的一个重要的目的是测试“ module_param_array(TNparam , int ,&TNparam_nr , S_IRUGO);”中&TNparam_nr对输入参数数目的限制作用。经过我的实验,表明&TNparam_nr并没有对输入参数的数目起到限制作用。真正起到限制作用的是static int TNparam[] = {1,2,3,4};本身定义的大小,我将程序进行修改:
static int TNparam[] = {1,2,3,4};  
改为 
static int TNparam[] = {1,2,3,4,5,6,7,8};
其他都不变。

编译后再进行实验,其结果是:

[Tekkaman2440@SBC2440V4]#insmod hello-param.ko howmany=2 whom="KeKe"TNparam=4,3,2,1,5,6,7,8
(0) Hello, KeKe !
(1) Hello, KeKe !
TNparam[0] : 4
TNparam[1] : 3
TNparam[2] : 2
TNparam[3] : 1
TNparam[4] : 5
TNparam[5] : 6
TNparam[6] : 7
TNparam[7] : 8
[Tekkaman2440@SBC2440V4]#


(15)“#include <linux/sched.h>”  最重要的头文件之一。包含驱动程序使用的大部分内核API的定义,包括睡眠函数以及各种变量声明。

(16)
#include <linux/version.h>” 包含所构造内核版本信息的头文件。

在学习过程中找到了几篇很好的参考文档:
(1)
第一章 模块(Modules) URL:http://greenlinux.blogcn.com/diary,103232026.shtml
(2)《
从 2.4 到 2.6:Linux 内核可装载模块机制的改变对设备驱动的影响》
URL:http://www.ibm.com/developerworks/cn/linux/l-module26/
(3)《Linux2.6内核驱动移植参考》 
URL:http://blog.chinaunix.net/u1/40912/showart_377391.html

以上就是我对《Linux设备驱动程序(第3版)》的《第二章 构造和运行模块》 的学习总结。
Linux设备驱动程序学习(1)-字符设备驱动程序
http://blog.chinaunix.net/u2/62910/showart_492084.html
今天进入《Linux设备驱动程序(第3版)》第三章字符设备驱动程序的学习。
这一章主要通过介绍字符设备scull(Simple Character Utility for Loading Localities,区域装载的简单字符工具)的
驱动程序编写,来学习Linux设备驱动的基本知识。scull可以为真正的设备驱动程序提供样板。


一、主设备号和此设备号
主设备号表示设备对应的驱动程序;次设备号由内核使用,用于正确确定设备文件所指的设备。
内核用dev_t类型(<linux/types.h>)来保存设备编号,dev_t是一个32位的数,12位表示主设备号,20为表示次设备号。
在实际使用中,是通过<linux/kdev_t.h>中定义的宏来转换格式。
 (dev_t)-->主设备号、次设备号 MAJOR(dev_t dev)
 MINOR(dev_t dev) 主设备号、次设备号-->(dev_t) MKDEV(int major,int minor) 
建立一个字符设备之前,驱动程序首先要做的事情就是获得设备编号。其这主要函数在<linux/fs.h>中声明:

int register_chrdev_region(dev_t first, unsigned int count,
char *name);   //指定设备编号

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
unsigned int count, char *name);   //动态生成设备编号

void unregister_chrdev_region(dev_t first, unsigned int count)
;      //释放设备编号


分配之设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。

以下是在scull.c中用来获取主设备好的代码:

if (scull_major) {
    dev = MKDEV(scull_major, scull_minor);
    result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
    result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,"scull");
    scull_major = MAJOR(dev);
}
if (result < 0) {
    printk(KERN_WARNING "scull: can't get major %d/n", scull_major);
    return result;
}


在这部分中,比较重要的是在用函数获取设备编号后,其中的参数name是和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中。

看到这里,就可以理解为什么mdev和udev可以动态、自动地生成当前系统需要的设备文件。
udev就是通过读取
sysfs下的信息来识别硬件设备的.
(请看《
理解和认识udev
URL:http://blog.chinaunix.net/u/6541/showart_396425.html)

 


 

二、一些重要的数据结构
大部分基本的驱动程序操作涉及及到三个重要的内核数据结构,分别是file_operations、file和inode,它们的定义都在
<linux/fs.h>

 


 

三、字符设备的注册

内核内部使用struct cdev结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或多个struct cdev。代码应包含<linux/cdev.h>,它定义了struct cdev以及与其相关的一些辅助函数。

注册一个独立的cdev设备的基本过程如下:

1、为struct cdev 分配空间(如果已经将struct cdev 嵌入到自己的设备的特定结构体中,并分配了空间,这步略过!)

struct cdev *my_cdev = cdev_alloc();

2、初始化struct cdev

void cdev_init(struct cdev *cdev, const struct file_operations *fops)

3、初始化cdev.owner

cdev.owner = THIS_MODULE;

4、cdev设置完成,通知内核struct cdev的信息(在执行这步之前必须确定你对struct cdev的以上设置已经完成!

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

从系统中移除一个字符设备:void cdev_del(struct cdev *p)

以下是scull中的初始化代码(之前已经为struct scull_dev 分配了空间):

/*
 * Set up the char_dev structure for this device.
 */

static void scull_setup_cdev(struct scull_dev *dev, int index)
{
    int err, devno = MKDEV(scull_major, scull_minor + index);
    
    cdev_init(&dev->cdev, &scull_fops);
    dev->cdev.owner = THIS_MODULE;
    dev->cdev.ops = &scull_fops
 //这句可以省略,在cdev_init中已经做过
    err = cdev_add (&dev->cdev, devno, 1);
    /* Fail gracefully if need be 这步值得注意*/
    if (err)
        printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}

 


四、scull模型的内存使用

 

 

以下是scull模型的结构体:

/*
 * Representation of scull quantum sets.
 */

struct scull_qset {
    void **data;
    struct scull_qset *next;
};

struct scull_dev {
    struct scull_qset *data; /* Pointer to first quantum set */
    int quantum; /* the current quantum size */
    int qset; /* the current array size */
    unsigned long size; /* amount of data stored here */
    unsigned int access_key; /* used by sculluid and scullpriv */
    struct semaphore sem; /* mutual exclusion semaphore */
    struct cdev cdev;     /* Char device structure        */
};

scull驱动程序引入了两个Linux内核中用于内存管理的核心函数,它们的定义都在<linux/slab.h>:

void *kmalloc(size_t size, int flags);
void kfree(void *ptr);

以下是scull模块中的一个释放整个数据区的函数(类似清零),将在scull以写方式打开和scull_cleanup_module中被调用:

int scull_trim(struct scull_dev *dev)
{
  struct scull_qset *next, *dptr;
     int qset = dev->qset; 
/* 量子集中量子的个数*/
     int i;
     for (dptr = dev->data; dptr; dptr = next) { /* 循环scull_set个数次,直到dptr为NULL为止。*/
         if (dptr->data) {
               for (= 0; i < qset; i++
)/* 循环一个量子集中量子的个数次*/
                    kfree(dptr->data[i]);/* 释放其中一个量子的空间*/

               kfree(dptr->data);/* 释放当前的scull_set的量子集的空间*/
               dptr->data = NULL;/* 释放一个scull_set中的void **data指针*/
          }
      next = dptr->next
/* 准备下个scull_set的指针*/
      kfree(dptr);/* 释放当前的scull_set*/
      }
  dev->size = 0
/* 当前的scull_device所存的数据为0字节*/
  dev->quantum = scull_quantum;/* 初始化一个量子的大小*/
  dev->qset = scull_qset;/* 初始化一个量子集中量子的个数*/
  dev->data = NULL;/* 释放当前的scull_device的struct scull_qset *data指针*/
  return 0;
}

以下是scull模块中的一个沿链表前行得到正确scull_set指针的函数,将在read和write方法中被调用:

/*Follow the list*/
struct scull_qset *scull_follow(struct scull_dev *dev, int n)
{
    struct scull_qset *qs = dev->data;
        /* Allocate first qset explicitly if need be */
    if (! qs) {
        qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
        if (qs == NULL)
            return NULL; /* Never mind */
        memset(qs, 0, sizeof(struct scull_qset));
    }
    /* Then follow the list */
    while (n--) {
        if (!qs->next) {
            qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
            if (qs->next == NULL)
                return NULL; /* Never mind */
            memset(qs->next, 0, sizeof(struct scull_qset));
        }
        qs = qs->next;
        continue;
    }
    return qs;
}

其实这个函数的实质是:如果已经存在这个scull_set,就返回这个scull_set的指针。如果不存在这个scull_set,一边沿链表为scull_set分配空间一边沿链表前行,直到所需要的scull_set被分配到空间并初始化为止,就返回这个scull_set的指针

 


、open和release

 

open方法提供给驱动程序以初始化的能力,为以后的操作作准备。应完成的工作如下:

(1)检查设备特定的错误(如设备未就绪或硬件问题);

(2)如果设备是首次打开,则对其进行初始化;

(3)如有必要,更新f_op指针;

(4)分配并填写置于filp->private_data里的数据结构。

而根据scull的实际情况,他的open函数只要完成第四步(将初始化过的struct scull_dev dev的指针传递到filp->private_data里,以备后用)就好了,所以open函数很简单。但是其中用到了定义在<linux/kernel.h>中的container_of宏,源码如下:

#define container_of(ptr, type, member) ({            /
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    /
    (type *)( (char *)__mptr - offsetof(type,member) );})

其实从源码可以看出,其作用就是:通过指针ptr,获得包含ptr所指向数据(是member结构体)type结构体的指针。即是用指针得到另外一个指针。

release方法提供释放内存,关闭设备的功能。应完成的工作如下:

(1)释放由open分配的、保存在file->private_data中的所有内容;

(2)在最后一次关闭操作时关闭设备。

由于前面定义了scull是一个全局且持久的内存区,所以他的release什么都不做。

 


、read和write

 

 read和write方法的主要作用就是实现内核与用户空间之间的数据拷贝。因为Linux的内核空间和用户空间隔离的,所以要实现数据拷贝就必须使用在<asm/uaccess.h>中定义的

unsigned long copy_to_user(void __user *to,
                           const void *from,
                           unsigned long count);
unsigned long copy_from_user(void *to,
                             const void __user *from,
                             unsigned long count);

而值得一提的是以上两个函数和

#define __copy_from_user(to,from,n)    (memcpy(to, (void __force *)from, n), 0)
#define __copy_to_user(to,from,n)    (memcpy((void __force *)to, from, n), 0)

之间的关系:通过源码可知,前者调用后者,但前者在调用前对用户空间指针进行了检查。

至于read和write 的具体函数比较简单,就在实验中验证好了。

 

 


 

七、模块实验


这次模块实验的使用是友善之臂SBC2440V4,使用Linux2.6.22.2内核。

模块程序链接:scull模块源程序
模块测试程序链接模块测试程序

测试结果:

量子大小为6:

[Tekkaman2440@SBC2440V4]#cd /lib/modules/ [Tekkaman2440@SBC2440V4]#insmod scull.ko scull_quantum=6

[Tekkaman2440@SBC2440V4]#cat /proc/devices
Character devices:
  1 mem
  2 pty
  3 ttyp
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 14 sound
 81 video4linux
 89 i2c
 90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
204 s3c2410_serial
252 scull
253 usb_endpoint
254 rtc

Block devices:
  1 ramdisk
256 rfd
  7 loop
 31 mtdblock
 93 nftl
 96 inftl
179 mmc
[Tekkaman2440@SBC2440V4]#mknod -m 666 scull0 c  252 0
[Tekkaman2440@SBC2440V4]#mknod -m 666 scull1 c  252 1
[Tekkaman2440@SBC2440V4]#mknod -m 666 scull2 c  252 2
[Tekkaman2440@SBC2440V4]#mknod -m 666 scull3 c  252 3


启动测试程序

[Tekkaman2440@SBC2440V4]#./scull_test

write error! code=6

write error! code=6

write error! code=6

write ok! code=2

read error! code=6

read error! code=6

read error! code=6

read ok! code=2

[0]=0 [1]=1 [2]=2 [3]=3 [4]=4

[5]=5 [6]=6 [7]=7 [8]=8 [9]=9

[10]=10 [11]=11 [12]=12 [13]=13 [14]=14

[15]=15 [16]=16 [17]=17 [18]=18 [19]=19


改变量子大小为默认值4000:
[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#rmmod scull
[Tekkaman2440@SBC2440V4]#insmod scull.ko


启动测试程序
[Tekkaman2440@SBC2440V4]#./scull_test
write ok! code=20
read ok! code=20
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4
[5]=5 [6]=6 [7]=7 [8]=8 [9]=9
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
[15]=15 [16]=16 [17]=17 [18]=18 [19]=19

[Tekkaman2440@SBC2440V4]#    

改变量子大小为6,量子集大小为2:
[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#rmmod scull
[Tekkaman2440@SBC2440V4]#insmod scull.ko scull_quantum=6 scull_qset=2

启动测试程序
[Tekkaman2440@SBC2440V4]#./scull_test
write error! code=6
write error! code=6
write error! code=6
write ok! code=2
read error! code=6
read error! code=6
read error! code=6
read ok! code=2
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4
[5]=5 [6]=6 [7]=7 [8]=8 [9]=9
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
[15]=15 [16]=16 [17]=17 [18]=18 [19]=19
               

 

实验不仅测试了模块的读写能力,还测试了量子读写是否有效。
Linux设备驱动程序学习(2)-调试技术
http://blog.chinaunix.net/u2/62910/showart_492085.html
 
今天进入《Linux设备驱动程序(第3版)》第四章调试技术的学习。 

一、内核中的调试支持
在前面已经建议过:学习编写驱动程序要构建安装自己的内核(标准主线内核)。最重要的原因之一是:内核开发者已经建立了多项用于调试的功能。但是由于这些功能会造成额外的输出,并导致能下降,因此发行版厂商通常会禁止发行版内核中的调试功能。
为了实现内核调试,我在内核配置上增加了几项:
  Kernel hacking  --->     
        [*] Magic SysRq key
        [*] Kernel debugging
        [*]   Debug slab memory allocations  
        [*]   Spinlock and rw-lock debugging: basic checks 
        [*]   Spinlock debugging: sleep-inside-spinlock checking
        [*]   Compile the kernel with debug info  
        [*] Magic SysRq key 
Device Drivers  --->  
        Generic Driver Options  --->
          [*] Driver Core verbose debug messages 
General setup  --->
       [*] Configure standard kernel features (for small systems)  --->
          [*]   Load all symbols for debugging/ksymoops
书上介绍的还有其他配置,有的我不需要,或是s3c2440不支持,菜单里看不见。

二、通过打印调试
(1)printk
 首先,printk有8个loglevel,定义在<linux/kernel.h>中:

#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 condition */
#define    KERN_INFO    "<6>"    /* informational            */
#define    KERN_DEBUG    "<7>"    /* debug-level messages   */


未指定优先级的默认级别定义在/kernel/printk.c中:

#define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING */


当优先级的值小于console_loglevel这个整数变量的值,信息才能显示出来。而console_loglevel的初始值DEFAULT_CONSOLE_LOGLEVEL也定义在/kernel/printk.c中:

#define DEFAULT_CONSOLE_LOGLEVEL 7 /* anything MORE serious than KERN_DEBUG */



而在运行是改变console_loglevel的程序(《Linux设备驱动程序(第3版)》提供)如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define __LIBRARY__ /* _syscall3 and friends are only available through this */
#include <linux/unistd.h>
/* define the system call, to override the library function */
_syscall3(int, syslog, int, type, char *, bufp, int, len);

int main(int argc, char **argv)
{
    int level;
    if (argc==2) {
    level = atoi(argv[1]); /* the chosen console */
    } else {
        fprintf(stderr, "%s: need a single arg/n",argv[0]); exit(1);
    }
    if (syslog(8,NULL,level) < 0) { 
        fprintf(stderr,"%s: syslog(setlevel): %s/n",
                argv[0],strerror(errno));
        exit(1);
    }
    exit(0);
}


最关键的syslog(8,NULL,level)语句我不理解,没有找到相关资料。但是通过在ARM9板上的实验表明:程序是ok的!我用Hello world模块做了实验,现象和书上的一致。

[Tekkaman2440@SBC2440V4]#cd /tmp/
[Tekkaman2440@SBC2440V4]#./setlevel 1
[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#insmod hello.ko
[Tekkaman2440@SBC2440V4]#rmmod hello
[Tekkaman2440@SBC2440V4]#cd /tmp/
[Tekkaman2440@SBC2440V4]#./setlevel 7
[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#insmod hello.ko
Hello, Tekkaman Ninja !
[Tekkaman2440@SBC2440V4]#rmmod hello
Goodbye, Tekkaman Ninja !
 Love Linux !Love ARM ! Love KeKe !
[Tekkaman2440@SBC2440V4]#


还有通过对/proc/sys/kernel/printk的访问来改变
console_loglevel的值:

[Tekkaman2440@SBC2440V4]#echo 1 > /proc/sys/kernel/printk

[Tekkaman2440@SBC2440V4]#cat /proc/sys/kernel/printk
1       4       1       7

[Tekkaman2440@SBC2440V4]#insmod hello.ko
[Tekkaman2440@SBC2440V4]#rmmod hello
[Tekkaman2440@SBC2440V4]#echo 7 > /proc/sys/kernel/printk
[Tekkaman2440@SBC2440V4]#cat /proc/sys/kernel/printk
      4       1       7

[Tekkaman2440@SBC2440V4]#insmod hello.ko
Hello, Tekkaman Ninja !
[Tekkaman2440@SBC2440V4]#rmmod hello
Goodbye, Tekkaman Ninja !
 Love Linux !Love ARM ! Love KeKe !


四个数字的含义:当前的loglevel、默认loglevel、最小允许的loglevel、引导时的默认loglevel。



 为了方便的打开和关闭调试信息,《Linux设备驱动程序(第3版)》提供以下源码:

/* Macros to help debugging */
#undef PDEBUG /* undef it, just in case */
#ifdef SCULL_DEBUG
# ifdef __KERNEL__
     /* This one if debugging is on, and kernel space */
# define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt, ## args)
# else     /* This one for user space */
# define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)
# endif
#else
# define PDEBUG(fmt, args...) /* not debugging: nothing */
#endif

#undef PDEBUGG
#define PDEBUGG(fmt, args...) /* nothing: it's a placeholder */

Makefile中要添加的语句:

# Comment/uncomment the following line to disable/enable debugging
DEBUG = y


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

CFLAGS += $(DEBFLAGS)

 


 

为了避免printk重复输出过快而阻塞系统,内核使用以下函数跳过部分输出:

int printk_ratelimit(void);

典型的应用如下:

if (printk_ratelimit( ))
    printk(KERN_NOTICE "The printer is still on fire/n");

可以通过修改/proc/sys/kernel/printk_ratelimit(重开信息前应等待的秒数)和/proc/sys/kernel/printk_ratelimit_burst(在速度限制前可接受的信息数)来定制printk_ratelimit的行为。


 

 Linux还提供了打印设备编号的宏(在<linux/kdev_t.h>中定义):

int print_dev_t(char *buffer, dev_t dev);
char *format_dev_t(char *buffer, dev_t dev);

两个函数的唯一区别是:print_dev_t返回打印字符数,format_dev_t返回缓冲区指针。注意缓冲区char *buffer的大小应至少有20B。

 


 

三、通过查询调试
多数情况中,获取相关信息的最好方法是在需要的时候才去查询系统信息,而不是持续不断地产生数据。
使用/proc文件系统
/proc文件系统是一种特殊的、由软件创建的文件系统,内核使用他向外界导出信息。/proc下面的每个文件都绑定于一个内核函数,用户读取其中的文件时,该函数动态的生成文件的内容。如以前用过的:

[Tekkaman2440@SBC2440V4]#cat /proc/devices
Character devices:
  1 mem
  2 pty
  3 ttyp
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 14 sound
 81 video4linux
 89 i2c
 90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
204 s3c2410_serial
252 scull
253 usb_endpoint
254 rtc

Block devices:
  1 ramdisk
256 rfd
  7 loop
 31 mtdblock
 93 nftl
 96 inftl
179 mmc


使用/proc的模块必须包含<linux/proc_fs.h>,而使用seq_file接口要包含<linux/seq_file.h>
具体的应用方法看源程序、做实验更有效果。


至于其他的调试方法,如gdb、LTT、SysRq等方法,在其他的书籍,如:《嵌入式Linux系统开发技术详解-基于ARM》、《构建嵌入式Linux系统》等,上讲解的更为详细,以后专门花时间研究。

四、源码实验
模块程序链接:模块程序
模块测试程序链接模块测试程序

实验现象:

[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#insmod scull_debug.ko scull_nr_devs=1 scull_quantum=6 scull_qset=2
[Tekkaman2440@SBC2440V4]#cd /tmp/
[Tekkaman2440@SBC2440V4]#./scull_test
write code=6
write code=6
write code=6
write code=2
read code=6
read code=6
read code=6
read code=2
[0]=[1]=[2]=[3]=[4]=4
[5]=[6]=[7]=[8]=[9]=9
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
[15]=15 [16]=16 [17]=17 [18]=18 [19]=19

[Tekkaman2440@SBC2440V4]#cd /proc/
[Tekkaman2440@SBC2440V4]#ls
1              751            cmdline        kallsyms       stat
2              769            cpu            kmsg           swaps
3              77             cpuinfo        loadavg        sys
4              778            crypto         locks          sysrq-trigger
5              779            devices        meminfo        sysvipc
59             78             diskstats      misc           timer_list
6              781            driver         modules        tty
60             783            execdomains    mounts         uptime
63             785            filesystems    mtd            version
65             79             fs             net            vmstat
707            80             ide            partitions     yaffs
708            819            interrupts     scullmem       zoneinfo
709            asound         iomem          scullseq
710            buddyinfo      ioports        self
742            bus            irq            slabinfo


[Tekkaman2440@SBC2440V4]#cat scullmem

Device 0: qset 2, q 6, sz 20
  item at c071ebd4, qset at c071ef7c
  item at c071ef14, qset at c071eee0
       0: c071eeac
       1: c071ee78
[Tekkaman2440@SBC2440V4]#cat scullseq

Device 0: qset 2, q 6, sz 20
  item at c071ebd4, qset at c071ef7c
  item at c071ef14, qset at c071eee0
       0: c071eeac
       1: c071ee78
[Tekkaman2440@SBC2440V4]#rmmod scull_debug
[Tekkaman2440@SBC2440V4]#ls
1              742            buddyinfo      iomem          self
2              751            bus            ioports        slabinfo
3              769            cmdline        irq            stat
4              77             cpu            kallsyms       swaps
5              778            cpuinfo        kmsg           sys
59             779            crypto         loadavg        sysrq-trigger
6              78             devices        locks          sysvipc
60             781            diskstats      meminfo        timer_list
63             783            driver         misc           tty
65             785            execdomains    modules        uptime
707            79             filesystems    mounts         version
708            80             fs             mtd            vmstat
709            824            ide            net            yaffs
710            asound         interrupts     partitions     zoneinfo

Linux设备驱动程序学习(3)-并发和竞态
http://blog.chinaunix.net/u2/62910/showart_492086.html

今天进入《Linux设备驱动程序(第3版)》第五章并发和竞态的学习。

对并发的管理是操作系统编程中核心的问题之一。 并发产生竞态,竞态导致共享数据的非法访问。因为竞态是一种极端低可能性的事件,因此程序员往往会忽视竞态。但是在计算机世界中,百万分之一的事件可能没几秒就会发生,而其结果是灾难性的。


一、并发及其管理
竞态通常是作为对资源的共享访问结果而产生的。
在设计自己的驱动程序时,第一个要记住的规则是:只要可能,就应该避免资源的共享。若没有并发访问,就不会有竞态。这种思想的最明显的应用是避免使用全局变量。
但是,资源的共享是不可避免的 ,如硬件资源本质上就是共享、指针传递等等。
资源共享的硬性规则:
(1)在单个执行线程之外共享硬件或软件资源的任何时候,因为另外一个线程可能产生对该资源的不一致观察,因此必须显示地管理对该资源的访问。--访问管理的常见技术成为“锁定”或者“互斥”:确保一次只有一个执行线程可操作共享资源。
(2)当内核代码创建了一个可能和其他内核部分共享的对象时,该对象必须在还有其他组件引用自己时保持存在(并正确工作)。对象尚不能正确工作时,不能将其对内核可用。

二、信号量和互斥体
一个信号量(semaphore: 旗语,信号灯)本质上是一个整数值,它和一对函数联合使用,这一对函数通常称为P和V。希望进入临届区的进程将在相关信号量上调用P;如果信号量的值大于零,则该值会减小一,而进程可以继续。相反,如果信号量的值为零(或更小),进程必须等待知道其他人释放该信号。对信号量的解锁通过调用V完成;该函数增加信号量的值,并在必要时唤醒等待的进程。
当信号量用于互斥时(即避免多个进程同是在一个临界区运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有事也称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称。Linux内核中几乎所有的信号量均用于互斥
使用信号量,内核代码必须包含<asm/semaphore.h> 。
 
以下是信号量初始化的方法:

/*初始化函数*/
void sema_init(struct semaphore *sem, int val);

 
由于信号量通常被用于互斥模式。所以以下是内核提供的一组辅助函数和宏:

/*方法一、声明+初始化宏*/
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);

/*方法二、初始化函数*/
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);

/*带有“_LOCKED”的是将信号量初始化为0,即锁定,允许任何线程访问时必须先解锁。没带的为1。*/

 
P函数为:

void down(struct semaphore *sem)/*不推荐使用,会建立不可杀进程*/
int down_interruptible(struct semaphore *sem);/*推荐使用,使用down_interruptible需要格外小心,若操作被中断,该函数会返回非零值,而调用这不会拥有该信号量。对down_interruptible的正确使用需要始终检查返回值,并做出相应的响应。*/
int down_trylock(struct semaphore *sem);/*带有“_trylock”的永不休眠,若信号量在调用是不可获得,会返回非零值。*/

 
V函数为:

void up(struct semaphore *sem);/*任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出错时,要特别小心;若在拥有一个信号量时发生错误,必须在将错误状态返回前释放信号量。*/

 
在scull中使用信号量
 
其实在之前的实验中已经用到了信号量的代码,在这里提一下应该注意的地方:
在初始化scull_dev的地方:

/* Initialize each device. */
    for (= 0; i < scull_nr_devs; i++) {
        scull_devices[i].quantum = scull_quantum;
        scull_devices[i].qset = scull_qset;
        init_MUTEX(&scull_devices[i].sem);/* 注意顺序:先初始化好互斥信号量 ,再使scull_devices可用。*/
        scull_setup_cdev(&scull_devices[i], i);
    }

 
而且要确保在不拥有信号量的时候不会访问scull_dev结构体。
 
 读取者/写入者信号量
只读任务可并行完成它们的工作,而不需要等待其他读取者退出临界区。Linux内核提供了读取者/写入者信号量“rwsem”,使用是必须包括<linux/rwsem.h> 。
初始化:

void init_rwsem(struct rw_semaphore *sem);

 
只读接口:

void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);

 
写入接口:

void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);


void downgrade_write(struct rw_semaphore *sem);/*该函数用于把写者降级为读者,这有时是必要的。因为写者是排他性的,因此在写者保持读写信号量期间,任何读者或写者都将无法访问该读写信号量保护的共享资源,对于那些当前条件下不需要写访问的写者,降级为读者将,使得等待访问的读者能够立刻访问,从而增加了并发性,提高了效率。*/

 

一个 rwsem 允许一个写者或无限多个读者来拥有该信号量. 写者有优先权; 当某个写者试图进入临界区, 就不会允许读者进入直到写者完成了它的工作. 如果有大量的写者竞争该信号量,则这个实现可能导致读者“饿死”,即可能会长期拒绝读者访问。因此, rwsem 最好用在很少请求写的时候, 并且写者只占用短时间.

completion
completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。代码必须包含<linux/completion.h>。使用的代码如下: 
 

DECLARE_COMPLETION(my_completion);/* 创建completion(声明+初始化) */

/////////////////////////////////////////////////////////

struct completion my_completion;/* 动态声明completion 结构体*/
static inline void init_completion(&my_completion);/*动态初始化completion*/

///////////////////////////////////////////////////////

void wait_for_completion(struct completion *c);/* 等待completion */
void complete(struct completion *c);/*唤醒一个等待completion的线程*/
void complete_all(struct completion *c);/*唤醒所有等待completion的线程*/

/*如果未使用completion_all,completion可重复使用;否则必须使用以下函数重新初始化completion*/
INIT_COMPLETION(struct completion c);/*快速重新初始化completion*/


 completion的典型应用是模块退出时的内核线程终止。在这种远行中,某些驱动程序的内部工作有一个内核线程在while(1)循环中完成。当内核准备清楚该模块时,exit函数会告诉该线程退出并等待completion。为此内核包含了用于这种线程的一个特殊函数:

void complete_and_exit(struct completion *c, long retval);



三、自旋锁
其实上面介绍的几种信号量和互斥机制,其底层源码都是使用自旋锁,可以理解为自旋锁的再包装。所以从这里就可以理解为什么自旋锁通常可以提供比信号量更高的性能。
自旋锁是一个互斥设备,他只能会两个值:“锁定”和“解锁”。它通常实现为某个整数之中的单个位。
“测试并设置”的操作必须以原子方式完成。
任何时候,只要内核代码拥有自旋锁,在相关CPU上的抢占就会被禁止。
适用于自旋锁的核心规则:
(1)任何拥有自旋锁的代码都必须使原子的,除服务中断外(某些情况下也不能放弃CPU,如中断服务也要获得自旋锁。为了避免这种锁陷阱,需要在拥有自旋锁时禁止中断),不能放弃CPU(如休眠,休眠可发生在许多无法预期的地方)。否则CPU将有可能永远自旋下去(死机)。
(2)拥有自旋锁的时间越短越好。
自旋锁原语所需包含的文件是<linux/spinlock.h> ,以下是自旋锁的内核API: 

spinlock_t my_lock = SPIN_LOCK_UNLOCKED;/* 编译时初始化spinlock*/
void spin_lock_init(spinlock_t *lock);/* 运行时初始化spinlock*/

/* 所有spinlock等待本质上是不可中断的,一旦调用spin_lock,在获得锁之前一直处于自旋状态*/
void spin_lock(spinlock_t *lock);/* 获得spinlock*/
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);/* 获得spinlock,禁止本地cpu中断,保存中断标志于flags*/
void spin_lock_irq(spinlock_t *lock);/* 获得spinlock,禁止本地cpu中断*/
void spin_lock_bh(spinlock_t *lock)/* 获得spinlock,禁止软件中断,保持硬件中断打开*/

/* 以下是对应的锁释放函数*/
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);

/* 以下非阻塞自旋锁函数,成功获得,返回非零值;否则返回零*/
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);


/*新内核的<linux/spinlock.h>包含了更多函数*/

 
读取者/写入者自旋锁:

rwlock_t my_rwlock = RW_LOCK_UNLOCKED;/* 编译时初始化*/


rwlock_t my_rwlock;
rwlock_init(&my_rwlock); /* 运行时初始化*/


void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);


void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);

/* 新内核已经有了read_trylock*/

void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);


void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);

/*新内核的<linux/spinlock.h>包含了更多函数*/

 



锁陷阱
锁定模式必须在一开始就安排好,否则其后的改进将会非常困难。
不明确规则:如果某个获得锁的函数要调用其他同样试图获取这个锁的函数,代码就会锁死。(不允许锁的拥有者第二次获得同个锁。)为了锁的正确工作,不得不编写一些函数,这些函数假定调用这已经获得了相关的锁。
锁的顺序规则:再必须获取多个锁时,应始终以相同顺序获取。
若必须获得一个局部锁和一个属于内核更中心位置的锁,应先获得局部锁。
若我们拥有信号量和自旋锁的组合,必须先获得信号量。
不得再拥有自旋锁时调用down。(可导致休眠)
尽量避免需要多个锁的情况。
细颗粒度和粗颗粒度的对比:应该在最初使用粗颗粒度的锁,除非有真正的原因相信竞争会导致问题。

四、锁之外的办法
 
(1)免锁算法
经常用于免锁的生产者/消费者任务的数据结构之一是循环缓冲区。它在设备驱动程序中相当普遍,如以前移植的网卡驱动程序。内核里有一个通用的循环缓冲区的实现在 <linux/kfifo.h> 。
 
(2)原子变量
完整的锁机制对一个简单的整数来讲显得浪费。内核提供了一种原子的整数类型,称为atomic_t,定义在<asm/atomic.h>。原子变量操作是非常快的, 因为它们在任何可能时编译成一条单个机器指令。
以下是其接口函数:

void atomic_set(atomic_t *v, int i); /*设置原子变量 v 为整数值 i.*/
atomic_t v = ATOMIC_INIT(0);  /*编译时使用宏定义 ATOMIC_INIT 初始化原子值.*/

int atomic_read(atomic_t *v); /*返回 v 的当前值.*/

void atomic_add(int i, atomic_t *v);/*由 v 指向的原子变量加 i. 返回值是 void*/
void atomic_sub(int i, atomic_t *v); /*从 *v 减去 i.*/

void atomic_inc(atomic_t *v); 
void atomic_dec(atomic_t *v); /*递增或递减一个原子变量.*/

int atomic_inc_and_test(atomic_t *v); 
int atomic_dec_and_test(atomic_t *v); 
int atomic_sub_and_test(int i, atomic_t *v); 
/*进行一个特定的操作并且测试结果; 如果, 在操作后, 原子值是 0, 那么返回值是真; 否则, 它是假. 注意没有 atomic_add_and_test.*/

int atomic_add_negative(int i, atomic_t *v); 
/*加整数变量 i 到 v. 如果结果是负值返回值是真, 否则为假.*/

int atomic_add_return(int i, atomic_t *v); 
int atomic_sub_return(int i, atomic_t *v); 
int atomic_inc_return(atomic_t *v); 
int atomic_dec_return(atomic_t *v); 
/*像 atomic_add 和其类似函数, 除了它们返回原子变量的新值给调用者.*/

atomic_t 数据项必须通过这些函数存取。 如果你传递一个原子项给一个期望一个整数参数的函数, 你会得到一个编译错误。需要多个 atomic_t 变量的操作仍然需要某种其他种类的加锁。
 
(3)位操作
内核提供了一套函数来原子地修改或测试单个位。原子位操作非常快, 因为它们使用单个机器指令来进行操作, 而在任何时候低层平台做的时候不用禁止中断. 函数是体系依赖的并且在<asm/bitops.h> 中声明. 以下函数中的数据是体系依赖的. nr 参数(描述要操作哪个位)在ARM体系中定义为unsigned int

void set_bit(nr, void *addr); /*设置第 nr 位在 addr 指向的数据项中。*/

void clear_bit(nr, void *addr); /*清除指定位在 addr 处的无符号长型数据.*/

void change_bit(nr, void *addr);/*翻转nr位.*/

test_bit(nr, void *addr); /*这个函数是唯一一个不需要是原子的位操作; 它简单地返回这个位的当前值.*/

/*以下原子操作如同前面列出的, 除了它们还返回这个位以前的值.*/

int test_and_set_bit(nr, void *addr); 
int test_and_clear_bit(nr, void *addr); 
int test_and_change_bit(nr, void *addr);

 
以下是一个使用范例:

/* try to set lock */
while (test_and_set_bit(nr, addr) != 0)
    wait_for_a_while(); 

/* do your work */ 

/* release lock, and check. */
if (test_and_clear_bit(nr, addr) == 0)
    something_went_wrong(); /* already released: error */

 
(4)seqlock
2.6内核包含了一对新机制打算来提供快速地, 无锁地存取一个共享资源。 seqlock要保护的资源小, 简单, 并且常常被存取, 并且很少写存取但是必须要快。seqlock 通常不能用在保护包含指针的数据结构。seqlock 定义在 <linux/seqlock.h> 。

/*两种初始化方法*/
seqlock_t lock1 = SEQLOCK_UNLOCKED;

seqlock_t lock2;
seqlock_init(&lock2);

这个类型的锁常常用在保护某种简单计算,读存取通过在进入临界区入口获取一个(无符号的)整数序列来工作. 在退出时, 那个序列值与当前值比较; 如果不匹配, 读存取必须重试.读者代码形式: 

unsigned int seq;
do {
    seq = read_seqbegin(&the_lock);
    /* Do what you need to do */
} while read_seqretry(&the_lock, seq);

 如果你的 seqlock 可能从一个中断处理里存取, 你应当使用 IRQ 安全的版本来代替:

unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned longflags);

写者必须获取一个排他锁来进入由一个 seqlock 保护的临界区,写锁由一个自旋锁实现, 调用:

void write_seqlock(seqlock_t *lock); 
void write_sequnlock(seqlock_t *lock);

因为自旋锁用来控制写存取, 所有通常的变体都可用:

void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags);
void write_seqlock_irq(seqlock_t *lock);
void write_seqlock_bh(seqlock_t *lock);

void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags);
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);

 还有一个 write_tryseqlock 在它能够获得锁时返回非零.

(5)读取-复制-更新

读取-拷贝-更新(RCU) 是一个高级的互斥方法, 在合适的情况下能够有高效率. 它在驱动中的使用很少。


 

五、开发板实验
在我的SBC2440V4开发板上
completion实验,因为别的实验都要在并发状态下才可以实验,所以本章的我只做了completion的实验。我将《Linux设备驱动程序(第3版)》提供的源码做了修改,将原来的2.4内核的模块接口改成了2.6的接口,并编写了测试程序。实验源码如下:

模块程序链接:complete模块
模块测试程序链接测试程序


[Tekkaman2440@SBC2440V4]#cd /lib/modules/
[Tekkaman2440@SBC2440V4]#insmod complete.ko
[Tekkaman2440@SBC2440V4]#echo 8 > /proc/sys/kernel/printk
[Tekkaman2440@SBC2440V4]#cat /proc/devices
Character devices:
  1 mem
  2 pty
  3 ttyp
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 14 sound
 81 video4linux
 89 i2c
 90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
204 s3c2410_serial
252 complete
253 usb_endpoint
254 rtc

Block devices:
  1 ramdisk
256 rfd
  7 loop
 31 mtdblock
 93 nftl
 96 inftl
179 mmc
[Tekkaman2440@SBC2440V4]#mknod -m 666 /dev/complete c 252 0
[Tekkaman2440@SBC2440V4]#cd /tmp/
[Tekkaman2440@SBC2440V4]#./completion_testr&
[Tekkaman2440@SBC2440V4]#process 814 (completion_test) going to sleep
[Tekkaman2440@SBC2440V4]#./completion_testr&
[Tekkaman2440@SBC2440V4]#process 815 (completion_test) going to sleep
[Tekkaman2440@SBC2440V4]#ps
  PID Uid VSZ Stat Command
    1 root 1744 S init
    2 root SW< [kthreadd]
    3 root SWN [ksoftirqd/0]
    4 root SW< [watchdog/0]
    5 root SW< [events/0]
    6 root SW< [khelper]
   59 root SW< [kblockd/0]
   60 root SW< [ksuspend_usbd]
   63 root SW< [khubd]
   65 root SW< [kseriod]
   77 root SW [pdflush]
   78 root SW [pdflush]
   79 root SW< [kswapd0]
   80 root SW< [aio/0]
  707 root SW< [mtdblockd]
  708 root SW< [nftld]
  709 root SW< [inftld]
  710 root SW< [rfdd]
  742 root SW< [kpsmoused]
  751 root SW< [kmmcd]
  769 root SW< [rpciod/0]
  778 root 1752 S -sh
  779 root 1744 S init
  781 root 1744 S init
  782 root 1744 S init
  783 root 1744 S init
  814 root 1336 D ./completion_testr
  815 root 1336 D ./completion_testr
  816 root 1744 R ps
[Tekkaman2440@SBC2440V4]#./completion_testw
process 817 (completion_test) awakening the readers...
awoken 814 (completion_test)
write code=0
[Tekkaman2440@SBC2440V4]#read code=0
[Tekkaman2440@SBC2440V4]#ps
  PID Uid VSZ Stat Command
    1 root 1744 S init
    2 root SW< [kthreadd]
    3 root SWN [ksoftirqd/0]
    4 root SW< [watchdog/0]
    5 root SW< [events/0]
    6 root SW< [khelper]
   59 root SW< [kblockd/0]
   60 root SW< [ksuspend_usbd]
   63 root SW< [khubd]
   65 root SW< [kseriod]
   77 root SW [pdflush]
   78 root SW [pdflush]
   79 root SW< [kswapd0]
   80 root SW< [aio/0]
  707 root SW< [mtdblockd]
  708 root SW< [nftld]
  709 root SW< [inftld]
  710 root SW< [rfdd]
  742 root SW< [kpsmoused]
  751 root SW< [kmmcd]
  769 root SW< [rpciod/0]
  778 root 1752 S -sh
  779 root 1744 S init
  781 root 1744 S init
  782 root 1744 S init
  783 root 1744 S init
  815 root 1336 D ./completion_testr
  818 root 1744 R ps
[1] - Done ./completion_testr
[Tekkaman2440@SBC2440V4]#./completion_testw
process 819 (completion_test) awakening the readers...
awoken 815 (completion_test)
write code=0
[Tekkaman2440@SBC2440V4]#read code=0
[Tekkaman2440@SBC2440V4]#ps
  PID Uid VSZ Stat Command
    1 root 1744 S init
    2 root SW< [kthreadd]
    3 root SWN [ksoftirqd/0]
    4 root SW< [watchdog/0]
    5 root SW< [events/0]
    6 root SW< [khelper]
   59 root SW< [kblockd/0]
   60 root SW< [ksuspend_usbd]
   63 root SW< [khubd]
   65 root SW< [kseriod]
   77 root SW [pdflush]
   78 root SW [pdflush]
   79 root SW< [kswapd0]
   80 root SW< [aio/0]
  707 root SW< [mtdblockd]
  708 root SW< [nftld]
  709 root SW< [inftld]
  710 root SW< [rfdd]
  742 root SW< [kpsmoused]
  751 root SW< [kmmcd]
  769 root SW< [rpciod/0]
  778 root 1752 S -sh
  779 root 1744 S init
  781 root 1744 S init
  782 root 1744 S init
  783 root 1744 S init
  820 root 1744 R ps
[2] + Done ./completion_testr
[Tekkaman2440@SBC2440V4]#ps
  PID Uid VSZ Stat Command
    1 root 1744 S init
    2 root SW< [kthreadd]
    3 root SWN [ksoftirqd/0]
    4 root SW< [watchdog/0]
    5 root SW< [events/0]
    6 root SW< [khelper]
   59 root SW< [kblockd/0]
   60 root SW< [ksuspend_usbd]
   63 root SW< [khubd]
   65 root SW< [kseriod]
   77 root SW [pdflush]
   78 root SW [pdflush]
   79 root SW< [kswapd0]
   80 root SW< [aio/0]
  707 root SW< [mtdblockd]
  708 root SW< [nftld]
  709 root SW< [inftld]
  710 root SW< [rfdd]
  742 root SW< [kpsmoused]
  751 root SW< [kmmcd]
  769 root SW< [rpciod/0]
  778 root 1752 S -sh
  779 root 1744 S init
  781 root 1744 S init
  782 root 1744 S init
  783 root 1744 S init
  821 root 1744 R ps
[Tekkaman2440@SBC2440V4]#./completion_testw
process 822 (completion_test) awakening the readers...
write code=0
[Tekkaman2440@SBC2440V4]#./completion_testr
process 823 (completion_test) going to sleep
awoken 823 (completion_test)
read code=0         


实验表明:如果先读数据,读的程序会被阻塞(因为驱动在wait_for_completion,等待写的完成)。如果先写,读程序会比较顺利的执行下去(虽然也会休眠,但马上会被唤醒!)。其原因可以从completion的源码中找答案。completion其实就是自旋锁的再包装,具体细节参见completion的源码。
原创粉丝点击