Linux Kernel学习笔记

http://www.ringkee.com/note/opensource/kernel/kernel.htm

Linux Kernel学习笔记

整理:Jims of 肥肥世家

发布时间:2007年3月16日

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


Table of Contents

1. 存储器寻址
2. 设备驱动程序开发
3. 字符设备驱动程序
3.1. 设备号
3.2. 设备号的分配和释放
3.3. 重要的数据结构
3.4. 读和写
4. PCI设备
5. 内核初始化优化宏
6. 访问内核参数的接口
7. 内核初始化选项
8. 内核模块编程
8.1. 入门
8.2. 为模块添加描述信息
8.3. 内核模块处理命令介绍
9. 网络子系统
9.1. sk_buff结构
9.2. sk_buff结构操作函数
9.3. net_device结构
9.4. 网络设备初始化
9.5. 网络设备与内核的沟通方式
9.6. 网络设备操作层的初始化
9.7. 内核模块加载器
9.8. 虚拟设备
9.9. 8139too.c源码分析
9.10. 内核网络数据流
10. 备忘录

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. 字符设备驱动程序

Table of Contents

3.1. 设备号
3.2. 设备号的分配和释放
3.3. 重要的数据结构
3.4. 读和写

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. 内核模块编程

Table of Contents

8.1. 入门
8.2. 为模块添加描述信息
8.3. 内核模块处理命令介绍

8.1. 入门

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

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 
    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. 网络子系统

Table of Contents

9.1. sk_buff结构
9.2. sk_buff结构操作函数
9.3. net_device结构
9.4. 网络设备初始化
9.5. 网络设备与内核的沟通方式
9.6. 网络设备操作层的初始化
9.7. 内核模块加载器
9.8. 虚拟设备
9.9. 8139too.c源码分析
9.10. 内核网络数据流

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
 
数据包在应用层称为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的实例
    

你可能感兴趣的:(Linux,Kernel)