Linux Kernel学习笔记

整理:Jims of 肥肥世家

发布时间:2007年3月16日

最近更新:2008年12月01日,增加字符设备驱动程序开发介绍。


Chapter 1. 存储器寻址

在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有以下几种:

    • 代码段描述符

    • 数据段描述符

    • 任务状态段描述符

    • 局部描述符表描述符

  •  

  •  

  •  

Chapter 2. 设备驱动程序开发

在编程思路上,机制表示需要提供什么功能,策略表示如何使用这些功能。区分机制和策略是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()             实现写入文件上的数据和实际硬件的同步

Chapter 3. 字符设备驱动程序

3.1. 设备号

字符设备在系统中以设备文件的形式表示,位于/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);

3.2. 设备号的分配和释放

在建立一个字符设备之前,需要为它分配一个或多个设备号。使用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()函数来自动分配设备号。

3.3. 重要的数据结构

文件操作结构: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);

3.4. 读和写

下面两个是字符设备读写操作最重要的内核函数。

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       要复制的字节数。

Chapter 4. PCI设备

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
...

Chapter 5. 内核初始化优化宏

内核使用了大量不同的宏来标记具有不同作用的函数和数据结构。如宏__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()函数。

  • 如果你不确定需不需要添加优化宏则不要添加。

Chapter 6. 访问内核参数的接口

内核通过不同的接口向用户输出内核信息。我们可通过这些接口访问和修改内核参数。共有三种接口,其中两种是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表示路由等。

Chapter 7. 内核初始化选项

我们可以通过内核初始化选项,在系统启动时或内核模块加载时微调内核的功能。

模块的初始化选项是通过模块程序中的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#           

Chapter 8. 内核模块编程

8.1. 入门

一个简单的hello world内核模块,基于2.6内核。

#include <linux/init.h>
#include <linux/module.h>

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函数中,以双下划线开头(__)开头的函数是低层操作函数,在使用时要特别注意。

8.2. 为模块添加描述信息

内核模块的描述信息包括模块作者,代码使用的许可协议等。添加方法是在源代码中使用一系列以MODULE_开头的宏。这些描述信息可以在shell环境下使用modinfo命令显示出来。

  • MODULE_AUTHOR("JIMS.YANG"),模块作者信息。

  • MODULE_DESCRIPTION("8139 NETWORK CARD DRIVER."),模块简单描述文本。

  • MODULE_LICENSE("GPL"),模块代码的许可协议。

  • MODULE_VERSION("xx.xx.xx"),模块的版本信息。

8.3. 内核模块处理命令介绍

上面已介绍了查询、安装和删除内核模块的命令。Linux中与内核模块相关的命令还有几个,下面分别介绍一下。

  • modinfo,查询模块信息。

    debian:~# modinfo snd
    filename:       /lib/modules/2.6.23.9/kernel/sound/core/snd.ko
    author:         Jaroslav Kysela <perex@suse.cz>
    description:    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界面。

Chapter 9. 网络子系统

Linux强大的网络功能是如何实现的,让我们一起进入Linux内核的网络系统了解一下吧。

一些有用的文档资源:

  • 网卡驱动程序目录:/usr/src/LINUX-KERNEL-VERSION/drivers/net/

  • 一些网络相关的文档:/usr/src/linux/Documentation/networking

  •  

9.1. sk_buff结构

在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

9.2. sk_buff结构操作函数

内核通过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内存区。。

9.3. net_device结构

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。

9.4. 网络设备初始化

在使用网络设备之前,必须对它进行初始化和向内核注册该设备。网络设备的初始化包括以下步骤:

  • 硬件初始化:分配IRQ和I/O端口等。

  • 软件初始化:分配IP地址等。

  • 功能初始化:QoS等

9.5. 网络设备与内核的沟通方式

网络设备(网卡)通过轮询和中断两种方式与内核沟通。

  • 轮询(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中。

9.6. 网络设备操作层的初始化

在系统启动阶段,网络设备操作层通过net_dev_init()进行初始化。net_dev_init()的代码在net/core/dev.c文件中。这是一个以__init标识的函数,表示它是一个低层的代码。

net_dev_init()的主要初始化工作内容包括以下几点:

  • 生成/proc/net目录和目录下相关的文件。

9.7. 内核模块加载器

kmod是内核模块加载器。该加载器在系统启动时会触发/sbin/modprobe和/sbin/hotplug自动加载相应的内核模块和运行设备启动脚本。modprobe使用/etc/modprobe.conf配置文件。当该文件中有"alias eth0 3c59x"配置时就会自动加3c59x.ko模块。

9.8. 虚拟设备

虚拟设备是在真实设备上的虚拟,虚拟设备和真实设备的对应关系可以一对多或多对一。即一个虚拟设备对应多个真实设备或多个真实设备一个虚拟设备。下面介绍网络子系统中虚拟设备的应用情况。

  • Bonding,把多个真实网卡虚拟成一个虚拟网卡。对于应用来讲就相当于访问一个网络接口。

  • 802.1Q,802.3以太网帧头扩展,添加了VLAN头信息。把多个真实网卡虚拟成一个虚拟网卡。

  • Bridging,一个虚拟网桥,把多个真实网卡虚拟成一个虚拟网卡。

  • Tunnel interfaces,实现GRE和IP-over-IP虚拟通道。把一个真实网卡虚拟成多个虚拟网卡。

  • True equalizer (TEQL),类似于Bonding。

上面不是一个完整列表,随着内核的不断开发完善,新功能新应用也会不断出现。

9.9. 8139too.c源码分析

程序调用流程:

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

9.10. 内核网络数据流

网络报文从应用程序产生,通过网卡发送,在另一端的网卡接收数据并传递给应用程序。这个过程网络报文在内核中调用了一系列的函数。下面把这些函数列举出来,方便我们了解网络报文的流程。

发送流程:

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
[Note] 
数据包在应用层称为data,在TCP层称为segment,在IP层称为packet,在数据链路层称为frame。

Chapter 10. 备忘录

  • 在编译使用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的实例
    
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值