Linux设计了一个通用的数据结构resource来描述各种I/O资源(如:I/O端口、外设内存、DMA和IRQ等)。
该结构定义在include/linux/ioport.h头文件中:
struct resource {
const char *name;
unsigned long start, end; \\\\表示资源的起始物理地址和终止物理地址。它们确定了资源的范围,也即是一个闭区间[start,end]
unsigned long flags; \\\\描述资源的标志
struct resource *parent, *sibling, *child; \\\\分别指向父、兄弟、子资源的指针 };
Linux是以一种倒置的树形结构来管理每一类I/O资源(如:I/O端口、外设内存、DMA和IRQ)的。每一类I/O资源都对应有一颗倒置的资源树,树中的每一个节点都是一个resource结构,而树的根结点root则描述了该类资源的整个资源空间。为什么使用树?例如,考虑一下IDE硬盘接口所使用的I/O端口地址-比如说从0xf000 到 0xf00f。那么,start字段为0xf000 且end 字段为0xf00f的这样一个资源包含在树中,控制器的常规名字存放在name字段中。但是,IDE设备驱动程序需要记住另外的信息,也就是IDE链主盘使用0xf000 到 0xf007的子范围,从盘使用0xf008 到 0xf00f的子范围。为了做到这点,设备驱动程序把两个子范围对应的孩子插入到从0xf000 到 0xf00f的整个范围对应的资源下。一般来说,树中的每个节点肯定相当于父节点对应范围的一个子范围。I/O端口资源树(ioport_resource)的根节点跨越了整个I/O地址空间(从端口0到65535,即64K)。
更详细可以参考:
每种类的PCI设备都可以用结构类型pci_dev来描述。更为准确地说,应该是每一个PCI功能,即PCI逻辑设备都唯一地对应有一个pci_dev设备描述符。该数据结构的定义如下(include/linux/pci.h):
struct pci_dev {
struct list_head global_list;
/* 全局链表元素global_list:每一个pci_dev结构都通过该成员连接到全局pci设备链表pci_devices中*/ struct list_head bus_list;
/* 总线设备链表元素bus_list:每一个pci_dev结构除了链接到全局设备链表中外,还会通过这个成员连接到其所属PCI总线的设备链表中。每一条PCI总线都维护一条它自己的设备链表视图,以便描述所有连接在该PCI总线上的设备,其表头由PCI总线的pci_bus结构中的 devices成员所描述t*/ struct pci_bus *bus;
/* 总线指针bus:指向这个PCI设备所在的PCI总线的pci_bus结构。因此,对于桥设备而言,bus指针将指向桥设备的主总线(primary bus),也即指向桥设备所在的PCI总线*/ struct pci_bus *subordinate;
/* 指针subordinate:指向这个PCI设备所桥接的下级总线。这个指针成员仅对桥设备才有意义,而对于一般的非桥PCI设备而言,该指针成员总是为NULL*/ void *sysdata;
/* 无类型指针sysdata:指向一片特定于系统的扩展数据*/ struct proc_dir_entry *procent;
/* 指针procent:指向该PCI设备在/proc文件系统中对应的目录项*/ unsigned int devfn;
/* devfn:这个PCI设备的设备功能号,也成为PCI逻辑设备号(0-255)。其中bit[7:3]是物理设备号(取值范围0-31),bit[2:0]是功能号(取值范围0-7)。 */ unsigned short vendor;
/* vendor:这是一个16无符号整数,表示PCI设备的厂商ID*/ unsigned short device;
/*device:这是一个16无符号整数,表示PCI设备的设备ID */
unsigned short subsystem_vendor;
/* subsystem_vendor:这是一个16无符号整数,表示PCI设备的子系统厂商ID*/ unsigned short subsystem_device;
/* subsystem_device:这是一个16无符号整数,表示PCI设备的子系统设备ID。*/ unsigned int class;
/* class:32位的无符号整数,表示该PCI设备的类别,其中,bit[7:0]为编程接口,bit[15:8]为子类别代码,bit [23:16]为基类别代码,bit[31:24]无意义。显然,class成员的低3字节刚好对应与PCI配置空间中的类代码*/
u8 hdr_type;
/* hdr_type:8位符号整数,表示PCI配置空间头部的类型。其中,bit[7]=1表示这是一
个多功能设备,bit[7]=0表示这是一个单功能设备。Bit[6:0]则表示PCI配置空间头部的布局类型,值00h表示这是一个一般PCI设备的配置空间头部,值01h表示这是一个PCI-to-PCI桥的配置空间头部,值02h表示CardBus桥的配置空间头部*/
u8 rom_base_reg;
/* rom_base_reg:8位无符号整数,表示PCI配置空间中的ROM基地址寄存器在PCI配置空间中的位置。ROM基地址寄存器在不同类型的PCI配置空间头部的位置是不一样的,对于type 0的配置空间布局,ROM基地址寄存器的起始位置是30h,而对于PCI-to-PCI桥所用的type 1配置空间布局,ROM基地址寄存器的起始位置是38h*/ struct pci_driver *driver;
/* 指针driver:指向这个PCI设备所对应的驱动程序定义的pci_driver结构。每一个pci设备驱动程序都必须定义它自己的pci_driver结构来描述它自己。*/ u64 dma_mask;
/*dma_mask:用于DMA的总线地址掩码,一般来说,这个成员的值是0xffffffff。数据类型dma_addr_t定义在include/asm/types.h中,在x86平台上,dma_addr_t类型就是u32类型*/
pci_power_t current_state;
/* 当前操作状态 */ struct device dev;
/* 通用的设备接口*/
unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE]; unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE]; /*定义这个PCI设备与哪些设备相兼容!/
unsigned int irq;
/* 无符号的整数irq:表示这个PCI设备通过哪根IRQ输入线产生中断,一般为0-15之间的某个值 */
struct resource resource[DEVICE_COUNT_RESOURCE];
/*表示该设备可能用到的资源,包括:I/O断口区域、设备内存地址区域以及扩展ROM地址区域。*/ int cfg_size;
/* 配置空间的大小 */
unsigned int transparent:1;
/* 透明 PCI 桥 */ unsigned int multifunction:1;
/* 多功能设备*/
unsigned int is_enabled:1;
/* pci_enable_device已经被调用*/ unsigned int is_busmaster:1;
/* 设备是主设备*/ unsigned int no_msi:1;
/* 设备不使用msi*/
unsigned int block_ucfg_access:1;
/* 配置空间访问形式用块的形式 */ u32 saved_config_space[16];
/* 在挂起时保存配置空间*/ struct bin_attribute *rom_attr;
/* sysfs ROM入口的属性描述*/ int rom_attr_enabled;
/* 能显示rom 属性*/
struct bin_attribute *res_attr[DEVICE_COUNT_RESOURCE];
/* 资源的sysfs文件*/ };
struct pci_bus {
struct list_head node;
/* 链表元素node:对于PCI根总线而言,其pci_bus结构通过node成员链接到本节一开始所述的根总线链表中,根总线链表的表头由一个list_head类型的全局变量pci_root_buses所描述。而对于非根pci总线,其pci_bus结构通过node成员链接到其父总线的子总线链表children中*/ struct pci_bus *parent;
/*parent指针:指向该pci总线的父总线,即pci桥所在的那条总线*/ struct list_head children;
/*children指针:描述了这条PCI总线的子总线链表的表头。这条PCI总线的所有子总线都通过上述的node链表元素链接成一条子总线链表,而该链表的表头就由父总线的children指针所描述*/ struct list_head devices;
/* list of devices on this bus */ struct pci_dev *self;
/* devices链表头:描述了这条PCI总线的逻辑设备链表的表头。除了链接在全局PCI设备链表中之外,每一个PCI逻辑设备也通过其 pci_dev结构中的bus_list成员链入其所在PCI总线的局部设备链表中,而这个局部的总线设备链表的表头就由pci_bus结构中的 devices成员所描述*/
struct resource *resource[PCI_BUS_NUM_RESOURCES];
/* 资源指针数组:指向应路由到这条pci总线的地址空间资源,通常是指向对应桥设备的pci_dev结构中的资源数组resource[10:7]*/ struct pci_ops *ops;
/* 指针ops:指向一个pci_ops结构,表示这条pci总线所使用的配置空间访问函数*/ void *sysdata;
/* 无类型指针sysdata:指向系统特定的扩展数据*/
struct proc_dir_entry *procdir;
/* 指针procdir:指向该PCI总线在/proc文件系统中对应的目录项*/ unsigned char number;
/* number:这条PCI总线的总线编号(bus number),取值范围0-255 */
unsigned char primary;
/* primary:表示引出这条PCI总线的“桥设备的主总线”(也即桥设备所在的PCI总线)编号,取值范围0-255*/
unsigned char secondary;
/* secondary:表示引出这条PCI总线的桥设备的次总线号,因此secondary成员总是等于number成员的值。取值范围0-255*/ unsigned char subordinate;
/* subordinate:这条PCI总线的下属PCI总线(Subordinate pci bus)的总线编号最大值,它应该等于引出这条PCI总线的桥设备的subordinate值*/ char name[48]; /* name[48]:这条PCI总线的名字字符串*/ unsigned short bridge_ctl; /**/
unsigned short pad2; /* */
struct device *bridge; /* */
struct device class_dev; /* */
struct bin_attribute *legacy_io;
struct bin_attribute *legacy_mem;
/* */ };
struct pci_device_id {
__u32 vendor,device; //厂商和设备ID
__u32 subvendor,subdevice; //子系统和设备ID
__u32 class,class_mask; //类、子类、prog-if三元组
kernel_ulong_t driver_data; //驱动私有数据
pci_device_id 用MODULE_DEVICE_TABLE宏到处到用户空间。
CPU对外设端口物理地址的编址方式有两种:一种是IO映射方式,另一种是内存映射方式。 Linux将基于IO映射方式的和内存映射方式的IO端口统称为IO区域(IO region)。IO region仍然是一种IO资源,因此它仍然可以用resource结构类型来描述。
Linux管理IO region:
1) request_region()
把一个给定区间的IO端口分配给一个IO设备。
2) check_region()
检查一个给定区间的IO端口是否空闲,或者其中一些是否已经分配给某个IO设备。
3) release_region()
释放以前分配给一个IO设备的给定区间的IO端口。
Linux中可以通过以下辅助函数来访问IO端口:
inb(),inw(),inl(),outb(),outw(),outl()
“b”“w”“l”分别代表8位,16位,32位。
对IO内存资源的访问
注:其实对IO内存和IO端口的访问的不同函数,只是对同一函数的不同调用而已,定义在include/linux/ioport.h:
#define request_region(start,n,name) __request_region(&ioport_resource, (start), (n), (name))
#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name))
#define rename_region(region, newname) do { (region)->name = (newname); } while (0) extern struct resource * __request_region(struct resource *, resource_size_t start,
resource_size_t n, const char *name);
1) request_mem_region()
请求分配指定的IO内存资源。
2) check_mem_region()
检查指定的IO内存资源是否已被占用。
3) release_mem_region()
释放指定的IO内存资源。
其中传给函数的start address参数是内存区的物理地址(以上函数参数表已省略)。
驱动开发人员可以将内存映射方式的IO端口和外设内存统一看作是IO内存资源。
ioremap()用来将IO资源的物理地址映射到内核虚地址空间(3GB - 4GB)中,参数addr是指向内核虚地址的指针。
Linux中可以通过以下辅助函数来访问IO内存资源:
readb(),readw(),readl(),writeb(),writew(),writel()。
Linux在kernel/resource.c文件中定义了全局变量ioport_resource和iomem_resource,来分别描述基于IO映射方式的整个IO端口空间和基于内存映射方式的IO内存资源空间(包括IO端口和外设内存)。
1)关于IO与内存空间:
在X86处理器中存在着I/O空间的概念,I/O空间是相对于内存空间而言的,它通过特定的指令in、out来访问。端口号标识了外设的寄存器地址。Intel语法的in、out指令格式为:
IN 累加器, {端口号│DX} OUT {端口号│DX},累加器
目前,大多数嵌入式微控制器如ARM、PowerPC等中并不提供I/O空间,而仅存在内存空间。内存空间可以直接通过地址、指针来访问,程序和程序运行中使用的变量和其他数据都存在于内存空间中。
即便是在X86处理器中,虽然提供了I/O空间,如果由我们自己设计电路板,外设仍然可以只挂接在内存空间。此时,CPU可以像访问一个内存单元那样访问外设I/O端口,而不需要设立专门的I/O指令。因此,内存空间是必须的,而I/O空间是可选的。
(2)inb和outb:
在Linux设备驱动中,宜使用Linux内核提供的函数来访问定位于I/O空间的端口,这些函数包括:
· 读写字节端口(8位宽) unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port); · 读写字端口(16位宽)
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port); · 读写长字端口(32位宽)
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port); · 读写一串字节
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
· insb()从端口port开始读count个字节端口,并将读取结果写入addr指向的内存;outsb()将addr指向的内存的count个字节连续地写入port开始的端口。 · 读写一串字
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);
上述各函数中I/O端口号port的类型高度依赖于具体的硬件平台,因此,只是写出了unsigned
(3)readb和writeb:
在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是工程师宜使用Linux内核的如下一组函数来完成设备内存映射的虚拟地址的读写,这些函数包括:
· 读I/O内存
unsigned int ioread8(void *addr); unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持): unsigned readb(address);
unsigned readw(address); unsigned readl(address); · 写I/O内存
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr); void iowrite32(u32 value, void *addr);
与上述函数对应的较早版本的函数为(这些函数在Linux 2.6中仍然被支持): void writeb(unsigned value, address); void writew(unsigned value, address); void writel(unsigned value, address);
(4)把I/O端口映射到“内存空间”:
void *ioport_map(unsigned long port, unsigned int count);
通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间”。然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。当不再需要这种映射时,需要调用下面的函数来撤消:
void ioport_unmap(void *addr);
实际上,分析ioport_map()的源代码可发现,所谓的映射到内存空间行为实际上是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的I/O内存访问接口访问I/O端口。
----------------------------------------------------------------------------------
Linux对I/O端口资源的管理
几乎每一种外设都是通过读写设备上的寄存器来进行的。外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一个外设的寄存器通常被连续地编址。CPU对外设IO端口物理地址的编址方式有两种:一种是I/O映射方式(I/O-mapped),另一种是内存映射方式(Memory-mapped)。而具体采用哪一种则取决于CPU的体系结构。
有些体系结构的CPU(如,PowerPC、m68k等)通常只实现一个物理地址空间(RAM)。在这种情况下,外设 I/O端口的物理地址就被映射到CPU的单一物理地址空间中,而成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立
专门的外设I/O指令。这就是所谓的“内存映射方式”(Memory-mapped)。
而另外一些体系结构的CPU(典型地如X86)则为外设专门实现了一个单独地地址空间,称为“I/O地址空间”或者“I/O端口空间”。这是一个与CPU地RAM物理地址空间不同的地址空间,所有外设的I/O端口均在这一空间中进行编址。CPU通过设立专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元(也即 I/O端口)。这就是所谓的“I/O映射方式”(I/O-mapped)。与RAM物理地址空间相比,I/O地址空间通常都比较小,如x86 CPU的I/O空间就只有64KB(0-0xffff)。这是“I/O映射方式”的一个主要缺点。
Linux将基于I/O映射方式的或内存映射方式的I/O端口通称为“I/O区域”(I/O region)。在讨论对I/O区域的管理之前,我们首先来分析一下Linux是如何实现“I/O资源”这一抽象概念的。
Linux对I/O资源的描述
--------------------------------------
Linux设计了一个通用的数据结构resource来描述各种I/O资源(如:I/O端口、外设内存、DMA和IRQ等)。该结构定义在include/linux/ioport.h头文件中:
struct resource {
resource_size_t start; resource_size_t end; const char *name;
unsigned long flags;
struct resource *parent, *sibling, *child; };
Linux对I/O资源的管理
--------------------------------------
Linux是以一种倒置的树形结构来管理每一类I/O资源(如:I/O端口、外设内存、DMA和IRQ)的。每一类I/O资源都对应有一颗倒置的资源树,树中的每一个节点都是一个resource结构,而树的根结点root则描述了该类资源的整个资源空间。
基于上述这个思想,Linux在kernel/resource.c文件中实现了对资源的申请、释放及查找等操作。
资源的申请: request_resource() 资源的释放: release_resource() 寻找可用资源: --find_resource() 分配资源: allocate_resource()
管理I/O Region资源
--------------------------------------
Linux将基于I/O映射方式的I/O端口和基于内存映射方式的I/O端口资源统称为“I/O区域”(I/O Region)。I/O Region仍然是一种I/O资源,因此它仍然可以用resource结构类型来描述。下面我们就来看看Linux是如何管理I/O Region的。
I/O Region的分配: __request_region()
I/O Region的释放: __release_region()
检查指定的I/O Region是否已被占用: __check_region()
管理I/O端口资源
--------------------------------------
我们都知道,采用I/O映射方式的X86处理器为外设实现了一个单独的地址空间,也即“I/O空间”(I/O Space)或称为“I/O端口空间”,其大小是64KB(0x0000-0xffff)。Linux在其所支持的所有平台上都实现了“I/O端口空间” 这一概念。
由于I/O空间非常小,因此即使外设总线有一个单独的I/O端口空间,却也不是所有的外设都将其I/O端口(指寄存器)映射到“I/O端口空间”中。比如,大多数PCI卡都通过内存映射方式来将其I/O端口或外设内存映射到CPU的RAM物理地址空间中。而老式的ISA卡通常将其I/O端口映射到I/O端口空间中。
Linux是基于“I/O Region”这一概念来实现对I/O端口资源(I/O-mapped 或 Memory-mapped)的管理的。
资源根节点的定义:
Linux在kernel/resource.c文件中定义了全局变量ioport_resource和iomem_resource,来分别描述基于I/O映射方式的整个I/O端口空间和基于内存映射方式的I/O内存资源空间(包括I/O端口和外设内存)。其定义如下: struct resource ioport_resource = { .name = \"PCI IO\ .start -= 0,
.end = IO_SPACE_LIMIT, .flags -= IORESOURCE_IO, };
struct resource iomem_resource = { .name = \"PCI mem\ .start -= 0, .end = -1,
.flags -= IORESOURCE_MEM,
};
其中,宏IO_SPACE_LIMIT表示整个I/O空间的大小,对于X86平台而言,它是0xffff(定义在include/asm-i386/io.h头文件中)。显然,I/O内存空间的大小是4GB。
对I/O端口空间的操作:
基于I/O Region的操作函数__xxx_region(),Linux在头文件include/linux/ioport.h中定义了三个对I/O端口空间进行操作的接口:
request_region() 请求在I/O端口空间中分配指定范围的I/O端口资源。
check_region() 检查I/O端口空间中的指定I/O端口资源是否已被占用。 release_region() 释放I/O端口空间中的指定I/O端口资源。
对I/O内存资源的操作:
基于I/O Region的操作函数__xxx_region(),Linux在头文件include/linux/ioport.h中定义了三个对I/O内存资源进行操作的接口:
request_mem_region() 请求分配指定的I/O内存资源。
check_mem_region() 检查指定的I/O内存资源是否已被占用。 release_mem_region() 释放指定的I/O内存资源。
访问I/O端口空间(I/O-mapped)
--------------------------------------
在驱动程序请求了I/O端口空间中的端口资源后,它就可以通过CPU的IO指令来读写这些I/O端口了。在读写I/O端口时要注意的一点就是,大多数平台都区分8位、16位和32位的端口,也即要注意I/O端口的宽度。 inb() outb() inw() outw() inl() outl()
unsigned char inb(unsigned port);
port参数指定I/O端口空间中的端口地址。在大多数平台上(如x86)它都是unsigned short类型的,其它的一些平台上则是unsigned int类型的。显然,端口地址的类型是由I/O端口空间的大小来决定的。
对I/O端口的字符串操作
除了上述这些“单发”(single-shot)的I/O操作外,某些CPU也支持对某个I/O端口进行连续的读写操作,也即对单个I/O端口读或写一系列字节、字或32位整数,这就是所谓的“字符串I/O指令”(String Instruction)。这种指令在速度上显然要比用循环来实现同样的功能要快得多。
insb() outsb() insw() outw() insl() outsl() Pausing I/O
在一些平台上(典型地如X86),对于老式总线(如ISA)上的慢速外设来说,如果CPU读写其I/O端口的速度太快,那就可能会发生丢失数据的现象。对于这个问题的解决方法就是在两次连续的I/O操作之间插入一段微小的时延,以便等待慢速外设。这就是所谓的“Pausing I/O”。 对于Pausing I/O,Linux也在io.h头文件中定义了它的I/O读写函数,而且都以XXX_p命名,比如:inb_p()、outb_p()等等。
访问I/O内存资源(I/O Memory)
--------------------------------------
尽管I/O端口空间曾一度在x86平台上被广泛使用,但是由于它非常小,因此大多数现代总线的设备都以内存映射方式(Memory-mapped)来映射它的I/O端口(指I/O寄存器)和外设内存。基于内存映射方式的I/O端口称为\"I/O内存\"资源(I/O Memory)。因为基于内存映射方式的I/O和外设内存在硬件实现上的差异对于软件来说是完全透明的,所以驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是“I/O内存”资源。
I/O内存资源是在CPU的单一内存物理地址空间内进行编址的,也即它和系统RAM同处在一个物理地址空间内。因此通过CPU的访内指令就可以访问I/O内存资源。
一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,这可以通过系统固件(如BIOS)在启动时分配得到,或者通过设备的硬连线(hardwired)得到。比如,PCI卡的I/O内存资源的物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。物理地址就是在系统启动时由PCI BIOS分配并写到PCI卡的配置空间中的BAR中的。而
ISA卡的I/O内存资源的物理地址则是通过设备硬连线映射到640KB-1MB范围之内的。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,因为它们是在系统启动后才已知的(某种意义上讲是动态的),所以驱动程序并不能直接通过
映射I/O内存资源
Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中
对于X86体系结构ioremap()定义在/usr/src/linux-2.6.21.5/include/asm-i386/io.h中
读写I/O内存资源
在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。但是,由于在某些平台上,对 I/O内存和系统内存有不同的访问处理,因此为了确保跨平台的兼容性,Linux实现了一系列读写I/O内存资源的函数,这些函数在不同的平台上有不同的实现。但在x86平台上,读写I/O内存与读写RAM无任何差别。如下所示(include/asm-i386/io.h): readb() readw() readl()
writeb() writew() writel()
memset_io() memcpy_fromio() memcpytoio()
显然,在x86平台上访问I/O内存资源与访问系统主存RAM是无差别的。但是为了保证驱动程序的跨平台的可移植性,我们应该使用上面的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。 本
文来自CSDN博客,转载请标明出处:
http://blog.csdn.net/wucongdonglai/archive/2011/01/14/6138345.aspx
一、PCI简介
PCI是一种外设总线规范。我们先来看一下什么是总线:总线是一种传输信号的路径或信道。典型情况是,总线是连接于一个或多个导体的电气连线,总线上连接的所有设备可在同一时间收到所有的传输内容。总线由电气接口和编程接口组成。本文讨论Linux 下的设备驱动,所以,重点关注编程接口。
PCI是Peripheral Component Interconnect(外围设备互联)的简称,是普遍使用在桌面及更大型的计算机上的外设总线。PCI架构被设计为ISA标准的替代品,他有三个主要目标:获得在计算机和外设之间传输数据时更好的性能;尽可能的平台无关;简化往系统中添加和删除外设的工作。
二、PCI寻址
从目前开始,我想尽可能通过一些实际的例子来说明问题,而减少理论方面的问题的描述,因为,相关的理论的东西,能在其他地方找到。
我们先来看一个例子,我的计算机装有1G的RAM,1G以后的物理内存地址空间都是外部设备IO在系统内存地址空间上的映射。/proc/iomem描述了系统中所有的设备I/O在
内存地址空间上的映射。我们来看地址从1G开始的第一个设备在/proc/iomem中是怎么描述的:
40000000-400003ff : 0000:00:1f.1
这是个PCI设备,40000000-400003ff是他所映射的内存地址空间,占据了内存地址空间的1024bytes的位置,而0000:00:1f.1则是个PCI外设的地址,以冒号和逗号分隔为4个部分:
第一个16位表示域;
第二个8位表示一个总线编号,2^8-256,故每个域最多能有256个总线;
第三个5位表示一个设备号,每个总线最多能挂载32个设备;
最后是3位,表示功能号,每个设备最多能有8种功能,也就是最多能够对应8个逻辑设备,每种功能都唯一的对应一个pci_dev结构体。
注:因为PCI规范允许单个系统拥有高达256个总线,但对于大型系统而言,这是不够的,所以,引入了域的概念。
由此,我们能得出上述的PCI设备的地址是0号域0号总线上的31号设备上的1号功能。那上述的这个PCI设备到底是什么呢?下面是我的计算机上的lspci命令的输出: 00:00.0 Host bridge: Intel Corporation 82845 845 (Brookdale) Chipset Host Bridge (rev 04) 00:01.0 PCI bridge: Intel Corporation 82845 845 (Brookdale) Chipset AGP Bridge(rev 04) 00:1d.0 USB Controller: Intel Corporation 82801CA/CAM USB (Hub #1) (rev 02) 00:1d.1 USB Controller: Intel Corporation 82801CA/CAM USB (Hub #2) (rev 02) 00:1e.0 PCI bridge: Intel Corporation 82801 Mobile PCI Bridge (rev 42) 00:1f.0 ISA bridge: Intel Corporation 82801CAM ISA Bridge (LPC) (rev 02) 00:1f.1 IDE interface: Intel Corporation 82801CAM IDE U100 (rev 02)
00:1f.3 SMBus: Intel Corporation 82801CA/CAM SMBus Controller (rev 02)
00:1f.5 Multimedia audio controller:Intel Corporation 82801CA/CAM AC’97 Audio Controller (rev 02)
00:1f.6 Modem: Intel Corporation 82801CA/CAM AC’97 Modem Controller (rev 02)
01:00.0 VGA compatible controller: nVidia Corporation NV17 [GeForce4 420 Go](rev a3) 02:00.0 FireWire (IEEE 1394): VIA Technologies, Inc. IEEE 1394 Host Controller(rev 46) 02:01.0 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL-8139/8139C/8139C+(rev 10)
02:04.0 CardBus bridge: O2 Micro, Inc. OZ6933 Cardbus Controller (rev 01) 02:04.1 CardBus bridge: O2 Micro, Inc. OZ6933 Cardbus Controller (rev 01)
lspci没有标明域,但对于一台PC而言,一般只有一个域,即0号域。通过这个输出我们能看到他是个IDE interface。由上述的输出能看到,我的计算机上共有3个PCI总线(0号,1号,2号)。在单个系统上,插入多个总线是通过桥(bridge)来完成的,桥是一种用来连接总线的特别PCI外设。所以,PCI系统的整体布局组织为树型,我们能通过上面的lspci输
出,来画出我的计算机上的PCI系统的树型结构: 0号总线 (主桥)--00:01(PCI桥)
|--00:1d(USB号控制器,提供了0、1这两个逻辑设备,即功能) |--00:1e:0(PCI桥)
|--00:1f.多功能卡(提供了0 ISA bridge、1 IDE interface、3SMBus、5 Multimedia audio controller、6 Modem这5个逻辑设备,即功能)
1号总线(主桥)--01:00.0 VGA compatible controller
2号总线(主桥)--02:00.0 IEEE1394
|--02:01.0 8139网卡
|--02:04 CardBus桥(提供了桥0、1两个逻辑设备,即功能) 由上图能得出,我的计算机上共有8个PCI设备,其中0号总线上(主桥)上连有4个,1号总线上连有1个,2号总线上连有3个。00:1f是个连有5个功能的多功能板卡。每一个PCI设备都有他映射的内存地址空间和他的I/O区域,这点是比较容易理解的。除此之外,PCI设备还有有他的设置寄存器。有了设置寄存器,PCI的驱动程式就不必探测就能访问设备。设置寄存器的布局是标准化的,设置空间的4个字节含有一个独一无二的功能ID,因此,驱动程式可通过查询外设的特定ID来识别其设备。所以,PCI接口标准在ISA之上的主要创新在于设置地址空间。
在系统引导阶段,PCI硬件设备保持未激活状态,但每个PCI主板均配备有能够处理PCI的固件,固件通过读写PCI控制器中的寄存器,提供了对设备设置地址空间的访问。设置地址空间的前64字节是标准化的,他提供了厂商号,设备号,版本号等信息,唯一标识一个PCI设备。同时,他也提供了最多可多达6个的I/O地址区域,每个区域能是内存也能是I/O地址。这几个I/O地址区域是驱动程式找到设备映射到内存和I/O空间的具体位置的唯一途径。有了这两点,PCI驱动程式就完成了相当于探测的功能。关于这64个字节的设置空间的周详情况,可参阅《Linux设备驱动程式第三版》P306,图12-2,不再详述。
下面,我们来看一下8139too网卡设备的设置空间的周详情况。在2.6内核的系统中,能在目录/sys/bus/pci/devices/下看到非常
多以PCI设备名命名的目录,但不是说这些设备都存在于你的系统中。我们进入8139too目录,其中有一个以他的设备地址0000:02:01.0命名,的目录。在这个目录下能找到该网卡设备相关的非常多信息。其中resource记录了他的6个I/O地址区域。内容如下: 0x0000000000003400 0x00000000000034ff 0x0000000000000101 0x00000000e0000800 0x00000000e00008ff 0x0000000000000200 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 0x0000000000000000
由该文件能看出,8139too设备使用了两个I/O地址区域,第一个是他映射的I/O端口范围,第二个是他映射的内存地址空间。关于这两个值能在/proc/iomem和/proc/ioport中得
到验证。为了能看到实际的运行效果,我们选择8139too网卡作为示例,从该网卡的linux驱动程式中裁剪相关代码。
一个PCI设备的驱动程式必须要向内核中的PCI核心描述自己。同时,他也必须告诉PCI核心自己能够驱动哪些设备。下面,就介绍两个相关的重要数据结构。
/* 用于定义该驱动程序支持的不同类型的PCI设备列表*/ struct pci_device_id {
__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_ANY_ID */
kernel_ulong_t driver_data; /* 如果需要,则用来保存PCI驱动程序用于区分不同设备的信息*/
};
/* 用于向PCI核心描述PCI驱动程序*/ struct pci_driver {
struct list_head node;
char *name; /* 在内核的所有PCI驱动程序的名字必须唯一,通常设置为和驱动程序模块名相同的名字*/
struct module *owner;
const struct pci_device_id *id_table; //驱动所能操纵的设备id列表。
int (*probe)(struct pci_dev *dev, const struct pci_device_id *id); //指向PCI驱动程序中的探测函数,用于插入新设备
void (*remove)(struct pci_dev *dev); //移除设备
int (*suspend)(struct pci_dev *dev, pm_message_t state); //指向挂起函数,挂起状态以state传递,该函数可选
int (*resume)(struct pci_dev *dev); //指向恢复函数,总是在被挂起之后调用,该函数也可选
int (*enable_wake) (struct pci_dev *dev, pci_power_t state, int enable); //使能唤事件
void (*shutdown) (struct pci_dev *dev); struct device_driver driver; struct pci_dynids dynids; };
pci_device_id唯一标识一个PCI设备。他的几个成员依次分别表示:厂商号、设备号、子厂商号、子设备号、类别、类别掩码(类可分为基类、子类)、私有数据。每一个PCI设备的驱动程式都有一个pci_device_id的数组,用于告诉PCI核心自己能够驱动哪些设备。8139too的驱动程式定义他的pci_device_id数组如下: static struct pci_device_id rtl8139_pci_tbl[];
该数组被初始化为8139系列的一组网卡,当PCI核心得到这个数组后,会拿数组中的每一项跟从PCI设置空间中读取到的数据进行比对,从而为该驱动程式找到正确的设备。而pci_driver代表一个pci驱动程序。成员id_talbe即是指向pci_device_id数组的指针。name是驱动程序的名字,probe完成探测工作,即拿pci_device_id数组和内核中的数据进行比对。remove完成驱动程式的移除工作。关键的成员就这几
个。
驱动程式通过pci_module_init向内核注册自己(我们有时会看到pci_register_driver函数,其实他们是同一个,在内核代码中会看到,只是个简单的#define):
pci_module_init(&pci_driver);
调用函数后,如果pci_device_id数组中标识的设备存在于系统中,并且该设备恰好还没有驱动程式,则该驱动程式会被安装。下面我们来看从8139too驱动代码中裁剪的pci设备初始化代码: pci_driver.h: /* pci_driver.h
* helinqiang@hotmail.com * 2006-3-5 */
#ifndef PCI_DRIVER_H
#define PCI_DRIVER_H
#include //for struct pci_device_id
#include //for MODULE_DEVICE_TABLE #include //for struct pci_driver #define DRV_NAME \"8139too\"
#define DRV_VERSION \"0.9.27\"
#define RTL8139_DRIVER_NAME DRV_NAME \" Fast Ethernet driver \" DRV_VERSION typedef enum{ RTL8139 = 0, RTL8129, }board_t;
static struct pci_device_id rtl8139_pci_tbl[] = {
{0x10ec, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x10ec, 0x8138, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x1113, 0x1211, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x1500, 0x1360, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x4033, 0x1360, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x1186, 0x1300, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x1186, 0x1340, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x13d1, 0xab06, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x1259, 0xa117, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x1259, 0xa11e, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x14ea, 0xab06, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x14ea, 0xab07, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x11db, 0x1234, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x1432, 0x9130, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x02ac, 0x1012, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x018a, 0x0106, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x126c, 0x1211, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x1743, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, {0x021b, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, #ifdef CONFIG_SH_SECUREEDGE5410
/* Bogus 8139 silicon reports 8129 without external PROM :-( */ {0x10ec, 0x8129, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, #endif
#ifdef CONFIG_8139TOO_8129
{0x10ec, 0x8129, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8129 }, #endif
/* some crazy cards report invalid vendor ids like
* 0x0001 here. The other ids are valid and constant, * so we simply don’t match on the main vendor id. */
{PCI_ANY_ID, 0x8139, 0x10ec, 0x8139, 0, 0, RTL8139 }, {PCI_ANY_ID, 0x8139, 0x1186, 0x1300, 0, 0, RTL8139 }, {PCI_ANY_ID, 0x8139, 0x13d1, 0xab06, 0, 0, RTL8139 }, {0,}
};
MODULE_DEVICE_TABLE(pci, rtl8139_pci_tbl);
static int __devinit rtl8139_init_one(struct pci_dev *pdev, const struct pci_device_id *id); static void __devexit rtl8139_remove_one(struct pci_dev *pdev); static struct pci_driver rtl8139_pci_driver = { .name = DRV_NAME, .id_table = rtl8139_pci_tbl,
.probe = rtl8139_init_one,
.remove = __devexit_p(rtl8139_remove_one), };
#endif //PCI_DRIVER_H pci_driver.c: /* pci_driver.c
* helinqiang@hotmail.com * 2006-3-5 */
#include \"pci_driver.h\"
#include
MODULE_AUTHOR(\"Linqiang He, Hangzhou China\"); MODULE_LICENSE(\"Dual BSD/GPL\"); static int __init rtl8139_init_module(void) {
/* when we’re a module, we always print a version message, * even if no 8139 board is found.
*/
#ifdef MODULE
printk (KERN_INFO RTL8139_DRIVER_NAME \"\n\"); #endif
return pci_module_init(&rtl8139_pci_driver); }
static void __exit rtl8139_cleanup_module (void) {
pci_unregister_driver(&rtl8139_pci_driver); }
module_init(rtl8139_init_module); module_exit(rtl8139_cleanup_module);
int __devinit rtl8139_init_one(struct pci_dev *pdev, const struct pci_device_id *id) {
//这里可插入各种调试代码,下文会有专门描述。 return 0; }
void __devexit rtl8139_remove_one (struct pci_dev *pdev)
{ }
注册驱动程式成功后,rtl8139_init_one会被调用,在这个函数中,我们能通过插入一些打印输出语句看到PCI的设置地址空间和I/O地址区域的一些情况。 首先,插入以下语句:
u16 vendor, device;
pci_read_config_word(pdev, 0, &vendor);
pci_read_config_word(pdev, 2, &device);
printk(KERN_INFO \"%x, %x\n\
这段代码读取了网卡设备的设置地址空间的前四位,他正好是设备的厂商号和设备号。下面是输出:
10ec, 8139
10ec和8139就是我的网卡的厂商号和设备号了。 再插入下列代码:
u32 addr1,addr2,addr3, addr4,addr5,addr6; pci_read_config_dword(pdev, 16, &addr1); pci_read_config_dword(pdev, 20, &addr2); pci_read_config_dword(pdev, 24, &addr3); pci_read_config_dword(pdev, 28, &addr4);
pci_read_config_dword(pdev, 32, &addr5);
pci_read_config_dword(pdev, 36, &addr6);
printk(KERN_INFO \"%x,%x,%x,%x,%x,%x\n\ 这段代码读取网卡设备的6个I/O地址区域的址始位置。下面是输出:
3401,e0000800,0,0,0,0
可见,该设备只使用了前两个I/O地址区域,分别标识他的I/O端口区域和内存地址空间。
另外,在这里,还可直接打印出网卡的MAC地址。不再详述。
我们能在rtl8139_init_one中插入一些不同的调试代码,观察设备驱动模块在内核中的一些动作。8139too 网卡设备的设备内存的头6个字节存放的是该网卡的48位的MAC地址,我们能通过访问设备内存得到这个MAC地址。下面通过在rtl8139_init_one在插入代码,以四种不同方式访问设备内存。第一种是通过访问I/O内存实现,后三种则是通过访问I/O端口的形式实现。 第一种:
unsigned long mmio_start, addr1, addr2; void __iomem *ioaddr;
mmio_start = pci_resource_start( pdev, 1); ioaddr = pci_iomap(pdev, 1, 0); addr1 = ioread32( ioaddr );
addr2 = ioread32( ioaddr + 4 );
printk(KERN_INFO \"mmio start: %lX\n\printk(KERN_INFO \"ioaddr: %p\n\
printk(KERN_INFO \"%02lX.%02lX.%02lX.%02lX.%02lX.%02lX\n\(addr1) & 0xFF,
(addr1 >> 8) & 0xFF, (addr1 >> 16 ) & 0xFF, (addr1 >> 24 ) & 0xFF, (addr2) & 0xFF, (addr2 >> 8) & 0xFF ); 运行结果:
mmio start: E0000800 ioaddr: f8aa6800 00.02.3F.AC.41.9D 第二种:
unsigned long pio_start, pio_len, addr1, addr2; void __iomem *ioaddr;
pio_start = pci_resource_start( pdev, 0); pio_len = pci_resource_len (pdev, 0); ioaddr = ioport_map(pio_start, pio_len);
addr1 = ioread32( ioaddr ); addr2 = ioread32( ioaddr + 4 );
printk(KERN_INFO \"pio start: %lX\n\
printk(KERN_INFO \"ioaddr: %p\n\
printk(KERN_INFO \"%02lX.%02lX.%02lX.%02lX.%02lX.%02lX\n\(addr1) & 0xFF,
(addr1 >> 8) & 0xFF, (addr1 >> 16 ) & 0xFF, (addr1 >> 24 ) & 0xFF, (addr2) & 0xFF, (addr2 >> 8) & 0xFF ); 运行结果: pio start: 3400
ioaddr: 00013400 00.02.3F.AC.41.9D 第三种:
unsigned long pio_start, addr1, addr2; pio_start = pci_resource_start( pdev, 0 ); addr1 = inl( pio_start );
addr2 = inl( pio_start + 4 );
printk(KERN_INFO \"port io start: %lX\n\
printk(KERN_INFO \"%02lX.%02lX.%02lX.%02lX.%02lX.%02lX\n\(addr1) & 0xFF, (addr1 >> 8) & 0xFF, (addr1 >> 16) & 0xFF, (addr1 >> 24) & 0xFF, (addr2) & 0xFF,
(addr2 >> 8) & 0xFF ); 运行结果: port io start: 3400 00.02.3F.AC.41.9D 第四种:
unsigned long pio_start;
u8 addr1, addr2, addr3, addr4, addr5, addr6; pio_start = pci_resource_start( pdev, 0 ); addr1 = inb( pio_start ); addr2 = inb( pio_start + 1 ); addr3 = inb( pio_start + 2 ); addr4 = inb( pio_start + 3 ); addr5 = inb( pio_start + 4 );
addr6 = inb( pio_start + 5 );
printk(KERN_INFO \"port io start: %lX\n\
printk(KERN_INFO \"%02X.%02X.%02X.%02X.%02X.%02X\n\addr1, addr2, addr3, addr4, addr5, addr6 ); 运行结果:
port io start: 3400 00.02.3F.AC.41.9D
pci驱动的注册和初始化
大家好,最近在看网络部分的代码,目前看到了网卡的初始化部分。书上讲到的内容主要是网卡驱动程序对网卡自身的初始化部分,即网卡驱动的probe函数是如何执行的,而很少讲到网卡是如何注册到系统中去的这一部分。
现在的网卡大部分都是连接到PCI总线上的。因此,网卡驱动是如何连接到PCI总线,又是如何与网卡设备联系起来,网卡在注册的最后又是如何调用到该网卡的probe函数的,这一个过程将在后面的文章中进行描述。整个文章分成两个部分,第一部分是讲解总线、设备以及驱动三者的联系,为第二部分具体讲解PCI总线、网卡设备和驱动做一点铺垫。
由于我在这方面也是初学,之所以想总结出来是想到在总结的过程中对自己的学习也是一个梳理的过程。所以有什么地方写得不好的,还请各位多多指正,非常感谢!也希望能在这里结识更多的朋友。
在总结的过程中参考了下面一些资料,在此表示感谢: [1] 1. 总线、设备和驱动 1.1 简单介绍 Linux设备模型中三个很重要的概念就是总线、设备和驱动,即 bus,device和driver。它们分别对应的数据结构分别为struct bus_type,struct device和struct device_driver。 总线是处理器与一个或多个设备之间的通道,在设备模型中,所有的设备都通过总线相连。在最底层,Linux系统中的每一个设备都用device结构的一个实例来表示。而驱动则是使总线上的设备能够完成它应该完成的功能。 在系统中有多种总线,如PCI总线、SCSI总线等。系统中的多个设备和驱动是通过总线让它们联系起来的。在bus_type中两个很重要的成员就是struct kset drivers和struct kset devices。它分别代表了连接在这个总线上的两个链,一个是设备链表,另一个则是设备驱动链表。也就是说,通过一个总线描述符,就可以找到挂载到这条总线上的设备,以及支持该总线的不同的设备驱动程序。 1.2 总线、设备与驱动的绑定 在系统启动时,它会对每种类型的总线创建一个描述符,并将使用该总线的设备链接到该总线描述符的devices链上来。也即是说在系统初始化时,它会扫描连接了哪些设备,并且为每个设备建立一个struce device变量,然后将该变量链接到这个设备所连接的总线的描述符上去。另一方面,每当加载了一个设备驱动,则系统也会准备一个struct device_driver结构的变量,然后再将这个变量也链接到它所在总线的描述符的drivers链上去。 对于设备来说,在结构体struct device中有两个重要的成员,一个是struct bus_type *bus,另一个是struct device_driver *driver。bus 成员就表示该设备是链接到哪一个总线上的,而driver成员就表示当前设备是由哪个驱动程序所驱动的。对于驱动程序来说,在结构体struct device_driver中也有两个成员,struct bus_type *bus和struct list_head devices,这里的bus成员也是指向这个驱动是链接到哪个总线上的,而devices这个链表则是表示当前这个驱动程序可以去进行驱动的那些设备。一个驱动程序可以支持一个或多个设备,而一个设备则只会绑定给一个驱动程序。 对于device与device_driver之间建立联系的方式,主要有两种方式。第一种,在计算机启动的时候,总线开始扫描连接在其上的设备,为每个设备建立一个struct device变量并链接到该总线的devices链上,然后开始初始化不同的驱动程序,驱动程序到它所在的总线的devices链上去遍历每一个还没有被绑定给某个驱动的设备,然后再查看是否能够支持这种设备,如果它能够支持这种设备,则将这个设备与这个驱动联系起来。即,将这个设备的device变量加到驱动的devices链上,同时让struct device中的device_driver指向当前这个驱动。第二种则是热插拔。也即是在系统运行时插入了设备,此时内核会去查找在该bus链上注册了的device_driver,然后再将设备与驱动联系起来。设备与驱动根据什么规则联系起来,它们是如何被联系起来的代码我们将在后面的章节进行详细的描述。 1.3 PCI总线 PCI是一种在CPU与I/O设备之间进行高速数据传输的一种总线。有很多设备都是使用PCI总线的,网卡就是其中之一。我们在前面 讲了那些总线、设备与驱动方面的知识,原因就在于网卡是连接到PCI总线上,所以PCI总线、网卡设备以及网卡驱动就成了我们研究网卡的一个很重要的线索,尤其是在网络的链路层部分。下图显示了在一个系统中PCI设备的一个框图: PCI子系统声明了一个bus_type结构,为pci_bus_type。它就是PCI总线的描述符。在这个变量上,链接了PCI设备以及支持PCI设备的驱动程序。 1.4 PCI设备与驱动 PCI设备通常由一组参数唯一地标识,它们被vendorID, deviceID和class nodes所标识,即设备厂商,型号等,这些参数保存在pci_device_id结构中。每个PCI设备都会被分配一个pci_dev变量,内核就用这个数据结构来表示一个PCI设备。 所有的PCI驱动程序都必须定义一个pci_driver结构变量,在该变量中包含了这个PCI驱动程序所提供的不同功能的函数,同时,在这个结构中也包含了一个device_driver结构,这个结构定义了PCI子系统与PCI设备之间的接口。在注册PCI驱动程序时,这个结构将被初始化,同时这个pci_driver变量会被链接到pci_bus_type中的驱动链上去。 在pci_driver中有一个成员struct pci_device_id *id_table,它列出了这个设备驱动程序所能够处理的所有PCI设备的ID值。 1.5 PCI设备与驱动的绑定过程 下面描述一下对于PCI设备与驱动绑定的过程。首先在系统启动的时候,PCI总线会去扫描连接到这个总线上的设备,同时为每一个设备建立一个pci_dev结构,在这个结构中有一个device成员,并将这些pci_dev结构链接到PCI总线描述符上的devices链。如下图所示: 第二步是当PCI驱动被加载时,pci_driver结构体将被初始化,这一过程在函数pci_register_driver中: drv->driver.bus = &pci_bus_type; drv->driver.probe = pci_device_probe; 最后会调用driver_register(&drv->driver)将这个PCI驱动挂载到总线描述符的驱动链上。同时在注册的过程中,会根据pci_driver中的id_table中的ID值去查看该驱动支持哪些设备,将这些设备挂载到pci_driver中的devices链中来。如下图所示: 对于不同的设备,可能驱动程序也不一样,因此,对于上图中的Dev3,可能就需要另外一个驱动程序来对其进行驱动。所以当加载了Dev3的驱动程序时,其示意图如下图所示: 上面这三个示意图就描述了总线、设备以及驱动在系统中是如何进行相互联系的。前面对于驱动注册这些函数的描述较为简单,因为网卡是一个PCI设备,因此在后面具体地讲到网卡注册时再来详细地讲解和PCI相关的注册等函数。 1.6 小结 本部分主要讲解了总线、设备以及驱动方面的一些知识,由于网卡是一个PCI设备,因此具体地讲到了一点PCI总线、PCI设备及相应的PCI驱动方面的知识,但是由于PCI本身就是很大的一个子系统,因此这里不可能对其进行详细地讲解,在后面对网卡的分析中,将对网卡中涉及到的和PCI相关的部分进行讲解。 2. 网卡在PCI层的注册 2.1 数据结构 前面第一章讲了总线、设备以及驱动方面的关系,也讲到了大多 数网卡设备实际上是一个PCI设备。因此,本章就讲解网卡设备在注册时是如何注册到PCI总线上去的。在这里,以Intel的E100网卡驱动进行讲解。 前面讲到每个PCI设备都由一组参数唯一地标识,这些参数保存在结构体pci_device_id中,如下所示: 1. 2. 3. 4. 5. 6. 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 */ }; 每个PCI设备驱动都有一个pci_driver变量,它描述了一个PCI驱动的信息,如下所示: struct pci_driver { struct list_head node; 3. char *name; 4. const struct pci_device_id *id_table; /* must be non-NULL for probe to be called */ 5. int (*probe) (struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */ 6. void (*remove) (struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */ 7. int (*suspend) (struct pci_dev *dev, pm_message_t state); /* Device suspended */ 8. int (*suspend_late) (struct pci_dev *dev, pm_message_t state); 9. int (*resume_early) (struct pci_dev *dev); 10. int (*resume) (struct pci_dev *dev); /* Device woken up */ 1. 2. int (*enable_wake) (struct pci_dev *dev, pci_power_t state, int enable); /* Enable wake event */ 12. void (*shutdown) (struct pci_dev *dev); 11. 13. 14. struct pci_error_handlers *err_handler; 15. 16. struct device_driver driver; struct pci_dynids dynids; 17. 18. int multithread_probe; 19. }; 每个PCI驱动中都有一个id_table成员变量,记录了当前这个驱动所能够进行驱动的那些设备的ID值。 对于E100网卡驱动来说,它的pci_driver变量定义为: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. static struct pci_driver e100_driver = { .name = DRV_NAME, .id_table = e100_id_table, .probe = e100_probe, .remove = __devexit_p(e100_remove), #ifdef CONFIG_PM /* Power Management hooks */ .suspend = e100_suspend, .resume = e100_resume, #endif .shutdown = e100_shutdown, .err_handler = &e100_err_handler, }; 里面e100_id_table就表示该E100驱动所能够支持的PCI设备的ID号,其定义为: 1. 2. 3. 4. 5. #define INTEL_8255X_ETHERNET_DEVICE(device_id, ich) {\\ PCI_VENDOR_ID_INTEL, device_id, PCI_ANY_ID, PCI_ANY_ID, \\ PCI_CLASS_NETWORK_ETHERNET << 8, 0xFFFF00, ich } static struct pci_device_id e100_id_table[] = { INTEL_8255X_ETHERNET_DEVICE(0x1029, 0), 6. 7. 8. 9. INTEL_8255X_ETHERNET_DEVICE(0x1030, 0), … { 0, } }; 当PCI层检测到一个PCI设备能够被某PCI驱动所支持时(这是通过函数pci_match_one_device来进行检测的),就会调用这个PCI驱动上的probe函数,在该函数中会对该特定的PCI设备进行一些具体的初始化等操作。比如对于E100设备驱动来说,其probe函数为e100_probe。在这个函数中,会对网卡设备进行初始化。 e100_probe主要就涉及到网卡设备net_device的初始化,我们现在先来关注一下从网卡注册一直到调用e100_probe这一个过程的整个流程。 2.2 E100初始化 E100驱动程序的初始化是在函数e100_init_module()中的,如下: 1. 2. 3. 4. 5. 6. 7. 8. static int __init e100_init_module(void) { if(((1 << debug) - 1) & NETIF_MSG_DRV) { printk(KERN_INFO PFX \"%s, %s\\n\DRV_DESCRIPTION, DRV_VERSION); printk(KERN_INFO PFX \"%s\\n\DRV_COPYRIGHT); } return pci_register_driver(&e100_driver); } 在这个函数中,调用了pci_register_driver()函数,对e100_driver这个驱动进行注册。 2.3 PCI注册 在前面我们已经看到,PCI的注册就是将PCI驱动程序挂载到其所在的总线的drivers链,同时扫描PCI设备,将它能够进行驱动的设备挂载到driver上的devices链表上来,这里,我们将详细地查看这整个流程的函数调用关系。 pci_register_driver()->__pci_register_driver() //注意:这可是2个不同的函数 /** 2. * __pci_register_driver - register a new pci driver 3. * @drv: the driver structure to register 4. * @owner: owner module of drv 5. * @mod_name: module name string 6. * 7. * Adds the driver structure to the list of registered drivers. 8. * Returns a negative value on error, otherwise 0. 9. * If no error occurred, the driver remains registered even if 10. * no device was claimed during registration. 11. */ 12. int __pci_register_driver(struct pci_driver *drv, struct module *owner, const char *mod_name); 1. 13. 14. 15. 16. 17. //在函数中有几个初始化语句: drv->driver.name = drv->name; drv->driver.bus = &pci_bus_type; drv->driver.owner = owner; drv->driver.mod_name = mod_name; 即是将PCI设备中的driver变量的总线指向pci_bus_type这个总线描述符,同时设置驱动的名字等。 pci_bus_type定义如下: 1. 2. struct bus_type pci_bus_type = { .name = \"pci\ 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. .match = pci_bus_match, .uevent = pci_uevent, .probe = pci_device_probe, .remove = pci_device_remove, .suspend = pci_device_suspend, .suspend_late = pci_device_suspend_late, .resume_early = pci_device_resume_early, .resume = pci_device_resume, .shutdown = pci_device_shutdown, .dev_attrs = pci_dev_attrs, }; 然后再调用函数driver_register(&drv->driver);通过这个函数将这个PCI驱动中的struct device_driver driver成员变量注册到系统中去。 pci_register_driver()->__pci_register_driver()->driver_register() driver_register()代码如下: /** 2. * driver_register - register driver with bus 3. * @drv: driver to register 4. * 5. * We pass off most of the work to the bus_add_driver() call, 6. * since most of the things we have to do deal with the bus 7. * structures. 8. * 9. * The one interesting aspect is that we setup @drv->unloaded 10. * as a completion that gets complete when the driver reference 11. * count reaches 0. 12. */ 13. int driver_register(struct device_driver * drv) 14. { 15. if ((drv->bus->probe && drv->probe) || 16. (drv->bus->remove && drv->remove) || 17. (drv->bus->shutdown && drv->shutdown)) { 18. printk(KERN_WARNING \"Driver '%s' needs updating - please use bus_type methods\\n\ 1. 19. 20. 21. 22. 23. } klist_init(&drv->klist_devices, NULL, NULL); init_completion(&drv->unloaded); return bus_add_driver(drv); } klist_init()是为设备驱动的klist_devices成员进行初始化,这个klist_devices是一个对链表进行操作的包裹结构,它会链接这个驱动能够支持的那些设备。 最后就调用bus_add_driver()函数。这个函数的功能就是将这个驱动加到其所在的总线的驱动链上。 pci_register_driver()->__pci_register_driver()->driver_register()->bus_add_driver()->driver_attach() 在bus_add_driver()函数中,最重要的是调用driver_attach()函数,其定义如下: /** * driver_attach - try to bind driver to devices. 3. * @drv: driver. 4. * 5. * Walk the list of devices that the bus has on it and try to 6. * match the driver with each one. If driver_probe_device() 7. * returns 0 and the @dev->driver is set, we've found a 8. * compatible pair. 9. */ 10. int driver_attach(struct device_driver * drv) 11. { 12. return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach); 13. } 1. 2. 该函数遍历这个驱动所在的总线上的所有设备,然后将这些设备与当前驱动进行匹配,以检测这个驱动是否能够支持某个设备,也即是 将设备与驱动联系起来。 bus_for_each_dev函数是扫描在drv->bus这个总线上的所有设备,然后将每个设备以及当前驱动这两个指针传递给__driver_attach函数。 pci_register_driver()->__pci_register_driver()->driver_register()->bus_add_driver()->driver_attach()->__driver_attach() __driver_attach()函数是将驱动与设备联系起来的函数。 1. 2. 3. 4. 5. static int __driver_attach(struct device * dev, void * data) { struct device_driver * drv = data; /* 6. * Lock device and try to bind to it. We drop the error 7. * here and always return 0, because we need to keep trying 8. * to bind to devices and some drivers will return an error 9. * simply if it didn't support the device. 10. * 11. * driver_probe_device() will spit a warning if there 12. * is an error. 13. */ 14. 15. if (dev->parent) /* Needed for USB */ 16. 17. 18. 19. 20. 21. 22. down(&dev->parent->sem); down(&dev->sem); if (!dev->driver) driver_probe_device(drv, dev); up(&dev->sem); if (dev->parent) up(&dev->parent->sem); } 23. 24. return 0; 25. 在函数中有两条语句: 1. if (!dev->driver) 2. driver_probe_device(drv, dev); 也即是判断当前设备是否已经注册了一个驱动,如果没有注册驱动,则调用driver_probe_device()函数。 pci_register_driver()->__pci_register_driver()->driver_register()->bus_add_driver()->driver_attach()->__driver_attach()->driver_probe_device() 如下: /** * driver_probe_device - attempt to bind device & driver together 3. * @drv: driver to bind a device to 4. * @dev: device to try to bind to the driver 5. * 6. * First, we call the bus's match function, if one present, which should 7. * compare the device IDs the driver supports with the device IDs of the 8. * device. Note we don't do this ourselves because we don't know the 9. * format of the ID structures, nor what is to be considered a match and 10. * what is not. 11. * 12. * This function returns 1 if a match is found, an error if one occurs 13. * (that is not -ENODEV or -ENXIO), and 0 otherwise. 14. * 15. * This function must be called with @dev->sem held. When called for a 16. * USB interface, @dev->parent->sem must be held as well. 17. */ 18. int driver_probe_device(struct device_driver * drv, struct device * dev) 19. { 20. struct stupid_thread_structure *data; 21. struct task_struct *probe_task; 22. int ret = 0; 1. 2. 23. 24. if (!device_is_registered(dev)) 25. return -ENODEV; 26. 27. if (drv->bus->match && !drv->bus->match(dev, drv)) goto done; 28. 29. pr_debug(\"%s: Matched Device %s with Driver %s\\n\ 30. drv->bus->name, dev->bus_id, drv->name); 31. 32. data = kmalloc(sizeof(*data), GFP_KERNEL); 33. 34. 35. 36. if (!data) return -ENOMEM; data->drv = drv; data->dev = dev; 37. 38. if (drv->multithread_probe) { 39. probe_task = kthread_run(really_probe, data, 40. 41. 42. 43. 44. \"probe-%s\ if (IS_ERR(probe_task)) ret = really_probe(data); } else ret = really_probe(data); return ret; } 45. 46. done: 47. 48. 该函数首先会调用总线上的match函数,以判断当前的PCI驱动能否支持该PCI设备,如果可以,则继续往后面执行。 drv->bus->match函数也即是pci_bus_type中的match成员变量,它为pci_bus_match函数。 pci_register_driver()->__pci_register_driver()->driver_register()->bus_add_driver()->driver_attach()->__driver_attach()->driver_probe_device()->pci_bus_match() 1. 2. 3. 4. 5. 6. /** * pci_bus_match - Tell if a PCI device structure has a matching PCI device id structure * @dev: the PCI device structure to match against * @drv: the device driver to search for matching PCI device id structures * * Used by a driver to check whether a PCI device present in the * system is in its list of supported devices. Returns the matching 8. * pci_device_id structure or %NULL if there is no match. 9. */ 10. static int pci_bus_match(struct device *dev, struct device_driver *drv) 11. { 12. struct pci_dev *pci_dev = to_pci_dev(dev); 13. struct pci_driver *pci_drv = to_pci_driver(drv); 14. const struct pci_device_id *found_id; 7. 15. 16. found_id = pci_match_device(pci_drv, pci_dev); 17. 18. if (found_id) return 1; 19. 20. return 0; 21. } pci_bus_match函数的作用就是将PCI设备与PCI驱动进行比较以检查该驱动是否能够支持这个设备。在函数的最前面是两个宏to_pci_dev和to_pci_driver。因为在函数执行的过程中,虽然最开始传进来的是pci_driver结构与pci_dev结构,但是在执行的时候却取了这两个结构体中的device_driver和device成员变量,所以现在就要通过这两个成员变量找到之前对应的pci_driver和pci_dev结构的地址。 #define to_pci_dev(n) container_of(n, struct pci_dev, dev) #define to_pci_driver(drv) container_of(drv,struct pci_driver, driver) 这两个宏在 pci_register_driver()->__pci_register_driver()->driver_register()->bus_add_driver()->driver_attach()->__driver_attach()->driver_probe_device()->pci_bus_match()->pci_match_device() /** 2. * pci_match_device - Tell if a PCI device structure has a matching PCI device id structure 3. * @drv: the PCI driver to match against 4. * @dev: the PCI device structure to match against 5. * 6. * Used by a driver to check whether a PCI device present in the 7. * system is in its list of supported devices. Returns the matching 8. * pci_device_id structure or %NULL if there is no match. 9. */ 10. const struct pci_device_id *pci_match_device(struct pci_driver *drv, 11. struct pci_dev *dev) 12. { 13. struct pci_dynid *dynid; 1. 14. 15. /* Look at the dynamic ids first, before the static ones */ 16. spin_lock(&drv->dynids.lock); 17. 18. 19. 20. 21. 22. 23. list_for_each_entry(dynid, &drv->dynids.list, node) { if (pci_match_one_device(&dynid->id, dev)) { spin_unlock(&drv->dynids.lock); return &dynid->id; } } spin_unlock(&drv->dynids.lock); } 24. 25. return pci_match_id(drv->id_table, dev); 26. pci_match_one_driver函数的作用是将一个PCI设备与PCI驱动进行比较,以查看它们是否相匹配。如果相匹配,则返回匹配的pci_device_id结构体指针。 此时,如果该PCI驱动已经找到了一个可以想符的PCI设备,则返回,然后再退回到之前的driver_probe_device函数中。在该函数最后将调用really_probe函数。将device_driver与device结构体指针作为参数传递到这个函数中。下面几行是调用驱动或者总线的probe函 数来扫描设备。 pci_register_driver()->__pci_register_driver()->driver_register()->bus_add_driver()->driver_attach()->__driver_attach()->driver_probe_device()->really_probe() 在函数really_probe()中: 1. 2. 3. 4. 5. 6. 7. 8. 9. if (dev->bus->probe) { ret = dev->bus->probe(dev); if (ret) goto probe_failed; } else if (drv->probe) { ret = drv->probe(dev); if (ret) goto probe_failed; } 此时的dev->bus为pci_bus_type,其probe函数则对应为:pci_device_probe。 pci_register_driver()->__pci_register_driver()->driver_register()->bus_add_driver()->driver_attach()->__driver_attach()->driver_probe_device()->really_probe()->pci_device_probe() 同样,在该函数中会获得当前的PCI设备的pci_dev结构体指针以及PCI驱动程序的pci_driver结构体指针。分别使用宏to_pci_dev和to_pci_driver。最后则调用函数__pci_device_probe。在该函数中还会调用函数pci_call_probe,这是最后的函数 pci_register_driver()->__pci_register_driver()->driver_register()->bus_add_driver()->driver_attach()->__driver_attach()->driver_probe_device()->really_probe()->pci_device_probe()->__pci_device_probe()->pci_call_probe() 在函数pci_call_probe里有一条语句: 1. 2. 3. static int pci_call_probe(struct pci_driver *drv, struct pci_dev *dev, const struct pci_device_id *id) { 4. 5. 6. int error; /* 省略 */ error = drv->probe(dev, id); 在此处就调用了pci_driver的probe函数,对于这里的E100驱动来说,它的probe函数是最开始注册的e100_probe函数,在该函数中会完成对网卡设备net_device的初始化等操作。 1. pci_register_driver()->__pci_register_driver()->driver_register()->bus_add_driver()->driver_attach()->__driver_attach()->driver_probe_device()->really_probe()->pci_device_probe()->__pci_device_probe()->pci_call_probe()->e100_probe() 到这里,我们对网卡驱动的PCI层的初始化分析就告一个段落了,剩下的部分就是网卡驱动对网卡设备本身的初始化等操作。 2.4 函数调用流程图 在这里,为网卡在PCI层的注册画了一个函数调用的流程图,能够更直观地展现网卡从注册到调用其自身的网卡初始化的这一个函数调用过程。 文笔实在不行,写了一个下午加晚上,才写这么点。希望能够对初学者有一点用吧。因为我最开始在看网卡驱动的时候,就是迷惑加载了网卡之后是如何调用到该网卡的probe函数的。所以就仔细地看了一下里面的源码。 这里主要还是起一个梳理的作用,很多代码也没有进一步地深入分析。不过对于网络架构来说,首先将整个调用的流程掌握了,对后面的理解 也就更加方便了。 因篇幅问题不能全部显示,请点此查看更多更全内容