Inside SW,FW and HW R&D

C/C++ | Visual C++ | 嵌入式系统 | 硬件设计 | 通信与网络

My Links

Blog统计

公告


  ----Counters-----
     Welcome to Inside SW,FW and HW R&D.
   Developer:宋宝华
21cnbao@21cn.com

文章

收藏

相册

技术网站

存档


正在读取评论……

4 打下基础

我们现在已经与编写设备驱动之间的距离已经非常逼近。但是,在此之前,让我们先装备一些驱动的概念。本章首先开始于对本书的问题陈述的理念,接下来分析PC兼容的系统和嵌入式计算机中典型的设备和I/O接口。中断处理在大多数驱动中的都存在,因此,本章讨论了编写中断服务程序的方法问题。之后,我们将注意力转移到了2.6内核中新引入的设备模型,该新模型建立于sysfskobject、设备类、udev等抽象事物上,它们是从设备驱动中提炼出来的有共性的东西。新的设备模型也需要内核空间之外的策略,这些策略被推到用户空间,这导致了/dev结点管理、热插拔、冷插拔、模块自动加载、固件下载等功能的改变。

设备和驱动介绍

由于对硬件的操作要求拥有执行特殊指令和处理中断等处理器特权,所以用户应用程序一般不能直接和硬件通信。设备驱动则承担了硬件交互的工作,它也向应用程序和内核中其他的部分引出接口这些接口。应用程序通过/dev目录中的设备结点可对设备进行操作,通过/sys目录中的结点可以收集设备信息[1]

[1]以后你将学习到,网络应用程序通过不同的机制将请求发给底层驱动。

4.1是一个典型的PC兼容的系统的硬件块结构图。从图中可以看出,系统支持各种各样的设备和接口,如内存、视频、音频、USBPCIWiFiI2CIDE、以太网、串口、键盘、鼠标、软驱、并口和红外等。内核控制器和图形控制器在PC体系结构中位于北桥芯片组中,然后外设总线则源自南桥芯片组。

4.2给出了一个假想的嵌入式设备的类似于图4.1的块图。该图中包含了数个PC中通常不存在的接口,如闪存、LCD、触摸屏和无线调制解调器。

显然,访问外设的能力是系统整体机能的重要组成部分。设备驱动提供了达到此目的的引擎。本书中剩余的章节将聚焦于设备结构,并将会读者怎样实现相应的设备驱动。

中断处理

由于I/O操作的不确定因素,以及处理器和I/O设备之间速度的不匹配,设备往往通过某种硬件信号异步地唤起处理器的注意。这些硬件信号就是所谓的中断。每个中断设备都被分配给一个相关的标识符,被称为中断请求(IRQ)号。当处理器检测到某一IRQ号对应的中断产生时,它将停止它现在的工作,并引用该IRQ所对应的中断服务例程(ISR)。中断处理函数ISR在中断上下文执行。

中断上下文

ISR是与硬件交互的非常重要的代码片段。它们被给予了立即执行的特权,以便最大化系统的性能。不过,如果ISR执行过慢、负载太重的化,就违背了自身的设计哲学。贵宾都被给予了优惠待遇,但是,尽量减少由此造成的对公众的不便也是他们的义务。为了对粗暴打断当前执行线程的行为进行补偿,ISR不得不礼貌地执行于受限制的环境下,即所谓的中断上下文(或原子上下文)。

下面给出了中断上下文可为和不可为事项的列表:

1. 如果你的中断上下文进入睡眠,它是一项应该被处以监禁的罪行。中断处理函数不能通过调用schedule_timeout()等睡眠函数放弃处理器,在中断处理函数中调用一个内核API之前,应该仔细分析它以确保其内部不会触发阻塞等待。例如,input_register_device()表面上看起来没有问题,但是它内部以GFP_KERNEL为参数调用了kmalloc()。从第2章《内核一瞥》可以看出,用这种方式调用kmalloc()的话,如果系统的空闲内存低于某门限,kmalloc()将睡眠等待swapper释放内存。

2. 为了在中断处理函数中保护临界区,你不能使用互斥体,因为它们也许导致睡眠。应该使用自旋锁代替互斥体,但是一定要记住的是只有真正需要的时候才采用它。

3. 中断处理函数不能与用户空间直接交互数据,因为它们经由进程上下文与用户空间建立连接。这也是为什么中断处理函数不能睡眠的第2个理由:调度器工作于进程之间,如果中断处理函数睡眠并被调度出去,它们怎么返回到运行队列呢?

4. 中断处理函数一方面需要快速地出来,另一方面又需要完成它的工作。为了规避这种冲突,中断处理函数通常被分成2个部分。瘦小的顶半部标志一个响应以宣称它已经服务了该中断,而重大的工作负载都被丢给了肥胖的底半部。底半部的执行被延后,在其执行环境中,所有的中断都是使能的。在讨论softirqtasklet的时候,你将学习到真也难怪开发底半部。

5. 中断处理函数不必是可重用的。当某中断被执行的时候,在它返回之前,相应的IRQ都被禁止了。因此,与进程上下文代码不同的是,同一中断处理函数的不同实例不可能同时运行在多个处理器上。

6. 中断处理函数可以被更高优先级IRQ的中断处理函数打断。如果你请求内核将你的中断处理函数作为快中断处理的话,此类中断嵌套将被禁止。快中断服务函数运行的时候,本处理器上的所有中断都会被禁止。在禁止中断或将你的中断标识为快中断之前,请意识到中断屏蔽对系统性能的坏处。中断屏蔽的时间越长,中断延迟就会更长,或者说已经被产生的中断得到服务的延迟就会越久。中断延迟与系统真实的响应时间成反比。

函数中可以检查in_interrupt()的返回值以查看自身是否位于中断上下文。

与外部硬件产生的异步中断不一样,也存在同步到达的中断。同步中断意味着它们不会不期而遇,它们由处理器本身执行某指令而产生。外部中断和同步中断在内核中使用相同的机制处理。

同步中断的例子包括:

1)异常,被用于报告严重的运行时错误;

2)软中断,如int 0x80指令,被用户实现x86体系结构上的系统调用。

分配IRQ

设备驱动必须将它们的IRQ号与一个中断处理函数连接。因此,它们需要知道它们正在驱动的设备的IRQ号。IRQ的分配可以很直接,也可能需要复杂的探测过程。在PC体系结构中,例如,定时器中断被分配了IRQ 0RTC中断也是IRQ 8。现代的中断技术(如PCI)足够强大,它能够响应对IRQ的查询(系统启动过程中由BIOS分配),PCI驱动能够访问设备配置空间的相应区域并获得IRQ。对于较老的设备,如基于工业标准体系结构(ISA)的卡而言,驱动也许不得不利用特定硬件的知识以探测和解析IRQ

通过/proc/interrupts可以查看系统中活动的IRQ的列表。

设备实例:辊轮

现在你已经学习了中断处理的基本知识,现在我们来实现一个辊轮设备实例的中断处理。在一些手机和PDA上能找到辊轮,它支持3种动作(顺时针旋转,逆时针旋转和按键),可便利菜单导航。本例辊轮中的任何运行都会向处理器产生IRQ 7。通用目的I/OGPIO)端口D的低3位与辊轮设备连接。这些引脚上产生的波形与图4.3中不同的辊轮运动一致。中断处理函数的工作是通过查看端口DGPIO数据寄存器解析出辊轮的运动。

4.3 辊轮运动产生的波形

驱动必须首先请求IRQ并将一个中断处理函数与其绑定:

#define ROLLER_IRQ  7

static irqreturn_t roller_interrupt(int irq, void *dev_id);

 

 

if (request_irq(ROLLER_IRQ, roller_interrupt, IRQF_DISABLED |

                IRQF_TRIGGER_RISING, "roll", NULL)) {

  printk(KERN_ERR "Roll: Can't register IRQ %d\n", ROLLER_IRQ);

  return -EIO;

}

我们看一下传递给request_irq()的参数,本例中没有查询或探测IRQ号,而是直接硬编码为ROLLER_IRQ。第2个参数roller_interrupt()是中断处理函数。中断处理函数的原型的返回值类型为irqreturn_t,如果中断处理成功,则返回IRQ_HANDLED,否则,返回IRQ_NONE。对于PCII/O而言,该返回值的意义更重要,因为多个设备可能共享同一IRQ

IRQF_DISABLED标志意味着这个中断处理为快中断,因此,在调用该处理函数的时候,内核将禁止所有的中断。IRQF_TRIGGER_RISING暗示辊轮将在中断线上产生一个上升沿以发出中断。换句话说,辊轮是一个边沿触发的设备。有一些设备是电平触发的,在CPU服务其中断之前,它一直将中断线保持在一个电平上。使用IRQF_TRIGGER_HIGHIRQF_TRIGGER_LOW可以标识一个中断为高/低电平触发。该参数其他的可能值包括IRQF_SAMPLE_RANDOM(第5章《字符设备驱动》的《伪字符设备驱动》一节会用到)、IRQF_SHARED(定义这个IRQ被多个设备共享)。

下一个参数"roll",用于标识这个设备,在/proc/interrupts等文件中也会利用它产生数据。最后一个参数(本例中为NULL),仅在共享中断的时候有用,用于区分共享同一IRQ线的每个设备。

2.6.19内核开始,中断处理接口发生了一些变化。以前的中断处理函数的第3个参数为struct pt_regs *,它指向存放CPU寄存器的地址,在2.6.19中已经移除。另外,IRQF_xxx型中断标志取代了SA_xxx型中断标志。例如,在较早的内核中,你应该使用SA_INTERRUPT而不是IRQF_DISABLED来将中断处理标识为快中断处理。

驱动初始化的时候申请IRQ并不是太好,因为这样会导致甚至设备未被使用的时候,有价值的资源也被占用。因此,设备驱动通常在设备被应用打开的时候申请IRQ。类似地,IRQ也在应用关闭设备的时候释放IRQ,而不是在退出驱动模块的时候进行。使用下面的方法可以释放一个IRQ

free_irq(int irq, void *dev_id);

清单4.1给出了辊轮中断处理的实现。

roller_interrupt()2个参数,IRQ和设备标识符(传递给request_irq()的最后一个参数)。请对照图4.3查看清单4.1

    清单4.1 辊轮中断处理

spinlock_t roller_lock = SPIN_LOCK_UNLOCKED;

static DECLARE_WAIT_QUEUE_HEAD(roller_poll);

 

static irqreturn_t

roller_interrupt(int irq, void *dev_id)

{

  int i, PA_t, PA_delta_t, movement = 0;

 

  /* Get the waveforms from bits 0, 1 and 2

     of Port D as shown in Figure 4.3 */

  PA_t = PORTD & 0x07;

 

  /* Wait until the state of the pins change.

     (Add some timeout to the loop) */

  for (i=0; (PA_t==PA_delta_t); i++){

    PA_delta_t = PORTD & 0x07;

  }

 

  movement = determine_movement(PA_t, PA_delta_t); /* See below */

 

  spin_lock(&roller_lock);

 

  /* Store the wheel movement in a buffer for

     later access by the read()/poll() entry points */

  store_movements(movement);

 

  spin_unlock(&roller_lock);

 

  /* Wake up the poll entry point that might have

     gone to sleep, waiting for a wheel movement */

  wake_up_interruptible(&roller_poll);

 

  return IRQ_HANDLED;

}

int

determine_movement(int PA_t, int PA_delta_t)

{

  switch (PA_t){

    case 0:

      switch (PA_delta_t){

      case 1:

        movement = ANTICLOCKWISE;

        break;

      case 2:

        movement = CLOCKWISE;

        break;

      case 4:

        movement = KEYPRESSED;

        break;

      }

      break;

    case 1:

      switch (PA_delta_t){

      case 3:

        movement = ANTICLOCKWISE;

        break;

      case 0:

        movement = CLOCKWISE;

        break;

      }

      break;

    case 2:

      switch (PA_delta_t){

      case 0:

        movement = ANTICLOCKWISE;

        break;

      case 3:

        movement = CLOCKWISE;

        break;

      }

      break;

    case 3:

      switch (PA_delta_t){

      case 2:

        movement = ANTICLOCKWISE;

        break;

      case 1:

        movement = CLOCKWISE;

        break;

      }

    case 4:

      movement = KEYPRESSED;

      break;

  }

}

驱动入口点(read()poll())尾随roller_interrupt()进行操作。例如,当中断处理函数解析完一个辊轮运动后,它唤醒正在等待的poll()线程(可能已经因为X Windows等应用发起的select()系统调用而睡眠)。请在学习完第5章字符设备驱动的知识后,重新查看清单4.1并实现辊轮设备的完整驱动。

7章《输入设备驱动》的清单7.3利用了内核的输入接口,将辊轮转化为辊鼠标。

在本节结束前,我们介绍一下使能和禁止特定IRQ的函数。enable_irq(ROLLER_IRQ)用于使能辊轮运动的中断发生,disable_irq(ROLLER_IRQ)则进行相反的工作。disable_irq_nosync(ROLLER_IRQ)禁止辊轮中断,并且不等待任何正在执行的roller_interrupt()实例的返回。disable_irq()的非同步变体执行地更快,但是可能导致潜在的竞态。只有在你确认没有竞争的尽快下,才可以这样使用。drivers/ide/ide-io.c由一个使用disable_irq_nosync()的例子,在初始化过程中,它阻止了一些中断,因为一些系统中可能在此方面存在问题。

软中断(SoftirqTasklet

正如以前讨论的那样,中断处理有2个矛盾的要求:它们需要完成大量的设备数据处理,但是又不得不尽可能快地退出。为了摆脱这一困境,中断处理过程被分成2部分:一个急切的且抢占的与硬件交互的顶半部,和一个在所有中断都使能情况下并非十分急切的处理大量工作的底半部。如顶半部不一样,底半部是同步的,因为内核决定了它什么时候会执行它们。如下机制都可用于内核中延后一个工作到底半部执行:softirqtasklet和工作队列(work queue)。

Softirq是一种基本的底半部机制,有较强的加锁需求。仅仅在一些对性能敏感的子系统(如网络层、SCSI层和内核定时器)中才会使用softirqTasklet建立在softirq之上,使用起来更简单。除非有严格的可扩展性和速度要求,都建议使用TaskletSoftirqTasklet的主要不同是前者是可重用的而后者则不需要。Softirq的不同实例可运行在不同的处理器上,而tasklet则不允许。

为了论证SoftirqTasklet的用法,假定前例中的辊轮由存在由于运动部件导致的潜在问题(如旋轮偶尔被卡住)从而导致不同于spec的波形。一个被卡住的旋轮会不停地产生假的中断,并可能使系统冻结。为了解决这个问题,可以捕获波形,进行一些分析,并在发现卡住的情况下动态地从中断模式切换到轮询模式,如果旋轮恢复正常,软件也恢复到正常模式。我们在中断处理函数中捕获波形,并在底半部分析它。清单4.24.3分别用SoftirqTasklet对此进行了实现。

它们都是清单4.1的简化的变体,它们将中断处理简化为2个函数:从GPIO端口D捕获波形的roller_capture()和对波形进行算术分析并按需切换到轮询模式的roller_analyze()

清单4.2 使用Softirq 分担中断处理的负载

void __init

roller_init()

{

  /* ... */

 

  /* Open the softirq. Add an entry for ROLLER_SOFT_IRQ in

     the enum list in include/linux/interrupt.h */

  open_softirq(ROLLER_SOFT_IRQ, roller_analyze, NULL);

}

 

 

/* The bottom half */

void

roller_analyze()

{

  /* Analyze the waveforms and switch to polled mode if required */

}

/* The interrupt handler */

static irqreturn_t

roller_interrupt(int irq, void *dev_id)

{

  /* Capture the wave stream */

  roller_capture();

 

  /* Mark softirq as pending */

  raise_softirq(ROLLER_SOFT_IRQ);

 

  return IRQ_HANDLED;

}

为了定义一个softirq,你必须在include/linux/interrupt.h中静态地添加一个入口。你不能动态地定义softirqraise_softirq()用于宣布相应的softirq需要被执行。内核会在下一个可获得的机会里执行它。可能发生在退出硬中断处理函数的时候,也可能在ksoftirqd内核线程中。

清单4.3使用tasklet分担中断处理的负载

struct roller_device_struct { /* Device-specific structure */

  /* ... */

  struct tasklet_struct tsklt;

  /* ... */

}

 

void __init roller_init()

{

  struct roller_device_struct *dev_struct;

  /* ... */

 

  /* Initialize tasklet */

  tasklet_init(&dev_struct->tsklt, roller_analyze, dev);

}

 

 

/* The bottom half */

void

roller_analyze()

{

/* Analyze the waveforms and switch to

   polled mode if required */

}

/* The interrupt handler */

static irqreturn_t

roller_interrupt(int irq, void *dev_id)

{

  struct roller_device_struct *dev_struct;

 

  /* Capture the wave stream */

  roller_capture();

 

  /* Mark tasklet as pending */

  tasklet_schedule(&dev_struct->tsklt);

 

  return IRQ_HANDLED;

}

tasklet_init()用于动态地初始化一个tasklet,该函数不会为tasklet_struct分配内存,相反地,你必须将已经分配好的地址传递给它。tasklet_schedule()用于宣布相应的tasklet需要被执行。和中断类似,内核提供了一系列用于控制在多处理器系统中tasklet执行状态的函数:

1tasklet_enable()使能tasklet

2tasklet_disable()禁止tasklet,并等待正在执行的tasklet退出;

3tasklet_disable_nosync()的语义和disable_irq_nosync()相似,它并不等待正在执行的tasklet退出。

你已经看到了中断处理函数和底半部的不同,但是,也有几个相似点。中断处理函数和tasklet都不需要可重用。而且,二者都不能睡眠。另外,中断处理函数、taskletsoftirq都不能被抢占。

工作队列是中断处理延后执行的第3种方式。它们在进程上下文执行,允许睡眠,因此可以使用mutex这类可能导致睡眠的函数。在前一章分析内核辅助接口的时候,我们已经讨论了工作队列。表4.1softirqtasklet和工作队列进行了对比分析。

4.1 softirqtasklet和工作队列对比

 

Softirq

Tasklet

Work Queue

执行上下文

延后的工作,运行于中断上下文

延后的工作,运行于中断上下文

延后的工作,运行于进程上下文

可重用

可以在不同的CPU上同时运行

不能在不同的CPU上同时运行,但是不同的CPU可以运行不同的tasklet

可以在不同的CPU上同时运行

睡眠

不能睡眠

不能睡眠

可以睡眠

抢占

不能抢占/调度

不能抢占/调度

可以抢占/调度

易用性

不容易使用

容易使用

容易使用

何时使用

如果延后的工作不会睡眠,而且有严格的可扩展性或速度要求

如果延后的工作不会睡眠

如果延后的工作会睡眠

LKML正在进行一项去除tasklet的可行性的讨论。Tasklet比进程上下文的代码优先级更高,因此它们可能存在延迟问题。另外,你已经学习到,它们不允许睡眠,且只能在同一CPU上执行。因此,有人提议将现存的tasklet基于其场景随机应变地转换为softirq或工作队列。

2章讨论的–rt补丁集将中断处理移到了内核线程执行,以实现更广泛的抢占支持。

Linux设备模型

新的Linux设备模型引入了类似于C++的抽象机制,它总结出设备驱动的共性,并提取出了总线和核心层。接下来,我们分析一下设备模型中的udevsysfskobject和设备类(device class)等组件,以及这些组件对/dev结点管理、热插拔、固件下载和模块自动加载等关键内核子系统的影响。Udev是分析设备模型优点的最佳入口点,我们先从它开始讲解。

Udev

几年前,Linux操作系统还很年轻,管理设备节点的工作一点都不好玩。所有需要的结点(达到数千个)都不得不在/dev目录下静态创建。该问题实际起源于原始的UNIX系统。在2.4内核中,引入了devfs,它支持设备结点的动态创建。Devfs提供在了位于内存的文件系统中创建设备结点的能力,而命名结点的负担还是落在了设备驱动头上。但是,设备命名策略是可管理的,不应与内核混在一起。策略可位于头文件、模块参数或用户空间中。而Udev将成功地设备管理的任务彻底推向了用户空间。

Udev的工作依赖于:

1.内核中的sysfs支持,sysfsLinux设备模型的一个重要组成部分。Sysfs位于内存中,在启动时被挂载在了/sys目录(见/etc/fstab)。下一节我们会分析sysfs,你可以认为访问sysfs是理所当然的。

2.一套用户空间守护程序和实用工具,如udevdudevinfo

3.用户自定义的规则,位于/etc/udev/rules.d/目录。你可以根据对应设备的特点设置规则。

为了理解udev的用法,我们先看一个例子。假定你有一个USB DVD驱动器或一个USB CD-RW驱动器。根据你热插拔设备顺序的不同,一个被命名为/dev/sr0,另一个被命名为/dev/sr1。在没有udev的情况下,你必须执行区分这些名字对应的设备。但是,有了udev以后,不管你以什么顺序热插拔它们,你都能分辨出二者,DVD命名为/dev/usbdvdCD-RW命名为/dev/usbcdrw

首先,从sysfs相应的文件中提取产品信息。假定Targus DVD驱动器被分配的设备结点为/dev/sr0Addonics CD-RW驱动器为/dev/sr1,使用udevinfo可收集设备信息:

bash> udevinfo -a -p /sys/block/sr0

...

looking at the device chain at

'/sys/devices/pci0000:00/0000:00:1d.7/usb1/1-4':

 BUS=»usb»

&nbs