1.函数指针是一种很方便的方式,使用函数指针的优点是可以根据不同准则以及该对象所扮演的角色进行初始化。函数指针在网络代码中 广为使用,举例说明:
a.当入口数据封包或出口数据封包由路由器子系统处理时,会对 缓冲区数据结构中的两个函数做初始化。
b.当数据封包已准备好在网络硬件上传输时,就会交给net_device数据结构的hard_start_xmit函数指针。该函数由该设备所关联的设备驱动程序进行初始化。
c.当L3协议想传输数据封包时,会调用一组函数指针中的一个。
2.互斥机制
2.1 回转锁
2.2 读-写回转锁
2.3 读取更新
3.数据结构:struct sk_buff:一个封包存储在这里,所有网络分层都会使用这个结构来储存其报头、有关用户的信息(有效载荷),以及用来协调其工作的其他内部信息
struct net_device 在Linux内核中每种网络设备都用这个数据结构表示,包括软硬件的配置信息。
4.分配内存:alloc_skb()和dev_alloc_skb,
建立一个缓冲区会涉及到两次内存分配(一个是分配缓冲区,而另一个是分配sk_buff结构)
5.释放内存kfree_skb和dev_kfree_skb
6.skb_push会把一个数据块添加到缓冲区的开端,而skb_put会把一个数据块添加到缓冲区的尾端。
7.列表管理函数:
7.1 skb_queue_head_init 用一个元素为空的队列对sk_buff_head
7.2 skb_queue_head, skb_queue_tail 把一个 缓冲区分别添加到队列的头和尾。
7.3 skb_dequeue,skb_dequque_tail 把一个元素分别从队列的头和尾去掉。
7.4 skb_quque_purge 把队列变为空队列
7.5 skb_quque_walk依次循环运行队列里的每个元素
8.net_device结构
net_device数据结构存储着特定网络设备的所有信息,每个设备都有一个这种结构无论是真实设备还是虚拟设备。
三个标识符:int ifindex int iflink unsigned short dev_id
9.根目录(通常是/usr/src/linux) 分为:Documention,include(linux if.h, if_packet.h, if_ether.h, skbuff.h, netdevice.h slab.h, if_arp.h), kernel, mm, drivers(net 3c59x.c, tulip.c, sys9000.c), net (core skbuff.c)
第三章 用户空间与内核的接口
本章主要了解给定目录如何添加到/proc及其所在位置、处理给定ioctl命令的内核处理函数(kernel handler), 以及目前用户空间网络配置最佳接口Nerlink提供了那些函数?
9.1 procfs(/proc文件系统) 这是一个虚拟文件系统,通常是挂在/proc,允许内核以文件的形式向用户输出内部信息。
sysctl(/proc/sys目录)此接口允许用户空间读取或修改内核变量的值,不能用此接口对每个内核变量进行操作:内核应明确指出哪些变量从此接口是可见的。
sysfs(/sys文件系统)procfs和sysctl已经被滥用了多年,这就导致引入了一种新的文件系统:sysfs。sysfs以非常干净而有组织的方式输出很多信息。
ioctl系统调用:ioctl(输入/输出控制)系统调用操作的对象是一个文件,通常是用于实现特殊设备所需但标准文件系统没有提供的操作。
netlink套接字(socket):这是网络应用程序与内核通信时最新的首选机制。IPROUTE2包中大多数命令都是用此接口。
大多数网络功能在其初始化时都会在/proc中注册一个或多个文件,不是在引导时就是在模块加载时。当一位用户读取该文件时,会引起内核间接运行一组内核函数,以返回某种输出内容。/proc中的目录可以使用proc_mkdir创建。/proc/net中的文件可以使用定义在include/linux/proc_fs.h中的proc_net_fops_create和proc_net_remove予以注册和除名。
以下是ARP协议在/proc/net中注册其arp文件:
static struct file_opeations arp_seq_fops = {
.owner = THIS_MODULE,
.open = arp_seq_open,
.read = seq_read,
.llseek = seq_lseek,
.release = seq_release_private,
};
static int __init arp_proc_init(void)
{
if (!proc_net_fops_create("arp", S_IRUGO, &arp_seq_fops))
return -ENOMEN;
return 0;
}
open所初始化的例程会做另一次重要的初始化:注册一个函数指针数组,包括procfs用于遍历要传回给用户数据的所有例程:一个例程启动倾卸(dump),另一个推进到一个项目,而另一个再倾卸一个项目。这些例程内部负责保存必要的环境信息,这些信息会就是倾卸点以及从正确位置重新继续倾卸所必需的。
static struct seq_operations arp_seq_ops = {
.start = clip_seq_start,
.next = neigh_seq_next,
.stop = neigh_seq_stop,
.show = clip_seq_show,
};
static int arp_seq_open(struct inode *inode, struct file *file)
{
....
rc = seq_open(file, &arp_seq_ops);
...
}
10.sysctl:目录/proc/sys
用户在/proc/sys下看到的一个文件,实际上是一个内核变量。内核可以定义:
要将其放在/proc/sys的何处。 命名 访问权限。
导致运行期间创建目录或文件的事件实例如下:
当一个内核模块实现一项新功能,或者有一个协议被加载或卸载(unload)时;
当一个新的网络设备被注册或除名时。
/proc/sys中的文件和目录都是以ctl_table结构定义的。ctl_table结构的注册和除名是通过在kernel/sysctl.c中定义的register_sysctl_table和unregister_sysctl_table函数完成
以下是ctl_table的关键字段:
const char *procname 在/proc/sys中所用的文件名
int maclen 输出的内核变量的尺寸大小
mode_t mode 分派给/proc/sys中关联的文件或目录的访问权限
ctl_table *child 用于建立目录与文件之间的父子关系
proc_handler 当你在/proc/sys中读取或写入一个文件时,完成读取或写入操作的函数。所有与文件相关联的ctl_instances都必须由proc_handler初始化。内核会给目录分派一个默认值。
strategy 当/proc/sys中的文件用sysctl系统调用访问时,此函数就会被调用
extra1 extra2 通常用于定义变量的最小值和最大值。
11.proc_dostring 读/写一个字符串
proc_dointvec 读/写一个包含一个或多个整数的数组
proc_dointvec_minmac 类似proc_dointvec,但是,要确定输入数据在min/max范围内。
proc_dointvec_jiffies 读/写一个整数数组。
proc_dointvec_ms_jiffies 读/写一个整数数组。
proc_doulongvec_minmax 类似proc_dointvec_minmac,但其值为长整数而非整数
proc_doulongvec_ms_ 读/写一个长整数数组。此内核变量以jiffies为单位表示。
jiffies_minmax
初始化strategy 的函数
sysctl_string 读/写一个字符串
sysctl_intvec 读/写一个整数数组,而且确定其值符合min/max范围
sysctl_jiffies 读/写一个以jiffies表示的值,然后将其转成秒数
sysctl_ms_jiffies 读/写一个以jiffies表示的值,然后将其转成毫秒数
{
.ctl_name = NET_IPV4_CONF_FORWARDING,
.procname = "forwarding",
.data = &ipv4_devconf.forwarding,
.maxlen = sizeof(int),
.mode = 0644,
.proc_handler = &devint_sysctl_forward,
}
{
.ctl_name = CTL_NET,
.procname = "net",
.mode = 0555,
.child = net_table,
}
在/proc/sys中注册文件
指向一个ctl_table 实体的指针
一个标识,指出新元素应该放在位于相同目录中ctl_table实体列表的何处:头(1)或尾(0).
static ctl_table scsi_table[] = {
.ctl_name = DEV_SCSI_LOGGING_LEVEL,
.procname = "logging_level",
.data = &scsi_logging_level,
.maxlen = sizeof(scsi_logging_level),
.mode = 0644,
.proc_handler = &proc_dointvec },
};
static ctl_table scsi_dir_tablep[] = {
{ .ctl_name = DEV_SCSI,
.procname = "scsi",
.mode = 0555,
.child = scsi_table },
};
static ctl_table scsi_root_table[] = {
{.ctl_name = CTL_DEV,
.procname = "dev",
.mode = 0555,
.child = scsi_dir_table },
{}
}; int __init scsi_init_sysctl(void)
{
scsi_table_header = register_sysctl_table(scsi_root_table, 1);
}
min_delay 文件被分派了proc_handler和strategy 两个例程。因为内核变量ip_rt_min_delay以jiffies表示,但是用户的输入和输出都是以秒来表示,因此这两个例程可以完成把秒转换为jiffies.
ip_local_port_range文件是一个有趣的案例。
当系统管理输入像ifconfig eth0 mtu 1250这样的命令,用以改变接口eth0的MTU时,ifconfig会打开一个套接字,用从系统管理员那里接收的信息初始化一个本地数据结构,然后以ioctl调用传送给内核。SIOCSIFMTU是命令标识符。
struct ifreq data;
fd = socket(PF_INET, SOCK_DGRAM, 0);
err = ioctl(fd, SIOCSIFMTU, &data);
SIOCGIFADDR 和SIOCSIFADDR这两个命令可以为接口新增或删除IP地址
netlink可作为内核内部以及多个用户空间进城之间的消息传输系统。
通过netlink套接字,你可以使用标准套接字API打开或关闭套接字、使用套接字传输数据或者接受套接字数据。
int socket(int domain, int type, int protocol)
NETLINK_ROUTE协议用于大多数网络功能,如路由和邻居协议,而NETLINK_FIREWALL用于防火墙(Netfilter).
netlink的功能之一是传送单播和多播消息:目的地终端点地址可以是一个PID、一个多播群组ID或 两者的结合。内核定义netlink多播群组的目的是传出特定种类事件的 通知消息,而用户程序如果对这类通知消息感兴趣,可以向这些群组注册。
netlink相对其他用户-内核接口(如ioctl)的优点之一就是内核可以启动传输,而不只是仅限于响应用户空间请求而返回信息。
配置改变串行化
每次应用配置改变时,内核中内负责处理此事的例程都会取得一个信号量(rtnl_sem),以确保对储存网络配置内容的数据结构的访问具有互斥性。无论该配置的改变时通过 ioctl还是netlink而施加,都是如此。
第二部分 系统初始化
PCI(Peripheral Component Interconnect,外设部件互联)设备上。
NIC(network interoce card,网络接口卡)有关的任务都必须先完成,网络才能开启并运行。首先,关键内核组件必须予以初始化。其次,设备驱动程序必须对其所负责的所有设备予以初始化和注册,然后分配一些资源(IRQ、I/O端口等)让内核能使用,以便与驱动程序通信。
第四章 通知链
内核的很多子系统之间具有很强的相互依赖性,因此,其中一个子系统侦测到的或者产生的事件,其他子系统可能都有兴趣。为了实现这种交互需求,LINUX使用了所谓的通知链(notification chain)
通知链如何声明以及网络代码定义了哪些链(chain).
内核 子系统如何向 通知链注册
内核 子系统如何在链上产生通知信息
注意:通知链只在内核子系统之间使用。内核和用户空间之间的通知信息则依赖其他机制。
通知链就是一份简单的函数列表,当给定时间发生时予以执行。每个函数都让另一个子系统知道,调用此函数的子系统内所发生的的一个事件或者子系统所侦测到的一个事件。
就每条通知链而言,都有被动端(被通知者)和主动端(通知链),也就是所谓的发布-订阅(publish-and-subscribe)模型:
被通知者(notified)就是要求接收某事件的子系统,而且会提供回调函数予以调用。
通知者(notifier)就是感受到一个事件并调用回呼函数的 子系统。
所执行的函数是由被通知的子系统所选取,绝不是链条的拥有者(产生通知信息的子系统)决定该执行什么函数。拥有者只是定义这份列表而已; 任何内核子系统都可以对该链条注册一个回调函数以接收通知信息。
通知链的使用使源码更易于编写和维护。
每位子系统维护者应该知道:
自己对来自其他子系统的哪种事件感兴趣。
自己知道的事件有哪几种,并且其他子系统可能感兴趣的事件又是哪种
通知链列表元素的类型是notifier_block,其定义如下:
struct notifier_block
{
int (*notifier_call)(struct notifier_block *self, unsigned long, void *);
struct notifier_block *next;
int priority;
};
notifier_call是要执行的函数,next用于链接列表的元素,而priority代表的是该函数的优先级。
notifier_block实例的常见名称有xxx_chain、xxx_notifier_chain以及xxx_notifier_list.
链注册
当一个内核组件对给定通知链的事件感兴趣时,可以用通用函数notifier_chain_register予以注册。内核也提供一组内含notifier_chain_register的包裹函数
注册 int notifier_chain_register(struct notifier_block **list, struct notifier_block *n)
除名 int notifier_chain_unregister(struct notifier_block **n1, struct notifier_block *n)
通知 int notifier_call_chain(struct notifier_block **n, unsigned long val void *v)
因为调用notifier_chain_register函数就是把回调函数插入到所有列表中,因此必须把列表指定为输入参数。然而,此函数很少被直接调用,通常是改用通用的包裹函数
int notifier_chain_register(struct notifier_block **list, struct notifier_block *n)
{
write_lock(¬ifier_lock);
while(*list)
{
if (n->priority > (*list)->priority)
break;
list = &((*list)->next);
}
n->next = *list;
*list = n;
write_unlock(¬ifier_lock);
return 0;
}
链上的通知事件
int notifier_call_chain(struct notifier_block **n, unsigned long val, void *v)
{
int ret = NOTIFY_DONE;
struct notifier_block *nb = *n;
while (nb)
{
ret = nb->notifier_call(nb, val, v);
if (ret & NOTIFY_STOP_MASK)
{
return ret;
}
nb = nb->next;
}
return ret;
}
NOTIFY_OK 通知信息被正确地处理了
NOTIFY_DONE 对通知信息不感兴趣
NOTIFY_BAD 有些事情出错
NOTIFY_STOP 函数被正确调用,然而,此事件不需要进一步调用其他回调函数
NOTIFY_STOP_MASK 此标识由notifier_call_chain检查,以了解是否停止调用回调函数,或者继续调用下去。NOTIFY_BAD和NOTIFY_STOP在其定义中都包括了此标识。
notifier_call_chain捕获并返回由最后一个调用的回调函数所接收的返回值。
inetaddr_chain
发送有关本地接口上的IPv4地址的插入、删除以及变更的通知信息。
netdev_chain
发送有关网络设备注册状态的通知信息。
某些NIC设备驱动程序可以用reboot_notifier_list链注册;当系统重新引导时,此链会发出警告。
int register_netdevice_notifier(struct notifier_block *nb)
{
return notifier_chain_register(&netdev_chain, nb);
}
static struct notifier_block fib_inetaddr_notifier = {
.notifier_call = fib_inetaddr_event,
};
static struct notifier_block fib_netdev_notifier = {
.notifier_call = fib_netdev_event,
};
void __ init ip_fib_init(void)
{
..........
register_netdevice_notifier(&fib_netdev_notifier);
register_inetaddr_notifier(&fib_inetaddr_notifier);
}
改变本地IP地址配置以及改变本地设备的注册状态,都会影响路由表。
notifier_chain_register + 包裹函数
notifier_chain_unregister + 包裹函数
notifier_call_chain
struct notifier_block 定义通知信息的处理函数,包括要调用的回调函数
与通知链相关的文件
/usr/src/linux net(core(dev.c),ipv4(devinet.c)) kernel(sys.c) include---linux----notifier.h
网络设备初始化
USB、PCI CardBus、IEEE1394(Apple称其为FireWire)以及其他。
一段内核网络代码的初始化。
NIC的初始化。
NIC如何使用中断,以及IRQ处理函数如何分配和释放。
用户如何给以模块方式加载的设备驱动程序提供配置参数。
设备初始化及配置期间,用户空间与内核之间的交互。
parse_early_param();-->parse_args(...)
parse_args(...)
.....
init_IRQ(); init_timers(); softirq_init(); rest_init();---->kernel_thread(init, ...)---->
do_basic_setup(); free_init_mem(); run_init_process(....)
user_modehelper_init() driver_init() sock_init() do_initcalls();
引导期间选项 中断和定时器(硬中断和软中断分别由init_IRQ和softirq_init做初始化。
初始化函数(内核子系统及内建的设备驱动程序由do_initcalls初始化。free_init_mem会释放一块被无用程序所持有的内存)
run_init_process确定在系统上运行的第一个进程,也就是所有其他进程的父进程,一直运行直到系统做完工作。
设备注册和初始化
一个网络设备可用,就必须被内核认可,并且关联正确的驱动程序。
硬件初始化 由设备驱动程序和通用总程序合作完成。
软件初始化 在设备能够被使用之前,依赖于所开启和配置的网络协议为何而定。
功能初始化 Linux内核有很多网络选项。
net_device数据结构包含一组函数指针,内核可用它们与设备驱动程序及特殊内核功能交互。这些函数的初始化部分取决于设备类型(例如Ethernet),部分取决于厂家和型号。
IRQ线 在"设备与内核之间的交互",NIC必须被分派一个IRQ,然后在必要时用它要求内核的注意。
I/O端口和内存注册
驱动程序将其设备的一个内存区域(例如,其配置寄存器)映射到系统内存,使得驱动程序的读/写操作可以通过系统内存地址直接进行,这样可以简化代码。I/O端口和内存分别使用request_region和release_region注册及释放。
设备与内核之间的交互
轮询(polling) 由内核端驱动。
中断 由设备端驱动,当设备需要内核注意时,会向内核发送出一个硬件信号(产生中断事件)
硬件中断
每个中断事件都会运行一个函数,被称为中断处理例程(interrupt handler),而中断处理例程必须按照设备的所需进行裁剪,因此由设备驱动程序安装。一般而言,当设备驱动程序注册一个NIC时,会请求并分派一个IRQ。然后,用两个依赖体系结构的函数为给定的IRQ注册或删除(如果驱动程序被卸载了)处理例程。这两个函数定义在kernel/irq/manage.c中,并由arch/XXX/kernel/irq.c中的体系结构描述函数改写,其中XXX是体系结构描述目录:
int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs*), unsigned long irqflags, const char *devname, void *dev_id)
此函数会注册一个处理例程。首先确保所请求的中断是一个有效的中断,而且还没分配给另一个设备,除非这两个设备能够共享IRQ
void free_irq(unsigned_int irq, void *dev_id)
给定的设备由dev_id标识,此函数会删除处理例程,而且如果没有其他设备注册在该IRQ线,就关闭该IRQ。
中断类型,通过中断,NIC能够告知其驱动程序几种不同的事情,其中包括:
接收一帧 传输失败 DMA传输已成功完成
设备有足够内存处理新传输
当出口队列没有足够空间保存一个最大尺寸的帧时,NIC设备驱动程序会停止出口队列而关闭传输。当内存可用时,该队列又会再次开启。
当内存可用时,该队列又会再次开启。
static int
el3_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
................
netif_stop_queue (dev);
................
if (inw(ioaddr + TX_FREE) > 1536)
netif_start_queue(dev);
else
outw(SetTxThreshold + 1536, ioaddr + EL3_CMD);
.....................
}
驱动程序可以用netif_stop_queue停止设备队列,因此能禁止内核提交后续的传输请求。然后,驱动程序会检查该设备的内存是否有足够的空间容纳一个1536个字节的包。如果有,驱动程序就会启动队列,允许内核再次提交传输请求;否则,就会指示设备(通过用一个outw调用写一个配置寄存器),当条件满足时产生一个中断。中断处理例程将使用netif_start_queue重启设备队列,使内核能够重新继续传输。
IP层的主要任务和所用策略
说明IP层接受函数如何处理入口封包,以及IP选项如何处理
转发和本地传递
说明入口IP封包如何传递至本地的L4协议处理函数,或者当目的IP地址不属于本地主机,而该主机有开启转发功能时,该如何给以转发。
说明L4协议如何衔接IP层来请求传输
处理分段:说明分段和重组是怎么处理的
L4协议和Raw IP的处理
说明L4协议如何为入口流量注册处理函数
因特网控制消息协议
说明ICMP协议的所有细节
版本(version)
报头长度
服务类型(type of service,TOS)
总长度:封包长度
识别(identity)
IP报头的TOS字段的新旧意义
1.原始版本 2.区分服务 3.显式拥塞通知
DF(不分段) MF(还有其他分段)
片段偏移量(fragment offset)
存活时间(Time To Live,TTL)
要识别一个片段所属的IP封包,内核会考虑下列参数:
1.发送方和目的地IP地址;
2.IP封包ID
3.L4协议
路径MTU发现:用于发现封包传输至制定目的地地址而不用被fen分段的最大尺寸。
更多细节建议读RFC 791
通过相同目的地的路径可能bu不只一条,这是为了冗余或负载平衡
检验和字段是一种冗余字段,网络协议用它发现传输错误。有些检验和不仅能jian检测错误,而且还可以自动修复特定类型的错误。
封包调整
由ip_init完成的主要任务
1.用dev_add_pack函数为IP封包注册处理程序,此函数程序为函数,名为ip_rcv
2.初始化路由器子系统,包括与协议无关的缓存
3.初始化用于管理IP端点的基础架构
封包接收,封包转发(路由决策前)
封包转发(路由决策后)
封包传输
NF_HOOK(PROTOCOL, HOOK_POSITION_IN_THE_STACK, SKB_BUFFER, IN_DEVICE, OUT_DEVICE, do_something_finish);
NF_HOOK输出值可以是下列值之一:
当do_something_finish稍后被执行时,就是其输出值;
如果因为某个过滤器的缘故使得SKB_BUFFER被丢弃,就返回-EPERM
如果没有足够的内存执行过滤运算,就返回-ENOMEM
ip_route_input决定输入封包的命运
ip_route_output_flow传输封包前使用
dst_pmtu给定一个路由表缓存项目,就可返回相关的PMTU
ip_route_xxx函数会查询路由表,并根据一组字段做出其决策:
1.目的IP地址;
2.源IP地址
3.服务类型(TOS);
4.进行接收的设备(接收时才有)
5.可进行传输的设备列表;
ip_rcv
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt);
netif_receive_skb函数会把指向L3协议的指针(skb->nh)设在L2报头尾端。因此,IP层函数可以安全地将它转换成iphdr结构
if(skb->pkt_type == PACKET_OTHERHOST) goto drop;
if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) {
IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
goto out;
}
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
goto inhdr_error;
iph = skb->nh.iph;
if(pskb_trim_rcsum(skb, len)) {
IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
goto drop;
}