在学习有关I/O总线的内容时,最好先看看相关的知识:
从PC总线到ARM的内部总线
I/O 端口和 I/O 内存
每种外设都是通过读写寄存器来进行控制。
在硬件层,内存区和 I/O 区域没有概念上的区别: 它们都是通过向在地址总线和控制总线发出电平信号来进行访问,再通过数据总线读写数据。
因为外设要与I\O总线匹配,而大部分流行的 I/O 总线是基于个人计算机模型(主要是 x86 家族:它为读和写 I/O 端口提供了独立的线路和特殊的 CPU 指令),所以即便那些没有单独I/O 端口地址空间的处理器,在访问外设时也要模拟成读写I\O端口。这一功能通常由外围芯片组(PC 中的南北桥)或 CPU 中的附加电路实现(嵌入式中的方法) 。
Linux 在所有的计算机平台上实现了 I/O 端口。但不是所有的设备都将寄存器映射到 I/O 端口。虽然ISA设备普遍使用 I/O 端口,但大部分 PCI 设备则把寄存器映射到某个内存地址区,这种 I/O 内存方法通常是首选的。因为它无需使用特殊的处理器指令,CPU 核访问内存更有效率,且编译器在访问内存时在寄存器分配和寻址模式的选择上有更多自由。
I/O 寄存器和常规内存
在进入这部分学习的时候,首先要理解一个概念:side effect,书中译为边际效应,第二版译为副作用。我觉得不管它是怎么被翻译的,都不可能精准表达原作者的意思,所以我个人认为记住side effect就好。下面来讲讲side effect的含义。我先贴出两个网上已有的两种说法(
在这里谢谢两位高人的分享):
第一种说法:
3. side effect(译为边际效应或副作用):是指读取某个地址时可能导致该地址内容发生变化,比如,有些设备的中断状态寄存器只要一读取,便自动清零。I/O 寄存器的操作具有side effect,因此,不能对其操作不能使用cpu缓存。
原文网址:http://qinbh.blog.sohu.com/62733495.html
第二种说法:
说一下我的理解:I/O端口与实际外部设备相关联,通过访问I/O端口控制外部设备,“边际效应”是指控制设备(读取或写入)生效,访问I/O口的主要目的就是边际效应,不像访问普通的内存,只是在一个位置存储或读取一个数值,没有别的含义了。我是基于ARM平台理解的,在《linux设备驱动程序》第二版中的说法是“副作用”,不是“边际效应”。
原文网址:http://linux.chinaunix.net/bbs/viewthread.php?tid=890636&page=1#pid6312646
结合以上两种说法和自己看《Linux设备驱动程序(第3版)》的理解,我个人认为可以这样解释:
side effect 是指:访问I/O寄存器时,不仅仅会像访问普通内存一样影响存储单元的值,更重要的是它可能改变CPU的I/O端口电平、输出时序或CPU对I/O端口电平的反应等等,从而实现CPU的控制功能。CPU在电路中的意义就是实现其side effect 。
I/O 寄存器和 RAM 的主要不同就是 I/O 寄存器操作有side effect, 而内存操作没有。
因为存储单元的访问速度对 CPU 性能至关重要,编译器会对源代码进行优化,主要是: 使用高速缓存保存数值 和 重新编排读/写指令顺序。但对I/O 寄存器操作来说,这些优化可能造成致命错误。因此,驱动程序必须确保在操作I/O 寄存器时,不使用高速缓存,且不能重新编排读/写指令顺序。
解决方法:
硬件缓存问题:只要把底层硬件配置(自动地或者通过 Linux 初始化代码)成当访问 I/O 区域时(不管内存还是端口)禁止硬件缓存即可。
硬件指令重新排序问题:在硬件(或其他处理器)必须以一个特定顺序执行的操作之间设置内存屏障(memory barrier)。
Linux 提供以下宏来解决所有可能的排序问题:
#include <linux/kernel.h> void barrier(void) /*告知编译器插入一个内存屏障但是对硬件没有影响。 编译后的代码会将当前CPU 寄存器中所有修改过的数值保存到内存中, 并当需要时重新读取它 们。可阻止在屏障前后的编译器优化,但硬件能完成自己的重新排序。其实<linux/kernel.h> 中并没有这个函数,因为它是在kernel.h包含的头文件compiler.h中定义的*/ #include <linux/compiler.h> # define barrier() __memory_barrier() #include <asm/system.h> void rmb(void); /*保证任何出现于屏障前的读在执行任何后续的读之前完成*/ void wmb(void); /*保证任何出现于屏障前的写在执行任何后续的写之前完成*/ void mb(void); /*保证任何出现于屏障前的读写操作在执行任何后续的读写操作之前完成*/ void read_barrier_depends(void); /*一种特殊的、弱些的读屏障形式。rmb 阻止屏障前后 的所有读指令的重新排序,read_barrier_depends 只阻止依赖于其他读指令返回的数据的读 指令的重新排序。区别微小, 且不在所有体系中存在。除非你确切地理解它们的差别, 并确 信完整的读屏障会增加系统开销,否则应当始终使用 rmb。*/ /*以上指令是barrier的超集*/ void smp_rmb(void); void smp_read_barrier_depends(void); void smp_wmb(void); void smp_mb(void); /*仅当内核为 SMP 系统编译时插入硬件屏障; 否则, 它们都扩展为一个简单的屏障调用。*/ |
典型的应用:
writel(dev->registers.addr, io_destination_address); writel(dev->registers.size, io_size); writel(dev->registers.operation, DEV_READ); wmb();/*类似一条分界线,上面的写操作必然会在下面的写操作前完成, 但是上面的三个写操作的排序无法保证*/ writel(dev->registers.control, DEV_GO); |
内存屏障影响性能,所以应当只在确实需要它们的地方使用。不同的类型对性能的影响也不同,因此要尽可能地使用需要的特定类型。值得注意的是大部分处理同步的内核原语,例如自旋锁和atomic_t,也可作为内存屏障使用。
某些体系允许赋值和内存屏障组合,以提高效率。它们定义如下:
#define set_mb(var, value) do {var = value; mb();} while 0 /*以下宏定义在ARM体系中不存在*/ #define set_wmb(var, value) do {var = value; wmb();} while 0 #define set_rmb(var, value) do {var = value; rmb();} while 0 |
使用do...while 结构来构造宏是标准 C 的惯用方法,它保证了扩展后的宏可在所有上下文环境中被作为一个正常的 C 语句执行。
使用 I/O 端口
I/O 端口是驱动用来和许多设备之间的通讯方式。
I/O 端口分配
在尚未取得端口的独占访问前,不应对端口进行操作。内核提供了一个注册用的接口,允许驱动程序声明它需要的端口:
#include <linux/ioport.h> struct resource *request_region(unsigned long first, unsigned long n, const char *name); /*告诉内核:要使用从 first 开始的 n 个端口,name 参数为设备名。 若分配成功返回非 NULL,否则将无法使用需要的端口。*/ /*所有的的端口分配显示在 /proc/ioports 中。若不能分配到需要的 端口,则可以到这里看看谁先用了。*/ /*当用完 I/O 端口集(可能在模块卸载时), 应当将它们返回给系统*/ void release_region(unsigned long start, unsigned long n); int check_region(unsigned long first, unsigned long n); /*检查一个给定的 I/O 端口集是否可用,若不可用, 返回值是一个负错 误码。不推荐使用*/ |
操作 I/O 端口
在驱动程序注册I/O 端口后,就可以读/写这些端口。大部分硬件会把8、16和32位端口区分开,不能像访问系统内存那样混淆使用。驱动必须调用不同的函数来存取不同大小的端口。
只支持内存映射的 I/O 寄存器的计算机体系通过重新映射I/O端口到内存地址来伪装端口I/O。为了提高移植性,内核向驱动隐藏了这些细节。Linux 内核头文件(体系依赖的头文件 <asm/io.h> ) 定义了下列内联函数(有的体系是宏,有的不存在)来访问 I/O 端口:
unsigned inb(unsigned port); void outb(unsigned char byte, unsigned port); /*读/写字节端口( 8 位宽 )。port 参数某些平台定义为 unsigned long , 有些为 unsigned short 。 inb 的返回类型也体系而不同。*/ unsigned inw(unsigned port); void outw(unsigned short word, unsigned port); /*访问 16位 端口( 一个字宽 )*/ unsigned inl(unsigned port); void outl(unsigned longword, unsigned port); /*访问 32位 端口。 longword 声明有的平台为 unsigned long , 有的为 unsigned int。*/ |
在用户空间访问 I/O 端口
以上函数主要提供给设备驱动使用,但它们也可在用户空间使用,至少在 PC上可以。 GNU C 库在 <sys/io.h> 中定义了它们。如果在用户空间代码中使用必须满足以下条件:
(1)程序必须使用 -O 选项编译来强制扩展内联函数。
(2)必须用ioperm 和 iopl 系统调用(#include <sys/perm.h>) 来获得对端口 I/O 操作的权限。ioperm 为获取单独端口操作权限,而 iopl 为整个 I/O 空间的操作权限。 (x86 特有的)
(3)程序以 root 来调用 ioperm 和 iopl,或是其父进程必须以 root 获得端口操作权限。(x86 特有的)
若平台没有 ioperm 和 iopl 系统调用,用户空间可以仍然通过使用 /dev/prot 设备文件访问 I/O 端口。注意:这个文件的定义是体系相关的,并且I/O 端口必须先被注册。
串操作
除了一次传输一个数据的I/O操作,一些处理器实现了一次传输一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字,这是所谓的串操作指令。它们完成任务比一个 C 语言循环更快。下列宏定义实现了串I/O,它们有的通过单个机器指令实现;但如果目标处理器没有进行串 I/O 的指令,则通过执行一个紧凑的循环实现。 有的体系的原型如下:
void insb(unsigned port, void *addr, unsigned long count); void outsb(unsigned port, void *addr, unsigned long count); void insw(unsigned port, void *addr, unsigned long count); void outsw(unsigned port, void *addr, unsigned long count); void insl(unsigned port, void *addr, unsigned long count); void outsl(unsigned port, void *addr, unsigned long count); |
使用时注意: 它们直接将字节流从端口中读取或写入。当端口和主机系统有不同的字节序时,会导致不可预期的结果。 使用 inw 读取端口应在必要时自行转换字节序,以匹配主机字节序。
暂停式 I/O
为了匹配低速外设的速度,有时若 I/O 指令后面还紧跟着另一个类似的I/O指令,就必须在 I/O 指令后面插入一个小延时。在这种情况下,可以使用暂停式的I/O函数代替通常的I/O函数,它们的名字以 _p 结尾,如 inb_p、outb_p等等。 这些函数定义被大部分体系支持,尽管它们常常被扩展为与非暂停式I/O 同样的代码。因为如果体系使用一个合理的现代外设总线,就没有必要额外暂停。细节可参考平台的 asm 子目录的 io.h 文件。以下是include\asm-arm\io.h中的宏定义:
#define outb_p(val,port) outb((val),(port)) #define outw_p(val,port) outw((val),(port)) #define outl_p(val,port) outl((val),(port)) #define inb_p(port) inb((port)) #define inw_p(port) inw((port)) #define inl_p(port) inl((port)) #define outsb_p(port,from,len) outsb(port,from,len) #define outsw_p(port,from,len) outsw(port,from,len) #define outsl_p(port,from,len) outsl(port,from,len) #define insb_p(port,to,len) insb(port,to,len) #define insw_p(port,to,len) insw(port,to,len) #define insl_p(port,to,len) insl(port,to,len) |
由此可见,由于ARM使用内部总线,就没有必要额外暂停,所以暂停式的I/O函数被扩展为与非暂停式I/O 同样的代码。
平台相关性
由于自身的特性,I/O 指令与处理器密切相关的,非常难以隐藏系统间的不同。所以大部分的关于端口 I/O 的源码是平台依赖的。以下是x86和ARM所使用函数的总结:
IA-32 (x86)
x86_64
这个体系支持所有的以上描述的函数,端口号是 unsigned short 类型。
ARM
端口映射到内存,支持所有函数。串操作 用C语言实现。端口是 unsigned int 类型。
ARM对io端口的访问使用io内存的方式
使用 I/O 内存
除了 x86上普遍使用的I/O 端口外,和设备通讯另一种主要机制是通过使用映射到内存的寄存器或设备内存,统称为 I/O 内存。因为寄存器和内存之间的区别对软件是透明的。I/O 内存仅仅是类似 RAM 的一个区域,处理器通过总线访问这个区域,以实现设备的访问。
根据平台和总线的不同,I/O 内存可以就是否通过页表访问分类。若通过页表访问,内核必须首先安排物理地址使其对设备驱动程序可见,在进行任何 I/O 之前必须调用 ioremap。若不通过页表,I/O 内存区域就类似I/O 端口,可以使用适当形式的函数访问它们。因为“side effect”的影响,
不管是否需要 ioremap ,都不鼓励直接使用 I/O 内存的指针。而使用专用的 I/O 内存操作函数,不仅在所有平台上是安全,而且对直接使用指针操作 I/O 内存的情况进行了优化。
I/O 内存分配和映射
I/O 内存区域使用前必须先分配,函数接口在 <linux/ioport.h> 定义:
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name); /* 从 start 开始,分配一个 len 字节的内存区域。成功返回一个非NULL指针, 否则返回NULL。所有的 I/O 内存分配情况都 /proc/iomem 中列出。*/ /*I/O内存区域在不再需要时应当释放*/ void release_mem_region(unsigned long start, unsigned long len); /*一个旧的检查 I/O 内存区可用性的函数,不推荐使用*/ int check_mem_region(unsigned long start, unsigned long len); |
然后必须设置一个映射,由 ioremap 函数实现,此函数专门用来为I/O 内存区域分配虚拟地址。经过ioremap 之后,设备驱动即可访问任意的 I/O 内存地址。注意:ioremap 返回的地址不应当直接引用;应使用内核提供的 accessor 函数。以下为函数定义:
#include <asm/io.h> void *ioremap(unsigned long phys_addr, unsigned long size); void *ioremap_nocache(unsigned long phys_addr, unsigned long size); /*如果控制寄存器也在该区域,应使用的非缓存版本,以实现side effect。*/ void iounmap(void * addr); |
访问I/O 内存
访问I/O 内存的正确方式是通过一系列专用于此目的的函数(在 <asm/io.h> 中定义的):
/*I/O 内存读函数*/ unsigned int ioread8(void *addr); unsigned int ioread16(void *addr); unsigned int ioread32(void *addr); /*addr 是从 ioremap 获得的地址(可能包含一个整型偏移量), 返回值是从给定 I/O 内存读取的值*/ /*对应的I/O 内存写函数*/ void iowrite8(u8 value, void *addr); void iowrite16(u16 value, void *addr); void iowrite32(u32 value, void *addr); /*读和写一系列值到一个给定的 I/O 内存地址,从给定的 buf 读或写 count 个值到给定的 addr */ void ioread8_rep(void *addr, void *buf, unsigned long count); void ioread16_rep(void *addr, void *buf, unsigned long count); void ioread32_rep(void *addr, void *buf, unsigned long count); void iowrite8_rep(void *addr, const void *buf, unsigned long count); void iowrite16_rep(void *addr, const void *buf, unsigned long count); void iowrite32_rep(void *addr, const void *buf, unsigned long count); /*需要操作一块 I/O 地址,使用一下函数*/ void memset_io(void *addr, u8 value, unsigned int count); void memcpy_fromio(void *dest, void *source, unsigned int count); void memcpy_toio(void *dest, void *source, unsigned int count); /*旧函数接口,仍可工作, 但不推荐。*/ unsigned readb(address); unsigned readw(address); unsigned readl(address); void writeb(unsigned value, address); void writew(unsigned value, address); void writel(unsigned value, address); |
像 I/O 内存一样使用端口
一些硬件有一个有趣的特性:一些版本使用 I/O 端口,而其他的使用 I/O 内存。为了统一编程接口,使驱动程序易于编写,2.6 内核提供了一个ioport_map函数:
void *ioport_map(unsigned long port, unsigned int count); /*重映射 count 个I/O 端口,使其看起来像 I/O 内存。,此后,驱动程序可以 在返回的地址上使用 ioread8 和同类函数。其在编程时消除了I/O 端口和I/O 内存的区别。 /*这个映射应当在它不再被使用时撤销:*/ void ioport_unmap(void *addr); /*注意:I/O 端口仍然必须在重映射前使用 request_region 分配I/O 端口 。ARM9不支持这两个函数!*/ |
上面是基于
《Linux设备驱动程序(第3版)》的介绍,以下分析 ARM9的s3c2440A的linux驱动接口。
ARM9的linux驱动接口
s3c24x0处理器是使用I/O内存的,也就是说:他们的外设接口是通过读写相应的寄存器实现的,这些寄存器和内存是使用单一的地址空间,并使用和读写内存一样的指令。所以推荐使用I/O内存的相关指令。
但这并不表示I/O端口的指令在s3c24x0中不可用。但是只要你注意其源码,你就会发现:其实I/O端口的指令只是一个外壳,内部还是使用和I/O内存一样的代码。以下列出一些:
I/O端口
#define outb(v,p) __raw_writeb(v,__io(p)) #define outw(v,p) __raw_writew((__force __u16) \ cpu_to_le16(v),__io(p)) #define outl(v,p) __raw_writel((__force __u32) \ cpu_to_le32(v),__io(p)) #define inb(p) ({ __u8 __v = __raw_readb(__io(p)); __v; }) #define inw(p) ({ __u16 __v = le16_to_cpu((__force __le16) \ __raw_readw(__io(p))); __v; }) #define inl(p) ({ __u32 __v = le32_to_cpu((__force __le32) \ __raw_readl(__io(p))); __v; }) |
I/O内存
#define ioread8(p) ({ unsigned int __v = __raw_readb(p); __v; }) #define ioread16(p) ({ unsigned int __v = le16_to_cpu(__raw_readw(p)); __v; }) #define ioread32(p) ({ unsigned int __v = le32_to_cpu(__raw_readl(p)); __v; }) #define iowrite8(v,p) __raw_writeb(v, p) #define iowrite16(v,p) __raw_writew(cpu_to_le16(v), p) #define iowrite32(v,p) __raw_writel(cpu_to_le32(v), p) |
我对I/O端口的指令和I/O内存的指令都写了相应的驱动程序,都通过了测试。在这里值得注意的有4点:
(1)所有的读写指令所赋的地址必须都是虚拟地址,你有两种选择:使用内核已经定义好的地址,如 S3C2440_GPJCON等等,这些都是内核定义好的虚拟地址,有兴趣的可以看源码。还有一种方法就是使用自己用ioremap映射的虚拟地址。绝对不能使用实际的物理地址,否则会因为内核无法处理地址而出现oops。
(2)在使用I/O指令时,可以不使用request_region和request_mem_region,而直接使用outb、ioread等指令。因为request的功能只是告诉内核端口被谁占用了,如再次request,内核会制止。
(3)在使用I/O指令时,所赋的地址数据有时必须通过强制类型转换为 unsigned long ,不然会有警告(具体原因请看Linux设备驱动程序学习(7)-内核的数据类型) 。虽然你的程序可能也可以使用,但是最好还是不要有警告为妙。
(4)在include\asm-arm\arch-s3c2410\hardware.h中定义了很多io口的操作函数,有需要可以在驱动中直接使用,很方便。
实验源码:
IO_port.tar.gz
IO_port_test.tar.gz
IO_mem.tar.gz
IO_mem_test.tar.gz
两个模块都实现了阻塞型独享设备的访问控制,并通知内核不支持llseek。具体的测试在IO_port中。
测试现象如下:
[Tekkaman2440@SBC2440V4]#cd /lib/modules/ [Tekkaman2440@SBC2440V4]#insmod IO_port.ko [Tekkaman2440@SBC2440V4]#insmod IO_mem.ko [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 153 spi 180 usb 189 usb_device 204 s3c2410_serial 251 IO_mem 252 IO_port 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/IO_port c 252 0 [Tekkaman2440@SBC2440V4]#mknod -m 666 /dev/IO_mem c 251 0 [Tekkaman2440@SBC2440V4]#cd /tmp/ [Tekkaman2440@SBC2440V4]#./IO_mem_test io_addr : c485e0d0 IO_mem: the module can not lseek! please input the command :1 IO_mem: ioctl 1 ok! please input the command :8 IO_mem: ioctl STATUS ok!current_status=0X1 please input the command :3 IO_mem: ioctl 3 ok! please input the command :q [Tekkaman2440@SBC2440V4]#./IO_porttest_sleep & [Tekkaman2440@SBC2440V4]#./IO_porttest_sleep & [Tekkaman2440@SBC2440V4]#./IO_porttest_sleep & [Tekkaman2440@SBC2440V4]#./IO_port_test IO_port: the module can not lseek! please input the command :1 IO_port: ioctl 1 ok! please input the command :8 IO_port: ioctl STATUS ok!current_status=0X1 please input the command :3 IO_port: ioctl 3 ok! please input the command :8 IO_port: ioctl STATUS ok! current_status=0X3 please input the command :q [1] Done ./IO_porttest_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] 61 root SW< [kblockd/0] 62 root SW< [ksuspend_usbd] 65 root SW< [khubd] 67 root SW< [kseriod] 79 root SW [pdflush] 80 root SW [pdflush] 81 root SW< [kswapd0] 82 root SW< [aio/0] 709 root SW< [mtdblockd] 710 root SW< [nftld] 711 root SW< [inftld] 712 root SW< [rfdd] 746 root SW< [kpsmoused] 755 root SW< [kmmcd] 773 root SW< [rpciod/0] 782 root 1752 S -sh 783 root 1744 S init 785 root 1744 S init 787 root 1744 S init 790 root 1744 S init 843 root 1336 S ./IO_porttest_sleep 844 root 1336 S ./IO_porttest_sleep 846 root 1744 R ps [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] 61 root SW< [kblockd/0] 62 root SW< [ksuspend_usbd] 65 root SW< [khubd] 67 root SW< [kseriod] 79 root SW [pdflush] 80 root SW [pdflush] 81 root SW< [kswapd0] 82 root SW< [aio/0] 709 root SW< [mtdblockd] 710 root SW< [nftld] 711 root SW< [inftld] 712 root SW< [rfdd] 746 root SW< [kpsmoused] 755 root SW< [kmmcd] 773 root SW< [rpciod/0] 782 root 1752 S -sh 783 root 1744 S init 785 root 1744 S init 787 root 1744 S init 790 root 1744 S init 847 root 1744 R ps [3] + Done ./IO_porttest_sleep [2] + Done ./IO_porttest_sleep |
程序是针对2440的,若是用2410只需要改改测试的io口就好了!
度量时间差
时钟中断由系统定时硬件以周期性的间隔产生,这个间隔由内核根据 HZ 值来设定,HZ 是一个体系依赖的值,在<linux/param.h>中定义或该文件包含的某个子平台相关文件中。作为通用的规则,即便如果知道 HZ 的值,在编程时应当不依赖这个特定值,而始终使用HZ。对于当前版本,我们应完全信任内核开发者,他们已经选择了最适合的HZ值,最好保持 HZ 的默认值。
对用户空间,内核HZ几乎完全隐藏,用户 HZ 始终扩展为 100。当用户空间程序包含 param.h,且每个报告给用户空间的计数器都做了相应转换。对用户来说确切的 HZ 值只能通过 /proc/interrupts 获得:/proc/interrupts 的计数值除以 /proc/uptime 中报告的系统运行时间。
对于ARM体系结构:在<linux/param.h>文件中的定义如下:
#ifdef __KERNEL__ # define HZ CONFIG_HZ /* Internal kernel timer frequency */ # define USER_HZ 100/* 用户空间使用的HZUser interfaces are in "ticks" */ # define CLOCKS_PER_SEC (USER_HZ) /* like times() */ #else # define HZ 100 #endif |
也就是说:HZ 由__KERNEL__和CONFIG_HZ决定。若未定义__KERNEL__,HZ为100;否则为CONFIG_HZ。而CONFIG_HZ是在内核的根目录的.config文件中定义,并没有在make menuconfig的配置选项中出现。Linux的\arch\arm\configs\s3c2410_defconfig文件中的定义为:
# # Kernel Features # # CONFIG_PREEMPT is not set # CONFIG_NO_IDLE_HZ is not set CONFIG_HZ=200 # CONFIG_AEABI is not set # CONFIG_ARCH_DISCONTIGMEM_ENABLE is not set |
所以正常情况下s3c24x0的HZ为200。这一数值在后面的实验中可以证实。
每次发生一个时钟中断,内核内部计数器的值就加一。这个计数器在系统启动时初始化为 0, 因此它代表本次系统启动以来的时钟嘀哒数。这个计数器是一个 64-位 变量( 即便在 32-位的体系上)并且称为 “jiffies_64”。但是驱动通常访问 jiffies 变量(unsigned long)(根据体系结构的不同:可能是 jiffies_64 ,可能是jiffies_64 的低32位)。使用 jiffies 是首选,因为它访问更快,且无需在所有的体系上实现原子地访问 64-位的 jiffies_64 值。
使用 jiffies 计数器
这个计数器和用来读取它的工具函数包含在 <linux/jiffies.h>, 通常只需包含 <linux/sched.h>,它会自动放入 jiffies.h 。 jiffies 和 jiffies_64 必须被当作只读变量。当需要记录当前 jiffies 值(被声明为 volatile 避免编译器优化内存读)时,可以简单地访问这个 unsigned long 变量,如:
#include <linux/jiffies.h> unsigned long j, stamp_1, stamp_half, stamp_n; j = jiffies; /* read the current value */ stamp_1 = j + HZ; /* 1 second in the future */ stamp_half = j + HZ/2; /* half a second */ stamp_n = j + n * HZ / 1000; /* n milliseconds */ |
以下是一些简单的工具宏及其定义:
#define time_after(a,b) \ (typecheck(unsigned long, a) && \ typecheck(unsigned long, b) && \ ((long)(b) - (long)(a) < 0)) #define time_before(a,b) time_after(b,a) #define time_after_eq(a,b) \ (typecheck(unsigned long, a) && \ typecheck(unsigned long, b) && \ ((long)(a) - (long)(b) >= 0)) #define time_before_eq(a,b) time_after_eq(b,a) |
用户空间的时间表述法(struct timeval 和 struct timespec )与内核表述法的转换函数:
#include <linux/time.h> /* #include <linux/jiffies.h> --> \kernel\time.c*/ struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; #endif struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ }; unsigned long timespec_to_jiffies(struct timespec *value); void jiffies_to_timespec(unsigned long jiffies, struct timespec *value); unsigned long timeval_to_jiffies(struct timeval *value); void jiffies_to_timeval(unsigned long jiffies, struct timeval *value); |
访问jiffies_64 对于 32-位 处理器不是原子的,这意味着如果这个变量在你正在读取它们时被更新你可能读到错误的值。若需要访问jiffies_64,内核有一个特别的辅助函数,为你完成适当的锁定:
#include <linux/jiffies.h> u64 get_jiffies_64(void); |
处理器特定的寄存器
若需测量非常短时间间隔或需非常高的精度,可以借助平台依赖的资源。许多现代处理器包含一个随时钟周期不断递增的计数寄存器,他是进行高精度的时间管理任务唯一可靠的方法。最有名的计数器寄存器是 TSC ( timestamp counter), 在 x86 的 Pentium 处理器开始引入并在之后所有的 CPU 中出现(包括 x86_64 平台)。它是一个 64-位 寄存器,计数 CPU 的时钟周期,可从内核和用户空间读取。在包含了 <asm/msr.h> (一个 x86-特定的头文件, 它的名子代表"machine-specific registers")的代码中可使用这些宏:
rdtsc(low32,high32);/*原子地读取 64-位TSC 值到 2 个 32-位 变量*/ rdtscl(low32);/*读取TSC的低32位到一个 32-位 变量*/ rdtscll(var64);/*读 64-位TSC 值到一个 long long 变量*/ /*下面的代码行测量了指令自身的执行时间:*/ unsigned long ini, end; rdtscl(ini); rdtscl(end); printk("time lapse: %li\n", end - ini); |
一些其他的平台提供相似的功能, 并且内核头文件提供一个体系无关的功能用来代替 rdtsc,称 get_cycles(定义在 <asm/timex.h>( 由 <linux/timex.h> 包含)),原型如下:
#include <linux/timex.h> cycles_t get_cycles(void); /*这个函数在每个平台都有定义, 但在没有时钟周期计数器的平台上返回 0 */ /*由于s3c2410系列处理器上没有时钟周期计数器所以get_cycles定义如下:*/ typedef unsigned long cycles_t; static inline cycles_t get_cycles (void) { return 0; } |
获取当前时间
驱动一般无需知道时钟时间(用年月日、小时、分钟、秒来表达的时间),只对用户程序才需要,如 cron 和 syslogd。 内核提供了一个将时钟时间转变为秒数值的函数:
unsigned long mktime(const unsigned int year0, const unsigned int mon0, const unsigned int day, const unsigned int hour, const unsigned int min, const unsigned int sec) { unsigned int mon = mon0, year = year0; /* 1..12 -> 11,12,1..10 */ if (0 >= (int) (mon -= 2)) { mon += 12; /* Puts Feb last since it has leap day */ year -= 1; } return ((((unsigned long) (year/4 - year/100 + year/400 + 367*mon/12 + day) + year*365 - 719499 )*24 + hour /* now have hours */ )*60 + min /* now have minutes */ )*60 + sec; /* finally seconds */ } /*这个函数将时间转换成从1970年1月1日0小时0分0秒到你输入的时间所经过的秒 数,溢出时间为2106-02-07 06:28:16。本人认为这个函数的使用应这样:若你要 计算2000-02-07 06:28:16 到2000-02-09 06:28:16 所经过的秒数: unsigned long time1 = mktime(2000,2,7,6,28,16)-mktime(2000,2,9,6,28,16); 若还要转成jiffies,就再加上: unsigned long time2 = time1*HZ. 注意溢出的情况!*/ |
为了处理绝对时间, <linux/time.h> 导出了 do_gettimeofday 函数,它填充一个指向 struct timeval 的指针变量。绝对时间也可来自 xtime 变量,一个 struct timespec 值,为了原子地访问它,内核提供了函数 current_kernel_time。它们的精确度由硬件决定,原型是:
#include <linux/time.h> void do_gettimeofday(struct timeval *tv); struct timespec current_kernel_time(void); /*得到的数据都表示当前时间距UNIX时间基准1970-01-01 00:00:00的相对时间*/ |
以上两个函数在ARM平台都是通过 xtime 变量得到数据的。
全局变量xtime:它是一个timeval结构类型的变量,用来表示当前时间距UNIX时间基准1970-01-01 00:00:00的相对秒数值。
结构timeval是Linux内核表示时间的一种格式(Linux内核对时间的表示有多种格式,每种格式都有不同的时间精度),其时间精度是微秒。该结构是内核表示时间时最常用的一种格式,它定义在头文件include/linux/time.h中,如下所示:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
其中,成员tv_sec表示当前时间距UNIX时间基准的秒数值,而成员tv_usec则表示一秒之内的微秒值,且1000000>tv_usec>=0。
Linux内核通过timeval结构类型的全局变量xtime来维持当前时间,该变量定义在kernel/timer.c文件中,如下所示:
/* The current time */
volatile struct timeval xtime __attribute__ ((aligned (16)));
但是,全局变量xtime所维持的当前时间通常是供用户来检索和设置的,而其他内核模块通常很少使用它(其他内核模块用得最多的是jiffies),因此对xtime的更新并不是一项紧迫的任务,所以这一工作通常被延迟到时钟中断的底半部(bottom half)中来进行。由于bottom half的执行时间带有不确定性,因此为了记住内核上一次更新xtime是什么时候,Linux内核定义了一个类似于jiffies的全局变量wall_jiffies,来保存内核上一次更新xtime时的jiffies值。时钟中断的底半部分每一次更新xtime的时侯都会将wall_jiffies更新为当时的jiffies值。全局变量wall_jiffies定义在kernel/timer.c文件中:
/* jiffies at the most recent update of wall time */
unsigned long wall_jiffies;
原文网址:http://blog.csdn.net/freedom1013/archive/2007/03/13/1528310.aspx
延迟执行
设备驱动常常需要延后一段时间执行一个特定片段的代码, 常常允许硬件完成某个任务.
长延迟
有时,驱动需要延后执行相对长时间,长于一个时钟嘀哒。
忙等待(尽量别用)
若想延迟执行若干个时钟嘀哒,精度要求不高。最容易的( 尽管不推荐 ) 实现是一个监视 jiffy 计数器的循环。这种忙等待实现的代码如下:
while (time_before(jiffies, j1)) cpu_relax(); |
对 cpu_relex 的调用将以体系相关的方式执行,在许多系统中它根本不做任何事,这个方法应当明确地避免。对于ARM体系来说:
#define cpu_relax() barrier() |
也就是说在ARM上运行忙等待相当于:
while (time_before(jiffies, j1)) ; |
这种忙等待严重地降低了系统性能。如果未配置内核为抢占式, 这个循环在延时期间完全锁住了处理器,计算机直到时间 j1 到时会完全死掉。如果运行一个可抢占的内核时会改善一点,但是忙等待在可抢占系统中仍然是浪费资源的。更糟的是, 当进入循环时如果中断碰巧被禁止, jiffies 将不会被更新, 并且 while 条件永远保持真,运行一个抢占的内核也不会有帮助, 唯一的解决方法是重启。
让出处理器
忙等待加重了系统负载,必须找出一个更好的技术:不需要CPU时释放CPU 。 这可通过调用schedule函数实现(在 <linux/sched.h> 中声明):
while (time_before(jiffies, j1)) { schedule(); } |
在计算机空闲时运行空闲任务(进程号 0, 由于历史原因也称为swapper)可减轻处理器工作负载、降低温度、增加寿命。
超时
实现延迟的最好方法应该是让内核为我们完成相应的工作。
(1)若驱动使用一个等待队列来等待某些其他事件,并想确保它在一个特定时间段内运行,可使用:
#include <linux/wait.h> long wait_event_timeout(wait_queue_head_t q, condition, long timeout); long wait_event_interruptible_timeout(wait_queue_head_t q, condition, long timeout); /*这些函数在给定队列上睡眠, 但是它们在超时(以 jiffies 表示)到后返回。 如果超时,函数返回 0; 如果这个进程被其他事件唤醒,则返回以 jiffies 表示的剩余的延迟实现;返回值从不会是负值*/ |
(2)为了实现进程在超时到期时被唤醒而又不等待特定事件(避免声明和使用一个多余的等待队列头),内核提供了 schedule_timeout 函数:
#include <linux/sched.h> signed long schedule_timeout(signed long timeout); /*timeout 是要延时的 jiffies 数。除非这个函数在给定的 timeout 流失前 返回,否则返回值是 0 。schedule_timeout 要求调用者首先设置当前的进程 状态。为获得一个不可中断的延迟, 可使用 TASK_UNINTERRUPTIBLE 代替。如 果你忘记改变当前进程的状态, 调用 schedule_time 如同调用 shcedule,建 立一个不用的定时器。一个典型调用如下:*/ set_current_state(TASK_INTERRUPTIBLE); schedule_timeout (delay); |
短延迟
当一个设备驱动需要处理硬件的延迟(latency潜伏期), 涉及到的延时通常最多几个毫秒,在这个情况下, 不应依靠时钟嘀哒,而是内核函数 ndelay, udelay和 mdelay ,他们分别延后执行指定的纳秒数, 微秒数或者毫秒数,定义在 <asm/delay.h>,原型如下:
#include <linux/delay.h> void ndelay(unsigned long nsecs); void udelay(unsigned long usecs); void mdelay(unsigned long msecs); |
重要的是记住这 3 个延时函数是忙等待; 其他任务在时间流失时不能运行。每个体系都实现 udelay, 但是其他的函数可能未定义; 如果它们没有定义, <linux/delay.h> 提供一个缺省的基于 udelay 的版本。在所有的情况中, 获得的延时至少是要求的值, 但可能更多。udelay 的实现使用一个软件循环, 它基于在启动时计算的处理器速度和使用整数变量 loos_per_jiffy确定循环次数。
为避免在循环计算中整数溢出, 传递给udelay 和 ndelay的值有一个上限,如果你的模块无法加载和显示一个未解决的符号:__bad_udelay, 这意味着你调用 udleay时使用太大的参数。
作为一个通用的规则:若试图延时几千纳秒, 应使用 udelay 而不是 ndelay; 类似地, 毫秒规模的延时应当使用 mdelay 完成而不是一个更细粒度的函数。
有另一个方法获得毫秒(和更长)延时而不用涉及到忙等待的方法是使用以下函数(在<linux/delay.h> 中声明):
void msleep(unsigned int millisecs); unsigned long msleep_interruptible(unsigned int millisecs); void ssleep(unsigned int seconds) |
若能够容忍比请求的更长的延时,应使用 schedule_timeout, msleep 或 ssleep。
内核定时器
当需要调度一个以后发生的动作, 而在到达该时间点时不阻塞当前进程, 则可使用内核定时器。内核定时器用来调度一个函数在将来一个特定的时间(基于时钟嘀哒)执行,从而可完成各类任务。
内核定时器是一个数据结构, 它告诉内核在一个用户定义的时间点使用用户定义的参数执行一个用户定义的函数,函数位于 <linux/timer.h> 和 kernel/timer.c 。被调度运行的函数几乎确定不会在注册它们的进程在运行时运行,而是异步运行。实际上, 内核定时器通常被作为一个"软件中断"的结果而实现。当在进程上下文之外(即在中断上下文)中运行程序时, 必须遵守下列规则:
(1)不允许访问用户空间;
(2)current 指针在原子态没有意义;
(3)不能进行睡眠或者调度. 例如:调用 kmalloc(..., GFP_KERNEL) 是非法的,信号量也不能使用因为它们可能睡眠。
通过调用函数 in_interrupt()能够告知是否它在中断上下文中运行,无需参数并如果处理器当前在中断上下文运行就返回非零。
通过调用函数 in_atomic()能够告知调度是否被禁止,若调度被禁止返回非零; 调度被禁止包含硬件和软件中断上下文以及任何持有自旋锁的时候。
在后一种情况, current 可能是有效的,但是访问用户空间是被禁止的,因为它能导致调度发生. 当使用 in_interrupt()时,都应考虑是否真正该使用的是 in_atomic 。他们都在 <asm/hardirq.h> 中声明。
内核定时器的另一个重要特性是任务可以注册它本身在后面时间重新运行,因为每个 timer_list 结构都会在运行前从激活的定时器链表中去连接,因此能够立即链入其他的链表。一个重新注册它自己的定时器一直运行在同一个 CPU.
即便在一个单处理器系统,定时器是一个潜在的态源,这是异步运行直接结果。因此任何被定时器函数访问的数据结构应当通过原子类型或自旋锁被保护,避免并发访问。
定时器 API
内核提供给驱动许多函数来声明、注册以及删除内核定时器:
#include <linux/timer.h> struct timer_list { struct list_head entry; unsigned long expires;
/*期望定时器运行的绝对 jiffies 值,不是一个 jiffies_64 值, 因为定时器不被期望在将来很久到时*/ void (*function)(unsigned long); /*期望调用的函数*/ unsigned long data;/*传递给函数的参数,若需要在参数中传 递多个数据项,可以将它们捆绑成单个数据结构并且将它的指针强制 转换为 unsiged long 的指针传入。这种做法在所有支持的体系上都 是安全的并且在内存管理中相当普遍*/ struct tvec_t_base_s *base; #ifdef CONFIG_TIMER_STATS void *start_site; char start_comm[16]; int start_pid; #endif }; /*这个结构必须在使用前初始化,以保证所有的成员被正确建立 (包括那些对调用者不透明的初始化):*/ void init_timer(struct timer_list *timer); struct timer_list TIMER_INITIALIZER(_function, _expires, _data); /*在初始化后和调用 add_timer 前,可以改变 3 个公共成员 : expires、function和data*/ void add_timer(struct timer_list * timer); int del_timer(struct timer_list * timer); /*在到时前禁止一个已注册的定时器*/ int del_timer_sync(struct timer_list *timer); /*如同 del_timer ,但 还保证当它返回时, 定时器函数不在任何 CPU 上运行,以避免在 SMP 系统上 竞态, 并且在 单处理器内核中和 del_timer 相同。这个函数应当在大部分情 况下优先考虑。 如果它被从非原子上下文调用, 这个函数可能睡眠,但是在 其他情况下会忙等待。当持有锁时要小心调用 del_timer_sync ,如果这个定 时器函数试图获得同一个锁, 系统会死锁。如果定时器函数重新注册自己, 调 用者必须首先确保这个重新注册不会发生; 这通常通过设置一个" 关闭 "标志 来实现, 这个标志被定时器函数检查*/ int mod_timer(struct timer_list *timer, unsigned long expires); /*更新一个定时器的超时时间, 常用于超时定时器。也可在正常使用 add_timer时 在不活动的定时器上调用mod_timer*/ int timer_pending(const struct timer_list * timer); /*通过调用timer_list结构中一个不可见的成员,返回定时器是否在被调度运行*/ |
内核定时器的实现《LDD3》介绍的比较笼统,以后看《ULK3》的时候再细细研究。
一个内核定时器还远未完善,因为它受到 jitter 、硬件中断,还有其他定时器和其他异步任务的影响。虽然一个简单数字 I/O关联的定时器对简单任务是足够的,但不合适在工业环境中的生产系统,对于这样的任务,你将最可能需要实时内核扩展(RT-Linux).
Tasklets
另一个有关于定时的内核设施是 tasklet。它类似内核定时器:在中断时间运行且运行同一个 CPU 上, 并接收一个 unsigned long 参数。不同的是:无法要求在一个指定的时间执行函数,只能简单地要求它在以后的一个由内核选择的时间执行。它对于中断处理特别有用:硬件中断必须尽快处理, 但大部分的数据管理可以延后到以后安全的时间执行。 实际上, 一个 tasket, 就象一个内核定时器, 在一个"软中断"的上下文中执行(以原子模式)。软件中断是在使能硬件中断时执行异步任务的一个内核机制。
tasklet 以一个数据结构形式存在,使用前必须被初始化。初始化能够通过调用一个特定函数或者通过使用某些宏定义声明结构:
#include <linux/interrupt.h> struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; }; void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data); #define DECLARE_TASKLET(name, func, data) \ struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data } #define DECLARE_TASKLET_DISABLED(name, func, data) \ struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data } void tasklet_disable(struct tasklet_struct *t); /*函数暂时禁止给定的 tasklet被 tasklet_schedule 调度,直到这个 tasklet 被再次被enable;若这个 tasklet 当前在运行, 这个函数忙等待直到这个tasklet退出*/ void tasklet_disable_nosync(struct tasklet_struct *t); /*和tasklet_disable类似,但是tasklet可能仍然运行在另一个 CPU */ void tasklet_enable(struct tasklet_struct *t); /*使能一个之前被disable的 tasklet;若这个 tasklet 已经被调度, 它会很快运行。 tasklet_enable 和tasklet_disable必须匹配调用, 因为内核跟踪每个 tasklet 的"禁止次数"*/ void tasklet_schedule(struct tasklet_struct *t); /*调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行; 这保证 了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet 重新调度它自己*/ void tasklet_hi_schedule(struct tasklet_struct *t); /*和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时, 它处理高 优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函 数, 可避免其他软件中断处理引入的附加周期*/ void tasklet_kill(struct tasklet_struct *t); /*确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被 调用。如果 tasklet 正在运行, 这个函数等待直到它执行完毕。若 tasklet 重新调度 它自己,则必须阻止在调用 tasklet_kill 前它重新调度它自己,如同使用 del_timer_sync*/ |
tasklet 的特点:
(1)一个 tasklet 能够被禁止并且之后被重新使能; 它不会执行,直到它被使能与被禁止相同的的次数;
(2)如同定时器, 一个 tasklet 可以注册它自己;
(3)一个 tasklet 能被调度来执行以正常的优先级或者高优先级;
(4) 如果系统不在重负载下,taslet 可能立刻运行, 但是从不会晚于下一个时钟嘀哒;
(5)一个 tasklet 可能和其他 tasklet 并发, 但是它自己是严格地串行的 ,且tasklet 从不同时运行在不同处理器上,通常在调度它的同一个 CPU 上运行。
工作队列
工作队列类似 taskets,允许内核代码请求在将来某个时间调用一个函数,不同在于:
(1)tasklet 在软件中断上下文中运行,所以 tasklet 代码必须是原子的。而工作队列函数在一个特殊内核进程上下文运行,有更多的灵活性,且能够休眠。
(2)tasklet 只能在最初被提交的处理器上运行,这只是工作队列默认工作方式。
(3)内核代码可以请求工作队列函数被延后一个给定的时间间隔。
(4)tasklet 执行的很快, 短时期, 并且在原子态, 而工作队列函数可能是长周期且不需要是原子的,两个机制有它适合的情形。
工作队列有 struct workqueue_struct 类型,在 <linux/workqueue.h> 中定义。一个工作队列必须明确的在使用前创建,宏为:
struct workqueue_struct *create_workqueue(const char *name); struct workqueue_struct *create_singlethread_workqueue(const char *name); |
每个工作队列有一个或多个专用的进程("内核线程"), 这些进程运行提交给这个队列的函数。 若使用 create_workqueue, 就得到一个工作队列它在系统的每个处理器上有一个专用的线程。在很多情况下,过多线程对系统性能有影响,如果单个线程就足够则使用 create_singlethread_workqueue 来创建工作队列。
提交一个任务给一个工作队列,在这里《LDD3》介绍的内核2.6.10和我用的新内核2.6.22.2已经有不同了,老接口已经不能用了,编译会出错。这里我只讲2.6.22.2的新接口,至于老的接口我想今后内核不会再有了。从这一点我们可以看出内核发展。
/*需要填充work_struct或delayed_work结构,可以在编译时完成, 宏如下: */ struct work_struct { atomic_long_t data; #define WORK_STRUCT_PENDING 0 /* T if work item pending execution */ #define WORK_STRUCT_FLAG_MASK (3UL) #define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK) struct list_head entry; work_func_t func; }; struct delayed_work { struct work_struct work; struct timer_list timer; }; DECLARE_WORK(n, f) /*n 是声明的work_struct结构名称, f是要从工作队列被调用的函数*/ DECLARE_DELAYED_WORK(n, f) /*n是声明的delayed_work结构名称, f是要从工作队列被调用的函数*/ /*若在运行时需要建立 work_struct 或 delayed_work结构, 使用下面 2 个宏定义:*/ INIT_WORK(struct work_struct *work, void (*function)(void *)); PREPARE_WORK(struct work_struct *work, void (*function)(void *)); INIT_DELAYED_WORK(struct delayed_work *work, void (*function)(void *)); PREPARE_DELAYED_WORK(struct delayed_work *work, void (*function)(void *)); /* INIT_* 做更加全面的初始化结构的工作,在第一次建立结构时使用. PREPARE_* 做 几乎同样的工作, 但是它不初始化用来连接 work_struct或delayed_work 结构到工作 队列的指针。如果这个结构已经被提交给一个工作队列, 且只需要修改该结构,则使用 PREPARE_* 而不是 INIT_* */ /*有 2 个函数来提交工作给一个工作队列:*/ int queue_work(struct workqueue_struct *queue, struct work_struct *work); int queue_delayed_work(struct workqueue_struct *queue, struct delayed_work *work, unsigned long delay); /*每个都添加work到给定的workqueue。如果使用 queue_delay_work, 则实际的工作 至少要经过指定的 jiffies 才会被执行。 这些函数若返回 1 则工作被成功加入到队 列; 若为0,则意味着这个 work 已经在队列中等待,不能再次加入*/ |
在将来的某个时间, 这个工作函数将被传入给定的 data 值来调用。这个函数将在工作线程的上下文运行, 因此它可以睡眠 (你应当知道这个睡眠可能影响提交给同一个工作队列的其他任务) 工作函数不能访问用户空间,因为它在一个内核线程中运行, 完全没有对应的用户空间来访问。
取消一个挂起的工作队列入口项可以调用:
int cancel_delayed_work(struct delayed_work *work); void cancel_work_sync(struct work_struct *work) |
如果这个入口在它开始执行前被取消,则返回非零。内核保证给定入口的执行不会在调用 cancel_delay_work 后被初始化. 如果 cancel_delay_work 返回 0, 但是, 这个入口可能已经运行在一个不同的处理器, 并且可能仍然在调用 cancel_delayed_work 后在运行. 要绝对确保工作函数没有在 cancel_delayed_work 返回 0 后在任何地方运行, 你必须跟随这个调用来调用:
void flush_workqueue(struct workqueue_struct *queue); |
在 flush_workqueue 返回后, 没有在这个调用前提交的函数在系统中任何地方运行。
而cancel_work_sync会取消相应的work,但是如果这个work已经在运行那么cancel_work_sync会阻塞,直到work完成并取消相应的work。
当用完一个工作队列,可以去掉它,使用:
void destroy_workqueue(struct workqueue_struct *queue); |
共享队列
在许多情况下, 设备驱动不需要它自己的工作队列。如果你只偶尔提交任务给队列, 简单地使用内核提供的共享的默认的队列可能更有效。若使用共享队列,就必须明白将和其他人共享它,这意味着不应当长时间独占队列(不能长时间睡眠), 并且可能要更长时间才能获得处理器。
使用的顺序:
(1) 建立 work_struct 或 delayed_work
static struct work_struct jiq_work; static struct delayed_work jiq_work_delay; /* this line is in jiq_init() */ INIT_WORK(&jiq_work, jiq_print_wq); INIT_DELAYED_WORK(&jiq_work_delay, jiq_print_wq); |
(2)提交工作
int schedule_work(&jiq_work);/*对于work_struct结构*/ int schedule_delayed_work(&jiq_work_delay, delay);/*对于delayed_work结构*/ /*返回值的定义和 queue_work 一样*/ |
若需取消一个已提交给工作队列入口项, 可以使用 cancel_delayed_work和cancel_work_sync, 但刷新共享队列需要一个特殊的函数:
void flush_scheduled_work(void); |
因为不知道谁可能使用这个队列,因此不可能知道 flush_schduled_work 返回需要多长时间。
ARM9 s3c2440AL 实验
jit模块:jit
jiq模块:jiq
实验数据:
[Tekkaman2440@SBC2440V4]#cd /lib/modules/ [Tekkaman2440@SBC2440V4]#insmod jit.ko [Tekkaman2440@SBC2440V4]#head -6 /proc/currentime 0x0002b82f 0x000000010002b82f 1191.119051 1191.115000000 0x0002b82f 0x000000010002b82f 1191.119204 1191.115000000 0x0002b82f 0x000000010002b82f 1191.119230 1191.115000000 [Tekkaman2440@SBC2440V4]#dd bs=20 count=5 < /proc/jitbusy 201604 201804 201804 202004 202004 202204 202204 202404 202404 202604 5+0 records in 5+0 records out [Tekkaman2440@SBC2440V4]#dd bs=20 count=5 < /proc/jitsched 212640 212840 212840 213040 213040 213240 213240 213440 213440 213640 5+0 records in 5+0 records out [Tekkaman2440@SBC2440V4]#dd bs=20 count=5 < /proc/jitqueue 218299 218499 218499 218699 218699 218899 218899 219099 219099 219299 5+0 records in 5+0 records out [Tekkaman2440@SBC2440V4]#dd bs=20 count=5 < /proc/jitschedto 228413 228613 228613 228813 228813 229013 229013 229213 229213 229413 5+0 records in 5+0 records out [Tekkaman2440@SBC2440V4]#cat /proc/jitimer time delta inirq pid cpu command 236945 0 0 832 0 cat 236955 10 1 0 0 swapper 236965 10 1 0 0 swapper 236975 10 1 0 0 swapper 236985 10 1 0 0 swapper 236995 10 1 0 0 swapper [Tekkaman2440@SBC2440V4]#cat /proc/jitasklet time delta inirq pid cpu command 238437 0 0 833 0 cat 238437 0 1 3 0 ksoftirqd/0 238437 0 1 3 0 ksoftirqd/0 238437 0 1 3 0 ksoftirqd/0 238437 0 1 3 0 ksoftirqd/0 238437 0 1 3 0 ksoftirqd/0 [Tekkaman2440@SBC2440V4]#cat /proc/jitasklethi time delta inirq pid cpu command 239423 0 0 834 0 cat 239423 0 1 3 0 ksoftirqd/0 239423 0 1 3 0 ksoftirqd/0 239423 0 1 3 0 ksoftirqd/0 239423 0 1 3 0 ksoftirqd/0 239423 0 1 3 0 ksoftirqd/0 [Tekkaman2440@SBC2440V4]#insmod jiq.ko [Tekkaman2440@SBC2440V4]#cat /proc/jiqwq time delta preempt pid cpu command 405005 0 0 5 0 events/0 405005 0 0 5 0 events/0 405005 0 0 5 0 events/0 405005 0 0 5 0 events/0 405005 0 0 5 0 events/0 405005 0 0 5 0 events/0 405005 0 0 5 0 events/0 405005 0 0 5 0 events/0 405005 0 0 5 0 events/0 405005 0 0 5 0 events/0 405005 0 0 5 0 events/0 [Tekkaman2440@SBC2440V4]#cat /proc/jiqwqdelay time delta preempt pid cpu command 406114 1 0 5 0 events/0 406115 1 0 5 0 events/0 406116 1 0 5 0 events/0 406117 1 0 5 0 events/0 406118 1 0 5 0 events/0 406119 1 0 5 0 events/0 406120 1 0 5 0 events/0 406121 1 0 5 0 events/0 406122 1 0 5 0 events/0 406123 1 0 5 0 events/0 406124 1 0 5 0 events/0 [Tekkaman2440@SBC2440V4]#cat /proc/jiqtimer time delta preempt pid cpu command 420605 0 0 853 0 cat 420805 200 256 0 0 swapper [Tekkaman2440@SBC2440V4]#cat /proc/jiqtasklet time delta preempt pid cpu command 431905 0 256 3 0 ksoftirqd/0 431905 0 256 3 0 ksoftirqd/0 431905 0 256 3 0 ksoftirqd/0 431905 0 256 3 0 ksoftirqd/0 431905 0 256 3 0 ksoftirqd/0 431905 0 256 3 0 ksoftirqd/0 431905 0 256 3 0 ksoftirqd/0 431905 0 256 3 0 ksoftirqd/0 431905 0 256 3 0 ksoftirqd/0 431905 0 256 3 0 ksoftirqd/0 431905 0 256 3 0 ksoftirqd/0 |
可以让设备在产生某个事件时通知处理器的方法就是中断。一个“中断”仅是一个信号,当硬件需要获得处理器对它的关注时,就可以发送这个信号。 Linux 处理中断的方式非常类似在用户空间处理信号的方式。 大多数情况下,一个驱动只需要为它的设备的中断注册一个处理例程,并当中断到来时进行正确的处理。本质上来讲,中断处理例程和其他的代码并行运行。因此,它们不可避免地引起并发问题,并竞争数据结构和硬件。 透彻地理解并发控制技术对中断来讲非常重要。
安装中断处理例程
内核维护了一个中断信号线的注册表,类似于 I/O 端口的注册表。模块在使用中断前要先请求一个中断通道(或者 IRQ中断请求),并在使用后释放它。所用的函数声明在 <linux/interrupt.h> (在此文件中并未真正包含,是通过它include的文件间接包含的,函数在/kernel/irq/Manage.h中),中断注册和释放的函数接口如下:
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) :安装的处理函数指针。
unsigned long flags :一个与中断管理相关的位掩码选项。
const char *dev_name :传递给 request_irq 的字符串,用来在 /proc/interrupts 来显示中断的拥有者。
void *dev_id :用于共享中断信号线的指针。它是唯一的标识,在中断线空闲时可以使用它,驱动程序也可以用它来指向自己的私有数据区(来标识哪个设备产生中断)。若中断没有被共享,dev_id 可以设置为 NULL,但推荐用它指向设备的数据结构。
flags 中可以设置的位如下:
SA_INTERRUPT :快速中断标志。快速中断处理例程运行在当前处理器禁止中断的状态下。
SA_SHIRQ : 在设备间共享中断标志。
SA_SAMPLE_RANDOM :该位表示产生的中断能对 /dev/random 和 /dev/urandom 使用的熵池(entropy pool)有贡献。 读取这些设备会返回真正的随机数,从而有助于应用程序软件选择用于加密的安全密钥。 若设备以真正随机的周期产生中断,就应当设置这个标志。若设备中断是可预测的,这个标志不值得设置。可能被攻击者影响的设备不应当设置这个标志。更多信息看 drivers/char/random.c 的注释。
中断处理例程可在驱动初始化时或在设备第一次打开时安装。推荐在设备第一次打开、硬件被告知产生中断前时申请中断,因为可以共享有限的中断资源。这样调用 free_irq 的位置是设备最后一次被关闭、硬件被告知不用再中断处理器之后。但这种方式的缺点是必须为每个设备维护一个打开计数。
以下是中断申请的示例(并口):
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); } } |
i386 和 x86_64 体系定义了一个函数来查询一个中断线是否可用:
int can_request_irq(unsigned int irq, unsigned long flags); /*当能够成功分配给定中断,则返回非零值。但注意,在 can_request_irq 和 request_irq 的调用之间给定中断可能被占用*/ |
快速和慢速处理例程
快速中断是那些能够很快处理的中断,而处理慢速中断会花费更长的时间。在处理慢速中断时处理器重新使能中断,避免快速中断被延时过长。在现代内核中,快速和慢速中断的区别已经消失,剩下的只有一个:快速中断(使用 SA_INTERRUPT )执行时禁止所有在当前处理器上的其他中断。注意:其他的处理器仍然能够处理中断。
除非你充足的理由在禁止其他中断情况下来运行中断处理例程,否则不应当使用SA_INTERRUPT.
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。在所有情况下,这个代码将中断号压栈并且跳转到一个公共段,公共段会调用 do_IRQ(在 irq.c 中定义)。do_IRQ 做的第一件事是应答中断以便中断控制器能够继续其他事情。它接着获取给定 IRQ 号的一个自旋锁,阻止其他 CPU 处理这个 IRQ,然后清除几个状态位(包括IRQ_WAITING )然后查找这个 IRQ 的处理例程。若没有找到,什么也不做;释放自旋锁,处理任何待处理的软件中断,最后 do_IRQ 返回。从中断中返回的最后一件事可能是一次处理器的重新调度。
IRQ的探测是通过为每个缺乏处理例程的IRQ设置 IRQ_WAITING 状态位来完成。当中断发生, 因为没有注册处理例程,do_IRQ 清除这个位并且接着返回。 当probe_irq_off被一个函数调用,只需搜索没有设置 IRQ_WAITING 的 IRQ。
/proc 接口
当硬件中断到达处理器时, 内核提供的一个内部计数器会递增,产生的中断报告显示在文件 /proc/interrupts中。这一方法可以用来检查设备是否按预期地工作。此文件只显示当前已安装处理例程的中断的计数。若以前request_irq的一个中断,现在已经free_irq了,那么就不会显示在这个文件中,但是它可以显示终端共享的情况。
/proc/stat记录了几个关于系统活动的底层统计信息, 包括(但不仅限于)自系统启动以来收到的中断数。 stat 的每一行以一个字符串开始, 是该行的关键词:intr 标志是中断计数。第一个数是所有中断的总数, 而其他每一个代表一个单独的中断线的计数, 从中断 0 开始(包括当前没有安装处理例程的中断),无法显示终端共享的情况。
以上两个文件的一个不同是:/proc/interrupts几乎不依赖体系,而/proc/stat的字段数依赖内核下的硬件中断,其定义在<asm/irq.h>中。ARM的定义为:
自动检测 IRQ 号
驱动初始化时最迫切的问题之一是决定设备要使用的IRQ 线,驱动需要信息来正确安装处理例程。自动检测中断号对驱动的可用性来说是一个基本需求。有时自动探测依赖一些设备具有的默认特性,以下是典型的并口中断探测程序:
if (short_irq < 0) /* 依靠使并口的端口号,确定中断*/ switch(short_base) { case 0x378: short_irq = 7; break; case 0x278: short_irq = 2; break; case 0x3bc: short_irq = 5; break; } |
有的驱动允许用户在加载时覆盖默认值:
当目标设备有能力告知驱动它要使用的中断号时,自动探测中断号只是意味着探测设备,无需做额外的工作探测中断。
但不是每个设备都对程序员友好,对于他们还是需要一些探测工作。这个工作技术上非常简单: 驱动告知设备产生中断并且观察发生了什么。如果一切顺利,则只有一个中断信号线被激活。尽管探测在理论上简单,但实现可能不简单。有 2 种方法来进行探测中断: 调用内核定义的辅助函数和DIY探测。
(1)调用内核定义的辅助函数
Linux 内核提供了一个底层设施来探测中断号,且只能在非共享中断模式下工作,它包括 2 个函数, 在<linux/interrupt.h> 中声明( 也描述了探测机制 ):
unsigned long probe_irq_on(void);
/*这个函数返回一个未分配中断的位掩码。驱动必须保留返回的位掩码,
并在后面传递给 probe_irq_off。在调用probe_irq_on之后, 驱动应当安
排它的设备产生至少一次中断*/
int probe_irq_off(unsigned long);
/*在请求设备产生一个中断后, 驱动调用这个函数, 并将 probe_irq_on
返回的位掩码作为参数传递给probe_irq_off。probe_irq_off 返回在
"probe_on"之后发生的中断号。如果没有中断发生, 返回 0 ;如果产生了
多次中断,probe_irq_off 返回一个负值*/
程序员应当注意在调用 probe_irq_on 之后启用设备上的中断, 并在调用 probe_irq_off 前禁用。此外还必须记住在 probe_irq_off 之后服务设备中待处理的中断。
以下是LDD3中的并口示例代码,(并口的管脚 9 和 10 连接在一起,探测五次失败后放弃):
int count = 0; do { unsigned long mask; mask = probe_irq_on(); outb_p(0x10,short_base+2); /* enable reporting */ outb_p(0x00,short_base); /* clear the bit */ outb_p(0xFF,short_base); /* set the bit: interrupt! */ outb_p(0x00,short_base+2); /* disable reporting */ udelay(5); /* give it some time */ short_irq = probe_irq_off(mask); if (short_irq == 0) { /* none of them? */ printk(KERN_INFO "short: no irq reported by probe\n"); short_irq = -1; } } while (short_irq < 0 && count++ < 5); if (short_irq < 0) printk("short: probe failed %i times, giving up\n", count); |
最好只在模块初始化时探测中断线一次。
大部分体系定义了这两个函数( 即便是空的 )来简化设备驱动的移植。
(2)DIY探测
DIY探测与前面原理相同: 使能所有未使用的中断, 接着等待并观察发生什么。我们对设备的了解:通常一个设备能够使用3或4个IRQ 号中的一个来进行配置,只探测这些 IRQ 号使我们能不必测试所有可能的中断就探测到正确的IRQ 号。
下面的LDD3中的代码通过测试所有"可能的"中断并且察看发生的事情来探测中断。 trials 数组列出要尝试的中断, 以 0 作为结尾标志; tried 数组用来跟踪哪个中断号已经被这个驱动注册。
int trials[] = {3, 5, 7, 9, 0}; int tried[] = {0, 0, 0, 0, 0}; int i, count = 0; for (i = 0; trials[i]; i++) tried[i] = request_irq(trials[i], short_probing, SA_INTERRUPT, "short probe", NULL); do { short_irq = 0; /* none got, yet */ outb_p(0x10,short_base+2); /* enable */ outb_p(0x00,short_base); outb_p(0xFF,short_base); /* toggle the bit */ outb_p(0x00,short_base+2); /* disable */ udelay(5); /* give it some time */ /* 等待中断,若在这段时间有中断产生,handler会改变 short_irq */ /* the value has been set by the handler */ if (short_irq == 0) { /* none of them? */ printk(KERN_INFO "short: no irq reported by probe\n"); } } while (short_irq <=0 && count++ < 5); /* end of loop, uninstall the handler */ for (i = 0; trials[i]; i++) if (tried[i] == 0) free_irq(trials[i], NULL); if (short_irq < 0) printk("short: probe failed %i times, giving up\n", count);
|
以下是handler的源码:
irqreturn_t short_probing(int irq, void *dev_id, struct pt_regs *regs) { if (short_irq == 0) short_irq = irq; /* found */ if (short_irq != irq) short_irq = -irq; /* ambiguous */ return IRQ_HANDLED; } |
若事先不知道"可能的" IRQ ,就需要探测所有空闲的中断,所以不得不从 IRQ 0 探测到 IRQ NR_IRQS-1 。
处理例程的参数及返回值
传递给一个中断处理例程的参数有: int irq、void *dev_id和 struct pt_regs *regs。
int irq (中断号):若要打印 log 消息时,是很有用。
void *dev_id:一种用户数据类型(驱动程序可用的私有数据),传递给 request_irq的 void* 参数,会在中断发生时作为参数传给处理例程。我们通常传递一个指向设备数据结构的指针到 dev_id 中,这样一个管理若干相同设备的驱动在中断处理例程中不需要任何额外的代码,就可以找出哪个设备产生了当前的中断事件。
struct pt_regs *regs很少用到。
中断处理例程的典型使用如下:
static irqreturn_t sample_interrupt(int irq, void *dev_id, struct pt_regs *regs) { struct sample_dev *dev = dev_id; /* now `dev' points to the right hardware item */ /* .... */ } |
和这个处理例程关联的打开代码如下:
static void sample_open(struct inode *inode, struct file *filp) { struct sample_dev *dev = hwinfo + MINOR(inode->i_rdev); request_irq(dev->irq, sample_interrupt,0 /* flags */, "sample", dev /* dev_id */); /*....*/ return 0; } |
中断处理例程应当返回一个值指示是否真正处理了一个中断。如果处理例程发现设备确实需要处理, 应当返回 IRQ_HANDLED; 否则返回值 IRQ_NONE。以下宏可产生返回值:
IRQ_RETVAL(handled) /*若要处理中断,handled应是非零*/ |
有位网友在处理返回值是按惯例return 0
;,导致了oops。吸取经验教训,我们应特别注意这种返回值,以下是有关中断处理例程的返回值的内核定义(#include <linux/irqreturn.h> ),看了就知道导致oops的原因了,以后应多多注意:
typedef int irqreturn_t; #define IRQ_NONE (0) #define IRQ_HANDLED (1) #define IRQ_RETVAL(x) ((x) != 0) |
实现中断处理例程
中断处理例程唯一的特别之处在中断时运行,它能做的事情受到了一些限制. 这些限制与我们在内核定时器上看到的相同:
(1)中断处理例程不能与用户空间传递数据, 因为它不在进程上下文执行;
(2)中断处理例程也不能做任何可能休眠的事情, 例如调用 wait_event, 使用除 GFP_ATOMIC 之外任何东西来分配内存, 或者锁住一个信号量;
(3)处理者不能调用schedule()。
中断处理例程的作用是将关于中断接收的信息反馈给设备并根据被服务的中断的含义读、写数据。中断处理例程第一步常常包括清除设备的一个中断标志位,大部分硬件设备在清除"中断挂起"位前不会再产生中断。这也要根据硬件的工作原理决定, 这一步也可能需要在最后做而不是开始; 这里没有通用的规则。一些设备不需要这步, 因为它们没有一个"中断挂起"位; 这样的设备是少数。
一个中断处理的典型任务是:如果中断通知它所等待的事件已经发生(例如新数据到达),就会唤醒休眠在设备上的进程。
不管是快速或慢速处理例程,程序员应编写执行时间尽可能短的处理例程。 如果需要进行长时间计算, 最好的方法是使用 tasklet 或者 workqueue 在一个更安全的时间来调度计算任务。
启用和禁止中断
有时设备驱动必须在一段时间(希望较短)内阻塞中断发生。并必须在持有一个自旋锁时阻塞中断,以避免死锁系统。注意:应尽量少禁止中断,即使是在设备驱动中,且这个技术不应当用于驱动中的互斥机制。
禁止单个中断
有时(但是很少!)一个驱动需要禁止一个特定中断。但不推荐这样做,特别是不能禁止共享中断(在现代系统中, 共享的中断是很常见的)。内核提供了 3 个函数,是内核 API 的一部分,声明在 <asm/irq.h>:
void disable_irq(int irq); /*禁止给定的中断, 并等待当前的中断处理例程结束。如果调用 disable_irq 的线程持有任何中断处理例程需要的资源(例如自旋锁), 系统可能死锁*/ void disable_irq_nosync(int irq);/*禁止给定的中断后立刻返回(可能引入竞态)*/ void enable_irq(int irq); |
调用任一函数可能更新在可编程控制器(PIC)中的特定 irq 的掩码, 从而禁止或使能所有处理器特定的 IRQ。这些函数的调用能够嵌套,即如果 disable_irq 被连续调用 2 次,则需要 2 个 enable_irq 重新使能 IRQ 。可以在中断处理例程中调用这些函数,但在处理某个IRQ时再打开它是不好的做法。
禁止所有中断
在 2.6 内核, 可使用下面 2 个函数中的任一个(定义在 <asm/system.h>)关闭当前处理器上所有中断:
void local_irq_save(unsigned long flags); /*在保存当前中断状态到 flags 之后禁止中断*/ void local_irq_disable(void);/* 关闭中断而不保存状态*/ /*如果调用链中有多个函数可能需要禁止中断, 应使用 local_irq_save*/ /*打开中断使用:*/ void local_irq_restore(unsigned long flags); void local_irq_enable ( void );
/* 在 2.6 内核, 没有方法全局禁用整个系统上的所有中断*/ |
顶半部和底半部
中断处理需要很快完成并且不使中断阻塞太长,所以中断处理的一个主要问题是如何在处理例程中完成耗时的任务。
Linux (连同许多其他系统)通过将中断处理分为两部分来解决这个问题:
“顶半部”是实际响应中断的例程(request_irq 注册的那个例程)。
“底半部”是被顶半部调度,并在稍后更安全的时间内执行的函数。
他们最大的不同在底半部处理例程执行时,所有中断都是打开的(这就是所谓的在更安全的时间内运行)。典型的情况是:顶半部保存设备数据到一个设备特定的缓存并调度它的底半部,最后退出: 这个操作非常快。底半部接着进行任何其他需要的工作。这种方式的好处是在底半部工作期间,顶半部仍然可以继续为新中断服务。
Linux 内核有 2 个不同的机制可用来实现底半部处理:
(1) tasklet (首选机制),它非常快, 但是所有的 tasklet 代码必须是原子的;
(2)工作队列, 它可能有更高的延时,但允许休眠。
tasklet和工作队列在《时间、延迟及延缓操作》已经介绍过,具体的实现代码请看实验源码!
中断共享
Linux 内核支持在所有总线上中断共享。
安装共享的处理例程
通过 request_irq 来安装共享中断与非共享中断有 2 点不同:
(1)当request_irq 时,flags 中必须指定SA_SHIRQ 位; (2)dev_id 必须唯一。任何指向模块地址空间的指针都行,但 dev_id 绝不能设置为 NULL。 |
内核为每个中断维护一个中断共享处理例程列表,dev_id 就是区别不同处理例程的签名。释放处理例程通过执行free_irq实现。 dev_id 用来从这个中断的共享处理例程列表中选择正确的处理例程来释放,这就是为什么 dev_id必须是唯一的.
请求一个共享的中断时,如果满足下列条件之一,则request_irq 成功:
(1)中断线空闲; (2)所有已经注册该中断信号线的处理例程也标识了IRQ是共享。 |
一个共享的处理例程必须能够识别自己的中断,并且在自己的设备没有被中断时快速退出(返回 IRQ_NONE )。
共享处理例程没有探测函数可用,但使用的中断信号线是空闲时标准的探测机制才有效。
一个使用共享处理例程的驱动需要小心:不能使用 enable_irq 或 disable_irq,否则,对其他共享这条线的设备就无法正常工作了。即便短时间禁止中断,另一设备也可能产生延时而为设备和其用户带来问题。所以程序员必须记住:他的驱动并不是独占这个IRQ,它的行为应当比独占这个中断线更加"社会化"。
中断驱动的 I/O
当与驱动程序管理的硬件间的数据传送可能因为某种原因而延迟,驱动编写者应当实现缓存。一个好的缓存机制需采用中断驱动的 I/O,一个输入缓存在中断时被填充,并由读取设备的进程取走缓冲区的数据,一个输出缓存由写设备的进程填充,并在中断时送出数据。
为正确进行中断驱动的数据传送,硬件应能够按照下列语义产生中断:
输入:当新数据到达时并处理器准备好接受时,设备中断处理器。
输出:当设备准备好接受新数据或确认一个成功的数据传送时,设备产生中断。
ARM9开发板实验
这次的实验和硬件相关,利用了友善之臂SBC2440V4提供的四个中断按键。
K1:中断+tasklet
K2:中断+工作队列
K3:中断+共享工作队列
K4:中断+linux内核缓冲kfifo
具体的实现方法请看源码!
中断模块源码:IO_irq.tar.gz
测试程序:IO_irq_test.tar.gz
实验数据:
[Tekkaman2440@SBC2440V4]#cd /lib/modules/ [Tekkaman2440@SBC2440V4]#insmod IO_irq.ko [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 153 spi 180 usb 189 usb_device 204 s3c2410_serial 252 IO_irq 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 IO_irq c 252 0 [Tekkaman2440@SBC2440V4]#/tmp/IO_irq_test IO_irq: opened ! IO_irq: the module can not lseek! please input the command : **************KEY = 1***************** NO prevkey now jiffies =0x00059ae5 count = 1 **************KEY = 1 END***************** **************key1_tasklet_start***************** time:00059ae7 delta: 2 inirq:1 pid: 0 cpu:0 command:swapper **************key1_tasklet_end***************** **************KEY = 2***************** prevkey=1 at 0x00059ae5 NO prekey2 now jiffies =0x00059c54 count = 1 result = 1 **************KEY = 2 END***************** **************key2_workqueue_start***************** time:00059c5f delta: 11 inirq:0 pid:832 cpu:0 command:tekkamanwork/0 **************key2_workqueu_end***************** **************KEY = 3***************** prevkey=2 at 0x00059c54 NO prekey3 now jiffies =0x00059f63 count = 1 result = 1 **************KEY = 3 END***************** **************key3_workqueue_start***************** time:00059f66 delta: 3 inirq:0 pid: 5 cpu:0 command:events/0 **************key3_workqueu_end***************** IO_irq: Invalid input ! only 1、2、3、4、5、6、7、8、q ! please input the command :6 IO_irq: ioctl IO_KFIFO_SIZE len=414 please input the command :8 **********KEY = 4********** prevkey=3 at 0x00059f63 NO prekey4 now jiffies =0x0005a2fd count = 1 *********KEY = 4 END*********** **********KEY = 4********** prevkey=4 at 0x0005a2fd prekey4 at 0x0005a2fd now jiffies =0x0005a304 count = 2 *********KEY = 4 END*********** **********KEY = 4********** prevkey=4 at 0x0005a304 prekey4 at 0x0005a304 now jiffies =0x0005a304 count = 3 *********KEY = 4 END*********** please input the command :6 IO_irq: ioctl IO_KFIFO_SIZE len=142 please input the command :7 IO_status= 1f ! IO_irq: ioctl IO_KFIFO_RESET please input the command :6 IO_irq: ioctl IO_KFIFO_SIZE len=0 please input the command :8 please input the command :q IO_irq: release ! [Tekkaman2440@SBC2440V4]#
|
Linux中的循环缓冲区
参考资料
:《Linux内核中的循环缓冲区》作者:西邮 王聪
严重感谢文章作者!
但是(可能是源码版本问题)有些结论并不正确: “
而kfifo_init只会接受一个已分配好空间的fifo->buffer,不能和kfifo_free搭配,用kfifo_init分配的kfifo只能用kfree释放。”
阅读源码可以得出这样的结论:kfifo_init和kfifo_alloc分配的kfifo都能用kfree释放。已经用实验证实。
在学习到
第十章 中断处理 时,其中的中断驱动的I/O需要使用缓冲区,我觉得与其自己实现一个缓冲区,不如利用内核已经写好的fifo。内核里有一个通用的循环缓冲区的实现在
<linux/kfifo.h>。
使用的数据结构如下:
struct kfifo { unsigned char *buffer; /* 使用的缓冲区头指针 */ unsigned int size; /* 缓冲区总大小 */ unsigned int in; /* 已写入缓冲区的数据总量,当前缓冲区写指针的偏移量:(in % size) */ unsigned int out; /* 已读出缓冲区的数据总量,当前缓冲区读指针的偏移量:(out % size) */ spinlock_t *lock; /* 为避免竞态的自旋锁 */ };/*当in==out时,缓冲区为空;当(in-out)==size时,缓冲区已满*/ |
kfifo提供的循环缓冲的部分函数分为2类:
(1)以双下划线开头,没有使用自旋锁函数;
(2)没有双下划线开头,需要额外加锁的情况下使用的函数。
其实第二类只是在第一类的基础上进行加锁后,实际的代码如下:
unsigned long flags; spin_lock_irqsave(fifo->lock, flags); /*第一类函数*/ spin_unlock_irqrestore(fifo->lock, flags); |
以下我按使用的顺序介绍每个函数的使用,部分函数源码在kernel/kfifo.c中定义,这些接口是经过精心构造的,可以小心地避免一些边界情况,原理其实很简单,建议去看源码弄清楚实现的原理,可以学到一些编程技巧。
(0)声明循环缓冲数据结构指针
struct kfifo *tekkamanfifo; |
(1)初始化循环缓冲结构体
struct kfifo *kfifo_init(unsigned char *buffer, unsigned int size, gfp_t gfp_mask, spinlock_t *lock); /*调用kfifo_init必须保证size是2的整数次幂,而且buffer只接受一个已分配 好空间的指针。也就是说之前要使用kmalloc分配好空间,将返回的指针传递到buffer*/ struct kfifo *kfifo_alloc(unsigned int size, gfp_t gfp_mask, spinlock_t *lock); /*调用kfifo_alloc不必保证size是2的幂,它内部会把size向上调整到2的整数次 幂。空间分配的内部实现使用kmalloc。函数内部调用kfifo_init/ |
buffer:之前要使用kmalloc分配好的空间指针;
size:循环缓冲空间大小;
gfp_mask:和kmalloc使用的分配标志(flags)一样。(参阅Linux设备驱动程序学习(8)-分配内存)
lock:是事先声明并初始化好的自旋锁结构体指针;
返回值 为初始化好的循环缓冲数据结构指针 。
(2) 向缓冲区里写入数据
unsigned int kfifo_put(struct kfifo *fifo,unsigned char *buffer, unsigned int len); unsigned int __kfifo_put(struct kfifo *fifo,unsigned char *buffer, unsigned int len); |
fifo:要写入数据的缓冲区结构体指针;
buffer:要写入的数据指针,指向内核空间。如需要用户空间数据,之前要用copy_from_user复制数据到内核空间;
len:要写入的数据大小;
返回值 为写入缓冲区的数据字节数。
(3)从缓冲区里读出数据
unsigned int kfifo_get(struct kfifo *fifo, unsigned char *buffer, unsigned int len); unsigned int __kfifo_get(struct kfifo *fifo, unsigned char *buffer, unsigned int len); |
参数定义和kfifo_put类似。
返回值 为从缓冲区读出的数据字节数。
(4)得到缓冲区已有的数据字节数
unsigned int kfifo_len(struct kfifo *fifo); unsigned int __kfifo_len(struct kfifo *fifo); |
fifo:要操作的缓冲区结构体指针;
函数返回缓冲区实际已有的数据字节数,内部实现十分简单,就是in - out;
返回值 为缓冲区已有的数据字节数。
(5)清空缓冲区
void __kfifo_reset(struct kfifo *fifo); void kfifo_reset(struct kfifo *fifo); |
内部实现十分简单,就是in = out = 0。
(6)使用结束,释放缓冲区。
void kfifo_free(struct kfifo *fifo); |
所有的kfifo提供的循环缓冲的函数就是这些。在理解内部实现原理的基础上才能更好的使用它,所以再次建议阅读源码,因为源码很简单,但是很精巧。
ARM9开发板实验
实验模块源码:scull-kfifo
测试程序源码:scull-kfifo-test
实验现象:
[Tekkaman2440@SBC2440V4]#cd /lib/modules/ [Tekkaman2440@SBC2440V4]#insmod scull_kfifo.ko [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 153 spi 180 usb 189 usb_device 204 s3c2410_serial 252 scull_kfifo 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/scull_kfifo c 252 0 [Tekkaman2440@SBC2440V4]#echo 1234567890 > /dev/scull_kfifo "sh" did write 11 bytes [Tekkaman2440@SBC2440V4]#/tmp/scull_kfifo_test scull_kfifo: the module can not lseek! please input the command :1 scull_kfifo: ioctl SCULL_KFIFO_SIZE len=11 please input the command :2 scull_kfifo: SCULL_KFIFO_RESET code=0 please input the command :1 scull_kfifo: ioctl SCULL_KFIFO_SIZE len=0 please input the command :q [Tekkaman2440@SBC2440V4]#echo 123456789012345678901234567890 > /dev/scull_kfifo "sh" did write 31 bytes [Tekkaman2440@SBC2440V4]#echo 123456789012345678901234567890 > /dev/scull_kfifo "sh" did write 31 bytes [Tekkaman2440@SBC2440V4]#echo 1234567890 > /dev/scull_kfifo "sh" did write 2 bytes "sh" did write 0 bytes "sh" did write 0 bytes "sh" did write 0 bytes "sh" did write 0 bytes "sh" did write 0 bytes "sh" did write 0 bytes "sh" did write 0 bytes "sh" did write 0 bytes printk: 204310 messages suppressed. "sh" did write 0 bytes 1234567890 [Tekkaman2440@SBC2440V4]#/tmp/scull_kfifo_test scull_kfifo: the module can not lseek! please input the command :1 scull_kfifo: ioctl SCULL_KFIFO_SIZE len=64 please input the command :q [Tekkaman2440@SBC2440V4]#cat /dev/scull_kfifo printk: 1493677 messages suppressed. "cat" did read 64 bytes 1234"cat" reading: going to sleep 56789012345678901234567890 123456789012345678901234567890 12 [Tekkaman2440@SBC2440V4]#/tmp/scull_kfifo_test scull_kfifo: the module can not lseek! please input the command :2 scull_kfifo: SCULL_KFIFO_RESET code=0 please input the command :q [Tekkaman2440@SBC2440V4]#rmmod scull_kfifo [Tekkaman2440@SBC2440V4]#lsmod Module Size Used by Not tainted [Tekkaman2440@SBC2440V4]# |