第5、6章 网络设备驱动程序及数据链路层数据帧的收发

本文详细介绍了网络设备驱动程序在Linux内核中的作用,包括如何与内核交互、硬件中断和软中断的处理。讨论了网络设备的探测、初始化、活动功能函数、管理任务,以及数据链路层数据帧的接收和发送机制。重点阐述了中断服务程序的Top Half和Bottom Half,以及NAPI(Network Access Protocol Interrupts)如何减少中断处理的开销。此外,还介绍了CS8900A网络适配器驱动程序的实现和数据链路层与网络层的接口。
摘要由CSDN通过智能技术生成

1. 网络设备驱动程序

网络设备驱动程序是网络物理设备与Linux内核之间的桥梁,它是将网络设备从主机外界:计算机网上收到的数据,从网络设备的数据空间传到内核空间的软件,也即网络设备驱动程序是网络设备硬件与Linux内核间的接口。

网络接口是Linux系统中第三类标准设备,在系统中是内核与外界交换数据的通道,以struct net_device向内核注册,两个功能:

  1. 最基本的要求:响应内核要求,向外发送数据包,异步接收数据包,推送给内核
  2. 管理任务:设置网络参数,管理流量和错误统计。

网络设备驱动程序的构成:

  1. 模块初始化函数由module_init宏来标记或直接命名为int_module,当网络设备驱动程序以模块的方式装载带内核中,该函数在装载网络设备驱动程序模块时被执行。它创建网络设备实体对应的struct net_device数据结构实例,初始化部分网络设备实例的数据域,然后调用Net-device驱动程序的xxx_probe函数,完成Net-device驱动程序对网络设备实例的特殊数据域的初始化。

  2. 网络硬件设备探测函数
    网络设备驱动程序直接编译到内核时,如果在内核配置时选择了某个网卡作为系统通信的设备,内核启动过程中首先探测指定的网络设备硬件;一旦探测到安装的网络设备硬件后,就调用网络设备驱动程序的初始化函数来初始化一个网络设备实例。

内核如何探测到硬件:

  • 首先,网络设备驱动程序必须实现自身的硬件设备自动探测函数(xxx_probe),将此函数放入 drivers/net/Space.c文件中;

    自动探测函数与设备硬件特性密切相关,它需要按照硬件呈现在系统中的一些特征来判断设备是否已连接到系统中。例如:每一种网络控制器上的寄存器、数据缓冲区映射到内存的区域,都可以作为判断的条件之一。

     extern struct net_device *cs89x0_probe(int unit);
    
  • 根据网络控制器连入系统总线类型(ISA总线、EISA总线、微通道MCA等),将网络设备的自动探测函数加入到struct devprobe2数据结构类型数组中。例如:CS8900x系统的网卡与系统通过ISA总线连接,自动探测需进行如下设置:

 static struct devprobe2 isa_probes[] __initdata = {
 ...
 #ifdef CONFIG_CS89x0
  	{cs89x0_probe, 0},
 #endif
 ...
 	{NULL, 0},
 };
  • 内核的以太网设备自动探测过程会根据内核配置依次调用ISA、EISA、MCA等总线类网络设备的探测函数,发现设备后就初始化网络设备实例,并注册到内核中。
//声明Linux支持的网络设备的探测函数
extern struct net_device *apne_probe(int unit);
extern struct net_device *cs89x0_probe(int unit);
//描述网络设备探测的数据结构,probe函数指针指向网络设备的自动探测函数,如果自动探测失败,status为非零值
struct devprobe2 {
	struct net_device *(*probe)(int unit);
	int status;	/* non-zero if autoprobe has failed */
};
//如果在内核配置时选择了某个网卡,将该网卡的自动探测函数加入到devprob2结构数组中,比如cs89x0_probe
static struct devprobe2 isa_probes[] __initdata = {
...
#ifdef CONFIG_CS89x0
 	{cs89x0_probe, 0},
#endif
...
}
//在probe_list2函数中依次执行传入参数struct devprob2 *p数组中的网络设备自动探测函数,设置自动探测状态status
static int __init probe_list2(int unit, struct devprobe2 *p, int autoprobe)
{
	struct net_device *dev;
	for (; p->probe; p++) {
		if (autoprobe && p->status) //如探测状态表明该设备已探测过,则继续
			continue;
		dev = p->probe(unit);       //执行自动探测函数
		if (!IS_ERR(dev))
			return 0;
		if (autoprobe)
			p->status = PTR_ERR(dev); //设置自动探测执行结果状态
	}
	return -ENODEV;
}
//执行所有以太网类网络接口和设备自动探测函数
static void __init ethif_probe2(int unit)
{
	unsigned long base_addr = netdev_boot_base("eth", unit);

	if (base_addr == 1)
		return;

	(void)(	probe_list2(unit, m68k_probes, base_addr == 0) &&
		probe_list2(unit, eisa_probes, base_addr == 0) &&
		probe_list2(unit, mca_probes, base_addr == 0) &&
		probe_list2(unit, isa_probes, base_addr == 0) &&
		probe_list2(unit, parport_probes, base_addr == 0));
}

__init标识 #define __init attribute ((section (".text.init"))) 表明此函数会在内核启动过程中被调用执行。

  1. 设备活动
    网络设备经创建、初始化后注册到内核,由用户激活,就可以开始网络数据包的收发操作。由于网络设备驱动程序本身并不知道什么时候数据会从网络或内核发送给它,所以网络数据包的收发是异步进行的。大多数网络设备驱动程序都以中断的方式来完成这种异步操作。描述网络设备活动的功能函数通常包括以下几类:
    (1)激活停止设备(xxx_open/xxx_close)
    (2)收发网络数据(xxx_tx/xxx_rx)
    (3)中断处理程序(xxx_interrupt)
  2. 管理任务
    (1)错误处理、状态统计(xxx_tx_timeout/xxx_get_state)
    (2)支持组发送功能函数(set_multicast_list/set_multicast_address)
    (3)改变设备配置(change_xxx)
    在这里插入图片描述

2. 网络设备与内核的交互

网络设备与内核的交互主要发生在网络设备接收到数据包后,在内核准备好处理网络数据包之前,它必须建立起系统的中断机制,使系统可以快速地处理网络数据包。设备和内核的交互方式:

  • 轮询
    在轮询工作方式中,内核每隔一段时间就查询设备的状态,看设备是否有数据需要与内核交换,这是通过读取设备的状态/控制寄存器来判断的。这种方式耗费CPU的时间较多,数据交换速度慢,目前大多数设备都部采用这种工作方式。轮询方式下设备与系统之间的连接如图所示:
    在这里插入图片描述
    系统通过地址译码和控制命令(IOW/IOR)选择访问设备的控制/状态寄存器,获取外部设备控制/状态寄存器的当前值,判断设备设备是否已有数据等待读取,如果外部设备中已有数据,则主机从设备的数据缓冲寄存器中获取数据。
  • 中断
    在系统运行过程中,如果发生了某种随机事件(引起中断的随机事件是设备向内核发送一个硬件信号),CPU将暂停现行程序的执行,转去执行为该随机事件服务的中断处理程序,中断处理程序执行完成后自动恢复原程序的执行。
    中断满足了网络数据包的发送和接收可以异步进行的要求,使CPU与网络设备之间可以并行工作。但是当网络负载很高时,网络设备每接到一个数据包就会产生一个中断请求,CPU就需要从现行程序切换到中断处理程序,随后在切换回原程序,这种频繁的程序切换会使CPU耗费很多时间。

Linux实现NAPI的工作方式:内核不禁止所有的硬件中断,只禁止当前有数据包进入,CPU正在执行中断处理程序的网络设备的硬件中断。

2.1 硬件中断

中断控制的硬件逻辑:
在这里插入图片描述

  1. 中断控制器接收外部请求信号IRQ0 ~ IRQi,将它们存放于中断请求寄存器中,目前中断控制器大多可以接收16 ~ 32个中断中断请求。
  2. 未被CPU屏蔽的中断请求会送入优先级分析电路参加判优,由中断控制器产生一个公共的中断请求信号INT,送至CPU。
  3. 当CPU响应中断请求时,发出中断响应信号INTA,送往中断控制器
    中断控制器将优先级最高的中断请求信号的硬件中断源类型码(中断号)经系统总线送至CPU,作为CPU寻找该中断服务程序入口地址的依据。

系统中每个中断信号线由一个唯一的标识符来识别,称为中断号。每一个硬件中断资源在内核中由一个struct irq_desc类型的数据结构表示。

// include/linux/irq.h
strcut irq_desc{
    unsigned int irq;       //中断号
    ...
    irq_flow_hanlder_t  handle_irq; //中断处理程序
    struct irq_chip *chip;          //中断控制器芯片的数据结构指针
    struct msi_desc *msi_desc;      //中断屏蔽数据结构指针
    void    *handler_data;          
    void    *chip_data;
    struct irqaction *action;       //指向描述中断信号与中断处理程序对应关系数据结构指针
    unsigned int status;            //中断信号状态
    unsigned int depth;             //中断嵌套深度
    unsigned int wake_depth;        //唤醒被打断的嵌套中断服务程序的深度
    unsigned int irq_count;         //本中断被打断的次数
    unsigned long last_unhandled;   //最后一次未被CPU响应的时间
    unsigned int  irqs_unhandled;   //未被CPU响应的次数
    spinlock_t  lock;
}

系统中有多少硬件中断信号线,就有多少个struct irq_desc结构的实例,也即struct irq_desc数据结构实例是硬件中断信号在Linux内核中的表示。所有struct irq_desc结构实例存放在irq_desc数组中,irq_desc[NR_IRQ]表示中断信号(中断向量表),NR_IRQ表明内核中断向量表可存放中断向量的个数,与具体的CPU平台相关。

中断信号与该中断信号产生的中断时间需要关联起来,在Linux中由struct irqaction数据结构描述。每个中断信号可以有多个struct irqaction数据结构的实例。

struct irqaction{
    irq_handler_t handler;  //中断处理函数
    unsigned long flags;    //中断类型标志
    cpumask_t mask;         //中断屏蔽位标识
    const char *name;       //产生中断信号的设备名
    void *dev_id;           //设备在内核中的数据结构
    struct irqaction *next; //共享同一中断信号的设备
    int irq;                
    struct proc_dir_entry *dir;
}

在这里插入图片描述

//注册中断
int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs), unsigned long irqflags, const char *devname, void *dev_id);
//释放中断
void free_irq(unsigned int irq, void *dev_id);

2.2 软中断

网络数据收发的处理不是都要在中断服务程序中进行,中断处理程序只执行最紧急的任务,如读入硬件缓冲区的数据,清除设备硬件状态等(top half);设备硬件状态清除后,设备就可以开始接收新数据,接着再次允许设备硬件中断;大部分对数据的处理都放在中断后半段(Bottom half)中完成,这样极大提高了内核响应硬件中断请求的速度,提高了硬件的吞吐量。在网络子系统中,主要使用的是软件中断(softirq)来完成网络数据包接收后半段的工作。
实现Bottom half的机制分为一下几个步骤:
1)将Bottom half适当分类
2)将Bottom half索引号与处理程序关联,向内核注册Bottom half和它的处理函数
3)在适当的时候调度Bottom half执行
4)通知内核执行Bottom half的处理函数

2.2.1 软件中断相关函数

Linux内核定义了6种类型的软件中断,即:

enum{
    HI_SOFTIRQ =0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    SCSI_SOFTIRQ,
    TASKLET_SOFTIRQ,
};

其中两种软件中断NET_TX_SOFTIRQ/NET_RX_SOFTIRQ用在网络子系统中。
软件中断处理的特点是,内核不允许在同一个CPU上同时运行相同类型软件中断的实例,这样极大减少了管理并发访问控制的工作量。同一软件中断实例可以在不同的CPU上同时运行,类似网络数据包处理这样较费时的任务,多个数据包可以在多个CPU上同时处理,会极大地提高处理速度。软件中断的处理函数只需控制多个CPU对共享数据的并发访问。

// kernel/softirq.c
//描述软件中断的数据结构
struct softirq_action{
    void (*action)(struct softirq_action *);
}

//软件中断中断向量表
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

/*注册软件中断
 * int nr  软件中断类型的索引号
 * void(*action)(struct softirq_action*) 指向软件中断处理函数的指针
 * void *data 传给软件中断处理程序的数据
 */
void open_softirq(int nr, void(*action)(struct softirq_action *), void *data);

2.2.2 调度软件中断的时刻

软件中断是实现硬件中断后半段的一种机制,调度软件中断来执行的时刻最直接的地方就应该是硬件中断执行完后立即调度。除此之外内核在其他几种例程的执行过程中,也会查看是否有中断的后半段在等待被调度执行,如果有,内核就用do_softirq方法来调度执行软件中断,内核会在以下几处检查是否有软件中断等待执行。

  1. do_IRQ
    当内核接到硬件中断请求时,它用函数do_IRQ来调度执行硬件中断的处理程序,因为大量的软件中断是由硬件中断处理程序传递的,所以在do_IRQ执行后就可能有软件中断等待被调度,这时立即调度软件中断执行所产生的延迟最小,例如在各种硬件中断、时钟中断产生后常常立即执行do_softirq。
  2. 从硬件中断和异常(包括系统调用)返回时
    从硬件中断返回时,硬件中断的处理程序已执行完,在硬件中断中我们只处理最紧急的任务,所以当它返回时,大量可以推迟的处理过程就留给软件中断来完成
  3. 在CPU上打开软件中断时,以前如有挂起等待被调度的软件中断,就用do_softirq函数调度它们执行。
  4. 内核线程ksoftirqd_cpun
  5. 其他调用软件中断的地方

2.2.3 标记传递的软件中断

硬件中断结束后,如果该硬件中断要中断服务程序的后半段来处理数据,它们调用以下几个函数来设置一个位图标志。位图标志每一位代表一种软件中断,如果某一位被设置,则代表有对应类型的软件中断挂起等待执行。

  1. __raise_softirq_irqoff
    此函数设置要运行的软件中断在位图的标志位,稍后内核查看到对应软件中断的标志位被设置后,与它相关的软件中断就会被调度到CPU上执行。
#define __raise_softirq_irqoff(nr) do{or_softirq_pending(1UL << (nr));} while(0)

此函数读出原软件中断的位图标志,将硬件新传递的软件中断与原位图标志做“或”操作,标志有新的软件中断在等待执行。

  1. raise_softirq_irqoff
    这是__raise_softirq_irqoff的包装函数,如果当前在硬件中断或软件中断的执行现场,等到中断服务程序或软件中断返回后,新传递的软件中断会在中断退出函数中被调度执行。如果当前不在以上执行现场,则显示的调度内核线程,保证软件中断在不久会被调度执行。
  2. raise_softirq
    它是raise_softirq_irqoff的包装函数,需在关闭了硬件中断的情况下执行raise_softirq_irqoff。

2.2.4 执行软件中断

调度执行软件中断的前提条件是:当前没有任务硬件中断或正在执行别的软件中断,否则do_softirq停止运行,什么也不做。

#ifndef __ARCH_HAS_DO_SOFTIRQ

asmlinkage void do_softirq(void)
{
	__u32 pending;
	unsigned long flags;

	if (in_interrupt())   //当前是否在中断现场
		return;           //如果是则返回,do_softirq什么都不做

	local_irq_save(flags);      //保存当前软件中断位图设置

	pending = local_softirq_pending();  // 读软件中断的位图标志

	if (pending)                 // 如果位图标志中有软件中断等待调度执行,执行软件中断
		__do_softirq();

	local_irq_restore(flags);    //恢复原软件中断位图标志
}

#endif

有的软件中断在do_softirq运行期间可以多次调度执行,因为在运行软件中断处理程序时,硬件中断是打开的,所以软件中断可能被硬件中断打断。硬件中断服务程序在执行时可以修改软件中断位图标志,传递新的软件中断等待调度。因此在__do_softirq打开中断之前,do_softirq保存当前软件中断位图标志,清除CPU软件中断位图设置,然后按照局部变量pending中保存的位图标志一个一个地执行软件中断。

#define MAX_SOFTIRQ_RESTART 10   //do_softirq能执行的最长时间

asmlinkage void __do_softirq(void)
{
	struct softirq_action *h;
	__u32 pending;
	int max_restart = MAX_SOFTIRQ_RESTART;
	int cpu;

	pending = local_softirq_pending();  //读取软件中断设置的位图标志
	account_system_vtime(current);

	__local_bh_disable((unsigned long)__builtin_return_address(0));
	trace_softirq_enter();

	cpu = smp_processor_id();
restart:
	/* Reset the pending bitmask before enabling irqs */
    //在开中断之前,清CPU的软件中断位图标志,以便硬件中断可以传递新的软件中断,随后开中断
	set_softirq_pending(0);

	local_irq_enable();
    //获取软件中断向量表,以便找到软件中断的处理函数,如果有挂起的软件中断,依次调用软件中断的处理函数
	h = softirq_vec;

	do {
		if (pending & 1) {
			int prev_count = preempt_count();

			h->action(h);

			if (unlikely(prev_count != preempt_count())) {
				printk(KERN_ERR "huh, entered softirq %td %p"
				       "with preempt_count %08x,"
				       " exited with %08x?\n", h - softirq_vec,
				       h->action, prev_count, preempt_count());
				preempt_count() = prev_count;
			}

			rcu_bh_qsctr_inc(cpu);
		}
		h++;
		pending >>= 1;
	} while (pending);

	local_irq_disable();
    //原挂起的软件中断执行后,如仍有终端等待被调度执行,并且do_softirq还没用完执行时间,则重复前面的过程
	pending = local_softirq_pending();
	if (pending && --max_restart)
		goto restart;
    //如果do_softirq已用完它的执行时间,则唤醒内核线程
	if (pending)
		wakeup_softirqd();

	trace_softirq_exit();

	account_system_vtime(current);
	_local_bh_enable();
}

一旦所有挂起的软件中断处理程序都被执行了,__do_softirq就会查看是否还有新的软件中断等待被调度执行,即使只有一个软件中断挂起,整个过程会再次重复。但__do_softirq只能连续执行MAX_SOFTIRQ_RESTART给定的时间。
使用MAX_SOFTIRQ_RESTART是为了避免单一的网络类型中断一直持续传递软件中断调度执行,别的软件中断传递的软件中断不能被执行。
在__do_softirq中,每一次一个软件中断被调度执行后,其对应的标志位就从本地变量pending中清除,当位图清空时,调度软件中断的循环结束。最后,如果do_softirq的执行时间已达到MAX_SOFTIRQ_RESTART,do_softirq必须返回,还有未执行的软件中断在等待被调度,它就唤醒内核线程ksoftirqd在以后来执行它们。因为do_softirq在内核中有多处被调用,可能在一段时间后,内核线程被调度执行前系统又调用了do_softirq来处理哪些挂起的软件中断。

内核线程的任务是作为后备来查看是否还要前面过程中未执行的软件中断在等待调度,如果有,在将CPU交给别的活动之前尽量调度软件中断执行,每个CPU都有一个内核线程,分别为ksoftirqd_cpu0、ksoftirqd_cpu1等。

3. 网络设备驱动程序的实现

样本为isa-skeletion,具体代码详见isa_skeletion.c文件
代码中内核启动后调用netcard_probe自动探测设备,do_net_probe传参,netcard_probe1执行实际探测的函数。

  • 常量及局部变量定义
 /*定义设备名,在申请I/O端口、中断、DMA时需要用*/
static const char* cardname = "netcard";
/* First, a few definitions that the brave might change. */
/* A zero-terminated list of I/O addresses to be probed. */
/*以0结尾的I/O地址列表,用于探测可能的I/O端口地址*/
static unsigned int netcard_portlist[] __initdata =
   { 0x200, 0x240, 0x280, 0x2C0, 0x300, 0x320, 0x340, 0};
/* use 0 for production, 1 for verification, >2 for debug */
/*设备的私有数据结构,用于保存配置信息和统计信息*/
struct net_local {
struct net_device_stats stats;
long open_time;	/* Useless example local info. */
/* Tx control lock.  This protects the transmit buffer ring
 - state along with the "tx full" state of the driver.  This
 - means all netif_queue flow control actions are protected
 - by this lock as well.
 */
spinlock_t lock;
};
  • 探测函数的包装函数
    两种探测网卡的情况:
    1) 指定基地址探测 ,以模块方式装载时,发送参数,指定I/O端口基地址;静态编译到内核时,通过命令行传参
    2) 在一个已知的地址范围内搜索,按照事先定义在地址列表中的I/O端口地址逐一地探测,如果所有的I/O端口基地址处都没有发现设备,就返回错误代码-ENODEV。
  • 自动探测函数
    1) 为网络设备的struct net_device数据结构实例分配空间
    2) 设置设备的配置信息
    3) 调用探测函数查看是否有此硬件
/*探测函数netcard_probe1的包装函数,探测所有地址或某个指定地址,或不探测*/
static int __init do_netcard_probe(struct net_device *dev)
{
   int i;
   int base_addr = dev->base_addr;
   int irq = dev->irq;
   /*探测指定I/O端口地址*/
   if(base_addr > 0x1ff)
       return netcard_probe1(dev, base_addr);
   /*不探测*/
   else if(base_addr != 0)
       return -ENXIO;
   /*探测所有i/O端口地址处有无网络设备*/
   for (i =0; netcard_portlist[i]; i++)
   {
       int ioaddr = netcard_portlist[i];
       if(netcard_probe1(dev, ioaddr)==0)
           return 0;
       dev->irq=irq;
   }
   return -ENODEV
}
/*自动探测函数*/
struct net_device * __init netcard_probe(int unit)
{
   /*分配网络设备的struct net_device 数据结构实例*/
   struct net_device *dev = alloc_etherdev(sizeof(struct net_local));
   int err;
   if(!dev)
       return ERR_PTR(-ENOMEM);
   /*分配设备名*/
   sprintf(dev->name, "eth%d", unit);
   /*查看系统启动时有无命令行参数*/
   netdev_boot_setup_check(dev);
   /*探测设备*/
   err = do_netcard_probe(dev);
   /*如探测到设备,则返回指针,否则释放dev*/
   if(err)
       goto out;
   return dev;
 out:
   free_netdev(dev);
   return ERR_PTR(err);
}

4 实际的探测函数

/*实际的网络设备探测和初始化函数*/
static int __init netcard_probe1(struct net_device *dev, int ioaddr)
{
   struct net_local *np;
   static unsigned version_printed = 0;
   int i;
   /*
    *  从以太网适配器读入MAC地址,比较前三个字节是否与生产厂商标识符相同
    */ 
   if (inb(ioaddr + 0) != SA_ADDR0
   ||	 inb(ioaddr + 1) != SA_ADDR1
   ||	 inb(ioaddr + 2) != SA_ADDR2) {
   return -ENODEV;
   }
   ...
   /* 如果探测包网络设备,将I/O端口基地址填入dev的base_addr数据域 */
   dev->base_addr = ioaddr;
   /* 读入网络设备的硬件地址,填入dev的dev_addr[]数据域 */
   for (i = 0; i < 6; i++)
   printk(" %2.2x", dev->dev_addr[i] = inb(ioaddr + i));
   #ifdef jumpered_interrupts
   /*
    * 如果该网络适配器支持跳线分配中断,在这里分配中断向量,跳线中断不由板卡报告,所以需要自动产生IRQ来发现中断号
    */
   if (dev->irq == -1)
   ;	/* 什么都不错,用户程序会设置中断号. */
   else if (dev->irq < 2) {	/* 自动探测中断号 */
   autoirq_setup(0);
   /* Trigger an interrupt here. */
   dev->irq = autoirq_report(0);
   if (net_debug >= 2)
   printk(" autoirq is %d", dev->irq);
   } else if (dev->irq == 2)
   /*
    * Fixup for users that don't know that IRQ 2 is really
    * IRQ9, or don't know which one to set.
    */
   dev->irq = 9;
   {
   /*为设备分配中断资源*/
   int irqval = request_irq(dev->irq, &net_interrupt, 0, cardname, dev);
   if (irqval) {
   printk("%s: unable to get IRQ %d (irqval=%d).n",
      dev->name, dev->irq, irqval);
   return -EAGAIN;
   }
   }
   #endif	/* jumpered interrupt */
   #ifdef jumpered_dma
   /*
    * 分配DMA资源
    */
   if (dev->dma == 0) {
   if (request_dma(dev->dma, cardname)) {
   printk("DMA %d allocation failed.n", dev->dma);
   return -EAGAIN;
   ...
   #endif	/* jumpered DMA */
   /* Initialize the device structure. */
   if (dev->priv == NULL) {
   dev->priv = kmalloc(sizeof(struct net_local), GFP_KERNEL);
   if (dev->priv == NULL)
   return -ENOMEM;
   }
   memset(dev->priv, 0, sizeof(struct net_local));
   np = (struct net_local *)dev->priv;
   spin_lock_init(&np->lock);
   /* Grab the region so that no one else tries to probe our ioports. */
   request_region(ioaddr, NETCARD_IO_EXTENT, cardname);
   dev->open	= net_open;
   dev->stop	= net_close;
   dev->hard_start_xmit	= net_send_packet;
   dev->get_stats	= net_get_stats;
   dev->set_multicast_list = &set_multicast_list;
           dev->tx_timeout	= &net_tx_timeout;
           dev->watchdog_timeo	= MY_TX_TIMEOUT; 
   /* Fill in the fields of the device structure with ethernet values. */
   ether_setup(dev);
   return 0;
}

5.3.1 网络设备活动功能函数

ifconfig

  • ioctl(SIOCSIFADDR) (Socket I/O Contreal Set Interface Address) 设置网络接口地址,由内核完成
  • ioctl(SIOCSIFFLAGS) (Socket I/O Contreal Set Interface Flags) 设置dev->flags标志,打开设备调用Open方法,关闭设备调用stop方法

open方法申请一次网络设备活动所需要的系统资源,初始化适配器硬件,激活网络设备。

int xxx_open(struct net_device *dev)
{
    /*申请各自需要的系统资源,如中断、I/O端口*/
    requset_region();
    request_irq();
    ...
    /*复制硬件地址,一般从网络接口中的EEPROM中读取网络适配器的硬件地址*/
    memcpy(dev->dev_addr, src_addr, length);
    ...
    /*激活硬件转发队列*/
    netif_start_queue(dev);
    return 0; 
}

stop方法释放open方法中分配的系统资源

int xxx_release(struct net_device *dev)
{
    /*释放网络设备I/O端口、中断资源、DMA通道等*/
    ...
    /*停止硬件发送队列,设备不能再发送网络数据包*/
    netif_stop_queue(dev);
    return 0;
}

数据传输

int xxx_tx(struct sk_buff *skb, struct net_device *dev)
{
	//停止发售队列
	//向网络控制器命令/状态寄存器写发送命令,等待硬件准备好
	//将Socket Buffer传给硬件
	//如果发送成功,更新统计信息
	//启动发送超时计时器
	//释放Socket buffer
	return 0;
}

数据发送超时,需要清除错误,让已经在进行的发送过程正确执行

void xxx_tx_timeout(struct net_device *dev)
{
	//将发送超时的出错信息发送给日志,记录发送出错的统计信息
	//复位
	//启动发送队列,恢复网络发送
	netif_wake_queue(dev);
}

接收数据包

  1. 分配一个Socket Buffer来存放数据网络包,此时在中断代码的执行现场,要用FP_ATOMIC标志分配,一旦分配成功就将数据从网络设备硬件缓冲区复制到Socket Buffer中。
  2. 对skb必要的数据域赋值
  3. 更新数据包统计信息
  4. 最后数据包的接收过程由netif_rx完成,它将数据包传给上层协议栈
void xxx_rx(struct net_device *dev, struct xxx_packet *pkt)
{
	struct sk_buff *skb;
	struct xxx_priv *priv = netdev_priv(dev);
	//为接收到的数据包分配Socket Buffer ,如果Socket Buffer分配失败,扔掉数据包
	skb= dev_alloc_skb(pkt->datalen +2);
	if(!skb){
		//分配失败
		goto out;
	}
	//将接收到的网络数据复制到新分配的Socket Buffer
	memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);
	//初始化skb的部分数据域,供上层协议使用
	skb->dev = dev;
	skb->protocol = eth_type_trans(skb,dev);
	skb->ip_summed = CHECKSUM_UNNECESSARY;
	priv->stats.rx_packets++;
	priv->stats.rx_bttes +=pkt->datalen;
	netif_rx(skb);
out:
	return;	
}

中断处理程序

static irqreturn xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
	int statusword;
	//读取中断状态寄存器,获取中断原因
		statusword = 中断原因;
	if(statusword& 接收中断原因的编码){ //如果是接收中断
		...
		xxx_rx(dev, pkt); //调用接收数据处理函数
		
	}
	if(statusword & 发送完成中断原因的编码){
		/*释放已发送数据包的Socket Buffer,修改统计信息*/
		priv->status.tx_packets ++;
		priv->stats.tx_bytes += priv->tx_packetlen;
		dev_kfree_skb(priv->skb);
	}
	if (pkt) release_buffer(pkt);
	return;
}

缓解接收中断:驱动程序模式(NAPI),基于轮询,结合中断

  1. 初始化代表网络设备的net_device数据结构实例的poll数据域
    dev->poll = xxx_poll; 指向驱动程序实现的轮询设备数据的函数指针
    dev->weight = 2; 描述的是在资源有限的情况下,CPU一次可从网络设备中读入多少个数据包
  2. 修改中断处理程序
static int xxx_poll(struct net_dev *dev, int *budget)
{
	//比较网络设备能传给内核的数据包数最大值与内核一次可读入数据包数的预算,取其最小值
	quota = min(dev->quota, *budget);
	while(已读入的数据包数小于预算且接受队列不为空){
		从网络接口的接收队列读入一个数据包
		分配sk_buff, 复制数据包
		填写skb相关数据域,skb->dev,skb->protocol,skb->ipsumed
		将skb传给上层协议:调用netif_receive_skb(skb)
		修改统计信息
		if(硬件接收队列为空)
			将设备放入数据接收完成设备队列,开接收中断,返回
	}
}

5.3.3 网络设备管理函数

定制ioctl命令

ioctl (fd, SIOCSIFADDR, &freq);
/*
 *fd : 创建的socket描述符
 * 调用哪个ioctl命令
 * 设置参数
 */

ioctl命令如果在协议层没有被识别就传给网络设备层的dev_ioctl,这些与设备相关的ioctl命令,从用户空间接收第三个参数,它是struct ifreq数据结构类型的指针,定义在include/linux/if.h文件中。如果用户程序要重新配置网络设备,就会将要设置的网络设备选项值通过ifreq数据结构传过来,由网络设备驱动程序的ioctl命令执行。如果用户程序要读取网络设备的配置,网络设备程序执行ioctl命令后将查询到的设备配置值,通过ifreq传回给应用程序。

struct ifreq {
#define IFHWADDRLEN	6
	union
	{
		char	ifrn_name[IFNAMSIZ];		/* if name, e.g. "en0" */
	} ifr_ifrn;
	
	union {
		struct	sockaddr ifru_addr; //接口IP地址
		struct	sockaddr ifru_dstaddr;
		struct	sockaddr ifru_broadaddr;
		struct	sockaddr ifru_netmask;
		struct  sockaddr ifru_hwaddr;//mac地址
		short	ifru_flags;
		int	ifru_ivalue;    //接口适配器索引
		int	ifru_mtu;
		struct  ifmap ifru_map;
		char	ifru_slave[IFNAMSIZ];	/* Just fits the size */
		char	ifru_newname[IFNAMSIZ];
		void __user *	ifru_data;
		struct	if_settings ifru_settings;
	} ifr_ifru;
};

内核实现了一系列标准的ioctl命令,除了这些标准调用外,每个网络接口还可以定义自己的ioctl命令。网络设备层socket还可以识别另外16个私有ioctl命令,命令索引号在SIOCDEVPRIVATE~SIOCDEVPRIVATE+15,一旦这些私有的ioctl命令被系统识别,就执行网络设备驱动程序提供的dev->do_ioctl;

统计信息:存放在设备的私有数据结构中

5.4 CS8900A网络适配器驱动程序实现

为一个新的网络控制器开发驱动程序时,首先要理解网络控制器的硬件特点,比如I/O端口地址设置、中断设置、是否支持DMA发送等基本组成特性和操作特点。这些信息可由网络控制芯片手册(Data sheet)获取。

第6章 数据链路层数据帧的收发

  1. 接收:硬件缓冲区复制数网络据帧挂到CPU的输入队列,通知上层协议有网络数据到达,随后上层协议从CPU的输入队列中获取网络数据帧并处理

  2. 发送:由数据链路层放到设备输出队列,再由设备驱动程序的硬件发送函数hard_start_xmit将设备输出队列中的数据帧复制到设备硬件缓冲区,实现对外发送。

struct napi_struct : 支持NAPI模式的网络设备
struct softnet_data : 每个CPU用来管理网络数据的输入输出流量

struct napi_struct {
	/* The poll_list must only be managed by the entity which
	 * changes the state of the NAPI_STATE_SCHED bit.  This means
	 * whoever atomically sets that bit can add this napi_struct
	 * to the per-CPU poll_list, and whoever clears that bit
	 * can remove from the list right before clearing the bit.
	 */
	struct list_head	poll_list;//加入CPU poll_list列表

	unsigned long		state;//NAPI设备
	int			weight;//从设备缓冲区中读入的最大数据帧数
	unsigned int		gro_count;
	int			(*poll)(struct napi_struct *, int);//poll函数指针
#ifdef CONFIG_NETPOLL
	int			poll_owner;
#endif
	struct net_device	*dev;
	struct sk_buff		*gro_list;//分片
	struct sk_buff		*skb;//socket Buffer
	struct hrtimer		timer;
	struct list_head	dev_list;
	struct hlist_node	napi_hash_node;
	unsigned int		napi_id;
};
struct softnet_data {
	struct list_head	poll_list;
	struct sk_buff_head	process_queue;

	/* stats */
	unsigned int		processed;
	unsigned int		time_squeeze;
	unsigned int		received_rps;
#ifdef CONFIG_RPS
	struct softnet_data	*rps_ipi_list;
#endif
#ifdef CONFIG_NET_FLOW_LIMIT
	struct sd_flow_limit __rcu *flow_limit;
#endif
	struct Qdisc		*output_queue;//管理输出队列
	struct Qdisc		**output_queue_tailp;
	struct sk_buff		*completion_queue;//已经成功发送或接收的套接字缓存区,可释放
#ifdef CONFIG_XFRM_OFFLOAD
	struct sk_buff_head	xfrm_backlog;
#endif
#ifdef CONFIG_RPS
	/* input_queue_head should be written by cpu owning this struct,
	 * and only read by other cpus. Worth using a cache line.
	 */
	unsigned int		input_queue_head ____cacheline_aligned_in_smp;

	/* Elements below can be accessed between CPUs for RPS/RFS */
	call_single_data_t	csd ____cacheline_aligned_in_smp;
	struct softnet_data	*rps_ipi_next;
	unsigned int		cpu;
	unsigned int		input_queue_tail;
#endif
	unsigned int		dropped;
	struct sk_buff_head	input_pkt_queue;//每个CPU的输入队列
	struct napi_struct	backlog;//将数据帧向上层协议实例推送

};

Linux实现了两种机制来实现在数据链路层中将数据帧放入CPU的输入队列:

  1. netif_rx
    每接收到一个数据帧就会产生一个中断,在以下几种场合调用:
  • 网络设备驱动程序接收中断的执行现场
  • 处理CPU掉线事件的回调函数dev_cpu_callback,将掉线的CPU的struct softnet_data数据结构实例中的发送数据帧完成队列、输出队列、input_pkt_queue加入到其他CPU的struct softnet_data数据结构实例相关队列中。
  • loopback 设备接收数据帧函数
    在这里插入图片描述
  1. NAPI
    一次中断接收多个数据帧,减少中断服务程序和现行程序切换
    使用中断和轮询相结合的方式来代替纯中断模式,当网络设备从网络上收到数据帧后,向CPU发出中断请求,内核执行设备驱动程序的中断服务程序接收数据帧;在内核处理完前面收到的数据帧,如果设备又收到新的数据帧,不需要产生新的中断(设备中断为关状态),内核继续读入设备输入缓冲区中的数据帧(通过poll函数完成),直到设备输入缓冲区为空,再重新打开设备中断。
    在这里插入图片描述
    在这里插入图片描述

6.1 网络接收软件中断

在这里插入图片描述

6.2 数据链路层与网络层的接口

当网络设备驱动程序接收到一个数据帧后,驱动程序将数据帧存放在sk_buff数据结构中,并初始化sk_buff的protocol,netif_receive_skb参考sk_buff->protocol数据域的值来确定应将输入数据帧上传给网络层的哪个协议的接收处理函数。
在这里插入图片描述
skb->protocol引用的值命名格式为ETH_P_XXX,定义在include/linux/if_ether.h中。

协议标识符处理函数
ETH_P_IP0x800ip_rcv
ETH_P_ARP0x806arp_rcv
ETH_P_IPV60x86ddipv6_rcv

struct packet_type数据结构描述了网络层协议标识符、接收处理程序与接收网络设备等的相关关联,每个在网络层的协议实例都有一个struct packet_type类型的变量来定义协议处理接收数据帧的实体。

//目录include/linux/netdevice.h
struct packet_type {
    /*协议标识符,eg:ETH_P_IP*/
	__be16			type;	/* This is really htons(ether_type).*/
    /*接收数据帧的网络设备*/
	struct net_device	*dev;	/* NULL is wildcarded here*/
    /*协议实例实现的接口处理函数*/
	int			(*func) (struct sk_buff *,
					 struct net_device *,
					 struct packet_type *,
					 struct net_device *);
	bool			(*id_match)(struct packet_type *ptype,
					    struct sock *sk);
    /*私有数据*/
	void			*af_packet_priv;
	struct list_head	list;
};

内核中有两个链表作为数据链路层与网络层之间接收数据帧的接口:

  • struct list_head ptype_all 接收所有数据帧,用于网络工具和网络探测器接收数据帧
  • struct list_head ptype_base[PTYPE_HASH_SIZE] 只接收与协议标识符相匹配的的网络数据帧

在这里插入图片描述
dev_add_pack添加链表,__dev_remove_pack删除链表

//目录net/core/dev.c
void dev_add_pack(struct packet_type *pt)
{
    int hash;
    spin_lock_bh(&ptype_lock);
    if(pt->type == htons(ETH_P_ALL))
    {
        list_add_rcu(&pt->list,&ptype_all);
    }
    else{
        hash = ntohs(pt->type)&PTYPE_HASH_MASK;
        list_add_rcu(&pt->list, &ptype_base[hash]);
    }
    spin_unlock_bh(&ptype_lock);
}
__dev_remove_pack(struct packet_type *pt)
{
    struct list_head *head;
    struct packet_type *pt1;
    spin_lock_bh(&ptype_lock);
    if(p->type==htons(ETH_P_ALL))
    {
        head = &ptype_all;
    }
    else{
        head = &ptype_base[ntos(pt->type)] & PTYPE_HASH_MASK;
    }
    list_for_each_entry(pt1,head,list)
    {
        if(pt= pt1){
            list_del_rcu(&pt->list);
            goto out;
        }
    }
    printk(KERN_WARNING"dev_remove_pack: %p not found.\n",pt);
out:
    spin_unlock_bh(&ptype_lock);
}

6.3 数据链路层对数据帧发送的处理

网络设备的发送缓冲区满,就不能再缓存内核向外发送的数据帧,如果不通知内核,会引起发送过程失败,此时,需要允许/禁止设备发送数据帧——通过启动/停止网络设备的发送队列来实现。
操作发送队列的API为:

//目录include/linux/netdevice.h
netif_start_queue //启动网络设备发送队列,允许网络设备发送
netif_stop_queue //停止网络设备发送队列,禁止网络设备发送过程
netif_wake_queue //唤醒网络设备发送队列,重新启动网络设备的发送过程

数据帧的发送使用dev_queue_xmit函数,该函数将上层协议发送来的数据帧放到网络设备的发送队列,随后流量控制系统按照内核配置的队列管理策略,将网络设备发送队列中的数据帧依次发送出去。发送时,从网络设备的输出队列上获取一个数据帧,将数据帧发送给设备驱动程序的dev->netdev_ops->ndo_start_xmit方法。
若发送失败,内核实现了_netif_schedule来重新调度网络设备发送数据帧
dev_queue_xmit可以通过两个途径来调用网络设备驱动程序的发送函数ndo_start_xmit:

  • 流量控制接口:通过调用qdisc_run函数执行
  • 直接调用ndo_start_xmit:用于网络设备不使用流量控制体系结构的情况。

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值