十、中断处理
1、安装中断处理例程
如果读者确实想 “看到” 产生的中断,那么仅仅通过向硬件设备写入是不够的,还必须要在系统中安装一个软件处理例程。如果没有通知 Linux 内核等待用户的中断,那么内核只会简单应答并忽略该中断。
中断信号线是非常珍贵且有限的资源,尤其是在系统上只有 15 根或 16 根中断信号线时更是如此。内核维护了一个中断信号线的注册表,该注册表类似于 I/O 端口的注册表。模块在在使用中断前要先请求一个中断通道(或者中断请求 IRQ),然后在使用后释放该通道。我们将会在后面看到,在很多场合下,模块也希望可以和其他的驱动程序共享中断信号线。下列在头文件 中声明的函数实现了该接口:
int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *), unsigned long flags, const char *dev_name, void *dev_id); void free_irq(unsigned int irq, void *dev_id);
通常,从 request_irq 函数返回给请求函数的值为 0 时表示申请成功,为负值时表示错误码。函数返回 -EBUSY 表示已经有另一个驱动程序占用了你要请求的中断信号线。这些函数的参数如下:
unsigned int irq
这是要申请的中断号。
irqreturn_t (*handler)(int, void *, struct pt_regs *)
这是要安装的中断处理函数指针。我们会在本章的后面部分讨论这个函数的参数含义。
unsigned long flags
如读者所想,这是一个与中断管理有关的位掩码选项(将在后面描述)。
const char *dev_name
传递给 request_irq 的字符串,用来在 /proc/interrupts 中显示中断的拥有者(参见下节)。
void *dev_id
这个指针用于共享的中断信号线。它是唯一的标识符,在中断信号线空闲时可以使
用它,驱动程序也可以使用它指向驱动程序自己的私有数据区(用来识别哪个设备
产生中断)。在没有强制使用共享方式时,dev_id 可以被设置为 NULL,总之用它
来指向设备的数据结构是一个比较好的思路。我们会在本章后面的 “实现处理例
程” 一节中看到 dev_id 的实际应用。
可以在 flags 中设置的位如下所示:
SA_INTERRUPT
当该位被设置时,表明这是一个 “快速” 的中断处理例程。快速处理例程运行在中
断的禁用状态下(更详细的主题将在本章后面的 “快速和慢速处理例程” 一节中讨论)。
SA_SHIRQ
该位表示中断可以在设备之间共享。共享的概念将在本章后面的 “中断共享” 一节描述。
SA_SAMPLE_RANDOM
该位指出产生的中断能对 /dev/random 设备和 /dev/urandom 设备使用的熵池
(entropy pool)有贡献。从这些设备读取,将会返回真正的随机数,从而有助于应
用软件选择用于加密的安全密钥。这些随机数是从一个熵池中得到的,各种随机事
件都会对该熵池作出贡献,如果读者的设备以真正随机的周期产生中断,就应该设
置该标志位。另一方面,如果中断是可预期的(列如,帧捕捉卡的垂直消隐),就
不值得设置这个标志位 —— 它对系统的熵没有任何贡献。能受到攻击者影响的设
备不应该设置该位,例如,网络驱动程序会被外部的事件影响到预定的数据包的时
间周期,因而也不会对熵池有贡献,更详细的信息请参见 drivers/char/random.c 文
件中的注释。
中断处理例程可在驱动程序初始化时或者设备第一次打开时安装。虽然在模块的初始化函数中安装中断处理例程看起来是个好主意,但实际上并非如此。因为中断信号线的数量是非常有限的,我们不想肆意浪费。计算机拥有的设备通常要比中断信号线多得多,如果一个模块在初始化时请求了 IRQ,那么即使驱动程序只是占用它而从未使用,也将会阻止任意一个其他的驱动程序使用该中断。而在设备打开的时候申请中断,则可以共享这些有限的资源。
这种情况很可能出现,例如,在运行一个与调制解调器共用同一中断的帧捕捉卡驱动程序时,只要不同时使用这两个设备就可以共享同一中断。用户在系统启动时装载特殊的设备模块是一种普遍做法,即使该设备很少使用。数据捕捉卡可能会和第二个串口使用相同的中断,我们可以在捕获数据时,避免使用调制解调器连接到互联网服务供应商(ISP),但是如果为了使用调制解调器而不得不卸载一个模块,总是令人不快的。
调用 request_irq 的正确位置应该是在设备第一次打开、硬件被告知产生中断之前。调用 free_irq 的位置是最后一次关闭设备、硬件被告知不用再中断处理器之后。这种技术的缺点是必须为每个设备维护一个打开计数,这样我们才能知道什么时候可以禁用中断。
尽管我们已经讨论了不应该在装载模块时调用 request_irq,但 short 模块还是在装载时请求了它的中断信号线,这样做的方便之处是,我们可以直接运行测试程序,而不需要额外运行其他的进程来保持设备的打开状态。因此,short 在它自己的初始化函数(short_init)中请求中断,而不是像真正的设备驱动那样在 short_open 中请求中断。
下面这段代码要请求的中断是 short_irq,对这个变量的实际赋值操作(例如,决定使用哪个 IRQ)会在后面给出,因为它与当前的讨论无关。short_base 是并口使用的 I/O 地址空间的基地址;向并口的 2 号寄存器写入,可以启用中断报告。
if (short_irq >= 0) result = request_irq(short_irq, short_interrupt, SA_INTERRUPT, "short", NULL); if (result) { printk(KERN_INFO "short: can't get assigned irq %i\n", short_irq); short_irq = -1; } else { /* 真正启用中断 —— 假定这是一个并口 */ outb(0x10, short_base + 2); } }
(1)内核帮助下的探测
Linux 内核提供了一个底层设施来探测中断号。它只能在非共享中断的模式下工作,但是大多数硬件有能力工作在共享中断的模式下,并可提供更好的找到配置中断号的方法。内核提供的这一设施由两个函数组成,在头文件 中声明(该文件也描述了探测机制):
unsigned long probe_irq_on(void) ;
这个函数返回一个未分配中断的位掩码。驱动程序必须保存返回的位掩码,并且将它传递给后面的 probe_irq_off 函数,调用该函数之后,驱动程序要安排设备产生至少一次中断。
int probe_irq_off(unsigned long);
在请求设备产生中断之后,驱动程序调用这个函数,并将前面 probe_irq_on 返回的位掩码作为参数传递给它。probe_irq_off 返回 “probe_irq_on” 之后发生的中断编号。如果没有中断发生,就返回 0(因此,IRQ 0 不能被探测到,但在任何已支持的体系结构上,没有任何设备能够使用 IRQ 0)。如果产生了多次中断(出现二义性),probe_irq_off 会返回一个负值。
程序员要注意在调用 probe_irq_on 之后启用设备上的中断,并在调用 probe_irq_off之前禁用中断。此外要记住,在 probe_irq_off 之后,需要处理设备上待处理的中断。
(2)x86 平台上中断处理的内幕
下面的描述是从 2.6 内核中的文件 arch/i386/kernel/irq.c、arch/i386/kernel/apic.c、arch/i386/kernel/entry.S、arch/i386/kernel/i8259.c 以及 include/asm-i386/hw_irq.h 中得出的。虽然基本概念是相同的,但是硬件细节还是与其他平台有所区别。
最底层的中断处理代码可见 entry.S 文件,该文件是一个汇编语言文件,完成了许多机器级的工作。这个文件利用几个汇编技巧及一些宏,将一段代码用于所有可能的中断。在所有情况下,这段代码将中断编号压入栈,然后跳转到一个公共段,而这个公共段会调用在 irq.c 中定义的 do_IRQ 函数。
do_IRQ 做的第一件事是应答中断,这样中断控制器就可以继续处理其他的事情了。然后该函数对于给定的 IRQ 号获得一个自旋锁,这样就阻止了任何其他的 CPU 处理这个 IRQ。接着清除几个状态位(包括一个我们很快会讲到的 IRQ_WAITING),然后寻找这个特定 IRQ 的处理例程。如果没有处理例程,就什么也不做;自旋锁被释放,处理任何待处理的软件中断,最后 do_IRQ 返回。
通常,如果设备有一个已注册的处理例程并且发生了中断,则函数:handle_IRQ_event 会被调用以便实际调用处理例程。如果处理例程是慢速类型(即 SA_INTERRUPT 未被设置),将重新启用硬件中断,并调用处理例程。然后只是做一些清理工作,接着运行软件中断,最后返回到常规工作。作为中断的结果(例如,处理例程可以 wake_up 一个进程),“常规工作” 可能已经被改变,所以,从中断返回时发生的最后一件事情可能就是一次处理器的重新调度。
IRQ 的探测是通过为每个缺少中断处理例程的 IRQ 设置 IRQ_WAITING 状态位来完成的。当中断产生时,因为没有注册处理例程,do_IRQ 请除该位然后返回。当 probe_irq_off 被一个驱动程序调用的时候,只需要搜索那些没有设置 IRQ_WAITING 位的 IRQ。
2、 顶半部和底半部
中断处理的一个主要问题是怎样在处理例程内完成耗时的任务。响应一次设备中断需要完成一定数量的工作,但是中断处理例程需要尽快结束而不能使中断阻塞的时间过长,这两个需求(工作和速度)彼此冲突,让驱动程序的作者多少有点困扰。
Linux(连同很多其他的系统)通过将中断处理例程分成两部分来解决这个问题。称为 “顶半部” 的部分,是实际响应中断的例程,也就是用 request_irq 注册的中断例程;而所谓的 “底半部” 是一个被顶半部调度,并在稍后更安全的时间内执行的例程。顶半部处理例程和底半部处理例程之间最大的不同,就是当底半部处理例程执行时,所有的中断都是打开的 —— 这就是所谓的在更安全时间内运行。典型的情况是顶半部保存设备的数据到一个设备特定的缓冲区并调度它的底半部,然后退出: 这个操作是非常快的。然后,底半部执行其他必要的工作,例如唤醒进程、启动另外的 I/O 操作等等。这种方式允许在底半部工作期间,顶半部还可以继续为新的中断服务。
几乎每一个严格的中断处理例程都是以这种方式分成两部分的。例如,当一个网络接口报告有新数据包到达时,处理例程仅仅接收数据并将它推到协议层上,而实际的数据包处理过程是在底半部执行的。
Linux 内核有两种不同的机制可以用来实现底半部处理、我们已经在第七章介绍过这两种机制了。tasklet 通常是底半部处理的优选机制;因为这种机制非常快,但是所有的 tasklet 代码必须是原子的。除了 tasklet 之外,我们还可以选择工作队列,它可以具有更高的延迟,但允许休眠。
下面再次用 short 驱动程序来进行我们的讨论。在使用某个模块选项装载时,可以通知 short 模块使用顶 / 底半部的模式进行中断处理,并采用 tasklet 或者工作队列处理例程。因此,顶半部执行得就很快,因为它仅保存当前时间并调度底半部处理。然后底半部负责这些时间的编码、并唤醒可能等待数据的任何用户进程。
(1)tasklet
记住 tasklet 是一个可以在由系统决定的安全时刻在软件中断上下文被调度运行的特殊函数。它们可以被多次调度运行,但 tasklet 的调度并不会累积;也就是说,实际只会运行一次,即使在激活 tasklet 的运行之前重复请求该 tasklet 的运行也是这样。不会有同一 tasklet 的多个实例并行地运行,因为它们只运行一次,但是 tasklet 可以与其他的 tasklet 并行地运行在对称多处理器(SMP)系统上。这样,如果驱动程序有多个 tasklet、它们必须使用某种锁机制来避免彼此间的冲突。
tasklet 可确保和第一次调度它们的函数运行在同样的 CPU 上。这样,因为 tasklet 在中断处理例程结束前并不会开始运行,所以此时的中断处理例程是安全的。不管怎样,在 tasklet 运行时,当然可以有其他的中断发生,因此在 tasklet 和中断处理例程之间的锁还是需要的。
必须使用宏 DECLARE_TASKLET 声明 tasklet:
DECLARE_TASKLET(name, function, data);
name 是给 tasklet 起的名字,function 是执行 tasklet 时调用的函数(它带有一个 unsigned long 型的参数并且返回 void),data 是一个用来传递给 tasklet 函数的 unsigned long 类型的值。
驱动程序 short 如下声明它自己的 tasklet:
void short_do_tasklet(unsigned long); DECLARE_TASKLET(short_tasklet, short_do_tasklet, 0);
函数 tasklet_schedule 用来调度一个 tasklet 运行。如果指定 tasklet=1 选项装载 short,它就会安装一个不同的中断处理例程,这个处理例程保存数据并如下调度 tasklet:
irqreturn_t short_tl_interrupt(int irq, void *dev_id, struct pt_regs *regs) { /* 强制转换以免出现 "易失性" 警告 */ do_gettimeofday((struct timeval *) tv_head); short_incr_tv(&tv_head); tasklet_schedule(&short_tasklet); short_wq_count++; /* 记录中断的产生 */ return IRQ_HANDLED; }
实际的 tasklet 例程,即 short_do_tasklet,将会在系统方便时得到执行。就像先前提到的,这个例程执行中断处理的大多数任务,如下所示:
void short_do_tasklet(unsigned long unused) { int savecount = short_wq_count, written; short_wq_count = 0; /* 已经从队列中移除 */ /* * 底半部读取由顶半部填充的 tv 数组, * 并向循环文本缓冲区中打印信息,而缓冲区的数据则由 * 读取进程获得 */ /* 首先将调用此 bh 之前发生的中断数量写入 */ written = sprintf((char *)short_head, "bh after %6i\n", savecount); short_incr_bp(&short_head, written); /* * 然后写入时间值。每次写入 16 字节, * 所以它与 PAGE_SIZE 是对齐的 */ do { written = sprintf((char *)short_head, "%08u.%06u\n", (int)(tv_tail->tv_sec % 100000000), (int)(tv_tail->tv_usec)); short_incr_bp(&short_head, written); short_incr_tv(&tv_tail); } while (tv_tail != tv_head); wake_up_interruptible(&short_queue); /* 唤醒任何读取进程 */ }
在其他动作之外,这个 tasklet 记录了自从它上次被调用以来产生了多少次中断。一个类似于 short 的设备可以在很短的时间内产生很多次中断、所以在底半部被执行前,肯定会有多次中断发生。驱动程序必须一直对这种情况有所准备,并且必须能根据顶半部保留的信息知道有多少工作需要完成。
// include/linux/interrupt.h static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) __tasklet_schedule(t); } // kernel/softirq.c void __tasklet_schedule(struct tasklet_struct *t) { unsigned long flags; local_irq_save(flags); t->next = NULL; *__get_cpu_var(tasklet_vec).tail = t; __get_cpu_var(tasklet_vec).tail = &(t->next); raise_softirq_irqoff(TASKLET_SOFTIRQ); local_irq_restore(flags); } EXPORT_SYMBOL(__tasklet_schedule);
(2)工作队列
读者应该记得,工作队列会在将来的某个时间、在某个特殊的工作者进程上下文中调用一个函数。因为工作队列函数运行在进程上下文中,因此可在必要时休眠。但是我们不能从工作队列向用户空间复制数据,除非使用将在第十五章中描述的高级技术,要知道,工作者进程无法访问其他任何进程的地址空间。
如果在装载 short 驱动程序时将 wq 选项设置为非零值,则该驱动程序将使用工作队列作为其底半部进程。它使用系统的默认工作队列,因此不需要其他特殊的设置代码;但是,如果我们的驱动程序具有特殊的延迟需求(或者可能在工作队列函数中长时间休眠),则应该创建我们自己的专用工作队列。我们需要一个 work_struct 结构,该结构如下声明并初始化:
static struct work_struct short_wq; /* 下面这行出现在 short_init() 中 */ INIT_WORK(&short_wq, (void (*)(void *)) short_do_tasklet, NULL);
我们的工作者函数是 short_do_tasklet,该函数已经在先前的小节中介绍过了。
在使用工作队列时,short 构造了另一个中断处理例程,如下所示:
irqreturn_t short_wq_interrupt(int irq, void *dev_id, struct pt_regs *regs) { /* 获取当前的时间信息。 */ do_gettimeofday((struct timeval *) tv_head); short_incr_tv(&tv_head); /* 排序 bh,不必关心多次调度的情况 */ schedule_work(&short_wq); short_wq_count++; /* 记录中断的到达 */ return IRQ_HANDLED; }
读者可以看到,该中断处理例程和 tasklet 版本非常相似,唯一的不同是它调用 schedule_work 来安排底半部处理。
// kernel/workqueue.c int schedule_work(struct work_struct *work) { return queue_work(keventd_wq, work); } EXPORT_SYMBOL(schedule_work); int queue_work(struct workqueue_struct *wq, struct work_struct *work) { int ret; ret = queue_work_on(get_cpu(), wq, work); put_cpu(); return ret; } EXPORT_SYMBOL_GPL(queue_work); int queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work) { int ret = 0; if (!test_and_set_bit(WORK_STRUCT_PENDING, work_data_bits(work))) { BUG_ON(!list_empty(&work->entry)); __queue_work(wq_per_cpu(wq, cpu), work); ret = 1; } return ret; } EXPORT_SYMBOL_GPL(queue_work_on); static void __queue_work(struct cpu_workqueue_struct *cwq, struct work_struct *work) { unsigned long flags; debug_work_activate(work); spin_lock_irqsave(&cwq->lock, flags); insert_work(cwq, work, &cwq->worklist); spin_unlock_irqrestore(&cwq->lock, flags); } static void insert_work(struct cpu_workqueue_struct *cwq, struct work_struct *work, struct list_head *head) { trace_workqueue_insertion(cwq->thread, work); set_wq_data(work, cwq); /* * Ensure that we get the right work->data if we see the * result of list_add() below, see try_to_grab_pending(). */ smp_wmb(); list_add_tail(&work->entry, head); wake_up(&cwq->more_work); }
3、中断共享
“IRQ 冲突” 这种说法和 “PC 架构” 几乎是同义语。通常,PC 上自 IRQ 信号线不能为一个以上的设备服务,它们从来都是不够用的,结果,许多没有经验的用户总是花费很多时间试图找到一种方法使所有的硬件能够协同工作,因此他们不得不总是打开自己计算机的外壳。
当然,现代硬件已经能允许中断的共享了,比如 PCL 总线就要求外设可共享中断。因此,Linux 内核支持所有总线的中断共享,即使在类似 ISA 这样原先并不支持共享的总线上。针对 2.6 内核的设备驱动程序,应该在目标硬件可以支持共享中断操作的情况下处理中断的共享。幸运的是,大多数情况下很容易使用共享的中断。
(1)安装共享的处理例程
就像普通非共享的中断一样,共享的中断也是通过 request_irq 安装的,但是有两处不同:
- 请求中断时,必须指定 flags 参数中的 SA_SHIRQ 位。
dev_id 参数必须是唯一的。任何指向模块地址空间的指针都可以使用,但 dev_id 不能设置成 NULL 。
内核为每个中断维护了一个共享处理例程的列表,这些处理例程的 dev_id 各不相同,就像是设备的签名。如果两个驱动程序在同一个中断上都注册 NULL 作为它们的签名,那么在卸载的时候引起混淆,当中断到达时造成内核出现 oops 消息。由于这个原因,在注册共享中断时如果传递了值为 NULL的 dev_id,现代的内核就会给出警告。当请求一个共享中断时,如果满足下面条件之一,那么 request_irq 就会成功:
中断信号线空闲。
任何已经注册了该中断信号线的处理例程也标识了 IRQ 是共享的。
无论何时,当两个或者更多的驱动程序共享同一根中断信号线,而硬件又通过这根信号线中断处理器时,内核会调用每一个为这个中断注册的处理例程,并将它们自己的 dev_id 传回去。因此,一个共享的处理例程必须能够识别属于自己的中断,并且在自己的设备没有被中断的时候迅速退出。
如果读者在请求中断请求信号线之前需要探测设备的话,则内核不会有所帮助,对于共享的处理例程是没有探测函数可以利用的。仅当要使用的中断信号线处于空闲时,标准的探测机制才能工作。但如果信号线已经被其他具有共享特性的驱动程序占用的话,即使你的驱动已经可以很好的工作了,探测也会失败。幸运的是,多数可共享中断的硬件能够告诉处理器它们在使用哪个中断,这样就消除了显式探测的必要。
释放处理例程同样是通过执行 free_irq 来实现的。这里 dev_id 参数被用来从该中断的共享处理例程列表中选择正确的处理例程来释放,这就是为什么 dev_id 指针必须唯一的原因。
使用共享处理例程的驱动程序需要小心一件事情:不能使用 enable_irq 和 disable_irq。如果使用了,共享中断信号线的其他设备就无法正常工作了;即使在很短的时间内禁用中断,也会因为这种延迟而为设备和其用户带来问题。通常,程序员必须记住他的驱动程序并不独占 IRQ,所以它的行为必须比独占中断信号线时更 “社会化” 。
(2)运行处理例程
如上所述,当内核收到中断时,所有已注册的处理例程都将被调用。一个共享中断处理例程必须能够将要处理的中断和其他设备产生的中断区分开来。
装载 short 时,如果指定 shared=1 选项,则将安装下面的处理例程而不是默认的处理例程:
irqreturn_t short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs) { int value, written; struct timeval tv; /* 如果不是 short 产生的, 则立即返回 */ value = inb(short_base); if (!(value & 0x80)) return IRQ_NONE; /* 清除中断位 */ outb(value & 0x7F, short_base); /* 其余部分没有什么变化 */ do_gettimeofday(&tv); written = sprintf((char *)short_head,"%08u.%06u\n", (int)(tv.tv_sec 100000000), (int)(tv.tv_usec)); short_incr_bp(&short_head, written); wake_up_interruptible(&short_queue); /* 唤醒任何的读取进程 */ return IRQ_HANDLED; }
4、快速参考
本章介绍了与中断管理相关的符号:
#include <linux/interrupt.h> int request_irq(unsigned int irq, irqreturn_t (*handler)(), unsigned long flags, const char *dev_name, void *dev_id); void free_irq(unsigned int irq, void *dev_id); // 上面这些调用用来注册和注销中断处理例程。 #include <linux/irq.h> int can_request_irq(unsigned int irq, unsigned long flags); // 上述函数只在 i386 和 x86_64 体系架构上可用。当试图分配某个给定中断线的请求成功时,则返回非零值。 #include <asm/signal.h> SA_INTERRUPT SA_SHIRQ SA_SAMPLE_RANDOM // request_irq 函数的标志。SA_INTERRUPT要求安装一个快速的处理例程(相对于慢速的)。SA_SHIRQ // 安装一个共享的处理例程,而第三个标志表明中断时间戳可用来产生系统熵。
/proc/interrupts
/proc/stat
这些文件系统节点用于汇报关于硬件中断和已安装处理例程的信息。
unsigned long probe_irq_on(void); int probe_irq_off(unsigned long); // 当驱动程序不得不探测设备,以确定该设备使用哪根中断信号线时,可以使用上述 // 函数。在中断产生之后,probe_irq_on 的返回值必须传回给 probe_irq_off,而 // probe_irq_off 的返回值就是检测到的中断号。
IRQ_NONE
IRQ_HANDLED
IRQ_RETVAL(int x)
中断处理例程的可能返回值,它们表示是否是一个真正来自设备的中断。
void disable_irq(int irq); void disable_irq_nosync(int irq); void enable_irq(int irq); // 驱动程序可以启用和禁用中断报告。如果硬件试图在中断被禁用的时候产生中断, // 中断将永久丢失。使用共享处理例程的驱动程序不能使用这些函数。 void local_irq_save(unsigned long flags); void local_irq_restore(unsigned long flags); // 使 local_irq_save 可禁用本地处理器上的中断,并记录先前的状态。flags 可传 // 递给 local_irq_restore 以恢复先前的中断状态。 void local_irq_disable(void); void local_irq_enable(void); // 用于无条件禁用和启用当前处理器中断的函数。
十一、内核的数据类型
1、快速参考
本章介绍了如下符号:
#include <linux/types.h> typedef u8; typedef u16; typedef u32; typedef u64; // 确保是 8、16、32 和 64 位的无符号整数值类型。对应的有符号类型同样存在。在 // 用户空间,读者可以使用 __u8 和 __u16 等类型。 #include <asm/page.h> PAGE_SIZE PAGE_SHIFT // 定义了当前体系架构的每页字节数和页偏移位数 (4 KB 页为 12、8 KB 页为 13) 的符号。 #include <asm/byteorder.h> __LITTLE_ENDIAN __BIG_ENDIAN // 这两个符号只有一个被定义,取决于体系架构。 #include <asm/byteorder.h> u32 __cpu_to_le32(u32); u32 __le32_to_cpu(u32); // 在已知字节序和处理器字节序之间进行转换的函数。有超过 60 个这样的函数; // 关于它们的完整列表和如何定义,请查阅 include/linux/byteorder/ 下的各种 // 文件 #include <asm/unaligned.h> get_unaligned(ptr); put_unaligned(val, ptr); // 某些体系架构需要使用这些宏来保护未对齐的数据。对于允许访问未对齐数据的 // 体系架构,这些宏扩展为普通的指针取值。 #include <linux/err.h> void *ERR_PTR(long error); long PTR_ERR(const void *ptr); long IS_ERR(const void *ptr); // 这些函数允许从返回指针值的函数中获得错误编码。 #include <linux/list.h> list_add(struct list_head *new, struct list_head *head); list_add_tail(struct list_head *new, struct list_head *head); list_del(struct list_head *entry); list_del_init(struct list_head *entry); list_empty(struct list_head *head); list_entry(entry, type, member); list_move(struct list_head *entry, struct list_head *head); list_move_tail(struct list_head *entry, struct list_head *head); list_splice(struct list_head *list, struct list_head *head); // 操作循环、双向链表的函数 list_for_each(struct list_head *cursor, struct list_head *list) list_for_each_prev(struct list_head *cursor, struct list_head *list) list_for_each_safe(struct list_head *cursor, struct list_head *next, struct list_head *list) list_for_each_entry(type *cursor, struct list_head *list, member) list_for_each_entry_safe(type *cursor, type *next, struct list_head *list, member)
十二、PCI驱动程序
1、PCI接口
外围设备互联 PCI (Peripheral Component Interconnect)。
(1)PCI 寻址
每个 PCI 外设由一个总线编号、一个设备编号及一个功能编号来标识。PCI 规范允许单个系统拥有高达 256 个总线,但是因为 256 个总线对于许多大型系统而言是不够的,因此,Linux 目前支持 PCI 域。每个 PCI 域可以拥有最多 256 个总线。每个总线上可支持 32 个设备,而每个设备都可以是多功能板(例如音频设备外加 CD-ROM 驱动器),最多可有八种功能。所以,每种功能都可以在硬件级由一个 16 位的地址(或键)来标识。不过,为 Linux 编写的设备驱动程序无需处理这些二进制的地址,因为它们使用一种特殊的数据结构(名为 pci_dev)来访问设备。
当前的工作站一般配置有至少两个 PCI 总线。在单个系统中插入多个总线,可通过桥(bridge)来完成,它是用来连接两个总线的特殊 PCI 外设。PCI 系统的整体布局组织为树型,其中每个总线连接到上一级总线,直到树根的 0 号总线。CardBus PC 卡系统也是通过桥连接到 PCI 系统的。典型的 PCI 系统可见图 12-1,其中标记出了各种不同的桥。
尽管和 PCI 外设关联的 16 位硬件地址通常隐藏在 struct pci_dev 对象中,但有时仍然可见,尤其是这些设备正在被使用时。Ispci(pciutils 包的一部分,包含在大多数发行版中)的输出以及 /proc/pci 和 /proc/bus/pci 中信息的布局就是这种情况。PCI 设备在 sysfs 中的表示同样展现了这种寻址方案,此外还有 PCI 域的信息(注1)。在显示硬件地址时,有时显示为两个值(一个 8 位的总线编号和一个 8 位的设备及功能编号),有时显示为三个值(总线、设备和功能),有时显示为四个值(域、总线、设备和功能);所有的值通常都以 16 进制显示。
例如,/proc/bus/pci/devices 使用单个16 位字段(便于解析及排序),而 /proc/bus/busnumber 将地址划分成了三个字段。下面说明了这些地址如何出现,只列出了输出行的开始部分:
这三个设备清单以相同的顺序排列、因为 Ispci 使用 /proc 文件作为其信息来源。以 VGA 视频控制器为例,当划分为域(16 位)、总线(8 位)、设备(5 位)和功能(3 位)时,0x00a0 表示 0000:00:14.0。
每个外设板的硬件电路对如下三种地址空间的查询进行应答:内存位置、I/O 端口和配置寄存器。前两种地址空间由同一 PCI 总线上的所有设备共享(也就是说,在访问内存位置时,该 PCI 总线上的所有设备将在同一时间看到总线周期)。另一方面,配置空间利用了地理寻址(geographical addressing)。配置查询每次只对一个槽寻址,因此它们根本不会发生任何冲突。
对驱动程序而言,内存和 I/O 区域是以惯常的方式,即通过 inb 和 readb 等等进行访问的。另一方面,配置事务是通过调用特定的内核函数访问配置寄存器来执行的。关于中断,每个 PCI 槽有四个中断引脚,每个设备功能可使用其中的一个,而不用考虑这些引脚如何连接到 CPU。这种路由是计算机平台的职责,实现在 PCI 总线之外。因为 PCI 规范要求中断线是可共享的,因此,即使是 IRQ 线有限的处理器(例如 x86)仍然可以容纳许多 PCI 接口板 (每个有四个中断引脚)。
PCI 总线中的 I/O 空间使用 32 位地址总线(因此可有 4 GB 个端口),而内存空间可通过 32 位或 64 位地址来访问。64 位地址在较新的平台上可用。通常假定地址对设备是唯一的,但是软件可能会错误地将两个设备配置成相同的地址,导致无法访问这两个设备。但是,如果驱动程序不去访问那些不应该访问的寄存器,就不会发生这样的问题。幸好,接口板提供的每个内存和 I/O 地址区域,都可以通过配置事务的方式进行重新映射。就是说,固件在系统引导时初始化 PCI 硬件,把每个区域映射到不同的地址以避免冲突(注 2)。这些区域所映射到的地址可从配置空间中读取,因此,Linux 驱动程序不需要探测就能访问其设备。在读取配置寄存器之后,驱动程序就可以安全访问其硬件。
注 2: 实际上,配置并不限于系统引导阶段,比如热插拔设备在引导阶段并不存在,而是在后来才会出现。这里的要点是,设备驱动程序不能修改 I/O 和内存区域的地址。
PCI 配置空间中每个设备功能由 256 个字节组成(除了 PCI 快速设备以外,它的每个功能有 4 KB 的配置空间),配置寄存器的布局是标准化的。配置空间的 4 个字节含有一个独一无二的功能 ID,因此,驱动程序可通过查询外设的特定 ID 来识别其设备(注 3)。概言之,每个设备板是通过地理寻址来获取其配置寄存器的;这些寄存器中的信息随后可以被用来执行普通的 I/O 寻址,而不再需要额外的地理寻址。
注 3:我们可从设备自己的硬件手册中找到 ID。文件 pci.ids 中包含有一个清单,该文件是 pciutils 包和内核源代码的一部分。该文件并不完整,而只是列出了最著名的制造商及设备。该文件的内核版本将在术来的版本中删除。
到此应该清楚的是,PCI 接口标准在 ISA 之上的主要创新在于配置地址空间。因此,除了通常的驱动程序代码之外,PCI 驱动动程序还需要访问配置空间的能力,以便免去冒险探测的工作。
在本章其余内容中,我们将使用 “设备” 一词来表示一种设备功能,因为多功能板上的每个功能都可以担当一个独立实体的角色。我们谈到设备时,表示的是一组 “域编号、总线编号、设备编号和功能编号”。
(2)引导阶段
为了解 PCI 的工作原理,我们需要从系统引导开始讲起,因为这是配置设备的阶段。
当 PCI 设备上电时,硬件保持未激活状态。换句话说,该设备只会对配置事务做出响应。上电时,设备上不会有内存和 I/O 端口映射到计算机的地址空间;其他设备相关功能,例如中断报告,也被禁止。
幸运的是,每个 PCI 主板均配备有能够处理 PCI 的固件,称为 BIOS 、NVRAM 或 PROM,这取决于具体的平台。固件通过读写 PCI 控制器中的寄存器,提供了对设备配置地址空间的访问。
系统引导时,固件(或者 Linux 内核,如果这样配置的话话)在每个 PCI 外设上执行配置事务,以便为它提供的每个地址区域分配一个安全的位置。当驱动程程序访问设备的时候,它的内存和 I/O 区域已经被映射到了处理器的地址空间。驱动程序可以修改这个默认配置,不过从来不需要这样做。
我们曾经讲过,用户可以通过读取 /proc/bus/pci/devices 和 /proc/bus/pci/*/* 来查看 PCI 设备清单和设备的配置寄存器。前者是个包含有十六进制的设备信息的文本文件,而后者是若干二进制文件,报告了每个设备的配置寄存器快照,每个文件对应一个设备。sysfs 树中的个别 PCI 设备目录可以在 /sys/bus/pci/devices 中找到。一个 PCI 设备目录包含许多不同的文件:
config 文件是一个二进制文件,使原始 PCI 配置信息可以从设备读取(就像 /proc/bus/pci/*/* 所提供的)。vendor、device、subsystem_device、subsystem_vendor 和 class 都表示该 PCI 设备的特定值(所有的 PCI 设备都提供这个信息)。irq 文件显示分配给该 PCI 设备的当前 IRQ,resource 文件显示该设备所分配的当前内存资源。
(3)配置寄存器和初始化
在本节中我们将查看 PCI 设备包含的配置寄存器。所有的 PCI 设备都有至少 256 字节的地址空间。前 64 字节是标准化的,而其余的是设备相关的。图 12-2 显示了设备无关的配置空间的布局。
如图 12-2 所示,某些 PCI 配置寄存器是必需的,而某些是可选的。每个 PCI 设备必须在必需的寄存器中包含有效值,而可选寄存器中的内容依赖于外设的实际功能。可选字段通常无用,除非必需字段的内容表明它们是有效的。这样,必需的字段声明了板子的功能,包括其他字段是否有用。
值得注意的是,PCI 寄存器始终是小头的。尽管标准被设计为体系结构无关的,但 PCI 设计者仍然有点偏好 PC 环境。驱动程序编写者在访问多字节的配置寄存器时,要十分注意字节序,因为能够在 PC 上工作的代码到其他平台上可能就无法工作。Linux 开发人员已经注意到了字节序问题(见下一节 “访问配置空间” ),但是这个问题必须被牢记心中。如果需要把数据从系统固有字节序转换成 PCI 字节序,或者相反,则可以借助定义在 中的函数,这些函数在第十一章中介绍过了,注意 PCI 字序是小头的。
对这些配置项的描述已经超过了本书讨论的范围。通常,随设备一同发布的技术文档会详细描述已支持的寄存器。我们所关心的是,驱动程序如何查询设备,以及如何访问设备的配置空间。
用三个或五个 PCI 寄存器可标识一个设备: vendorID、deviceID 和 class 是常用的三个寄存器。每个 PCI 制造商会将正确的值赋予这三个只读寄存器,驱动程序可利用它们查询设备。此外,有时厂商利用 subsystem vendorID 和 subsystem deviceID 两个字段来进一步区分相似的设备。
下面是这些寄存器的详细介绍。
vendorID
这是一个 16 位的寄存器,用于标识硬件制造商。例如,每个 Intel 设备被标识为同一个厂商编号,即 0x8086。PCI Special Interest Group 维护有一个全球的厂商编号注册表,制造商必须申请一个唯一编号并赋于它们的寄存器。
deviceID
这是另外一个 16 位寄存器,由制造商选择;无需对设备 ID 进行官方注册。该 ID 通常和厂商 ID 配对生成一个唯一的 32 位硬件设备标识符。我们使用签名(signature)一词来表示一对厂商和设备 ID。设备驱动程序通常依靠于该签名来识别其设备;可以从硬件手册中找到目标设备的签名值。
class
每个外部设备属于某个类(class),class 寄存器是一个 16 位的值,其中高 8 位标识了 “基类(base class)”、或者组。例如,“ethernet(以太网)” 和 “token ring(令牌环)” 是同属 “network(网络)” 组的两个类,而 “serial(串行)” 和 “parallel(并行)” 类属 “communication(通信)” 组。某些驱动程序可支持多个相似的设备,每个具有不同的签名,但都属于同一个类;这些驱动程序可依靠 class 寄存器来识别它们的外设,如后所述。
subsystem vendorID
subsystem deviceID
这两个字段可用来进一步识别设备。如果设备中的芯片是一个连接到本地板载(onboard)总线上的通用接口芯片,则可能会用于完全不同的多种用途,这时,驱动程序必须识别它所关心的实际设备。子系统标识符就用于此目的。
PCI 驱动程序可以使用这些不同的标识符来告诉内核它支持什么样的设备。struct pci_device_id 结构体用于定义该驱动程序支持的不同类型的 PCI 设备列表。该结构体包含下列字段:
// include/linux/mod_devicetable.h struct pci_device_id { __u32 vendor, device; /* Vendor and device ID or PCI_ANY_ID*/ __u32 subvendor, subdevice; /* Subsystem ID's or PCI_ANY_ID */ __u32 class, class_mask; /* (class,subclass,prog-if) triplet */ kernel_ulong_t driver_data; /* Data private to the driver */ };
__u32 vendor;
__u32 device;
它们指定了设备的 PCI 厂商和设备 ID。如果驱动程序可以处理任何厂商或者设备 ID、这些字段应该使用值 PCI_ANY_ID。
__u32 subvendor;
__u32 subdevice;
它们指定设备的 PCI 子系统厂商和子系统设备 ID。如果驱动程序可以处理任何类型的子系统 ID,这些字段应该使用值 PCI_ANY_ID。
__u32 class;
__u32 class_mask;
这两个值使驱动程序可以指定它支持一种 PCI 类(class)设备。PCI 规范中描述了不同类的 PCI 设备(例如 VGA 控制器)。如果驱动程序可以处理任何类型的子系统 ID,这些字段应该使用值 PCI_ANY_ID。
kernel_ulong_t driver_data;
该值不是用来和设备相匹配的,而是用来保存 PCI 驱动程序用于区分不同设备的信息,如果它需要的话。
应该使用两个辅助宏来进行 struct pci_device_id 结构体的初始化:
PCI_DEVICE(vendor, device)
它创建一个仅和特定厂商及设备 ID 相匹配的 struct pci_device_id 。这个宏把结构体的 subvendor 和 subdevice 字段设置为 PCI_ANY_ID。
PCI_DEVICE_CLASS(device_class, device_class_mask)
它创建一个和特定 PCI 类相匹配的 struct pci_device_id 。
下面的内核文件中给出了一个使用这些宏来定义驱动程序支持的设备类型的例子:
// drivers/usb/host/ehci-hcd.c: static const struct pci_device_id pci_ids[ ] = { { /* 由任何 USB 2.0 EHCI 控制器处理 */ PCI_DEVICE_CLASS(((PCI_CLASS_SERIAL_USB << 8) | 0x20), ~0), .driver_data = (unsigned long) &ehci_driver, }, { /* 结束: 全部为零 */ } }; // drivers/i2c/busses/i2c-i810.c: static struct pci_device_id i810_ids[ ] = { { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82810_IG1) }, { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82810_IG3) ), { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82810E_IG) }, { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82815_CGC) }, { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82845G_IG) }, { 0, }, }
这些例子创建了一个 struct pci_device_id 结构体数组,数组的最后一个值是全部设置为 0 的空结构体。这个 ID 数组被用在 struct pci_driver 中(稍后描述),它还被用于告知用户空间这个特定的驱动程序支持什么设备。
(4)MODULE_DEVICE_TABLE
这个 pci_device_id 结构体需要被导出到用户空间,使热插拔和模块装载系统知道什么模块针对什么硬件设备。宏 MODULE_DEVICE_TABLE 完成这个工作。例子:
MODULE_DEVICE_TABLE(pci, i810_ids);
该语句创建一个名为 __mod_pci_device_table 的局部变量,指向 struct pci_device_id 数组。在稍后的内核构建过程中,depmod 程序在所有的模块中搜索符号 __mod_pci_device_table。如果找到了该符号,它把数据从该模块中抽出,添加到文件 /lib/modules/KERNEL_VERSION/modules.pcimap 中。当 depmod 结束之后,内核模块支持的所有 PCI 设备连同它们的模块名都在该文件中被列出。当内核告知热插拔系统一个新的 PCI 设备已经被发现时,热插拔系统使用 modules.pcimap 文件来寻找要装载的恰当的驱动程序。
(5)注册 PCI 驱动程序
为了正确地注册到内核,所有的 PCI 驱动程序都必须创建的主要结构体是 struct pci_driver 结构体。该结构体由许多回调函数和变量组成,向 PCI 核心描述了 PCI 驱动程序。下面列出了该结构体中 PCI 驱动程序必须注意的字段:
// include/linux/pci.h struct pci_driver { struct list_head node; char *name; const struct pci_device_id *id_table; /* must be non-NULL for probe to be called */ int (*probe) (struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */ void (*remove) (struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */ int (*suspend) (struct pci_dev *dev, pm_message_t state); /* Device suspended */ int (*suspend_late) (struct pci_dev *dev, pm_message_t state); int (*resume_early) (struct pci_dev *dev); int (*resume) (struct pci_dev *dev); /* Device woken up */ void (*shutdown) (struct pci_dev *dev); struct pci_error_handlers *err_handler; struct device_driver driver; struct pci_dynids dynids; };
const char *name;
驱动程序的名字。在内核的所有 PCI 驱动程序中它必须是唯一的,通常被设置为和驱动程序的模块名相同的名字。当驱动程序运行在内核中时,它会出现在 sysfs 的 /sys/bus/pci/drivers/ 下面。
const struct pci_device_id *id_table;
指向本章前面介绍的 struct pci_device_id 表的指针。
int (*probe) (struct pci_dev *dev, const struct pci_device_id *id);
指向 PCI 驱动程序中的探测函数的指针。当 PCI 核心有一个它认为驱动程序需要控制的 struct pci_dev 时,就会调用该函数。PCI 核心用来做判断的 struct pci_device_id 指针也被传递给该函数。如果 PCI 驱动程序确认传递给它的 struct pci_dev,则应该恰当地初始化设备然后返回 0。如果驱动程序不确认该设备,或者发生了错误,它应该返回一个负的错误值。本章稍后将对该函数做更详细的介绍。
void (*remove) (struct pci_dev *dev);
指向一个移除函数的指针,当 struct pci_dev 被从系统中移除,或者 PCI 驱动程序正在从内核中卸载时,PCI 核心调用该函数。本章稍后将对该函数做更详细的介绍。
int (*suspend) (struct pci_dev *dev, u32 state);
指向一个挂起函数的指针,当 struct pci_dev 被挂起时 PCI 核心调用该函数。挂起状态以 state 变量来传递。该函数是可选的,驱动程序不一定要提供。
int (*resume) (struct pci_dev *dev);
指向一个恢复函数的指针,当 struct pci_dev 被恢复时 PCI 核心调用该函数。它总是在挂起函数已经被调用之后被调用。该函数是可选的,驱动程序不一定要提供。
概言之,为了创建一个正确的 struct pci_driver 结构体,只需要初始化四个字段:
static struct pci_driver pci_driver = { .name = "pci_skel". .id_table = ids, .probe = probe, .remove = remove, };
为了把 struct pci_driver 注册到 PCI 核心中,需要调用以 struct pci_driver 指针为参数的 pci_register_driver 函数。通常在 PCI 驱动程序的模块初始化代码中完成该工作:
static int __init pci_skel_init(void) { return pci_register_driver(&pci_driver); }
注意,如果注册成功,pci_register_driver 函数返回 0;否则,返回一个负的错误编号。它不会返回绑定到驱动程序的设备的数量,或者在没有设备绑定到驱动程序时返回一个错误编号。这是 2.6 发布之后的一个变化,基于下列情形的考虑:
在支持 PCI 热插拔的系统或者 CardBus 系统上,PCI 设备可以在任何时刻出现或者消失。如果驱动程序能够在设备出现之前被装载的话是很有帮助的,这样可以减少初始化设备所花的时间。
2.6 内核允许在驱动程序被装载之后动态地分配新的 PCI ID 给它。这是通过文件 new_id 来完成的,该文件位于 sysfs 的所有 PCI 驱动程序目录中。这是非常有用的,如果正在使用的新的设备还没有被内核所认知的话。用户可以把 PCI ID 的值写到 new_id 文件,之后驱动程序就可绑定新的设备。如果在设备没有出现在系统中之前不允许装载驱动程序的话,该接口将不起作用。
当 PCI 驱动程序将要被卸载的时候,需要把 struct pci_driver 从内核注销。这是通过调用 pci_unregister_driver 来完成的。当该函数被调用时,当前绑定到该驱动程序的任何 PCI 设备都被移除,该 PCI 驱动程序的移除函数在 pci_unregister_driver 函数返回之前被调用。
static void __exit pci_skel_exit(void) { pci_unregister_driver(&pci_driver); }
(6)激活 PCI 设备
在 PCI 驱动程序的探测函数中,在驱动程序可以访问 PCI 设备的任何设备资源之前(I/O 区域或者中断),驱动程序必须调用 pci_enable_device 函数:
int pci_enable_device(struct pci_dev *dev);
该函数实际地激话设备。它把设备唤醒,在某些情况下还指派它的中断线和 I/O 区域。CardBus 设备就是这种情况(在驱动程序层和 PCI 完全一样)。
(7)访问配置空间
在驱动程序检测到设备之后,它通常需要读取或写入三个地址空间:内存、端口和配置。对驱动程序而言,对配置空间的访问至关重要,因为这是它找到设备映射到内存和 I/O 空间的什么位置的唯一途径。
因为处理器没有任何直接访问配置空间的途径,因此,计算机厂商必须提供一种办法。为了访问配置空间,CPU 必须读取或写入 PCI 控制器的寄存器,但具体的实现取决于计算机厂商,和我们这里的讨论无关,因为 Linux 提供了访问配置空间的标准接口。
对于驱动程序而言,可通过 8 位、16 位或 32 位的数据传输访问配置空间。相关函数的原型定义在 中:
int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val); int pci_read_config_word(struct pci_dev *dev, int where, u16 *val); int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val);
从由 dev 标识的设备配置空间读入一个、两个或四个字节。where 参数是从配置空间起始位置计算的字节偏移量。从配置空间获得的值通过 val 指针返回,函数本身的返回值是错误码。word 和 dword 函数会将读取到的 little-endian 值转换成处理器固有的字节序,因此,我们自己无需处理字节序。
int pci_write_config_byte(struct pci_dev *dev, int where, u8 val); int pci_write_config_word(struct pci_dev *dev, int where, u16 val); int pci_write_config_dword(struct pci_dev *dev, int where, u32 val);
向配置空间写入一个、两个或四个字节。和上面的函数一样,dev 标识设备,要写入的值通过 val 传递。word 和 dword 函数在把值写入外设之前,会将其转换成小头字节序。
所有前面的函数都实现为 inline 函数,它们实际上调用下面的函数。在驱动程序不能访问 struct pci_dev 的任何时刻,都可以使用这些函数来代替上述函数。
int pci_bus_read_config_byte(struct pci_bus *bus, unsigned int devfn, int where, u8 *val); int pci_bus_read_config_word(struct pci_bus *bus, unsigmed int devfn, int where, u16 *val); int pci_bus_read_config_dword(struct pci_bus *bus, unsigned int devfn, int where, u32 *val);
类似于 pci_read_ 函数,但需用到 pci_bus * 和 devfn 变量,而不用 struct pci_dev *
int pci_bus_write_config_byte (struct pci_bus *bus, unsigned int devfn, int where, u8 val); int pci_bus_write_config_word (struct pci_bus *bus, unsigned int devfn, int where, u16 val); int pci_bus_write_config_dword (struct pci_bus *bus, unsigned int devfn, int where, u32 val);
和 pci_write_ 系列函数类似,但是需要 struct pci_bus * 和 devfn 变量,而不是 struct pci_dev* 。
使用 pci_read 系列函数读取配置变量的首选方法,是使用 中定义的符号名。例如,下面的函数通过给 pci_read_config_byte 函数的 where 参数传递一个符号名来获取设备的修订号 ID。
static unsigned char skel_get_revision(struct pci_dev *dev) { u8 revision; pci_read_config_byte(dev, PCI_REVISION_ID, &revision); return revision; }
(8)访问 I/O 和内存空间
一个 PCI 设备可实现多达 6 个 I/O 地址区域。每个区域可以是内存也可以是 I/O 地址。大多数设备在内存区域实现 I/O 寄存器,因为这通常是一个更明智的方法(如第九章的 “I/O 端口和 I/O 内存” 一节所述)。但是,不像常规内存,I/O 寄存器不应该由 CPU 缓存,因为每次访问都可能有边际作用。将 I/O 寄存器实现为内存区域的 PCI 设备通过在其配置寄存器中设置 “内存是可预取的(memory-is-prefetchable)” 标志来标记这个不同(注 4)。如果内存区域被标记为可预取(prefetchable),则 CPU 可缓存共内容,并进行各种优化。另一方面,对非可预取的(nonprefetchable)内存的访问不能被优化,因为每个访问都可能有边际作用、就像 I/O 端口。把控制寄存器映射到内存地址范围的外设把该范围声明为非可预取的,不过像 PCI 板载视频内存这样的东西是可预取的。在本节中,我们使用 “区域” 一词来表示一般的 I/O 地址空间、包括内存映射的和端口映射。
注 4:该信息保存在 PCI 寄存器基地址的低位中,这些位定义在 中。
一个接口板通过配置寄存器报告其区域的大小和当前位置 —— 即图 12-2 中的 6 个 32 位寄存器,它们的符号名称为 PCI_BASE_ADDRESS_0 到 PCI_BASE_ADDRESS_5。因为 PCI 定义的 I/O 空间是 32 位地址空间,因此,内存和 I/O 使用相同的配置接口是有道理的 。 如果设备使用 64 位的地址总线,它可以为每个区域使用两个连续的 PCI_BASE_ADDRESS 寄存器来声明 64 位内存空间中的区域(低位优先)。对一个设备来说,既提供 32 位区域也提供 64 位区域是可能的。
在内核中,PCI 设备的 I/O 区域已经被集成到通用资源管理。因此,我们无需访问配置变量来了解设备被映射到内存或 I/O 空间的何处。获得区域信息的首选接口由如下函数组成:
unsigned long pci_resource_start(struct pci_dev *dev, int bar);
该函数返回六个 PCI I/O 区域之一的首地址(内存地址或 I/O 端口号)。该区域由
整数的 bar(base address register,基地址寄存器)指定,bar 的取值为 0 到 5。
unsigned long pci_resource_end(struct pci_dev *dev, int bar);
该数返回第 bar 个 I/O 区域的尾地址。注意这是最后一个可用的地址,而不是该区域之后的第一个地址。
unsigned long pci_resource_flags(struct pci_dev *dev, int bar);
该函数返回和该资源相关联的标志。
资源标志用来定义单个资源的某些特性。对与 PCI I/O 区域相关联的 PCI 资源,该信息从基地址寄存器中获得,但对于和 PCI 设备无关的资源,它可能来自其他地方。
所有资源标志定义在 中;下面列出其中最重要的几个:
IORESOURCE_IO
IORESOURCE_MEM
如果相关的 I/O 区域存在,将设置这些标志之一。
IORESOURCE_PREFETCH
IORESOURCE_READONLY
这些标志表明内存区域是否为可预取的和/或是写保护的。对 PCI 资源来说,从来不会设置后面的那个标志。
通过使用 pci_resource_ 系列函数,设备驱动程序可完全忽略底层的 PCI 寄存器,因为系统已经使用这些寄存器构建了资源信息。
(9)PCI 中断
很容易处理 PCI 的中断。在 Linux 的引导阶段,计算机固件已经为设备分配了一个唯一的中断号,驱动程序只需使用该中断号。中断号保存在配置寄存器 60( PCI_INTERRUPT_LINE)中,该寄存器为一个字节宽。这允许多达 256 个中断线,但实际的限制取决于所使用的 CPU。驱动程序无需检测中断号,因为从 PCI_INTERRUPT_LINE 中找到的值肯定是正确的。
如果设备不支持中断,寄存器 61 (PCI_INTERRUPT_PIN)是 0;否则为非零。但是,因为驱动程序知道自己的设备是否是中断驱动的,因此,它通常不需要读取 PCI_INTERRUPT_PIN 寄存器。
这样,处理中断的 PCI 特定代码仅仅需要读取配置字节,以获取保存在一个局部变量中的中断号,如下面的代码所示。否则,要利用第十章的内容。
result = pci_read_config_byte(dev, PCI_INTERRUPT_LINE, &myirq); if (result) { /* 处理错误 */ }
本节的剩余内容为好奇的读者提供了一些附加信息,但它们对编写驱动程序没有多少帮助。
PCI 连接器有四个中断引脚,外设板可使用其中任意一个或者全部。每个引脚被独立连接到主板的中断控制器,因此,中断可被共享而不会出现任何电气问题。然后,中断控制器负责将中断线(引脚)映射到处理器硬件;这一依赖于平台的操作由控制器来完成,这样,总线本身可以获得平台无关性。
位于 PCI_INTERRUPT_PIN 的只读配置寄存器用来告诉计算机实际使用的是哪个引脚。要记得每个设备板可容纳最多 8 个设备;而每个设备使用单独的中断引脚,并在自己的配置寄存器中报告引脚的使用情况。同一设备板上的不同设备可使用不同的中断引脚,或者共享同一个中断引脚。
另一方面,PCI_INTERRUPT_LINE 寄存器是可读 / 写的。在计算机的引导阶段,固件扫描其 PCI 设备,并根据中断引脚如何连接到它的 PCI 槽来设置每个设备的寄存器。这个值由固件分配,因为只有固件知道主板如何将不同的中断引脚连接至处理器。但是,对设备驱动程序而言,PCI_INTERRUPT_LINE 是只读的。有趣的是,新近的 Linux 内核在某些情况下无需借助于 BIOS 就可以分配中断线。
(10)硬件抽象
到此为止,我们通过了解系统如何处理市场上各种各样的 PCI 控制器,已经完整地讨论了 PCI 总线。本节只是提供一些资料,以帮助感兴趣的读者了解内核是如何将面向对象的布局扩展至最底层的。
用于实现硬件抽象的机制,就是包含方法的普通结构。这是一种强有力的技术,它只是在普通的函数调用开销之上增加了对指针取值这样一点最小的开销。在 PCI 管理中,唯一依赖于硬件的操作是读取和写入配置寄存器,因为 PCI 世界中的任何其他工作,都是通过直接读取和写入 I/O 及内存地址空间来完成的,而这些工作是由 CPU 直接控制的。
为此,用于配置寄存器访问的相关结构仅包含 2 个字段:
struct pci_ops { int (*read) (struct pci_bus *bus, unsigned int devfn, int where, int size, u32 *val); int (*write) (struct pci_bus *bus, unsigned int devfn, int where, int size, u32 val); };
该结构在 中定义,并由 drivers/pci/pci.c 使用,后者定义了实际的公共函数。
作用于 PCI 配置空间的这两个函数比对指针取值要花费更多的开销;因为代码是高度面向对象的,它们使用了级联指针,但该开销对于执行次数极少而且从来不会在速度要求很高的路径上执行的操作来说并不是一个问题。例如,pci_read_config_byte(dev, where,val) 的实际实现扩展为:
dev->bus->ops->read (bus, devfn, where, 8, val);
系统中的各种 PCI总线在系统引导阶段得到检测,这时,struct pci_bus 项被创建并与其功能关联起来,其中包括 ops 字段。
通过 “硬件操作” 数据结构实现硬件抽象在 Linux 中很典型。一个重要的例子是 struct alpha_machine_vector 数据结构。该结构体在 中定义,并用来处理各种 Alpha 计算机之间的不同。
2、ISA回顾
3、PC/104 和 PC/104+
4、其他的PC总线
- MCA
- EISA
- VLB
5、SBus
6、NuBus
7、外部总线
8、快速参考
本节总结本章中介绍过的符号:
#include <linux/pci.h> // 这个头文件包含 PCI 寄存器的符号名称,以及若干厂商和设备 ID 值。 struct pci_dev; // 代表内核中 PCI 设备的结构体。 struct pci_driver; // 代表 PCI 驱动程序的结构体。所有的 PCI 驱动程序必须定义该结构体。 struct pci_device_id: // 描述该驱动程序所支持的 PCI 设备类型的结构体。 int pci_register_driver(struct pci_driver *drv); int pci_module_init(struct pci_driver *drv); void pci_unregister_driver(struct pci_driver *drv); // 从内核注册或者注销 PCI 驱动程序的函数。 struct pci_dev *pci_find_device(unsigned int vendor, unsiged int device, struct pci_dev *from); struct pci_dev *pci_find_device_reverse(unsigned int vendor, unsigned int device, const struct pci_dev *from); struct pci_dev *pci_find_subsys (unsigned int vendor, unsigned int device, unsigned int ss_vendor, unsigned int ss_device, const struct pci_dev *from); struct pci_dev *pci_find_class(unsigned int class, struct pci_dev *from) ; // 在设备列表中查找具有特定签名或者属于某一特定类的设备的函数。如果没有找 // 到,返回值为 NULL。 from 被用来继续查找;在第一次调用函数时它必须为 NULL, // 如果想要查找更多的设备,它必须指向前一个找到的设备。这些函数不建议使用, // 应该用 pci_get_ 系列面数代替。 struct pci_dev *pci_get_device(unsigned int vendor, unsigned int device, struct pci_dev *from); struct pci_dev *pci_get_subsys(unsigned int vendor, unsigned int device, unsigned int ss_vendor, unsigned int ss_device, struct pci_dev *from); struct pci_dev *pci_get_slot(struct pci_bus *bus, unsigned int devfn); // 在设备列表中查找具有特定签名或者属于某一特定类的设备的函数。如果没有找 // 到,返回值为 NULL。 from 被用来继续查找;在第一次调用函数时它必须为 NULL, // 如果想要查找更多的设备,它必须指向前一个找到的设备。返回的结构体的引用计 // 数被增加、在使用完该结构体之后,必须调用 pci_dev_put 函数。 int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val); int pci_read_config_word(struct pci_dev *dev, int where, u16 *val); int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val); int pci_write_config_byte (struct pci_dev *dev, int where, u8 *val); int pci_write_config_word (struct pci_dev *dev, int where, u16 *val); int pci_write_config_dword (struct pci_dev *dev, int where, u32 *val); // 读取或者写入 PCI 配置寄存器的函数。尽管 Linux 内核处理了字节序问题,但从单 // 字节装配多字节值时,程序员必须小心处理字节序问题。PCI 总线是小头的。 int pci_enable_device(struct pci_dev *dev); // 激活一个 PCI 设备。 unsigned long pci_resource_start(struct pci_dev *dev, int bar); unsigned long pci_resource_end(struct pci_dev *dev, int bar); unsigned long pci_resource_flags(struct pci_dev *dev, int bar); // 处理 PCI 设备资源的函数。
Linux 设备驱动程序(二)(中):https://developer.aliyun.com/article/1597444