Linux+Kernel学习笔记.docx
《Linux+Kernel学习笔记.docx》由会员分享,可在线阅读,更多相关《Linux+Kernel学习笔记.docx(25页珍藏版)》请在冰豆网上搜索。
![Linux+Kernel学习笔记.docx](https://file1.bdocx.com/fileroot1/2022-12/12/500b1534-f023-424e-8d1f-91723dc95bca/500b1534-f023-424e-8d1f-91723dc95bca1.gif)
Linux+Kernel学习笔记
LinuxKernel学习笔记
TableofContents
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微处理器中,有三种存储器地址:
∙逻辑地址(logicaladdress),包含在机器语言指令中用来指定一个操作数或一条指令的地址。
每个逻辑地址都由一个段(segment)和一个偏移量(offset)组成。
偏移量指明了从段的开始到实际地址之间的距离。
∙线性地址(linearaddress)(也称为虚拟地址,virtualaddress),它是一个32位无符号整数,可用以表达高达4G的地址(2的32次方)。
通常以十六进制数表示,值的范围从0X00000000到0Xffffffff。
∙物理地址(physicaladdress),用于存储器芯片级存储单元寻址,它们与从微处理器的地址引脚发送到存储器总线上的电信号相对应。
物理地址由32位无符号整数表示。
CPU控制单元通过一种称为分段单元(segmentationunit)的硬件电路把一个逻辑地址转换成线性地址;线性地址又通过一个分页单元(pagingunit)的硬件电路把一个线性地址转换成物理地址。
逻辑地址由两部份组成,一个段标识符和一个指定段由相对地址的偏移量。
段标识符是一个16位长的字段,称为段选择符(segmentselector),偏移量是一个32位长的字段。
处理器提供专门的段寄存器以快速处理段选择符,段寄存器的唯一目的就是存放段选择符。
共有6个段寄存器,分别是cs、ss、ds、es、fs和gs。
其中cs、ss、ds寄存器有专门的用途。
∙cs是代码段寄存器,指向包含程序指令的段。
∙ss是栈寄存器,指向包含当前程序栈的段。
∙ds是数据段寄存器,指向包含静态数据或者外部数据的段。
cs寄存器有一个重要功能,它包含有一个两位的字段,用以指明CPU当前特权级别(CurrentPrivilegeLevel,CPL)。
值0表示最高优先级,值3表示最低优先级。
Linux只用到0级和3级,分别表示内核态和用户态。
每个段由一个8字节的段描述符表示,它描述了段的特征。
段描述符放在全局描述符表(GlobalDescriptorTable,GDT)中或局部描述符表(LocalDescriptorTable,LDT)中。
段描述符的组成:
∙32位的Base字段,含有段的第一个字节的线性地址。
∙粒度标记G。
如果该位清0,则段大小以字节为单位,否则以4096字节的倍数计。
∙20位的Limit字段指定段的长度(以字节为单位,Limit字段为0的段被认为是空段)。
当G为0时,段的大小在1字节到1MB之间;否则段的大小在4KB到4GB之间。
∙系统标记S。
如果它被清0,则这是一个系统段,用于存储内核数据结构,否则,它是一个普通的代码段或数据段。
∙4位Type字段,描述段的类型和它的访问权限。
常用的Type有以下几种:
o代码段描述符
o数据段描述符
o任务状态段描述符
o局部描述符表描述符
∙
∙
∙
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. 字符设备驱动程序
TableofContents
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_tdev);#获得主设备号
MINOR(dev_tdev);#获得次设备号
这些宏定义位于linux/kdev_t.h中。
如果要把主次设备号转换成dev_t类型,则可使用:
MKDEV(intmajor,intminor);
3.2. 设备号的分配和释放
在建立一个字符设备之前,需要为它分配一个或多个设备号。
使用register_chrdev_region()函数完成设备号的分配。
该函数在linux/fs.h中声明。
原型如下:
intregister_chrdev_region(dev_tfirst,unsignedintcount,char*name);
first:
是要分配的主设备号范围的起始值,次设备号一般设置为0;
count:
是所请求的连续设备号的个数;
name:
是和该设备号范围关联的设备名称,它将出现在/proc/devices或/sysfs中。
如果分配成功则返回0,分配失败则返回一个负的错误码,所请求的设备号无效。
还有一个自动分配设备号的函数alloc_chrdev_region(),原型如下:
intalloc_chrdev_region(dev_t*dev,unsignedintfirstminor,unsignedintcount,char*name);
dev:
自动分配到设备号范围中的第一个主设备号;
firstminor:
自动分配的第一个次设备号,通常为0;
count:
是所请求的连续设备号的个数;
name:
是和该设备号范围关联的设备名称,它将出现在/proc/devices或/sysfs中。
如果我们不再使用设备号,则要使用unregister_chrdev_region()函数释放它。
函数原型如下:
voidunregister_chrdev_region(dev_tfirst,unsignedintcount);
函数的参数作用同上
我们一般在模块的清除函数中调用设备号释放函数。
在内核源码目录的Documentation/devices.txt文件中列出了已静态分配给常用设备的主设备号。
为了减少设备号分配的冲突,我们一般要使用alloc_chrdev_region()函数来自动分配设备号。
3.3. 重要的数据结构
文件操作结构:
structfile_operations,在linux/fs.h中定义。
它包含一组函数指针,实现文件操作的系统调用,如read、write等。
每个打开的文件都和一个文件操作结构关联(通过file结构中指向file_operations结构的f_op字段进行关联)。
文件结构:
structfile,在linux/fs.h中定义。
file结构代表一个打开的文件,由内核在open时创建。
指向文件结构的指针在内核中通常称为filp(文件指针)。
当文件的所有实例都被关闭之后,内核会释放这个数据结构。
节点结构:
structinode,在linux/fs.h中定义。
inode结构是内核表示文件的方法,而file结构是以文件描述符的方式表示文件的方法。
结构中以下两个字段对编写驱动程序有用:
∙dev_ti_rdev,该字段包含了真正的设备编号。
∙structcdev*i_cdev,该字段包含指向structcdev结构的指针。
从设备的inode获取主次设备号的宏:
unsignedintiminor(structinode*inode);
unsignedintimajor(structinode*inode);
3.4. 读和写
下面两个是字符设备读写操作最重要的内核函数。
unsignedlongcopy_to_user(void__user*to,constvoid*from,unsignedlongn);
读操作,把数据从内核空间复制到用户空间,返回不能复制的字节数,如果成功则返回0。
to目的地址,在用户空间中;
from源地址,在用户空间;
n要复制的字节数。
unsignedlongcopy_from_user(void*to,constvoid__user*from,unsignedlongn);
写操作,把数据从用户空间复制到内核空间,返回不能复制的字节数,如果成功则返回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头文件中定义。
∙structpci_device_id{
∙__u32vendor,device;/*VendoranddeviceIDorPCI_ANY_ID*/
∙__u32subvendor,subdevice;/*SubsystemID'sorPCI_ANY_ID*/
∙__u32class,class_mask;/*(class,subclass,prog-if)triplet*/
∙kernel_ulong_tdriver_data;/*Dataprivatetothedriver*/
∙};
PCI设备的vendor、device和class的值都是预先定义好的,通过这些参数可以唯一确定设备厂商和设备类型。
这些PCI设备的标准值在include/linux/pci_ids.h头文件中定义。
pci_device_id需要导出到用户空间,使模块装载系统在装载模块时知道什么模块对应什么硬件设备。
宏MODULE_DEVICE_TABLE()完成该工作。
设备id一般用数组形式。
如:
staticstructpci_device_idrtl8139_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头文件中定义。
∙staticstructpci_driverrtl8139_pci_driver={
∙.name=DRV_NAME,#设备名
∙.id_table=rtl8139_pci_tbl,#pci设备的id表组
∙.probe=rtl8139_init_one,#初始化函数
∙.remove=__devexit_p(rtl8139_remove_one),#退出函数
∙#ifdefCONFIG_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#catmodules.pcimap|more
#pcimodulevendordevicesubvendorsubdeviceclassclass_maskdriver_data
snd-trident0x000010230x000020000xffffffff0xffffffff0x000401000x00ffff000x0
snd-trident0x000010230x000020010xffffffff0xffffffff0x000000000x000000000x0
...
8139cp0x000010ec0x000081390xffffffff0xffffffff0x000000000x000000000x0
8139cp0x000003570x0000000a0xffffffff0xffffffff0x000000000x000000000x0
...
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-->Pseudofilesystems-->procfilesystemsupport"。
procfs文件系统挂载在/proc目录,可用cat、more等shell命令查看目录中的文件。
∙sysctl命令也可以修改和查看内核变量,sysctl操作的内核变量位于/proc/sys目录下。
启用sysctl支持的内核选项是"Generalsetup-->Sysctlsupport"。
∙procfs和sysctl接口已使用多年,从2.6内核开始,引入新的sysfs虚拟文件系统,它挂载在/sys目录下。
启用sysfs的内核选项是"Filesystems-->Pseudofilesystems-->sysfsfilesystemsupport(NEW)"。
sysfs以更整齐更直观的方式向用户展示了内核的各种参数。
/proc将会向sysfs迁移。
另外,通过ioctl(input/outputcontrol)systemcall和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宏的第一