您的当前位置:首页正文

网卡设备驱动

2023-10-18 来源:星星旅游


网卡设备驱动

1. 8139too网卡设备简介

一个PCI设备,总共有三个地址空间:内存,端口和设备。内存和端口事实上是同一个内容的不合拜望路径罢了。PCI设备的设备空间是标准化的,每个PCI设备的设备空间的前64个字节的含义差不多上一样的。但各个设备的内存和端口空间是完全不一样的。在硬件概念上,设备的I/O内存和I/O端口空间没有差别,事实上差不多上设备拥用的一些读写存放器。

8139too网卡拥有256字节的读写存放器空间。它的全部构造如下:

/* Symbolic offsets to registers. */

enum RTL8139_registers {

MAC0 = 0, /* Ethernet hardware address. */

MAR0 = 8, /* Multicast filter. */

TxStatus0 = 0x10, /* Transmit status (Four 32bit registers). */

TxAddr0 = 0x20, /* Tx descriptors (also four 32bit). */

RxBuf = 0x30,

ChipCmd = 0x37,

RxBufPtr = 0x38,

RxBufAddr = 0x3A,

IntrMask = 0x3C,

IntrStatus = 0x3E,

TxConfig = 0x40,

RxConfig = 0x44,

Timer = 0x48, /* A general-purpose counter. */

RxMissed = 0x4C, /* 24 bits valid, write clears. */

Cfg9346 = 0x50,

Config0 = 0x51,

Config1 = 0x52,

FlashReg = 0x54,

MediaStatus = 0x58,

Config3 = 0x59,

Config4 = 0x5A, /* absent on RTL-8139A */

HltClk = 0x5B,

MultiIntr = 0x5C,

TxSummary = 0x60,

BasicModeCtrl = 0x62,

BasicModeStatus = 0x64,

NWayAdvert = 0x66,

NWayLPAR = 0x68,

NWayExpansion = 0x6A,

/* Undocumented registers, but required for proper operation. */

FIFOTMS = 0x70, /* FIFO Control and test. */

CSCR = 0x74, /* Chip Status and Configuration Register. */

PARA78 = 0x78,

PARA7c = 0x7c, /* Magic transceiver parameter register. */

Config5 = 0xD8, /* absent on RTL-8139A */

};

每个存放器都有它专门的含义和用处。举个例子(我们假设应用I/O内存的方法,ioaddr为设备内存映射在CPU内存地址空间的肇端地址,下述代码能读取板卡的版本号):

#define HW_REVID(b30, b29, b28, b27, b26, b23, b22) \\

(b30<<30 | b29<<29 | b28<<28 | b27<<27 | b26<<26 | b23<<23 | b22<<22)

#define HW_REVID_MASK HW_REVID(1, 1, 1, 1, 1, 1, 1)

/* identify chip attached to board */

version = ioread32( ioaddr + TxConfig ) & HW_REVID_MASK;

在我的电脑上,version读出来的值的0x74400000,经由比对,板卡名称为:RTL-8100B/8139D。

下面是全部系例版本号的表格:

enum chip_flags {

HasHltClk = (1 << 0),

HasLWake = (1 << 1),

};

/* directly indexed by chip_t, above */

const static struct {

const char *name;

u32 version; /* from RTL8139C/RTL8139D docs */

u32 flags;

} rtl_chip_info[] = {

{ \"RTL-8139\HW_REVID(1, 0, 0, 0, 0, 0, 0),HasHltClk,},

{ \"RTL-8139 rev K\

{ \"RTL-8139A\HW_REVID(1, 1, 1, 0, 0, 0, 0),HasHltClk,},

{ \"RTL-8139A rev G\

{ \"RTL-8139B\HW_REVID(1, 1, 1, 1, 0, 0, 0),HasLWake, },

{ \"RTL-8130\HW_REVID(1, 1, 1, 1, 1, 0, 0),HasLWake, },

{ \"RTL-8139C\HW_REVID(1, 1, 1, 0, 1, 0, 0),HasLWake, },

{ \"RTL-8100\HW_REVID(1, 1, 1, 1, 0, 1, 0),HasLWake, },

{ \"RTL-8100B/8139D\

{ \"RTL-8101\HW_REVID(1, 1, 1, 0, 1, 1, 1),HasLWake, },

};

在那个表格中,RTL_8139B以下(包含RTL_8139B)的板卡都属于较新的版本。新老版本之间有不合的唤醒方法。先看新版本的:

#define LWAKE 0x10

#define Cfg1_PM_Enable 0x01

u8 new_tmp8, tmp8;

enum Config4Bits {

LWPTN = (1 << 2), /* not on 8139, 8139A */

};

enum Cfg9346Bits {

Cfg9346_Lock = 0x00,

Cfg9346_Unlock = 0xC0,

};

new_tmp8 = tmp8 = ioread8( ioaddr + Config1 );

if( (rtl_chip_info[tp->chipset].flags & HasLWake) && (tmp8 & LWAKE))

new_tmp8 &= ~LWAKE;

new_tmp8 |= Cfg1_PM_Enable;

if (new_tmp8 != tmp8) {

iowrite8( Cfg9346_Unlock, ioaddr + Cfg9346 );

iowrite8( tmp8, ioaddr + Config1 );

iowrite8( Cfg9346_Lock, ioaddr + Cfg9346 );

}

if (rtl_chip_info[tp->chipset].flags & HasLWake) {

tmp8 = ioread8( ioaddr + Config4 );

if (tmp8 & LWPTN) {

iowrite8( Cfg9346_Unlock, ioaddr + Cfg9346 );

iowrite8( tmp8 & ~LWPTN, ioaddr + Config4 );

iowrite8( Cfg9346_Lock, ioaddr + Cfg9346 );

}

}

全然的一个流程是:假如板卡版本本身支撑了HasLWake,而Config1中读出的值带有LWAKE,把Config1的值写回,并把Config4 中的LWPTN去除。而我的网卡中从Config1, Config4读取的值分别为0x8d, 0x88,因此,无需做任何操作。

下面是旧版本的唤醒方法:

enum Config1Bits {

Cfg1_PM_Enable = 0x01,

Cfg1_VPD_Enable = 0x02,

Cfg1_PIO = 0x04,

Cfg1_MMIO = 0x08,

LWAKE = 0x10, /* not on 8139, 8139A */

Cfg1_Driver_Load = 0x20,

Cfg1_LED0 = 0x40,

Cfg1_LED1 = 0x80,

SLEEP = (1 << 1), /* only on 8139, 8139A */

PWRDN = (1 << 0), /* only on 8139, 8139A */

};

tmp8 = ioread8( ioaddr + Config1 );

tmp8 &= ~(SLEEP | PWRDN);

iowrite8( tmp8, ioaddr + Config1 );

下面是一个板卡的复位操作:

enum ChipCmdBits {

CmdReset = 0x10,

CmdRxEnb = 0x08,

CmdTxEnb = 0x04,

RxBufEmpty = 0x01,

};

static void rtl8139_chip_reset (void __iomem *ioaddr)

{

int i;

/* Soft reset the chip. */

iowrite8( CmdReset, ioaddr + ChipCmd );

/* Check that the chip has finished the reset. */

for (i = 1000; i > 0; i--) {

barrier();

if ((ioread8( ioaddr + ChipCmd ) & CmdReset) == 0)

break;

udelay (10);

}

}

写一个CmdReset敕令到ChipCmd地位,等待该敕令消掉,即可。

关于网卡的存放器操作,还有一些,再进行过程中碰到时再介绍。

2. 收集设备的初始化

3.一个简单的收集流量统计法度榜样

4. net_device构造体详解

构造体net_device代表了一个收集设备接口,它是我们明白得收集设备驱动法度榜样的关键。那个地点,我们选择个中的一些重要成员,一一作具体分析,并结合以太网设备,看看Linux内核是若何为以太网设备供给构造体中某些成员的缺省值的。

在Linux内核源代码中是如许为那个构造体作注释的:实际上,那个构造体是一个专门大年夜的缺点,它把I/O数据和更高层的数据混淆在一路,同时它几乎必须明白INET模块中的每个数据构造。

毫无疑问,这是一个巨型构造体。但我们为编写收集设备驱动法度榜样,只须要明白得个中的一部分,下面选择个中的一些作分析,并给出以太网设备的缺省值。

unsigned short flags;

void (*set_multicast_list)(struct net_device *dev);

这是一个接口标记,包含了专门多值的位掩码。在以太网的缺省初始化函数中,该标记被设置为:IFF_BROADCAST|IFF_MULTICAST,表示以太网卡是可广播的,同时是能够或许进行组播发送的。别的,该标记接口还有一些只读标记,如IFF_UP,当接口被激活并能够开端传输数据包时,内核设置该标记。而IFF_PROMISC被设置或清除时,会调用set_multicast_list函数通知板卡上的硬件过滤器。

unsigned short hard_header_len;

unsigned short type;

hard_header_len是硬件头的长度,在以太网设备的初始化函数中,该成员被赋为ETH_HLEN,即以太网头的长度,该值为14,下面是以太网头的定义:

struct ethhdr {

unsigned char h_dest[ETH_ALEN]; /* destination eth addr */

unsigned char h_source[ETH_ALEN]; /* source ether addr */

unsigned short h_proto; /* packet type ID field */

} __attribute__((packed));

ETH_ALEN被定义为6,即以太网MAC地址的长度。h_proto储存type的值。type是接口的硬件类型,以太网设备的初始化函数中将其赋值为ARPHRD_ETHER,即10Mb以太网。

int (*hard_header) (struct sk_buff *skb, struct net_device *dev,

unsigned short type, void *daddr,void *saddr,

unsigned len);

该函数在数据被传输之前被调用,它依照先前检索到的源和目标地址建立硬件头。以

太网设备的默认函数是eth_header:

int eth_header(struct sk_buff *skb, struct net_device *dev,

unsigned short type, void *daddr,

void *saddr, unsigned len)

{

struct ethhdr *eth = (struct ethhdr *)skb_push(skb,ETH_HLEN);

/*

* Set the protocol type. For a packet of

* type ETH_P_802_3 we put the length

* in here instead. It is up to the 802.2

* layer to carry protocol information.

*/

if(type!=ETH_P_802_3)

eth->h_proto = htons(type);

else

eth->h_proto = htons(len);

//Set the source hardware address.

if(!saddr)

saddr = dev->dev_addr;

memcpy(eth->h_source,saddr,dev->addr_len);

//Anyway, the loopback-device should never

//use this function...

if (dev->flags & (IFF_LOOPBACK|IFF_NOARP)){

memset(eth->h_dest, 0, dev->addr_len);

return ETH_HLEN;

}

if(daddr){

memcpy(eth->h_dest,daddr,dev->addr_len);

return ETH_HLEN;

}

return -ETH_HLEN;

}

它依照传入的参数建立以太网头,假如传入的源地址为空,则应用设备的地址作为源地址。与之相干的还有一个int (*rebuild_header)(struct sk_buff *skb)函数,在以太网设备中,它是用于在ARP地址解析完成今后,从新建立以太网头,主假如更新目标MAC地址,因为在前一次建立时,因为没有经由 ARP协定,有可能目标MAC地址是缺点的(未完,待续)。

5. net_device构造体详解(续)

int (*set_mac_address)(struct net_device *dev, void *addr);

改变网卡的mac地址。实际上,专门多硬件设备接口全然不支撑这种功能,就算支撑,也须要硬件厂商供给的对象修改EPROM中的硬件地址。一样我们在某一操作体系下所谓的修改MAC地址,只是修改了操作体系在安装网卡时从网卡EPROM中读出来的值,假如体系被重装,则MAC地址又复原为本来的值。以太网设备的默认函数是eth_mac_addr,

它只是简单地修改了dev->dev_addr的值。

int (*hard_header_cache)(struct neighbour *neigh,struct hh_cache *hh);

void (*header_cache_update)(struct hh_cache *hh,struct net_device *dev,

unsigned char * haddr);

int (*hard_header_parse)(struct sk_buff *skb,unsigned char *haddr);

这三个函数在以太网设备接口中都有默认的值,hard_header_cache把硬件地址缓存到struct hh_cache中;header_cache_update在地址产生变更中,更新hh_cache构造中的目标地址;而 hard_header_parse则从skb中的数据包中获得源地址,并将其复制到位于haddr的缓冲区。

int (*change_mtu)(struct net_device *dev, int new_mtu);

下面是以太网设备接口的change_mtu的实现:

static int eth_change_mtu(struct net_device *dev, int new_mtu)

{

if ((new_mtu < 68) || (new_mtu > 1500))

return -EINVAL;

dev->mtu = new_mtu;

return 0;

}

在net_device中,还有专门多收集驱动法度榜样必须涉及的重要成员,跟着我们的8139too网卡驱动法度榜样的进一步深刻,在涉及时再作具体分析

6.分派中断号 & 为网卡驱动设置MAC地址

PCI设备的中断事实上专门轻易处理。在Linux的引导时期,运算机固件差不多为设备分派了一个独一的中断号。中断号储存在设备存放器60 (PCI_INTERRUPT_LINE)中,该存放器为一个字节宽。因此驱动法度榜样无需检测中断号。只需从设备存放器中读取即可:

result = pci_read_config_byte(pci_dev, PCI_INTERRUPT_LINE, &myirq);

但事实上,留给具体的PCI驱动法度榜样编写者的工作可能更为简单。在我们前面讲到过的8139too网卡的初始化法度榜样的rtl8139_init_one函数(PCI设备注册成功即被调用的函数,留意,不是收集设备)的第一行加上以下语句:

printk(KERN_INFO \"the irq: %d\\n\

OK,我们能获得成果:

the irq: 10

也确实是说PCI设备驱动法度榜样在注册本身时,差不多主动完成中断号的猎取。

再回到我们前面讲过的8139too网卡的初始化驱动法度榜样,编译完成,insmod后,我们运行体系的ifconfig敕令:

ifconfig eth0

获得成果:

eth0

Link encap:Ethernet HWaddr 00:00:00:00:00:00

inet addr:192.168.0.100 Bcast:192.168.0.255 Mask:255.255.255.0

inet6 addr: fe80::200:ff:fe00:0/64 Scope:Link

UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1

RX packets:0 errors:0 dropped:0 overruns:0 frame:0

TX packets:0 errors:0 dropped:7 overruns:0 carrier:0

collisions:0 txqueuelen:1000

RX bytes:0 (0.0 b) TX bytes:0 (0.0 b)

Base address:0x4800

如何样?五脏俱全?没错,因为以太网设备接口本身已由内核供给了专门多缺省成员值,差不多能够或许精确显示MTU,同时有了传输队列的长度。但它的MAC地址: HWaddr 00:00:00:00:00:00看起来不大年夜对。这是因为代表收集设备接口的net_device构造体的成员dev_addr没有被精确设置。收集驱动法度榜样还须要从网卡设备的EPROM中读取MAC地址填入到dev_addr字符数组中才行。

前文差不多有讲到,8139too网卡的设备内存的头6个字节等于MAC地址,直截了当读取即可。但我不明白什么缘故,8139too网卡的驱动法度榜样采取了一个专门复杂的从EPROM读守信息的函数,也许是为了安稳,或者通用。下面的代码插入到rtl8139_init_one函数中注册收集设备之前的地位:

int i, addr_len;

void __iomem *ioaddr;

struct rtl8139_private *tp;

tp = netdev_priv(dev);

ioaddr = tp->mmio_addr;

addr_len = read_eeprom (ioaddr, 0, 8) == 0x8129 ? 8 : 6;

for (i = 0; i < 3; i++)

((u16 *) (dev->dev_addr))[i] =

le16_to_cpu (read_eeprom (ioaddr, i + 7, addr_len));

read_eeprom的实现可直截了当查阅8139too.c源代码,那个地点不列出了。其全然的实现道理是经由过程location | (EE_READ_CMD << addr_len)形成一个完全的读敕令,个中EE_READ_CMD是5。然后经由过程操作网卡设备内存的Cfg9346地位完成一个读操作,每次读16 位。addr_len在不合版本的设备上是不合的,因此须要先读location=0的地位上的16位值,假如是0x8129,则addr_len是8,不然是6。然后经由过程对location为7,8,9的地位的读取,形成一个完全的MAC地址。

函数le16_to_cpu完成一个字节序的转换,把PCI设备的固有字节序(始终是小端先序的)转换成CPU字节序。

现在再来看看ifconfig的输出:

eth0

Link encap:Ethernet HWaddr 00:02:3F:AC:41:9D

inet addr:192.168.0.100 Bcast:192.168.0.255 Mask:255.255.255.0

inet6 addr: fe80::202:3fff:feac:419d/64 Scope:Link

UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1

RX packets:0 errors:0 dropped:0 overruns:0 frame:0

TX packets:0 errors:0 dropped:43 overruns:0 carrier:0

collisions:0 txqueuelen:1000

RX bytes:0 (0.0 b) TX bytes:0 (0.0 b)

Base address:0x4800

OK,现在能够了。

7. 中断处理初步

大年夜多半硬件接口经由过程中断处理例程来操纵。接口在两种可能的事宜下中断处理器:新数据包达到,或者外发数据包的传输差不多完成。收集接口还能产生中断以通知缺点的产生、连接状况等等情形。

要捕捉并处理中断,仅仅向硬件设备写入是不敷的,还必须要在体系中安装一个软件处理例程。假如没有通知Linux内核等待用户的中断,那么内核只会简单应答并忽视该中断。内核模块在应用中断前要先要求一个中断通道,然后在应用后开释该通道,下面是要乞降开释函数的原型:

int request_irq( unsigned int irq,

irqreturn_t (*handler)(int, void *, struct pt_regs *),

unsigned long flags, const char *dev_name,

void *dev_id );

void free_irq( unsigned int irq, void *dev_id);

在8139too网卡的设备驱动的初始化过程中,我们在rtl8139_open函数(这是收集设备注册成功后,即被调用的初始化函数)中参加以下语句:

int retval;

retval = request_irq (dev->irq, rtl8139_interrupt, SA_SHIRQ, dev->name, dev);

if (retval) return retval;

dev->irq为要申请的中断号,这是我们从PCI设备接口中直截了当获得的;rtl8139_interrupt为要安装的中断处理例程; SA_SHIRQ是一个标记,表示中断能够在设备之间共享;dev->name是用来在/proc/interrupts中显示中断的拥有者; dev表示本设备用于共享的中断旌旗灯号线。看一下/proc/interrupts的内容:

CPU0

0: 2127786 XT-PIC timer

1: 13977 XT-PIC i8042

2: 0 XT-PIC cascade

7: 4 XT-PIC parport0

8: 1 XT-PIC rtc

9: 11965 XT-PIC acpi, uhci_hcd:usb2

10: 122101 XT-PIC Intel 82801CA-ICH3, Intel 82801CA-ICH3 Modem, uhci_hcd:usb1, yenta, yenta, ohci1394, nvidia

12: 110 XT-PIC i8042

14: 23729 XT-PIC ide0

15: 18471 XT-PIC ide1

NMI: 0

ERR: 0

10号中断线的共享应用率有点高,然则没有看到我们的eth0设备,对了,还没有insmod我们的网卡驱动,insmod后:

CPU0

0: 2157753 XT-PIC timer

1: 14051 XT-PIC i8042

2: 0 XT-PIC cascade

7: 4 XT-PIC parport0

8: 1 XT-PIC rtc

9: 13359 XT-PIC acpi, uhci_hcd:usb2

10: 123908 XT-PIC Intel 82801CA-ICH3, Intel 82801CA-ICH3 Modem, uhci_hcd:usb1, yenta, yenta, ohci1394, nvidia, eth0

12: 110 XT-PIC i8042

14: 23875 XT-PIC ide0

15: 18741 XT-PIC ide1

NMI: 0

ERR: 0

现在有了。然后不要忘了,在一个内核模块中,注册与刊出,分派与开释必须成对显

现,在rtl8139_close函数中:

free_irq (dev->irq, dev);

下面实现我们的中断处理例程,那个地点只是简单地在体系log里打印一点信息,然后返回说差不多处理了:

static irqreturn_t rtl8139_interrupt (int irq, void *dev_instance,

struct pt_regs *regs)

{

printk(KERN_INFO \"interrupt!\\n\");

return IRQ_HANDLED;

}

那个中断到底有没有后果?下面是insmod后,体系日记中的一段,能说明问题:

Mar 18 20:59:33 localhost kernel: interrupt!

Mar 18 20:59:34 localhost last message repeated 59 times

Mar 18 20:59:34 localhost kernel: get stats!

Mar 18 20:59:34 localhost kernel: interrupt!

Mar 18 20:59:35 localhost last message repeated 62 times

Mar 18 20:59:35 localhost kernel: get stats!

Mar 18 20:59:35 localhost kernel: interrupt!

Mar 18 20:59:36 localhost last message repeated 65 times

Mar 18 20:59:36 localhost kernel: get stats!

Mar 18 20:59:36 localhost kernel: interrupt!

Mar 18 20:59:37 localhost last message repeated 58 times

Mar 18 20:59:37 localhost kernel: get stats!

8. 一个简化的pcnet32驱动初始化法度榜样

9. AMD 79C970A硬件的初始化过程

PCNET32网卡驱动法度榜样的初始化过程,包含了以下内容的初始化:

PCI设备初始化(我们忽视非PCI设备的情形)。这是一个一样性的步调,代码对所有PCI设备均有用(只有设备相干的一些微小差别),经由过程PCI设备的初始化步调,我们

获得了79C970A网卡的I/O地址范畴是:0x1400-0x147f, 并申请储存0x1400开端的32字节的I/O地址空间,今后,我们对设备的操作都经由过程这32字节的I/O地址范畴。

以太网收集设备的初始化,这也是一个一样性的步调,具有必定的通用性和广泛性,甚至内核差不多为以太网的初始化供给一些通用函数供应用。以太网收集设备的初始化重要工作是把描述设备的构造struct net_device注册到一个内核空间的全局链表中,并为其注册各类操作函数。struct net_device中的priv指针须要重点存眷,它是一个私稀有据指针,各个设备间可能存在着较大年夜的差别。

硬件的初始化。在我的实验情形中,是指对AMD 79C970A芯片的初始化,经由过程操作芯片的I/O地址实现,完成硬件级的设备复位,启用,禁止,中断等各类操作。

以上三部分内容的初始化在PCNET32驱动法度榜样中并没有明显的界线,专门多代码差不多上在一路的。下面重要描述一下AMD 79C970A芯片的初始化过程。PCI设备的初始化不再赘述,PCNET32网卡设备的priv指针别的专门描述。

硬件级的初始化过程,开端于pcnet32_probe1函数,这时,PCI设备相干的初始化工作已全然完成。第一步工作确实是复位硬件,并确信I/O端口的宽度(16位照样32位),下面是相干代码:

/* reset the chip */

pcnet32_wio_reset(ioaddr);

/* NOTE: 16-bit check is first, otherwise some older PCnet chips fail */

if (pcnet32_wio_read_csr(ioaddr, 0) == 4 && pcnet32_wio_check(ioaddr)){

a = &pcnet32_wio;

}else{

pcnet32_dwio_reset(ioaddr);

if (pcnet32_dwio_read_csr(ioaddr, 0) == 4 &&

pcnet32_dwio_check(ioaddr)){

a = &pcnet32_dwio;

}else

goto err_release_region;

}

reset操作确实是从端口0x1414(16位宽度的端口)或者端口0x1418(32位宽度的端口)读取响应宽度的数据。代码先测验测验16位宽度的端口,假如掉败,则测验测验32位宽度的端口,

接下来读取芯片的版本:

chip_version = a->read_csr(ioaddr, 88) | (a->read_csr(ioaddr,89) << 16);

版本总共28位,低12位固定为0x003,不然为弗成辨认版本,79C970A的版本为:0x2621003。

接下来读取网卡的MAC地址:

for (i = 0; i < 3; i++) {

unsigned int val;

val = a->read_csr(ioaddr, i+12) & 0x0ffff;

/* There may be endianness issues here. */

dev->dev_addr[2*i] = val & 0x0ff;

dev->dev_addr[2*i+1] = (val >> 8) & 0x0ff;

}

将网卡切换到32位模式:

a->write_bcr(ioaddr, 20, 2);

将私稀有据中的init_blok的地址存入硬件中:

a->write_csr(ioaddr, 1, (lp->dma_addr + offsetof(struct pcnet32_private,

init_block)) & 0xffff);

a->write_csr(ioaddr, 2, (lp->dma_addr + offsetof(struct pcnet32_private,

init_block)) >> 16);

在pcnet32_probe1函数的最后:

/* enable LED writes */

a->write_bcr(ioaddr, 2, a->read_bcr(ioaddr, 2) | 0x1000);

接下来,在pcnet32_open函数中,上述的专门多操作被从新履行了,并最终初始化完毕,启动硬件,那个地点不再具体描述。

10. PCNET32驱动法度榜样的pcnet32_private构造

一个收集设备,在其驱动法度榜样顶用一个net_device构造描述,该构造有一个重要的成员priv,它是一个指针,指向驱动法度榜样本身定义的私稀有据。不合的收集设备驱动法度榜样,其私稀有据各不雷同。下面是PCNET32驱动法度榜样的私稀有据:

struct pcnet32_private {

struct pcnet32_rx_head rx_ring[RX_RING_SIZE];

struct pcnet32_tx_head tx_ring[TX_RING_SIZE];

struct pcnet32_init_block init_block;

dma_addr_t dma_addr;

struct pci_dev *pci_dev;

const char *name;

struct sk_buff *tx_skbuff[TX_RING_SIZE];

struct sk_buff *rx_skbuff[RX_RING_SIZE];

dma_addr_t tx_dma_addr[TX_RING_SIZE];

dma_addr_t rx_dma_addr[RX_RING_SIZE];

struct pcnet32_access a;

spinlock_t lock;

unsigned int cur_rx, cur_tx;

unsigned int dirty_rx, dirty_tx;

struct net_device_stats stats;

char tx_full;

int options;

unsigned int dxsuflo:1,

mii:1;

struct net_device *next;

struct mii_if_info mii_if;

struct timer_list watchdog_timer;

struct timer_list blink_timer;

};

下面逐个分析该构造的重要成员。

tx_skbuff和rx_skbuff。这是两个sk_buff的数组,其长度分别为TX_RING_SIZE(16)和RX_RING_SIZE (32)。sk_buff是一个套接字缓冲区,在Linux内核中处于收集子体系的核心肠位,由它储存发送和接收的数据,关于该构造的具体描述参阅《Linux设备驱动法度榜样》第三版512页。个中tx_skbuff是发送缓冲区数组,总共有16个,而rx_skbuff是接收缓冲区数组,总共有32 项。

tx_dma_addr和rx_dma_addr。关于分派好的sk_buff缓冲区,我们都要把它们映射成为DMA缓冲区,这两个数组用于记录响应的DMA缓冲区的地址。

rx_ring和tx_ring。这是pcnet32_rx_head和pcnet32_tx_head的构造体,它们用于记录对应的sk_buff的当前状况,能够称为描述符。这两个构造体的定义如下:

struct pcnet32_rx_head {

u32 base;

s16 buf_length;

s16 status;

u32 msg_length;

u32 reserved;

};

struct pcnet32_tx_head {

u32 base;

s16 length;

s16 status;

u32 misc;

u32 reserved;

};

关于tx_skbuff和rx_skbuff数组,在应用逻辑上是呈环形的,即用完最后1个后,从新应用第1个,因此,在源代码中会看到专门多的ring定名。下面是对rx ring和tx ring的初始化函数:

static int pcnet32_init_ring(struct net_device *dev)

{

struct pcnet32_private *lp = dev->priv;

int i;

lp->tx_full = 0;

lp->cur_rx = lp->cur_tx = 0;

lp->dirty_rx = lp->dirty_tx = 0;

for (i = 0; i < RX_RING_SIZE; i++) {

struct sk_buff *rx_skbuff = lp->rx_skbuff[i];

if (rx_skbuff == NULL) {

if (!(rx_skbuff = lp->rx_skbuff[i] = dev_alloc_skb (PKT_BUF_SZ))) {

printk(KERN_ERR \"%s: pcnet32_init_ring dev_alloc_skb failed.\\n\

dev->name);

return -1;

}

skb_reserve (rx_skbuff, 2);

}

rmb();

if (lp->rx_dma_addr[i] == 0)

lp->rx_dma_addr[i] = pci_map_single(lp->pci_dev, rx_skbuff->data,

PKT_BUF_SZ-2, PCI_DMA_FROMDEVICE);

lp->rx_ring[i].base = (u32)le32_to_cpu(lp->rx_dma_addr[i]);

lp->rx_ring[i].buf_length = le16_to_cpu(2-PKT_BUF_SZ);

wmb(); /* Make sure owner changes after all others are visible */

lp->rx_ring[i].status = le16_to_cpu(0x8000);

}

for (i = 0; i < TX_RING_SIZE; i++) {

lp->tx_ring[i].status = 0; /* CPU owns buffer */

wmb(); /* Make sure adapter sees owner change */

lp->tx_ring[i].base = 0;

lp->tx_dma_addr[i] = 0;

}

lp->init_block.tlen_rlen = le16_to_cpu(TX_RING_LEN_BITS RX_RING_LEN_BITS);

|

for (i = 0; i < 6; i++)

lp->init_block.phys_addr[i] = dev->dev_addr[i];

lp->init_block.rx_ring = (u32)le32_to_cpu(lp->dma_addr +

offsetof(struct pcnet32_private, rx_ring));

lp->init_block.tx_ring = (u32)le32_to_cpu(lp->dma_addr +

offsetof(struct pcnet32_private, tx_ring));

wmb(); /* Make sure all changes are visible */

return 0;

}

该初始化函数中,对接收缓冲区数组,差不多上预先分派好,并置好了描述符的响应状况。而关于发送缓冲区,并没有分派,只是初始化了描述符,比及应用时再进行分派。

该初始化函数中,还有一些pcnet32_private中的成员,我们还未描述到,下面一一描述。

cur_rx,cur_tx,dirty_rx和dirty_tx。这是四个关于套接字缓冲区的统计变量,记录当前缓冲区的应用情形。

init_block。这是一个pcnet32_init_block的构造,该构造定义如下:

struct pcnet32_init_block {

u16 mode;

u16 tlen_rlen;

u8 phys_addr[6];

u16 reserved;

u32 filter[2];

/* Receive and transmit ring base, along with extra bits. */

u32 rx_ring;

u32 tx_ring;

};

phys_addr记录mac地址,rx_ring和tx_ring记录缓冲区数组的首地址。

其它几个成员,在用到时再具体描述。

11. 让网卡具备数据发送功能

先给出源代码,改日开端再进行代码分析。这是两个函数,分别是中断处理和数据发送:

static irqreturn_t pcnet32_interrupt( int irq, void *dev_id,

struct pt_regs *regs )

{

struct net_device *dev = dev_id;

struct pcnet32_private *lp;

unsigned long ioaddr;

u16 csr0, rap;

int boguscnt = max_interrupt_work;

int must_restart;

if( !dev ){

printk(KERN_DEBUG \"%s(): irq %d for unknown device\\n\

__FUNCTION__, irq );

return IRQ_NONE;

}

ioaddr = dev->base_addr;

lp = dev->priv;

spin_lock( &lp->lock );

rap = lp->a.read_rap( ioaddr );

while( (csr0 = lp->a.read_csr(ioaddr, 0)) & 0x8f00 && --boguscnt >= 0 ){

if( csr0 == 0xffff ) break;

}

lp->a.write_csr( ioaddr, 0, csr0 & ~0x004f );

must_restart = 0;

printk(KERN_INFO \"%s: interrupt csr0=%#2.2x new csr=%#2.2x.\\n\

dev->name, csr0, lp->a.read_csr( ioaddr, 0 ) );

if( csr0 & 0x0200 ){

unsigned int dirty_tx = lp->dirty_tx;

int delta;

while( dirty_tx != lp->cur_tx ){

int entry = dirty_tx & TX_RING_MOD_MASK;

int status = (short)le16_to_cpu( lp->tx_ring[entry].status );

if( status < 0 )

break;

lp->tx_ring[entry].base = 0;

if( status & 0x4000 ){

int err_status = le32_to_cpu( lp->tx_ring[entry].misc );

lp->stats.tx_errors++;

printk(KERN_ERR \"%s: Tx error status=%04x err status=%08x\\n\

dev->name, status, err_status );

if( err_status & 0x04000000 )

lp->stats.tx_aborted_errors++;

if( err_status & 0x08000000 )

lp->stats.tx_carrier_errors++;

if( err_status & 0x10000000 )

lp->stats.tx_window_errors++;

if( err_status & 0x40000000 )

lp->stats.tx_fifo_errors++;

if( !lp->dxsuflo ){

printk( KERN_ERR, \"%s: Tx FIFO error! CSR0=%4.4x\\n\

dev->name, csr0 );

must_restart = 1;

}

}else{

if( status & 0x1800 )

lp->stats.collisions++;

lp->stats.tx_packets++;

}

if( lp->tx_skbuff[entry] ){

pci_unmap_single( lp->pci_dev, lp->tx_dma_addr[entry],

lp->tx_skbuff[entry]->len, PCI_DMA_TODEVICE);

dev_kfree_skb_irq( lp->tx_skbuff[entry] );

lp->tx_skbuff[entry] = NULL;

lp->tx_dma_addr[entry] = 0;

}

dirty_tx ++;

}

delta = (lp->cur_tx - dirty_tx)&(TX_RING_MOD_MASK + TX_RING_SIZE);

if( delta > TX_RING_SIZE ){

printk( KERN_ERR \"%s: out-of_svnc dirty pointer. %d vs. %d.\"

\"full=%d.\\n\

dev->name, dirty_tx, lp->cur_tx, lp->tx_full );

dirty_tx += TX_RING_SIZE;

delta -= TX_RING_SIZE;

}

if( lp->tx_full && netif_queue_stopped(dev) &&

delta < TX_RING_SIZE -2 ){

lp->tx_full = 0;

netif_wake_queue(dev);

}

lp->dirty_tx = dirty_tx;

}

lp->a.write_csr( ioaddr, 0, 0x0040 );

lp->a.write_rap( ioaddr, rap );

printk(KERN_INFO \"%s: exiting interrupt, csr0=%#4.4x.\\n\

dev->name, lp->a.read_csr( ioaddr, 0 ) );

spin_unlock( &lp->lock );

return IRQ_HANDLED;

}

static int pcnet32_start_xmit( struct sk_buff *skb, struct net_device *dev )

{

struct pcnet32_private *lp = dev->priv;

unsigned long ioaddr = dev->base_addr;

u16 status;

int entry;

unsigned long flags;

spin_lock_irqsave( &lp->lock, flags );

status = 0x8300;

entry = lp->cur_tx & TX_RING_MOD_MASK;

printk(KERN_INFO \"entry: %d\\n\

lp->tx_ring[entry].length = le16_to_cpu( -skb->len );

lp->tx_ring[entry].misc = 0x00000000;

lp->tx_skbuff[entry] = skb;

lp->tx_dma_addr[entry] = pci_map_single( lp->pci_dev, skb->data,

skb->len, PCI_DMA_FROMDEVICE );

lp->tx_ring[entry].base = (u32)le32_to_cpu( lp->tx_dma_addr[entry] );

wmb();

lp->tx_ring[entry].status = le16_to_cpu( status );

lp->cur_tx++;

lp->stats.tx_bytes += skb->len;

lp->a.write_csr( ioaddr, 0, 0x0048 );

dev->trans_start = jiffies;

if( lp->tx_ring[(entry+1) & TX_RING_MOD_MASK].base != 0 ){

lp->tx_full = 1;

netif_stop_queue( dev );

}

spin_unlock_irqrestore( &lp->lock, flags );

return 0;

}

12. 网卡数据发送的全然流程

发送函数把数据预备好(放在私稀有据构造中的特定变量中),并触发硬件发送。

1、从发送缓冲区环中选择一个空的缓冲区,包含tx_ring, tx_skbuff, tx_dma_addr。

2、设置tx_ring的成员,length设为待发送数据长度取反,misc设零,base设为tx_dma_addr,status设为缺省的0x8300。

3、tx_skbuff指向待发送数据套接字缓冲区skb。

4、将skb映射成的dma地址存放在tx_dma_addr中。

5、cur_tx ++;

6、写网卡硬件,触发一个急速发送的旌旗灯号。

7、检查环是否已用完,假如是,暂停后续发送。

中断处理函数中,其重要工作是把已发送过的缓冲区收受接收,使其从新可用。

1、假如dirty_tx < cur_tx,则说明有已送完的缓冲区还充公受接收掉落,轮回处理。

2、检查tx_ring的成员status,假如<0 不消处理,退出。

3、假如(status & 0x4000 != 0),则发送掉足,处理处理。

4、收受接收已发送掉落的缓冲区。

5、dirty_tx ++;

6、假如前面差不多因为环用完,而发送被暂停,而现在已有空余缓冲区,则连续发送。

13. 网卡数据接收过程简述

当网卡设备上有新的数据达到时,硬件会产生一个中断,驱动法度榜样处理那个中断,就能够接收数据。其接收过程大年夜致能够分为以下几步:

1、检查rx_ring[entry].status,大年夜于等于零,表示稀有据达到,须要处理。

2、取status的高8位,假如高8位不等于0x03,表示出缺点产生。

3、不然从rx_ring[entry].msg_length中能够取到数据包的长度,并确信长度是否在合理的范畴内。

4、假如长度值跨过一个阀值(能够经由过程参数设定),则取下rx_ring的接收缓冲区作为传给上层的缓冲区,同时,还给rx_ring一个空的缓冲区,假如没有跨过阀值,则

把rx_ring的接收缓冲区数据拷贝出来。

5、更新统计信息,把接收缓冲区传给接收层。

14. 构建以太网头

上层传给网卡驱动法度榜样的待发送数据包是不包含以太网头的,以太网头须要在网卡驱动法度榜样中进行构建。一个完全的以太网头包含源MAC地址,目标MAC地址,和协定类型。那个地点问题的关键确实是:若何获得目标MAC地址?即明白了目标IP地址后,若何将那个IP地址和一个MAC地址接洽关系起来?平日处理那个问题是经由过程地址解析协定(ARP)来>完成。

连续往常文的ping法度榜样为例,主机172.16.48.2要向172.16.48.1发还显要求icmp包之前,会先向MAC地址ff:ff:ff: ff:ff:ff广播一个ARP要求包,询问172.16.48.1的MAC地址。当172.16.48.1收到那个包时,会向172.16.48.2发一个ARP应答包,告诉本身的MAC地址。如许,48.2就可认为回显要求的ICMP包构建以太网头了。

然后,为了防止48.1的MAC地址会变更,每隔必定的时刻,48.2都邑向48.1发ARP要求包,询问48.1的MAC地址。

在网卡驱动法度榜样中,代表收集接口的构造体struct net_device共有5个成员函数用于构建以太网头。它们分别是:

int (*rebuild_header)(struct sk_buff *skb );

该函数用来在传输数据包之前,完成ARP解析之后,从新建立硬件头。在我们的测试

法度榜样中,发明那个函数并没有被用到,设为 NULL也不阻碍驱动正常工作。

int (*hard_header)(... .../*省略*/);

该函数依照ARP协定获得的MAC地址构建以太网头。从测试情形来看,每次ARP应答包收到时,那个函数都邑被调用,同时,在没有ARP应答包时,也会被调用几回,但不是每次发送包都被调用,有待深究。

int (*header_cache)(... .../*省略*/);

将ARP应答包中获得的构造填充hh_cache构造,从测试来看,它是作一个缓冲,每次有一个新的MAC地址获得时,将其作一个缓冲,储存下来。

int (*header_cache_update)(....../*省略*/);

目标主机的MAC址产生变更时,更新hh_cache构造,从而更新缓存。测试中没有测到如许的情形。

int (*hard_header_parse)(... .../*省略*/);

测试中没有效到,用处不明。

15.对组播的支撑

对组播数据包的支撑由如下几项构成:若干设备标记,一个设备函数和一个数据构造。下面分别对其进行介绍。

net_device构造体的成员flags是一个unsigned short型的数据类型,储存收集接口的一组设备标记。关于以太网收集设备,内核供给的缺省的初始化函数中,如许给flags赋值:

dev->flags = IFF_BROADCAST|IFF_MULTICAST;

memset(dev->broadcast,0xFF, ETH_ALEN);

即缺省状况下,以太网卡是可广播的,同时,它能够或许进行组播发送。并把接口的广播地址设置为ff:ff:ff:ff:ff:ff。

相干的标记还有:IFF_ALLMULTI,那个标记告诉驱动法度榜样检索来自收集的所有组播数据包。IFF_PROMISC,那个标记设置接口为混淆模式,使接口接收所稀有据包。

一个设备函数,即函数void (*dev->set_multicast_list)(struct net_device *dev)。当与设备相干的机械地址清单产生变更时,调用那个设备函数。该函数还在dev->flags被修改时调用,

pcnet32网卡驱动法度榜样中,该函数的履行流程是:

1、假如接口当前处于混淆模式,设置私稀有据的init_block的模式,使网卡不再应用硬件过滤器来过滤数据包。

2、不然,设置响应的init_block的模式,同时,载入新的组播地址列表。

3、复位网卡。

一个数据构造是struct dev_mc_list *dev->mc_list。这是与设备接洽关系的所有组播地址形成的一个链表。这些组播地址在pcnet32网卡驱动法度榜样中,经由过程pcnet32_load_multicast函数,以必定的情势把它们储存在init_block的filter中。

16. 关于收集设备

在全然完成了全部初始化过程今后,我们须要再回到收集设备上来,看看全部TCP/IP协定如何说是假如跟收集设备相接洽关系,并最终一路完成各类复杂工作的。

在网卡驱动相干的分析中,我们提到,代表一个收集设备接口的是一个构造体struct net_device。而在my_inet模块的初始化过程中,mydevinet_init的工作是为MY_PF_INET域的工作找到可用的收集设备,并进行须要的初始化,在mydevinet_init中有这么一行代码:

register_netdevice_notifier(&myip_netdev_notifier);

它是把一个构造体struct notifier_block myip_netdev_notifier注册到一个体系全局的链表netdev_chain中,构造体myip_netdev_notifier含有一个优先级,在netdev_chain中,它跟其它的notifier_block按优先级的先后次序分列,同时它有一个notifier_call的回调函数指针,在它被注册到netdev_chain中后,收集设备上有事宜产生,该回调函数就会被调用到。关于linux的notify的实现道理细节,我们以落后行专门的分析。

关于收集设备会发出的通知的类型,在include/linux/notifier.h中有完全的定义:

#define NETDEV_UP 0x0001 //设备开启

#define NETDEV_DOWN 0x0002 //设备封闭

#define NETDEV_REBOOT 0x0003 //告诉协定栈一个收集接口探测到硬件崩溃并重启了。

#define NETDEV_CHANGE 0x0004 //设备状况改变

#define NETDEV_REGISTER 0x0005 //设备注册

#define NETDEV_UNREGISTER 0x0006 //设备刊出

#define NETDEV_CHANGEMTU 0x0007 //设备改变MTU

#define NETDEV_CHANGEADDR 0x0008 //设备改变地址

#define NETDEV_GOING_DOWN 0x0009 //设备预备封闭

#define NETDEV_CHANGENAME 0x000A //设备改变名字

#define NETDEV_FEAT_CHANGE 0x000B //... ...

当我们把my_inet作为一个模块insmod到内核中时,我们立时会收到设备注册和设备开启两个通知。关于每一个已开启的收集设备, myip_netdev_notifier供给的回调函数myinetdev_event会被两次调用到。我们能够在那个地点完成一些我们欲望做的初始化

工作。

协定栈跟收集设备的接洽关系主假如经由过程struct net_device构造体中的六个协定相干的指针:atalk_ptr,ip_ptr,dn_ptr,ip6_ptr,ec_ptr, ax25_ptr。我们重点存眷的是ipv4的实现,因此只处理指针ip_ptr。ip_ptr指向的是一个构造体struct in_device,其成员struct in_ifaddr ifa_list是一个IP地址列表,关于一个linux收集设备来讲,它能够拥有多大年夜256个ip地址,每一个IP地址在in_device中的表示确实是一个构造体struct in_ifaddr,所有的IP地址以一个链表的情势组织在一路。下面是struct in_ifaddr构造体的定义:

struct in_ifaddr

{

struct in_ifaddr *ifa_next;

struct in_device *ifa_dev;

struct rcu_head rcu_head;

u32 ifa_local;

u32 ifa_address;

u32 ifa_mask;

u32 ifa_broadcast;

u32 ifa_anycast;

unsigned char ifa_scope;

unsigned char ifa_flags;

unsigned char ifa_prefixlen;

char ifa_label[IFNAMSIZ];

};

ifa_next是链表指针,ifa_dev指向它地点的in_device。ifa_local跟ifa_address差不多上IP地址,具体差别今朝还不清晰,ifa_mask是子网掩码,ifa_broadcast是广播地址,ifa_anycast是raw协定相干的一个地址,具体不明。 ifa_scope的取值如下:

enum rt_scope_t

{

RT_SCOPE_UNIVERSE=0,

/* User defined values */

RT_SCOPE_SITE=200,

RT_SCOPE_LINK=253,

RT_SCOPE_HOST=254,

RT_SCOPE_NOWHERE=255

};

实际上,那个成员并非如它的字面意思所表示的scope,它是按到目标端距离进行的一种分类,nowhere表示不存在的目标端,host表示目标端是本机(回环地址),link是直连的一个目标端,site表示在一个本地封闭体系中的内部路由,而universe是除此之外的其它任何情形。

ifa_flags的取值范畴如下:

#define IFA_F_SECONDARY 0x01 //从属设备。

#define IFA_F_TEMPORARY IFA_F_SECONDARY //临时设备。

#define IFA_F_DEPRECATED 0x20

#define IFA_F_TENTATIVE 0x40

#define IFA_F_PERMANENT 0x80 //永久设备。

ifa_prefixlen是子网掩码的收集号的长度,ifa_label是设备和设备名,比如eth0, eth1等。

17. 环回设备接口(loopback)上若何发送和接收数据报

Linux内核用构造体struct net_device表示一个收集设备接口,该构造体的成员hard_start_xmit是一个函数指针,用于完成数据报在收集上的发送工作,其原型是:

int (*hard_start_xmit)( struct sk_buff *skb, struct net_device *dev );

skb是待发送的数据缓冲区,dev是该收集设备接口本身的一个指针。环回设备接口因为是把数据报发给本机,因此其发送数据报函数比较专门,它把skb稍加处理后,又转回给协定栈的数据报接收函数netif_rx。其发送函数的函数名是loopback_xmit。

起首,loopback_xmit调用skb_orphan把skb孤立,使它跟发送socket和协定栈不再有任何接洽,也即对本机来说,那个skb的数据内容差不多发送出去了,而skb相当于差不多被开释掉落了。skb_orphan所做的实际工作是,起首从skb->sk(发送那个skb的那个socket)的sk_wmem_alloc减去skb->truesize,也即从socket的已提交发送队列的字节数中减去那个skb,表示那个skb差不多发送出去了,同时,假如有过程在那个socket上写等待,则唤醒这些过程连续发送数据报,然后把socket的引用计数减1,最后,令sk->destructor和skb->sk都为NULL,使skb完全孤立。实际上,关于环回设备接口来说,数据的发送工作至此差不多全部完成,接下来,只要把那个实际上还未被开释的skb传回给协定栈的接收函数即可。

在传归去之前,还须要做一些工作,因为协定栈在发送数据报时,为数据报打以太网

首部时,skb->data是指向以太网首部的开端地位的,收集设备接口传回的数据报是须要差不多剥离了以太网首部的,因此令skb->data加上ETH_HLEN(14),令skb->mac.raw指向本来data的地位,即以太网首部。然后,须要确信skb->pkt_type的值,即该数据报的类型,假如以太网首部中的目标mac地址是个组播或广播地址,则skb->pkt_type为PACKET_BROADCAST或PACKET_MULTICAST,关于MAC地址类型的确信方法今后再具体分析。不然,假如目标mac地址不等于本接口设备的mac地址,则skb->pkt_type为PACKET_OTHERHOST,不然就为默认值PACKET_HOST。最后,设置skb->protocol(ETH_P_IP,ETH_P_ARP或其它),设置skb->dev,再更新一下统计值,把skb交回给协定协栈即可。

struct net_device还有一些跟协定栈的处理流程关系比较紧密的成员函数,下面一一介绍。

hard_header,该成员被以太网设备驱动法度榜样用于为每一个待发送数据报构建以太网首部,体系中所有以太网设备驱动法度榜样共享一个函数即eth_header。所稀有据报在传递给该函数之前,其skb头部预留了以太网首部的空间,data成员指向收集层首部,eth_header将data成员指向以太网首部,并为以太网首部填入目标以太网地址,源以太网地址和收集层协定类型(ETH_P_IP或ETH_P_ARP),该函数在协定栈中重要有两处被用到,一是ARP模块中,发送ARP要求或应答前,构建以太网首部用;另一处是在IP数据发送过程中,构建以太网首部用。

hard_header_cache,用于创建以太网首部的高速缓冲,每一个邻居节点都有一个struct hh_cache *hh成员,用于缓冲该邻居节点的以太网首部,有了那个缓冲,今后再向那个邻居发数IP数据的时刻,不必再调用hard_header构建以太网首部,而是直截了当从hh中拷贝即可。

18. 数据报在链路层的发送

前面讲述IP数据报的分片与重组时,已完全描述了数据报在收集层是若何被传递的,收集层发送数据报的最后一站是ip_finish_output2,它依照skb->dst->hh是否已被创建来决定若何调用链路层的输出函数,hh实际是neighbour的hh成员,它在ARP解析完成,邻居节点被更新时进行创建,关于不须要ARP解析的设备接口(loopback等),它在第一次发送数据报时被创建。因此,不管收集层若何调用链路层的输出函数,链路层的第一个输出函数始终是dev_queue_xmit。

该函数起首检查skb_shinfo(skb)->frag_list是否有值,假如有,然则收集设备接口不支撑skb的碎片列表(NETIF_F_FRAGLIST),则须要把这些碎片重组到一个完全的skb中(经由过程函数__skb_linearize)。第二步检查skb_shinfo(skb)->nr_frags,假如不为0,表示那个skb应用了分散/聚焦IO,假如收集设备接口不支撑(NETIF_F_SG),同样须要从新线性化(经由过程函数__skb_linearize)。

第三步检查是关于校验和的,须要留意的是那个校验和不是IP首部的首部校验和,IP首部校验和在每个IP数据报中是必须的,由软件来完成,对IP首部以16bit为段进行反码乞降获得,只覆盖到IP首部,而未覆盖到IP数据。而那个地点的校验和是其上层协定(比如UDP)的校验和,它覆盖到上层协定的首部和数据。

struc sk_buff有一个成员ip_summed,表示校验和的履行策略,其可能的取值有三种,CHECKSUM_HW表示由硬件来履行校验和,CHECKSUM_NONE表示完全由软件来履行校验和,CHECKSUM_UNNECESSARY表示没有须要履行校验和。关于新分派的一个skb,老是默认由软件来履行校验和,假如收集设备接口拥有以下三个标记之一,并知足其它一些相干前提,就由硬件履行校验和: NETIF_F_IP_CSUM(硬件只能履行IPv4上的

TCP/UDP协定的校验和),NETIF_F_NO_CSUM(硬件不须要履行校验和,比如环回设备),NETIF_F_HW_CSUM(硬件能履行所稀有据报的校验和)。假如校验和由软件履行,则在ip_generic_getfrag拷贝应用数据的时刻履行,运算获得的校验和存放在skb->csum,由上层协定填写本身的协定首部时填入。不然,假如校验和由硬件履行,则上层协定在填写本身的协定首部时,为skb->csum填上本身首部中校验和所处的地位,以备硬件生成校验和时能够找到那个地位填入。

dev_queue_xmit检查校验和,只是为了作一个挽救方法,即:假如skb->ip_summed==CHECKSUM_HW(由硬件履行校验和,即当前还未生成校验和),然则收集设备接口的成员features上没有标记NETIF_F_HW_CSUM,NETIF_F_NO_CSUM或NETIF_F_IP_CSUM,即收集设备接口既没有表示不须要履行校验和,也说明本身没有履行校验和的才能,或者,假如features上有NETIF_F_IP_CSUM,然则数据报又不是IP协定的。这时刻,还须要履行软件校验和,dev_queue_xmit就调用skb_checksum_help补上那个校验和,并把skb->ip_summed设为CHECKSUM_NONE。

struct net_device的成员qdisc是一个发送队列,缓冲等待收集设备进行发送的skb,假如收集设备设置了那个队列,则把skb加到那个队列中,并启动队列的发送。不然,假如收集设备处于启用状况,则直截了当调用收集设备的输出函数进行发送,但在发送前,还须要做一件工作,确实是,假如有ETH_P_ALL数据报类型被添加到ptype_all中来,则须要把数据报复制一份给那个数据报类型的接收函数,因为该类型须要接收到所有的数据报,包含输出的数据报。

19. 数据报在链路层的接收

收集设备在接收到来自收集中其它主机的数据报,或本地环回接口的数据报之后,交

给协定栈的netif_rx函数,该函数起重要为收到的那个skb打上当前的时刻戳(skb->tstamp成员),那个时刻戳表示该数据达到的时刻,它不是必选的,能够经由过程套接字选项SO_TIMESTAMP将其打开,该选项打开时刻戳时,会将链路层的全局变量netstamp_needed加1,netif_rx在检查到那个变量不为零时,为skb打上时刻戳。

softnet_data是类型为struct softnet_data构造体的全局变量,每个CPU定义一个,它是链路层的数据接收队列,该构造体的定义如下:

struct softnet_data

{

struct net_device *output_queue;

struct sk_buff_head input_pkt_queue;

struct list_head poll_list;

struct sk_buff *completion_queue;

struct net_device backlog_dev;

};

input_pkt_queue是skb的队列,接收到的skb全部进入该队列等待后续处理,netif_rx起首检查该队列当前的长度input_pkt_queue.qlen,即当前排在队列中的skb的

数量,当数量跨过netdev_max_backlog的值时,直截了当丢弃新收到的包,netdev_max_backlog在协定栈中定义的缺省值为1000,能够经由过程文件/proc/sys/net/core/netdev_max_backlog进行修改。假如当前队列长度未达到上限,把新收到的skb加到那个队列中,在加到队列之前,要确保对那个队列的接收处理已启动,假如当前队列为空,则要先调用netif_rx_schedule启动队列的处理,再把skb加到队列中。须要留意的是softnet_data是CPU绑定的,但不是收集设备绑定的,多个收集设备收到的数据报可能存放在同一个队列中待处理。

netif_rx_schedule函数的重要感化是触发一个软中断NET_RX_SOFTIRQ,使中断处理函数net_rx_action处理接收队列中的数据报。net_rx_action开端时会记录下体系的当前时刻,然落后行处理,当处理时刻连续跨过1个时钟嘀嗒时,它会再触发一个中断NET_RX_SOFTIRQ,并退出,鄙人一个中断中连续处理。一次中断处理除了时刻上有限制,处理的数据报的数量上也有限制。

softnet_data的成员poll_list中存放的是成员backlog_dev的地址,由netif_rx_schedule存入,backlog_dev的成员poll在体系初始化时被指向函数process_backlog,net_rx_action调用该函数进行实际的数据报处理,process_backlog把数据报从input_pkt_queue队列中掏出,传给netif_receive_skb,由netif_receive_skb传给响应的收集层接收函数。process_backlog的处理时刻也有1个时钟嘀嗒的限制,同时一次处理的数据报的数量不得跨过backlog_dev->quota和netdev_budget两个值中较小的那个值,backlog_dev->quota由netif_rx_schedule初始化为全局变量weight_p的值,缺省为64,netdev_budget缺省为300。从代码能够看出,process_backlog一次处理最大年夜数据报数量为64,而net_rx_action为300。weight_p和netdev_budget这两个值分别能够在文件/proc/sys/net/core/dev_weight/proc/sys/net/core/netdev_budget中查看和修改。

netif_receive_skb是链路层接收数据报的最后一站。它依照注册在全局数组ptype_all和ptype_base里的收集层数据报类型,把数据报递交给不合的收集层协定的接收函数(INET域中主假如ip_rcv和arp_rcv)。

因篇幅问题不能全部显示,请点此查看更多更全内容