2.1 网络驱动程序的结构
网络驱动程序的体系结构如图1所示。
可以划分为四层:
(1)协议接口层
(2)网络设备接口层
(3)设备驱动功能层
(4)及网络设备和网络媒介层。
网络驱动程序重点:完成设备驱动功能层
在Linux中,所有网络设备都抽象为一个接口(interface)。这个接口提供了对所有网络设备的操作集合,由数据结构 struct net_device表示。
数据结构net_device中有很多供系统访问和协议层调用的设备方法,包括供设备初始化和往系统注册用的init函数、打开和关闭网络设备的open和stop函数、处理数据包发送的函数hard_ start_xmit,以及中断处理函数等。有关net_device数据结构(在内核中也就是net_device)的详细内容,请参看/linux/include/linux/netdevice.h
2.2
网络驱动实现模式
实现Linux网络设备驱动功能主要有两种形式:
(1)通过内核来进行加载,当内核启动的时候,就开始加载网络设备驱动程序,内核启动完成之后,网络驱动功能也随即实现了;
(2)再就是通过模块加载的形式。
比较两者,第二种形式更加灵活。
在此着重对模块加载形式进行讨论。
(1)insmod 来把网络设备驱动程序插入到内核之中。
(2) insmod将调用init_module()函数
(3) 对dev->init
函数指针赋值
,
(4)register_netdev()函数注册该网络设备。如果成功,网络设备初始化,
将net_device 数据结构插入到dev_base链表的末尾。
(5)模块卸载 rmmod
具体实现过程见图2所示。
2.3 网络驱动程序的基本方法
2.3.1
初始化
net_device->init 所指的函数来完成。
(1)检查设备是否存在
(2)资源分配
(3)构造net_device, 用检测到的数据值对net_device 变量初始化。
(4)向Linux内核注册该设备并申请内存空间。
2.3.2 打开(open)
open:网络设备被激活时调用(即设备状态由down-->up)。所以实际上很多在initialize中的工作可以放到这里来做。比如资源的申请,硬件的激活。如果dev->open返回非0(error),则硬件的状态还是down。
open
另一个作用是如果驱动程序做为一个模块被装入,则要防止模块卸载时设备处于打开状态。在open方法里要调用MOD_INC_USE_COUNT宏。
2.3.3 关闭(stop)
close释放资源,设备状态由up转为down时被调用的。做为模块装入的驱动程序,close里应该调用MOD_DEC_USE_COUNT,减少设备被引用的次数,以使驱动程序可以被卸载。close方法必须返回成功(0==success)。
2.3.4 数据包的发送
(1) 网络设备驱动加载时,网络设备的初始化函数(net_device->init)
(2)打开网络设备 (net_device->open)
(3)通过net_device->hard_header 建立硬件包头函数指针
(4)最后通过协议接口层函数dev_queue_xmit(详见/linux/net/core/dev.c)来调用net_device->hard_start_xmit,完成数据包的发送。
(5)如果发送成功,hard_start_xmit方法里释放sk_buff,返回0(发送成功)。
如果暂时无法处理,比如硬件忙,则返回1。这时如果dev->tbusy置为非0,则系统认为硬件忙,要等到dev->tbusy置0以后才会再次发送。
tbusy的置0任务一般由中断完成。硬件在发送结束后产生中断,这时可以把tbusy置0,然后用mark_bh()调用通知系统可以再次发送。
2.3.5
接收(reception)
驱动程序不存在一个接收方法。有数据收到应该是驱动程序来通知系统的。
(1)设备收到数据后都会产生一个中断
(2)中断处理程序中驱动程序申请一块sk_buff(skb),从硬件读出数据放置到申请好的缓冲区里。
(3)填充sk_buff中的相关信息。skb->dev = dev,判断收到帧的协议类型,填入skb->protocol(多协议的支持)。把指针skb->mac.raw指向硬件数据然后丢弃硬件帧头(skb_pull)。还要设置skb->pkt_type,标明第二层(链路层)数据类型。可以是以下类型:
PACKET_BROADCAST : 链路层广播
PACKET_MULTICAST : 链路层组播
PACKET_SELF : 发给自己的帧
PACKET_OTHERHOST : 发给别人的帧(监听模式时会有这种帧)
(4)最后调用netif_rx()把数据传送给协议层。
2.3.6 硬件帧头(hard_header)
14字节的帧头)
相关函数 hard_header
2.3.7 地址解析(xarp) 有些网络有硬件地址(比如Ethernet),并且在发送硬件帧时需要知道目的硬件地址。这样就需要上层协议地址(ip、ipx)和硬件地址的对应。这个对应是通过地址解析完成的。需要做arp的的设备在发送之前会调用驱动程序的rebuild_header方法。调用的主要参数包括指向硬件帧头的指针,协议层地址。如果驱动程序能够解析硬件地址,就返回1,如果不能,返回0。
对rebuild_header的调用在net/core/dev.c的do_dev_queue_xmit()里。
2.3.8 参数设置和统计数据 在驱动程序里还提供一些方法供系统对设备的参数进行设置和读取信息。一般只有超级用户(root)权限才能对设备参数进行设置。设置方法有:
dev->set_mac_address()
当用户调用ioctl类型为SIOCSIFHWADDR时是要设置这个设备的mac地址。一般对mac地址的设置没有太大意义的。
dev->set_config()
当用户调用ioctl时类型为SIOCSIFMAP时,系统会调用驱动程序的set_config方法。用户会传递一个ifmap结构包含需要的I/O、中断等参数。
dev->do_ioctl()
dev->do_ioctl()
如果用户调用ioctl时类型在SIOCDEVPRIVATE和SIOCDEVPRIVATE+15之间,系统会调用驱动程序的这个方法。一般是设置设备的专用数据。
读取信息也是通过ioctl调用进行。除次之外驱动程序还可以提供一个
dev->get_stats方法,返回一个enet_statistics结构,包含发送接收的统计信息。
ioctl的处理在net/core/dev.c的dev_ioctl()和dev_ifsioc()里。
2.4 网络驱动程序中用到的数据结构 net_device是网络驱动最核心的部分,最重要的是网络设备的数据结构。定义在include/linux/netdevice.h里。它的注释已经足够详尽。
the devie methods
int (*open)(struct net_device *dev);
int (*stop)(struct net_device *dev);
int (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev);
初始化数据包的传输。
2.5 常用的系统支持
2.5.1 内存申请和释放
include/linux/kernel.h
里声明了kmalloc()和kfree()。用于在内核模式下申请和释放内存。
void *kmalloc(unsigned int len,int priority);
void kfree(void *__ptr);
与用户模式下的malloc()不同,kmalloc()申请空间有大小限制。长度是2的整次方。可以申请的最大长度也有限制。另外kmalloc()有priority参数,通常使用时可以为GFP_KERNEL,如果在中断里调用用GFP_ATOMIC参数,因为使用GFP_KERNEL则调用者可能进入sleep状态,在处理中断时是不允许的。
kfree()
释放的内存必须是kmalloc()申请的。如果知道内存的大小,也可以用kfree_s()释放。
2.5.2 request_irq()、free_irq()
这是驱动程序申请中断和释放中断的调用。在include/linux/sched.h里声明。request_irq()调用的定义:
int request_irq( unsigned int irq,
irqreturn_t (*handler)( ),
unsigned long flags,
const char *dev_name,
void *dev_id);
irq:
要申请的硬件中断号。在Intel平台,范围0--15。
handler: 向系统登记的中断处理函数。
irqflags: 中断处理的属性。
(1)SA_INTERRUPT,标明快速中断,该中断调用时屏蔽所有中断。
(2)SA_SHIRQ属性,设置了以后运行多个设备共享中断。
(3)
SA_SAMPLE_RANDOM 中断时间戳可以产生系统熵。
dev_id
:在中断共享时会用到。一般设置为这个设备的device结构本身或者NULL。中断处理程序可以用dev_id找到相应的控制这个中断的设备,或者用rq2dev_map找到中断对应的设备。
void free_irq(unsigned int irq,void *dev_id);
2.5.3 时钟
时钟的处理类似中断,也是登记一个时间处理函数,在预定的时间过后,系统会调用这个函数。在include/linux/timer.h里声明。
struct timer_list {
struct list_head list;
unsigned long expires;
unsigned long data;
void (*function)(unsigned long);
};
void add_timer(struct timer_list * timer);
int del_timer(struct timer_list * timer);
void init_timer(struct timer_list * timer);
(1)声明一个timer_list结构,调用init_timer对它进行初始化。
(2)time_list结构里expires是标明这个时钟的周期,单位jiffies的单位(1/HZ)。
(3)function就是时间到了以后的回调函数,它的参数就是timer_list中的data。
(4)data这个参数在初始化时钟的时候赋值,一般赋给它设备的device结构指针。
在预置时间到系统调用function,同时系统把这个time_list从定时队列里清除。所以如果需要一直使用定时函数,要在function里再次调用add_timer()把这个函数。
2.5.4 I/O端口的存取使用:
inline unsigned int inb(unsigned short port);
inline unsigned int inb_p(unsigned short port);
inline void outb(char value, unsigned short port);
inline void outb_p(char value, unsigned short port);
在include/adm/io.h里定义。
inb_p()、outb_p()与inb()、outb_p()的不同在于前者在存取I/O时有等待
(pause)一适应慢速的I/O设备
防止存取I/O时发生冲突 (1)端口使用之前,检查需要的I/O是否正在被使用,如果没有,则把端口标记为正在使用,使用完后再释放。
int check_region(unsigned int from, unsigned int extent);
void request_region(unsigned int from,
unsigned int extent,
const char *name);
void release_region(unsigned int from, unsigned int extent);
其中的参数from表示用到的I/O端口的起始地址,extent标明从from开始的端口数目。name为设备名称。
2.5.5 中断打开关闭
系统提供给驱动程序开放和关闭响应中断的能力。是在include/asm/system.h中的两个定义。
#define cli() __asm__ __volatile__ ("cli"::)
#define sti() __asm__ __volatile__ ("sti"::)
2.5.6 打印信息
类似普通程序里的printf(),驱动程序要输出信息使用printk()。在include/linux/kernel.h里声明。
int printk(const char* fmt, ...);
其中fmt是格式化字符串。...是参数。都是和printf()格式一样的。
2.5.7 注册驱动程序
如果使用模块(module)方式加载驱动程序,需要在模块初始化时把设备注册到系统设备表里去。不再使用时,把设备从系统中卸除。定义在drivers/net/net_init.h里的两个函数完成这个工作。
int register_netdev(struct net_device *dev);
void unregister_netdev(struct net_device *dev);
dev:待注册的设备结构指针。注:net_device最重要的是name指针和init方法。name指针空(NULL)或者内容为''或者name[0]为空格(space),则系统把设备做为以太网设备处理。
register_netdev()返回0表示成功,非0不成功。
2.5.8 sk_buff
Linux网络各层之间的数据传送都是通过sk_buff,是Linux系统网络高效运行的关键。sk_buff包括控制方法和数据缓冲区。控制方法按功能分为两种类型: (1)一种是控制整个buffer链的方法 (2)控制数据缓冲区的方法。
sk_buff组织成双向链表的形式,根据网络应用的特点,对链表的操作主要是删除链表头的元素和添加到链表尾。
.alloc_skb() 申请一个sk_buff并对它初始化。返回就是申请到的sk_buff。
.dev_alloc_skb()类似alloc_skb,在申请好缓冲区后,保留16字节的帧头空间。主要用在Ethernet驱动程序。
.kfree_skb() 释放一个sk_buff。
.skb_clone() 复制一个sk_buff,但不复制数据部分。
.skb_copy()完全复制一个sk_buff。
.skb_dequeue() 从一个sk_buff链表里取出第一个元素。返回取出的sk_buff,如果链表空则返回NULL。这是常用的一个操作。
.skb_queue_head() 在一个sk_buff链表头放入一个元素。
.skb_queue_tail() 在一个sk_buff链表尾放入一个元素。这也是常用的一个操作。网络数据的处理主要是对一个先进先出队列的管理,skb_queue_tail()和skb_dequeue()完成这个工作。
.skb_insert() 在链表的某个元素前插入一个元素。
.skb_append() 在链表的某个元素后插入一个元素。一些协议(如TCP)对没按顺序到达的数据进行重组时用到skb_insert()和skb_append()。
.skb_reserve() 在一个申请好的sk_buff的缓冲区里保留一块空间。这个空间一般是用做下一层协议的头空间的。
.skb_put() 在一个申请好的sk_buff的缓冲区里为数据保留一块空间。在
alloc_skb以后,申请到的sk_buff的缓冲区都是处于空(free)状态,有一个tail指针指向free空间,实际上开始时tail就指向缓冲区头。skb_reserve()在free空间里申请协议头空间,skb_put()申请数据空间。见下面的图。
.skb_push() 把sk_buff缓冲区里数据空间往前移。即把Head room中的空间移一部分到Data area。
.skb_pull() 把sk_buff缓冲区里Data area中的空间移一部分到Head room中。