Copyright © 2007 本文遵从GNU 的自由文档许可证(Free Document License)的条款,欢迎转载、修改、散布。
发布时间:2007年3月16日
最近更新:2008年12月01日,增加字符设备驱动程序开发介绍。
Table of Contents
在80x86微处理器中,有三种存储器地址:
逻辑地址(logical address),包含在机器语言指令中用来指定一个操作数或一条指令的地址。每个逻辑地址都由一个段(segment)和一个偏移量(offset)组成。偏移量指明了从段的开始到实际地址之间的距离。
线性地址(linear address)(也称为虚拟地址,virtual address),它是一个32位无符号整数,可用以表达高达4G的地址(2的32次方)。通常以十六进制数表示,值的范围从0X00000000到0Xffffffff。
物理地址(physical address),用于存储器芯片级存储单元寻址,它们与从微处理器的地址引脚发送到存储器总线上的电信号相对应。物理地址由32位无符号整数表示。
CPU控制单元通过一种称为分段单元(segmentation unit)的硬件电路把一个逻辑地址转换成线性地址;线性地址又通过一个分页单元(paging unit)的硬件电路把一个线性地址转换成物理地址。
逻辑地址由两部份组成,一个段标识符和一个指定段由相对地址的偏移量。段标识符是一个16位长的字段,称为段选择符(segment selector),偏移量是一个32位长的字段。
处理器提供专门的段寄存器以快速处理段选择符,段寄存器的唯一目的就是存放段选择符。共有6个段寄存器,分别是cs、ss、ds、es、fs和gs。其中cs、ss、ds寄存器有专门的用途。
cs是代码段寄存器,指向包含程序指令的段。
ss是栈寄存器,指向包含当前程序栈的段。
ds是数据段寄存器,指向包含静态数据或者外部数据的段。
cs寄存器有一个重要功能,它包含有一个两位的字段,用以指明CPU当前特权级别(Current Privilege Level,CPL)。值0表示最高优先级,值3表示最低优先级。Linux只用到0级和3级,分别表示内核态和用户态。
每个段由一个8字节的段描述符表示,它描述了段的特征。段描述符放在全局描述符表(Global Descriptor Table,GDT)中或局部描述符表(Local Descriptor Table,LDT)中。
段描述符的组成:
32位的Base字段,含有段的第一个字节的线性地址。
粒度标记G。如果该位清0,则段大小以字节为单位,否则以4096字节的倍数计。
20位的Limit字段指定段的长度(以字节为单位,Limit字段为0的段被认为是空段)。当G为0时,段的大小在1字节到1MB之间;否则段的大小在4KB到4GB之间。
系统标记S。如果它被清0,则这是一个系统段,用于存储内核数据结构,否则,它是一个普通的代码段或数据段。
4位Type字段,描述段的类型和它的访问权限。常用的Type有以下几种:
代码段描述符
数据段描述符
任务状态段描述符
局部描述符表描述符
在编程思路上,机制表示需要提供什么功能,策略表示如何使用这些功能。区分机制和策略是UNIX设计最重要和最好的思想之一。如X系统就由X服务器和X客户端组成。X服务器实现机制,负责操作硬件,给用户程序提供一个统一的接口。而X客户端实现策略,负责如何使用X服务器提供的功能。设备驱动程序也是机制与策略分离的典型应用。在编写硬件驱动程序时,不要强加任何特定的策略。
Linux系统将设备分成三种类型,分别是字符设备、块设备和网络接口设备。
在linux中通过设备文件访问硬件,设备文件位于/dev目录下。设备文件是一种信息文件,普通文件的目的在于存储数据,设备文件的目的在于向内核提供控制硬件的设备驱动程序的信息。设备文件保存了多种信息,其中重要的有设备类型信息,主设备号(major),次设备号(minor)。主设备号与次设备号起到连接应用程序和设备驱动程序的作用。当应用程序利用open()函数打开设备文件时,内核从相应的设备文件中得到主设备号,从而查找到相应的设备驱动程序,由次设备号查找实际设备。所以主设备号对应设备驱动程序,次设备号对应由该驱动程序所驱动的实际设备。通过设备文件可以向硬件传送数据,也可从硬件接收数据。
设备文件使用mknod命令生成。mknod命令语法如下:
mknod [设备文件名] [设备文件类型] [主设备号] [次设备号]
字符设备用c表示,块设备用b表示,网络设备没有专门的设备文件。
读写设备文件时要使用低级输入输出函数,不要使用带缓冲的以f开头的流文件输入输出函数。但并不是所有低级输入输出函数都可以用在设备文件上,可以用在设备文件的低级输入输出函数有以下几个:
open() 打开文件或设备 close() 关闭文件 read() 读取数据 write() 写数据 lseek() 改变文件的读写位置 ioctl() 实现read(),write()外的特殊控制,该函数只在设备文件中使用 fsync() 实现写入文件上的数据和实际硬件的同步
Table of Contents
字符设备在系统中以设备文件的形式表示,位于/dev目录下。每个字符设备都有一个主设备号和次设备号,主设备号标识设备对应的驱动程序,次设备号标识设备文件所指的具体设备。
主次设备号的数据类型是dev_t,在/linux/types.h中定义。在2.6内核中,dev_t是一个32位的数,其中12位用来表示主设备号,其余20位用来表示次设备号。要获得设备的主次设备号可以使用内核提供的宏:
MAJOR(dev_t dev); #获得主设备号 MINOR(dev_t dev); #获得次设备号
这些宏定义位于linux/kdev_t.h中。如果要把主次设备号转换成dev_t类型,则可使用:
MKDEV(int major, int minor);
在建立一个字符设备之前,需要为它分配一个或多个设备号。使用register_chrdev_region()函数完成设备号的分配。该函数在linux/fs.h中声明。原型如下:
int register_chrdev_region(dev_t first, unsigned int count, char *name); first:是要分配的主设备号范围的起始值,次设备号一般设置为0; count:是所请求的连续设备号的个数; name:是和该设备号范围关联的设备名称,它将出现在/proc/devices或/sysfs中。
如果分配成功则返回0,分配失败则返回一个负的错误码,所请求的设备号无效。
还有一个自动分配设备号的函数alloc_chrdev_region(),原型如下:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name); dev: 自动分配到设备号范围中的第一个主设备号; firstminor:自动分配的第一个次设备号,通常为0; count: 是所请求的连续设备号的个数; name: 是和该设备号范围关联的设备名称,它将出现在/proc/devices或/sysfs中。
如果我们不再使用设备号,则要使用unregister_chrdev_region()函数释放它。函数原型如下:
void unregister_chrdev_region(dev_t first, unsigned int count); 函数的参数作用同上
我们一般在模块的清除函数中调用设备号释放函数。
在内核源码目录的Documentation/devices.txt文件中列出了已静态分配给常用设备的主设备号。为了减少设备号分配的冲突,我们一般要使用alloc_chrdev_region()函数来自动分配设备号。
文件操作结构:struct file_operations,在linux/fs.h中定义。它包含一组函数指针,实现文件操作的系统调用,如read、write等。每个打开的文件都和一个文件操作结构关联(通过file结构中指向file_operations结构的f_op字段进行关联)。
文件结构:struct file,在linux/fs.h中定义。file结构代表一个打开的文件,由内核在open时创建。指向文件结构的指针在内核中通常称为filp(文件指针)。当文件的所有实例都被关闭之后,内核会释放这个数据结构。
节点结构:struct inode,在linux/fs.h中定义。inode结构是内核表示文件的方法,而file结构是以文件描述符的方式表示文件的方法。结构中以下两个字段对编写驱动程序有用:
dev_t i_rdev,该字段包含了真正的设备编号。
struct cdev *i_cdev,该字段包含指向struct cdev结构的指针。
从设备的inode获取主次设备号的宏:
unsigned int iminor(struct inode *inode); unsigned int imajor(struct inode *inode);
下面两个是字符设备读写操作最重要的内核函数。
unsigned long copy_to_user (void __user * to, const void * from, unsigned long n); 读操作,把数据从内核空间复制到用户空间,返回不能复制的字节数,如果成功则返回0。 to 目的地址,在用户空间中; from 源地址,在用户空间; n 要复制的字节数。 unsigned long copy_from_user (void * to, const void __user * from, unsigned long n); 写操作,把数据从用户空间复制到内核空间,返回不能复制的字节数,如果成功则返回0。 to 目的地址,在内核空间中; from 源地址,在用户空间; n 要复制的字节数。
pci设备上电时,硬件保持未激活状态。设备不会有内存和I/O端口映射到计算机的地址空间。每个PCI主板上都配备有能够处理PCI的BIOS、NVRAM或PROM等固件。这些固件通过读写PCI控制器中的寄存器,提供了对设备配置地址空间的访问。系统引导时,固件在每个PCI设备上执行配置事务,以便为它提供的每个地址区域分配一个安全的位置。当驱动程序访问设备时,它的内存和I/O区域已经被映射到了处理器的地址空间。
所有PCI设备都有至少256字节的地址空间。前64字节是标准化的,每种设备都有且意义相同,其余字节是设备相关的。
在内核中有三个主要的数据结构与PCI接口有关,在开发PCI设备驱动程序时要用到,分别是:
pci_device_id,PCI设备类型的标识符。在include/linux/mod_devicetable.h头文件中定义。
struct pci_device_id { __u32 vendor, device; /* Vendor and device ID or PCI_ANY_ID*/ __u32 subvendor, subdevice; /* Subsystem ID's or PCI_ANY_ID */ __u32 class, class_mask; /* (class,subclass,prog-if) triplet */ kernel_ulong_t driver_data; /* Data private to the driver */ };
PCI设备的vendor、device和class的值都是预先定义好的,通过这些参数可以唯一确定设备厂商和设备类型。这些PCI设备的标准值在include/linux/pci_ids.h头文件中定义。
pci_device_id需要导出到用户空间,使模块装载系统在装载模块时知道什么模块对应什么硬件设备。宏MODULE_DEVICE_TABLE()完成该工作。
设备id一般用数组形式。如:
static struct pci_device_id rtl8139_pci_tbl[] = { {0x10ec, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 }, .... }; MODULE_DEVICE_TABLE (pci, rtl8139_pci_tbl);
pci_dev,标识具体的PCI设备实例,与net_device类似。内核通过该内核结构来访问具体的PCI设备。在include/linux/pci.h头文件中定义。
pci_driver,设备驱动程序数据结构,它是驱动程序与PCI总线的接口,有大量的回调函数和指针,向PCI核心描述了PCI驱动程序。在include/linux/pci.h头文件中定义。
static struct pci_driver rtl8139_pci_driver = { .name = DRV_NAME, #设备名 .id_table = rtl8139_pci_tbl, #pci设备的id表组 .probe = rtl8139_init_one, #初始化函数 .remove = __devexit_p(rtl8139_remove_one), #退出函数 #ifdef CONFIG_PM #如果设备支持电源管理 .suspend = rtl8139_suspend, #休眠 .resume = rtl8139_resume, #从休眠恢复 #endif /* CONFIG_PM */ };
内核通过pci_register_driver和pci_unregister_driver函数来注册和注消PCI设备驱动程序。这两个函数在drivers/pci/pci.c源码中定义。pci_register_driver函数需要使用pci_driver数据结构作为参数。通过注册,PCI设备就与PCI设备驱动程序关联起来了。
PCI设备最大的优点是可以自动探测每个设备所需的IRQ和其它资源。有两种探测方式,一种是静态探测,一种是动态探测。静态探测是通过设备驱动程序自动选择相关资源,动态探测是指支持热插拔设备的功能。
PCI设备通过pci_driver结构中的suspend和resume函数指针支持电源管理。可实现暂停和重新启动PCI设备的功能。
/lib/modules/KERNEL_VERSION/modules.pcimap文件列出内核所支持的所有PCI设备和它们的模块名。
debian:/lib/modules/2.6.23.9# cat modules.pcimap | more # pci module vendor device subvendor subdevice class class_mask driver_data snd-trident 0x00001023 0x00002000 0xffffffff 0xffffffff 0x00040100 0x00ffff00 0x0 snd-trident 0x00001023 0x00002001 0xffffffff 0xffffffff 0x00000000 0x00000000 0x0 ... 8139cp 0x000010ec 0x00008139 0xffffffff 0xffffffff 0x00000000 0x00000000 0x0 8139cp 0x00000357 0x0000000a 0xffffffff 0xffffffff 0x00000000 0x00000000 0x0 ...
内核使用了大量不同的宏来标记具有不同作用的函数和数据结构。如宏__init、__devinit等。这些宏在include/linux/init.h头文件中定义。编译器通过这些宏可以把代码优化放到合适的内存位置,以减少内存占用和提高内核效率。
下面是一些常用的宏:
__init,标记内核启动时使用的初始化代码,内核启动完成后不再需要。以此标记的代码位于.init.text内存区域。它的宏定义是这样的:
#define _ _init _ _attribute_ _ ((_ _section_ _ (".text.init")))
__exit,标记退出代码,对于非模块无效。
__initdata,标记内核启动时使用的初始化数据结构,内核启动完成后不再需要。以此标记的代码位于.init.data内存区域。
__devinit,标记设备初始化使用的代码。
__devinitdata,标记初始化设备数据结构的函数。
__devexit,标记移除设备时使用的代码。
xxx_initcall,一系列的初始化代码,按降序优先级排列。
初始化代码的内存结构 _init_begin ------------------- | .init.text | ---- __init |-------------------| | .init.data | ---- __initdata _setup_start |-------------------| | .init.setup | ---- __setup_param __initcall_start |-------------------| | .initcall1.init | ---- core_initcall |-------------------| | .initcall2.init | ---- postcore_initcall |-------------------| | .initcall3.init | ---- arch_initcall |-------------------| | .initcall4.init | ---- subsys_initcall |-------------------| | .initcall5.init | ---- fs_initcall |-------------------| | .initcall6.init | ---- device_initcall |-------------------| | .initcall7.init | ---- late_initcall __initcall_end |-------------------| | | | ... ... ... | | | __init_end -------------------
初始化代码的特点是:在系统启动运行,且一旦运行后马上退出内存,不再占用内存。
对于驱动程序模块来说,这些优化标记使用的情况如下:
通过module_init()和module_exit()函数调用的函数就需要使用__init和__exit宏来标记。
pci_driver数据结构不需标记。
probe()和remove()函数应该使用__devinit和__devexit标记,且只能标记probe()和remove()
如果remove()使用__devexit标记,则在pci_driver结构中要用__devexit_p(remove)来引用remove()函数。
如果你不确定需不需要添加优化宏则不要添加。
内核通过不同的接口向用户输出内核信息。我们可通过这些接口访问和修改内核参数。共有三种接口,其中两种是procfs和sysfs虚拟文件系统,第三种是sysctl命令。
启用procfs虚拟文件系统的内核选项是"Filesystems-->Pseudo filesystems-->proc file system support"。procfs文件系统挂载在/proc目录,可用cat、more等shell命令查看目录中的文件。
sysctl命令也可以修改和查看内核变量,sysctl操作的内核变量位于/proc/sys目录下。启用sysctl支持的内核选项是"General setup-->Sysctl support"。
procfs和sysctl接口已使用多年,从2.6内核开始,引入新的sysfs虚拟文件系统,它挂载在/sys目录下。启用sysfs的内核选项是"Filesystems-->Pseudo filesystems-->sysfs filesystem support (NEW)"。sysfs以更整齐更直观的方式向用户展示了内核的各种参数。/proc将会向sysfs迁移。
另外,通过ioctl(input/output control)system call和Netlink接口也可以向内核发送命令,执行内核参数配置工作,大多数的网络配置参数都可以用这两个接口修改。ifconfig和route命令使用ioctl接口,IPROUTE2使用Netlink接口。
网络的ioctl命令在include/linux/sockios.h中定义。这些命令被定义成类似于SIOCSIFMTU的宏,宏的命令规则是这样的,开头四个字符SIOC代表ioctl命令;S表示set,G表示get;if表示接口类型;MTU表示mtu。其它字符的表示方式还有:ADD表示添加,RT表示路由等。
我们可以通过内核初始化选项,在系统启动时或内核模块加载时微调内核的功能。
模块的初始化选项是通过模块程序中的module_param宏传递的。如:
... module_param(multicast_filter_limit, int, 0444); module_param(max_interrupt_work, int, 0444); module_param(debug, int, 0444); ...
module_param宏的第一个参数是选项名,可在/sys虚拟文件系统中该模块的parameter目录中中查看到。第二个参数是选项类型,第三个参数是选项的值。上面的宏是sis900网卡的模块选项。在我的系统中显示为:
debian:/sys/module/sis900/parameters# ls -l 总计 0 -r--r--r-- 1 root root 4096 2007-12-20 11:51 max_interrupt_work -r--r--r-- 1 root root 4096 2007-12-20 11:51 multicast_filter_limit -r--r--r-- 1 root root 4096 2007-12-20 11:51 sis900_debug debian:/sys/module/sis900/parameters#
Table of Contents
一个简单的hello world内核模块,基于2.6内核。
#include#include MODULE_LICENSE("GPL"); static int hello_init(void) { printk(KERN_ALERT "hello, world/n"); return 0; } static void hello_exit(void) { printk(KERN_ALERT "Goodby,cruel world/n"); } module_init(hello_init); module_exit(hello_exit);
上面的内核模块定义了两个函数,一个是module_init,该函数在内核模块加载时执行hello_init函数;另一个是module_exit,该函数在内核模块卸载时执行hello_exit函数。module_init()函数和module_exit()函数在include/linux/init.h头文件中声明。module_init()是模块的入口函数,当模块是编译进内核的话,则该函数会在系统启动时,被do_initcalls()函数调用来装入模块。当模块不编译进内核的话,则该函数会在加载模块时执行。
在编译内核模块前,要先写一个Makefile文件。
ifneq ($(KERNELRELEASE),) obj-m := HelloModule.o else KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules endif
正式编译。
debian:~/c/kernelmodule# make make -C /lib/modules/2.6.17.1/build M=/root/c/kernelmodule modules make[1]: Entering directory `/usr/src/linux-2.6.17.1' Building modules, stage 2. MODPOST make[1]: Leaving directory `/usr/src/linux-2.6.17.1' debian:~/c/kernelmodule#
编译完成后,在当前目录会生成HelloModule.ko内核模块,用insmod命令就可以装载内核模块。
debian:~/c/kernelmodule# insmod HelloModule.ko
运行上面的命令后会直接返回命令行状态。用lsmod命令查看已加载的模块。
debian:~/c/kernelmodule# lsmod Module Size Used by HelloModule 992 0 ppdev 6788 0 lp 8260 0 thermal 11016 0 ...
用rmmod命令卸载模块。
debian:~/c/kernelmodule# rmmod HelloModule.ko
通过查看系统内核日志,可以看到内核模块打印的信息。
debian:~/c/kernelmodule# vi /var/log/kern.log ... Nov 14 15:11:04 debian kernel: hello, world Nov 14 15:16:43 debian kernel: Goodby,cruel world ...
注意:本机的gcc版本要和编译内核的gcc版本一致。否则内核模块在加载时会出现"insmod: error inserting 'HelloModule.ko': -1 Invalid module format"的错误提示。内核日志也有更细致的提示。
Nov 14 14:46:49 debian kernel: HelloModule: version magic '2.6.17.1 mod_unload PENTIUMIII gcc-4.2' should be '2.6.17.1 mod_unload PENTIUMIII gcc-3.3'
在内核的API函数中,以双下划线开头(__)开头的函数是低层操作函数,在使用时要特别注意。
内核模块的描述信息包括模块作者,代码使用的许可协议等。添加方法是在源代码中使用一系列以MODULE_开头的宏。这些描述信息可以在shell环境下使用modinfo命令显示出来。
MODULE_AUTHOR("JIMS.YANG"),模块作者信息。
MODULE_DESCRIPTION("8139 NETWORK CARD DRIVER."),模块简单描述文本。
MODULE_LICENSE("GPL"),模块代码的许可协议。
MODULE_VERSION("xx.xx.xx"),模块的版本信息。
上面已介绍了查询、安装和删除内核模块的命令。Linux中与内核模块相关的命令还有几个,下面分别介绍一下。
modinfo,查询模块信息。
debian:~# modinfo snd filename: /lib/modules/2.6.23.9/kernel/sound/core/snd.ko author: Jaroslav Kyseladescription: Advanced Linux Sound Architecture driver for soundcards. license: GPL alias: char-major-116-* vermagic: 2.6.23.9 mod_unload PENTIUMIII depends: soundcore parm: cards_limit:Count of auto-loadable soundcards. (int) parm: major:Major # for sound driver. (int)
modprobe,自动化的模块安装删除工具。配置文件位于/ect/modules.conf,该文件由update-modules工具自动生,不要手动去修改。对比insmod和rmmod命令,modprobe能自动处理模块间的依赖关系。在安装一个模块时,能自动安装该模块所需的其它模块。
depmod,生成内核模块依赖关系表modules.dep,位于/lib/modules/KERNEL_VERSION目录。该表被modprobe命令使用。
modconf,模块配置工具,它有一个GUI界面。
Table of Contents
Linux强大的网络功能是如何实现的,让我们一起进入Linux内核的网络系统了解一下吧。
一些有用的文档资源:
网卡驱动程序目录:/usr/src/LINUX-KERNEL-VERSION/drivers/net/
一些网络相关的文档:/usr/src/linux/Documentation/networking
在Linux内核的网络实现中,使用了一个缓存结构(struct sk_buff)来管理网络报文,这个缓存区也叫套接字缓存。sk_buff是内核网络子系统中最重要的一种数据结构,它贯穿网络报文收发的整个周期。该结构在内核源码的include/linux/skbuff.h文件中定义。我们有必要了解结构中每个字段的意义。
一个套接字缓存由两部份组成:
报文数据:存储实际需要通过网络发送和接收的数据。
管理数据(struct sk_buff):管理报文所需的数据,在sk_buff结构中有一个head指针指向内存中报文数据开始的位置,有一个data指针指向报文数据在内存中的具体地址。head和data之间申请有足够多的空间用来存放报文头信息。
struct sk_buff结构在内存中的结构示意图:
sk_buff ----------------------------------- ------------> skb->head | headroom | |-----------------------------------| ------------> skb->data | DATA | | | | | | | | | | | | | | | | | | | |-----------------------------------| ------------> skb->tail | tailroom | ----------------------------------- ------------> skb->end
内核通过alloc_skb()和dev_alloc_skb()为套接字缓存申请内存空间。这两个函数的定义位于net/core/skbuff.c文件内。通过这alloc_skb()申请的内存空间有两个,一个是存放实际报文数据的内存空间,通过kmalloc()函数申请;一个是sk_buff数据结构的内存空间,通过 kmem_cache_alloc()函数申请。dev_alloc_skb()的功能与alloc_skb()类似,它只被驱动程序的中断所调用,与alloc_skb()比较只是申请的内存空间长度多了16个字节。
内核通过kfree_skb()和dev_kfree_skb()释放为套接字缓存申请的内存空间。dev_kfree_skb()被驱动程序使用,功能与kfree_skb()一样。当skb->users为1时kfree_skb()才会执行释放内存空间的动作,否则只会减少skb->users的值。skb->users为1表示已没有其他用户使用该缓存了。
skb_reserve()函数为skb_buff缓存结构预留足够的空间来存放各层网络协议的头信息。该函数在在skb缓存申请成功后,加载报文数据前执行。在执行skb_reserve()函数前,skb->head,skb->data和skb->tail指针的位置的一样的,都位于skb内存空间的开始位置。这部份空间叫做headroom。有效数据后的空间叫tailroom。skb_reserve的操作只是把skb->data和skb->tail指针向后移,但缓存总长不变。
运行skb_reserve()前sk_buff的结构 sk_buff ---------------------- ----------> skb->head,skb->data,skb->tail | | | | | | | | | | | | | | | | | | --------------------- ----------> skb->end 运行skb_reserve()后sk_buff的结构 sk_buff ---------------------- ----------> skb->head | | | headroom | | | |--------------------- | ----------> skb->data,skb->tail | | | | | | | | | | --------------------- ----------> skb->end
skb_put()向后扩大数据区空间,tailroom空间减少,skb->data指针不变,skb->tail指针下移。
skb_push()向前扩大数据区空间,headroom空间减少,skb->tail指针不变,skb->data指针上移
skb_pull()缩小数据区空间,headroom空间增大,skb->data指针下移,skb->tail指针不变。
skb_shared_info结构位于skb->end后,用skb_shinfo函数申请内存空间。该结构主要用以描述data内存空间的信息。
--------------------- -----------> skb->head | | | | | sk_buff | | | | | | | |---------------------| -----------> skb->end | | | skb_share_info | | | ---------------------
skb_clone和skb_copy可拷贝一个sk_buff结构,skb_clone方式是clone,只生成新的sk_buff内存区,不会生成新的data内存区,新sk_buff的skb->data指向旧data内存区。skb_copy方式是完全拷贝,生成新的sk_buff内存区和data内存区。。
net_device结构是Linux内核中所有网络设备的基础数据结构。包含网络适配器的硬件信息(中断、端口、驱动程序函数等)和高层网络协议的网络配置信息(IP地址、子网掩码等)。该结构的定义位于include/linux/netdevice.h
每个net_device结构表示一个网络设备,如eth0、eth1...。这些网络设备通过dev_base线性表链接起来。内核变量dev_base表示已注册网络设备列表的入口点,它指向列表的第一个元素(eth0)。然后各元素用next字段指向下一个元素(eth1)。使用ifconfig -a命令可以查看系统中所有已注册的网络设备。
net_device结构通过alloc_netdev函数分配,alloc_netdev函数位于net/core/dev.c文件中。该函数需要三个参数。
私有数据结构的大小
设备名,如eth0,eth1等。
配置例程,这些例程会初始化部分net_device字段。
分配成功则返回指向net_device结构的指针,分配失败则返回NULL。
在使用网络设备之前,必须对它进行初始化和向内核注册该设备。网络设备的初始化包括以下步骤:
硬件初始化:分配IRQ和I/O端口等。
软件初始化:分配IP地址等。
功能初始化:QoS等
网络设备(网卡)通过轮询和中断两种方式与内核沟通。
轮询(polling),由内核发起,内核周期性地检查网络设备是否有数据要处理。
中断(interrupt),由设备发起,设备向内核发送一个硬件中断信号。
Linux网络系统可以结合轮询和中断两方式以提高网络系统的性能。共小节重点介绍中断方式。
每个中断都会调用一个叫中断处理器的函数。当驱动程序向内核注册一个网卡时,会请求和分配一个IRQ号。接着为分配的这个IRQ注册中断处理器。注册和释放中断处理器的代码是架构相关的,不同的硬件平台有不同的代码实现。实现代码位于kernel/irq/manage.c和arch/XXX/kernel/irq.c源码文件中。XXX是不同硬件架构的名称,如我们所使用得最多的i386架构。下面是注册和释放中断处理器的函数原型。
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id) void free_irq(unsigned int irq, void *dev_id)
内核是通过IRQ号来找到对应的中断处理器并执行它的。为了找到中断处理器,内核把IRQ号和中断处理器函数的关联起来存储在全局表(global table)中。IRQ号和中断处理器的关联性可以是一对一,也可以是一对多。因为IRQ号是可以被多个设备所共享的。
通过中断,网卡设备可以向驱动程序传送以下信息:
帧的接收,这是最用的中断类型。
传送失败通知,如传送超时。
DMA传送成功。
设备有足够的内存传送数据帧。当外出队列没有足够的内存空间存放一个最大的帧时(对于以太网卡是1535),网卡产生一个中断要求以后再传送数据,驱动程序会禁止数据的传送,。当有效内存空间多于设备需传送的最大帧(MTU)时,网卡会发送一个中断通知驱动程序重新启用数据传送。这些逻辑处理在网卡驱动程序中设计。 netif_stop_queue()函数禁止设备传送队列,netif_start_queue()函数重启设备的传送队列。这些动作一般在驱动程序的xxx_start_xmit()中处理。
系统的中断资源是有限的,不可能为每种设备提供独立的中断号,多种设备要共享有限的中断号。上面我们提到中断号是和中断处理器关联的。在中断号共享的情况下内核如何正确找到对应的中断处理器呢?内核采用一种最简单的方法,就是不管三七二一,当同一中断号的中断发生时,与该中断号关联的所有中断处理器都一起被调用。调用后再靠中断处理器中的过滤程序来筛选执行真正的中断处理。
对于使用共享中断号的设备,它的驱动程序在注册时必须先指明允许中断共享。
IRQ与中断处理器的映射关系保存在一个矢量表中。该表保存了每个IRQ的中断处理器。矢量表的大小是平台相关的,从15(i386)到超过200都有。 irqaction数据结构保存了映射表的信息。上面提到的request_irq()函数创建irqaction数据结构并通过setup_irq()把它加入到irq_des矢量表中。irq_des在 kernel/irq/handler.c中定义,平台相关的定义在arch/XXX/kernel/irq.c文件中。setup_irq()在kernel/irq/manage.c,平台相关的定义在arch/XXX/kernel/irq.c中。
在系统启动阶段,网络设备操作层通过net_dev_init()进行初始化。net_dev_init()的代码在net/core/dev.c文件中。这是一个以__init标识的函数,表示它是一个低层的代码。
net_dev_init()的主要初始化工作内容包括以下几点:
生成/proc/net目录和目录下相关的文件。
kmod是内核模块加载器。该加载器在系统启动时会触发/sbin/modprobe和/sbin/hotplug自动加载相应的内核模块和运行设备启动脚本。modprobe使用/etc/modprobe.conf配置文件。当该文件中有"alias eth0 3c59x"配置时就会自动加3c59x.ko模块。
虚拟设备是在真实设备上的虚拟,虚拟设备和真实设备的对应关系可以一对多或多对一。即一个虚拟设备对应多个真实设备或多个真实设备一个虚拟设备。下面介绍网络子系统中虚拟设备的应用情况。
Bonding,把多个真实网卡虚拟成一个虚拟网卡。对于应用来讲就相当于访问一个网络接口。
802.1Q,802.3以太网帧头扩展,添加了VLAN头信息。把多个真实网卡虚拟成一个虚拟网卡。
Bridging,一个虚拟网桥,把多个真实网卡虚拟成一个虚拟网卡。
Tunnel interfaces,实现GRE和IP-over-IP虚拟通道。把一个真实网卡虚拟成多个虚拟网卡。
True equalizer (TEQL),类似于Bonding。
上面不是一个完整列表,随着内核的不断开发完善,新功能新应用也会不断出现。
程序调用流程:
module_init(rtl8139_init_module) static int __init rtl8139_init_module (void) pci_register_driver(&rtl8139_pci_driver) #注册驱动程序 static int __devinit rtl8139_init_one (struct pci_dev *pdev, const struct pci_device_id *ent) static int __devinit rtl8139_init_board (struct pci_dev *pdev, struct net_device **dev_out) dev = alloc_etherdev (sizeof (*tp)) #为设备分配net_device数据结构 pci_enable_device (pdev) #激活PCI设备 pci_resource_start (pdev, 0) #获取PCI I/O区域1的首地址 pci_resource_end (pdev, 0) #获取PCI I/O区域1的尾地址 pci_resource_flags (pdev, 0) #获取PCI I/O区域1资源标记 pci_resource_len (pdev, 0) #获取区域资源长度 pci_resource_start (pdev, 1) #获取PCI I/O区域2的首地址 pci_resource_end (pdev, 1) #获取PCI I/O区域2的尾地址 pci_resource_flags (pdev, 1) #获取PCI I/O区域2资源标记 pci_resource_len (pdev, 1) #获取区域资源长度 pci_request_regions(pdev, DRV_NAME) #检查其它PCI设备是否使用了相同的地址资源 pci_set_master(pdev) #通过设置PCI设备的命令寄存器允许DMA
网络报文从应用程序产生,通过网卡发送,在另一端的网卡接收数据并传递给应用程序。这个过程网络报文在内核中调用了一系列的函数。下面把这些函数列举出来,方便我们了解网络报文的流程。
发送流程:
write | sys_write | sock_sendmsg | inet_sendmsg | tcp_sendmsg | tcp_push_one | tcp_transmit_skb | ip_queue_xmit | ip_route_output | ip_queue_xmit | ip_queue_xmit2 | ip_output | ip_finish_output | neith_connected_output | dev_queue_xmit ----------------| | | | queue_run | queue_restart | | hard_start_xmit-----------------
接收流程:
netif_rx | netif_rx_schedule | _cpu_raise_softirq | net_rx_action | ip_rcv | ip_rcv_finish | ip_route_input | ip_local_deliver | ip_local_deliver_finish | tcp_v4_rcv | tcp_v4_do_rcv | tcp_rcv_established------------------| | | tcp_data_queue | | | _skb_queue_tail----------------------| | data_ready | sock_def_readable | wake_up_interruptible | tcp_data_wait | tcp_recvmsg | inet_recvmsg | sock_recvmsg | sock_read | read
数据包在应用层称为data,在TCP层称为segment,在IP层称为packet,在数据链路层称为frame。 |
在编译使用pcap包的程序时出现undefined reference to `pcap_open_live'出错提示。
出错原因是gcc找不到pcap的静态链接库文件,在编译时加-lpcap参数就可以了。
字符设备和块设备都有主设备号和次设备号,主设备号用来标记设备的驱动程序,次设备号用来区分同一驱动程序下的不同的设备。
在编程思路上,机制表示需要提供什么功能,策略表示如何使用这些功能。区分机制和策略是UNIX设计最重要和最好的思想之一。如X系统就由X服务器和X客户端组成。X服务器实现机制,负责操作硬件,给用户程序提供一个统一的接口。而X客户端实现策略,负责如何使用X服务器提供的功能。
内核目录清单
Documentation/ 关于内核的各种文档 arch/ 与平台有关的代码 crypto/(2.6) 加密代码 drivers/ 设备驱动程序 fs/ 文件系统 include/ 内核代码的头文件 init/ 内核的初始化代码 ipc/ System V进程间通信 kernel/ 进程,timing,程序运行,信号,模块等核心代码 lib/ 内核内部使用的库函数 mm/ 内存管理 net/ 网络协议栈 scripts/ 编译内核时用到的shell脚本和程序 security/(2.6) 安全 usr/(2.6) initramfs的实例