linux网卡驱动

Linux网卡驱动

 
NE2000 以太网卡的基础上进行的。
只要看懂一块网卡的驱动,那么其他网卡的驱动是类似的,模块的划分也是一致的,只是具体的函数和芯片的操作有区别。
文档中红色的标注都是重点。
自己看代码的时候避免依赖于硬件和芯片的代码,而对整个网络设备管理机制的学习,并关注一般网络设备所共有的东西。
 
 

一. 网络设备驱动

网络的物理设备,称为接口( Interface )。所有对网络硬件的访问都是通过接口进行的,接口提供了一个对所有类型的硬件一致化的操作集合来处理基本数据的发送和接收。
一个网络接口被看作是一个发送和接收数据包( packets )的实体。
对于每个网络接口,都用一个 device 的数据结构 表示,有关该数据结构的具体内容,将在本文的后面详细介绍,周日的代码中已经将主要的部分注释了。
网络设备是一个物理设备如以太网卡,但软件也可以作为网络设备,如回送设备( loopback )。
在内核启动时,通过网络设备驱动程序,将登记存在的网络设备。设备用标准的支持网络的机制来转递收到的数据到相应的网络层。
所有被发送和接收的包都用数据结构 sk_buff 表示。这是一个具有很好的灵活性的数据结构,可以很容易增加或删除网络协议数据包的首部,是和上层的接口,对于 IP 层的网络来说,只能识别和区分 skbuff
网络接口的核心用一个 device 数据结构表示的。网络设备在做数据包发送和接收时,直接通过接口访问。
网络接口是在系统初始化时实时生成的,对于核心支持的但不存在的物理网络设备,将不可能有与之相对应的 device 结构
在内核中也存在着一张网络接口管理表 dev_base ,但与前两张表不同, dev_base 是指向 device 结构的指针,因为网络设备是通过 device 数据结构来表示的。 dev_base 实际上是一条 device 结构链表的表头,在系统初始化完成以后,系统检测到的网络设备将自动地保存在这张链表中,其中每一个链表单元表示一个存在的物理网络设备。当要发送数据时,网络子系统将根据系统路由表选择相应的网络接口进行数据传输,而当接收到数据包时,通过驱动程序登记的中断服务程序进行数据的接收处理
网络设备工作原理图:
 
每一个具体的网络接口都应该有一个名字,以在系统中能唯一标识一个网络接口。 通常一个名字仅表明该接口的类型。 Linux 对网络设备命名有以下约定: ( 其中 N 为一个非负整数 )
ethN              以太网接口,包括 10Mbps 100Mbps ,对应到代码里的字符串“ eth%d
trN         令牌环接口;
slN         SLIP 网络接口;
pppN      PPP 网络接口,包括同步和异步;
plipN      PLIP 网络接口,其中 N 与打印端口号相同;
tunlN      IPIP 压缩频道网络接口;
nrN               NetROM 虚拟设备接口;
isdnN      ISDN 网络接口;
dummyN        空设备;
lo           回送网络接口。
 

二. struct device

这里是我从代码里拷回来的,和代码里的注释是对应的,最好和代码一起看;
/*  from  include/linux/netdevice.h  */
struct device
{

1. 属性

  char                          *name;
设备的名字。如果第一字符为 NULL (即 ’\0’ ), register_netdev (drivers/net/net_init.c) 将会赋给它一个 n 最小的可用网络设备名 eth n
 
  unsigned long            rmem_end;            /* shmem "recv" end     */
  unsigned long            rmem_start;           /* shmem "recv" start    */
  unsigned long            mem_end;             /* shared mem end */
  unsigned long            mem_start;            /* shared mem start       */
这些域段标识被设备使用的共享内存的首地址及尾地址。如果设备用来接收和发送的内存块不同,则 mem 域段用来标识发送的内存位置, rmem 用来标识接收的内存位置。 mem_start mem_end 可在系统启动时用内核的命令行指定,用 ifconfig 可以查看它们的值。 rmem 域段从来不被驱动程序以外的程序所引用。
 
  unsigned long            base_addr;             /* device I/O address     */
  unsigned char            irq;                /* device IRQ number    */
I/O 基地址和中断号。它们都是在设备检测期间被赋值的,但也可以在系统启动时指定传入(如传给 LILO )。 ifconfig 命令可显示及修改他们的当前值。
 
  volatile unsigned char        start             /* start an operation       */
  volatile unsigned char        interrupt;        /* interrupt arrived */
这是两个二值的低层状态标志。通常在设备打开时置 start 标志,在设备关闭时清 start 标志。当 interrupt 置位时,表示有一个中断已到达且正在进行中断服务程序理。
 
  unsigned long            tbusy;             /* transmitter busy must be long for bitops */
标识“发送忙”。在驱动程序不能接受一个新的需传输的包时,该域段应该为非零。
 
  struct device              *next;
指向下一个网络设备,用于维护链表。
 
  unsigned char            if_port;
记录哪个硬件 I/O 端口正在被接口所用,如 BNC AUI TP 等( drivers/net/de4x5.h )。
  unsigned char            dma;      
设备用的 DMA 通道。
一些设备可能需要以上两个域段,但非必需的。
 
  unsigned long            trans_start;      /* Time (in jiffies) of last Tx */
上次传输的时间点( in jiffies
  unsigned long            last_rx;    /* Time of last Rx         */
上次接收的时间点( in jiffies )。如 trans_start 可用来帮助内核检测数据传输的死锁( lockup )。
 
  unsigned short           flags;      /* interface flags (BSD) */
该域描述了网络设备的能力和特性。它包括以下 flags :( include/linux/if.h
IFF_UP
表示接口在运行中。当接口被激活时,内核将置该标志位。
IFF_BROADCAST
表示设备中的广播地址时有效的。以太网支持广播。
IFF_DEBUG
调试模式,表示设备调试打开。当想控制 printk 及其他一些基于调试目的的信息显示时,可利用这个标志位。虽然当前没有正式的驱动程序使用它,但它可以在程序中通过 ioctl 来设置从而使用它。
IFF_LOOPBACK
表示这是一个回送( loopback )设备,回送接口应该置该标志位。核心是通过检查此标志位来判断设备是否是回送设备的,而不是看设备的名字是否是 lo
IFF_POINTTOPOINT
表示这是一个点对点链接( SLIP and PPP ),点对点接口必须置该标志位。 Ifconfig 也可以置此标志位及清除它。若置上该标志位,则 dev->dstaddr 应也相应的置为链接对方的地址。
IFF_MASTER              /* master of a load balancer */
IFF_SLAVE          /* slave of a load balancer     */
此两个标志位在装入平等化中要用到。
IFF_NOARP
表示不支持 ARP 协议。通常的网络接口能传输 ARP 包,如果想让接口不执行 ARP ,可置上该标志位。如点对点接口不需要运行 ARP
IFF_PROMISC
全局接受模式。在该模式下,设备将接受所有的包,而不关这些包是发给谁的。在缺省情况下,以太网接口会使用硬件过滤,以保证只接受广播包及发给本网络接口的包。 Sniff 的原理就是通过设置网络接口为全局接受模式,接受所有到达本接口媒介的包,来“偷听”本子网的“秘密”。
IFF_MULTICAST
能接收多点传送的 IP 包,具有多点传输的能力。 ether_setup 缺省是置该标志位的,故若不想支持多点传送,必须在初始化时清除该标志位。
IFF_ALLMULTI
接收所有多点传送的 IP 包。
IFF_NOTRAILERS       /* 无网络 TRAILER*/
IFF_RUNNING            /* 资源被分配 */
 
  unsigned short           family;    /* address family ID (AF_INET)  */
该域段标识本设备支持的协议地址簇。大部分为 AF_INET (英特网 IP 协议),接口通常不需要用这个域段或赋值给它。
 
  unsigned short           metric;    /* routing metric (not used)   */
  unsigned short           mtu;
不包括数据链路层帧首帧尾的最大传输单位( Maximum Transfer Unit )。网络层在包传输时要用到。对以太网而言,该域段为 1500 ,不包括 MAC 帧的帧首和帧尾( MAC 帧格式稍后所示)。
 
  unsigned short           type;              /* interface hardware type     */
接口的硬件类型,描述了与该网络接口绑在一起的媒介类型。 Linux 网络设备支持许多不同种类的媒介,如以太网, X.25 ,令牌环, SLIP PPP Apple Localtalk 等。 ARP 在判定接口支持哪种类型的物理地址时要用到该域段。若是以太网接口,则在 ether_setup 中将之设为 ARPHRD_ETHER Ethernet 10Mbps )。
 
  unsigned short           hard_header_len;    /* hardware hdr length   */
在被传送的包中 IP 头之前的字节数。对于以太网接口,该域段为 14 ETH_HLEN include\linux\if_ether.h ),这个值可由 MAC 帧的格式得出:
MAC 帧格式:
目的地址( 6 字节) + 源地址( 6 字节) + 数据长度( 2 字节) + 数据( 46~~1500 +FCS
 
  void                         *priv;      /* pointer to private data       */
该指针指向私有数据,通常该数据结构中包括 struct enet_statistics 。类似于 struct file private_data 指针,但 priv 指针是在设备初始化时被分配内存空间的(而不是在设备打开时),因为该指针指向的内容包括设备接口的统计数据,而这些数据即使在接口卸下( down )时也应可以得到的,如用户通过 ifconfig 查看。
 
  unsigned char            pad;                      /* make dev_addr aligned to 8 bytes */
  unsigned char            broadcast[MAX_ADDR_LEN];    /* hw bcast add */
广播地址由六个 0xff 构成,即表示 255.255.255.255
memset(dev->broadcast,0xFF, ETH_ALEN); drivers/net/net_init.c
 
  unsigned char            dev_addr[MAX_ADDR_LEN];     /* hw address  */
设备的物理地址。当包传送给驱动程序传输时,要用物理地址来产生正确的帧首。
 
  unsigned char            addr_len;        /* hardware address length    */
物理地址的长度。以太网网卡的物理地址为 6 字节( ETH_ALEN )。
 
  unsigned long            pa_addr;         /* protocol address        */
  unsigned long            pa_brdaddr;    /* protocol broadcast addr     */
  unsigned long            pa_mask;        /* protocol netmask       */
该三个域段分别描述接口的协议地址、协议广播地址和协议的网络掩码。若 dev->family AF_INET ,则它们即为 IP 地址。这些域段可用 ifconfig 赋值。
 
  unsigned short           pa_alen;         /* protocol address length      */
协议地址的长度。 AF_INET 的为 4
 
  unsigned long            pa_dstaddr;     /* protocol P-P other side addr      */
点对点协议接口(如 SLIP PPP )用这个域记录连接另一边的 IP 值。
 
  struct dev_mc_list      *mc_list;        /* Multicast mac addresses     */
  int                            mc_count;      /* Number of installed mcasts       */
  struct ip_mc_list        *ip_mc_list;    /* IP multicast filter chain    */
这三个域段用于处理多点传输。其中 mc_count 表示 mc_list 中的项目数。
 
  __u32                       tx_queue_len; /* Max frames per queue allowed */
一个设备的传输队列能容纳的最大的帧数。对以太网,缺省为 100 ;而 plip 则为节省系统资源,仅设为 10
   
  /* For load balancing driver pair support */
  unsigned long            pkt_queue;      /* Packets queued */
  struct device              *slave;           /* Slave device */
  struct net_alias_info   *alias_info;     /* main dev alias info */
  struct net_alias           *my_alias;      /* alias devs */
 
  struct sk_buff_head    buffs[DEV_NUMBUFFS];
指向网络接口缓冲区的指针。

2. 结构中指向函数的指针,用来作为服务操作

网络接口操作可以分为两部分,一部分为基本操作,即每个网络接口都必须有的操作;另一部分是可选操作。
 
/* 基本操作 */
  int       (*init) (struct device *dev);  /* Called only once. */
初始化函数的指针,仅被调用一次。当登记一个设备时,核心一般会让驱动程序初始化该设备。初始化函数功能包括以下内容:检测设备是否存在;自动检测该设备的 I/O 端口和中断号;填写该设备 device 结构的大部分域段;用 kmalloc 分配所需的内存空间等。 若初始化失败,该设备的 device 结构就不会被链接到全局的网络设备表上。在系统启动时,每个驱动程序都试图登记自己,当只有那些实际存在的设备才会登记成功。这与用主设备号及次设备号索引的字符设备和块设备不同。
 
  int       (*open) (struct device *dev);
打开网络接口。每当接口被 ifconfig 激活时,网络接口都要被打开。 Open 操作做以下工作:登记一些需要的系统资源,如 IRQ DMA I/O 端口等;打开硬件;将 module 使用计数器加一。
 
  int       (*stop) (struct device *dev);
停止网络接口。操作内容与 open 相逆。
 
  int       (*hard_start_xmit) (struct sk_buff *skb,  struct device *dev);
硬件开始传输。这个操作请求对一个包的传输,这个包原保存在一个 socket 缓冲区结构中( sk_buff )。
 
  int       (*hard_header) (struct sk_buff *skb,  struct device *dev,  unsigned short type,
  void *daddr,        void *saddr,  unsigned len);
这个函数可根据先前得到的源物理地址和目的物理地址建立硬件头( hardware header )。以太网接口的缺省函数是 eth_header
 
  int       (*rebuild_header)(void *eth, struct device *dev,  unsigned long raddr, struct sk_buff *skb);
在一个包被发送之前重建硬件头。对于以太网设备,若有未知的信息,缺省函数将使用 ARP 填写。
 
  struct enet_statistics*         (*get_stats)(struct device *dev);
当一个应用程序需要知道网络接口的一些统计数据时,可调用该函数,如 ifconfig netstat 等。
 
/* 可选操作 */
  void    (*set_multicast_list)(struct device *dev);
设置多点传输的地址链表( *mc_list )。
 
  int       (*set_mac_address)(struct device *dev, void *addr);
改变硬件的物理地址。如果网络接口支持改变它的硬件物理地址,就可用这个操作。许多硬件不支持该功能。
 
  int       (*do_ioctl)(struct device *dev, struct ifreq *ifr, int cmd);
执行依赖接口的 ioctl 命令。
 
  int       (*set_config)(struct device *dev, struct ifmap *map);
改变接口配置。设备的 I/O 地址和中断号可以通过该函数进行实时修改。
 
  void    (*header_cache_bind)(struct hh_cache **hhp,  struct device *dev, 
  unsigned short htype,  __u32 daddr);
  void    (*header_cache_update)(struct hh_cache *hh, struct device *dev, unsigned char * haddr);
 
  int       (*change_mtu) (struct device *dev, int new_mtu);
这个函数负责使接口 MTU 改变后生效。如果当 MTU 改变时驱动程序要作一些特殊的事情,就应该写这个函数。
 
  struct iw_statistics*    (*get_wireless_stats) (struct device *dev);
};
 

三. 网卡的初始化

网络设备的初始化主要工作是检测设备的存在、初始化设备的 device 结构及在系统中登记该设备。 系统内核中存在着一张网络接口管理表 dev_base ,但与 dev_base 是指向 device 结构的,因为网络设备是通过 device 数据结构来表示的。
dev_base 实际上是一条 device 结构链表的表头,在系统初始化完成以后,系统检测到的网络设备将自动地保存在这张链表中,其中每一个链表单元表示一个存在的物理网络设备。登记成功的网络设备必定可在 dev_base 链表中找到。
 
网络设备的初始化从触发角度看可分为两类:
一类是由 shell 命令 insmod 触发的模块化驱动程序( module ),只有模块化的网络设备驱动程序才能用这种方式对设备进行初始化,称为“模块初始化模式”;
另一类是系统驱动时由核心自动检测网络设备并进行初始化,我们称为“启动初始化模式”。

1. “模块初始化模式”的分析

insmod 命令将调用相应模块的 init_module (),装载模块。 init_module 函数在初始化 dev->init 函数指针后,将调用 register_netdev ()在系统登记该设备。
若登记成功,则模块装载成功,否则返回出错信息。
register_netdev 首先检查设备名是否已确定,若没赋值则给它一个缺省的值 ethN N 为最小的可用以太网设备号
然后,网络设备自己的 init_function ,即刚在 init_module 中赋值的 dev->init ,将被调用,用来实现对网络接口的实际的初始化工作。
若初始化成功,则将该网络接口加到网络设备管理表 dev_base 的尾部。
 
NE2000 网卡为例。 NE2000 网卡的主要驱动程序在文件 drivers/net/ne.c 中。
 
 

init_module

init_module--- 模块初始化函数,当装载模块时,核心将自动调用该函数。在次此函数中一般处理以下内容:
1. 处理用户可能传入的参数 name ports irq 的值。若有,则赋给相应的接口
 
2. dev->init 函数指针进行赋值,对于任何网络设备这一步必不可少!因为在 register_netdev 中要用到该函数指针;
3. 调用 register_netdev ,完成检测、初始化及设备登记等工作。
 
/* from  drivers/net/ne.c */
init_module (void)
{
int this_dev, found = 0;
 
/* 对所有可能存在的以太网接口进行检测并试图去登记, MAX_NE_CARDS 4
 * 即最多可以使用 4 NE2000 兼容网卡。 */
for (this_dev = 0; this_dev < MAX_NE_CARDS; this_dev++) {
        struct device *dev = &dev_ne[this_dev];
        /* 可能有用户传入的参数:指定的 name ports irq 的值 */
        dev->name = namelist+(NAMELEN*this_dev);
        dev->irq = irq[this_dev];
        dev->base_addr = io[this_dev];
        dev->init = ne_probe;           /* NE2000 的检测和初始化函数 */
        dev->mem_end = bad[this_dev];
        if (register_netdev(dev) == 0) { /* 试图登记该设备 */
               found++;
               continue;                      /* 设备登记成功,继续登记下一个设备 */
        }
        /* 第一次发生登记不成功事件 */
        if (found != 0)             /* 在这之前没有成功登记 NE2000 接口,返回 */
               return 0;
        /* 显示出错信息 */
        if (io[this_dev] != 0)
               printk(KERN_WARNING "ne.c: No NE*000 card found at i/o = %#x\n", io[this_dev]);
        else
               printk(KERN_NOTICE "ne.c: No PCI cards found. Use \"io=0xNNN\" value(s) for
 
…………
 

register_netdev

该函数实现对网络接口的登记功能。其实现步骤如下:
1. 首先检查设备名是否已确定,若没赋值则以以太网设备待之并给它一个缺省的值 ethN N 为最小的可用以太网设备号;
2. 然后,网络设备自己的 init_function ,即刚在 init_module 中赋值的 dev->init ,将被调用,用来实现对网络接口的实际的初始化工作
3. 若初始化成功,则将该网络接口加到网络设备管理表 dev_base 的尾部
 
/* from  drivers/net/net_init.c  */
int register_netdev(struct device *dev)
{
struct device *d = dev_base; /* 取得网络设备管理表的表头指针 */
…………
if (dev && dev->init) {
        /* 若设备名字没确定,则将之看作是以太网设备!! */
        if (dev->name &&
               ((dev->name[0] == '\0') || (dev->name[0] == ' '))) {
               /* 找到下一个最小的空闲可用以太网设备名字 */
               for (i = 0; i < MAX_ETH_CARDS; ++i)
                      if (ethdev_index[i] == NULL) {
                             sprintf(dev->name, "eth%d", i);
                             printk("loading device '%s'...\n", dev->name);
                             ethdev_index[i] = dev;
                             break;
                      }
        }
…………
/* 调用初始化函数进行设备的初始化 */
        if (dev->init(dev) != 0) {
…………
        /* 将设备加到网络设备管理表中,加在最后 */
        if (dev_base) {
               /* 找到链表尾部 */
               while (d->next)
                      d = d->next;
               d->next = dev;
        }
        else
               dev_base = dev;
        dev->next = NULL;
…………
 

init_function

函数原型: int init_function (struct device *dev);
当系统登记一个网络设备时,核心一般会请求该设备的驱动程序初始化自己。初始化函数功能包括以下内容:
1. 检测设备是否存在,一般和第二步一起作;
2. 自动检测该设备的 I/O 地址和中断号;
对于可以与其他共享中断号的设备,我们应尽量避免在初始化函数中登记 I/O 地址和中断号, I/O 地址和中断号的登记最好在设备被打开的时候,因为中断号有可能被其他设备所共享。若不准备和其他设备共享,则可在此调用 request_irq request_region 马上向系统登记。
3. 填写传入的该设备 device 结构的大部分域段;
对于以太网接口, device 结构中许多有关网络接口信息都是通过调用 ether_setup 函数( driver/net/net_init.c )统一来设置的,因为以太网卡有很好的共性。 对于非以太网接口,也有一些类似于 ether_setup 的函数,如 tr_setup (令牌网), fddi_setup 。若添加的网络设备都不属于这些类型,就需要自己填写 device 结构的各个分量。
4.kmalloc 需要的内存空间。
 
若初始化失败,该设备的 device 结构就不会被链接到全局的网络设备表上。在系统启动时,每个驱动程序都试图登记自己,当只有那些实际存在的设备才会登记成功。这与用主设备号及次设备号索引的字符设备和块设备不同。
 
在胶片的代码里物理设备 NE2000 网卡的初始化函数是由 ne_probe ne_probe1 ethdev_init 共同实现
 
/* from  drivers/net/ne.c */
int ne_probe(struct device *dev)
{
   …………
   int base_addr = dev ? dev->base_addr : 0;
   /* I/O 地址 . User knows best. <cough> */
   if (base_addr > 0x1ff)             /* I/O 地址有指定值 */
      return ne_probe1(dev, base_addr);  /* 这个函数在下面分析 */
   else if (base_addr != 0)     /* 不自动检测 I/O */
      return ENXIO;
   …………
   /* base_addr=0 ,自动检测,若有第二块 ISA 网卡则是一个冒险! J
   * 对所有 NE2000 可能的 I/O 地址都进行检测,可能的 I/O 地址在存在
   * netcard_portlist 数组中:
   * static unsigned int netcard_portlist[]={ 0x300, 0x280, 0x320, 0x340, 0x360, 0};
   */
   for (i = 0; netcard_portlist[i]; i++) {
      int ioaddr = netcard_portlist[i];
      if (check_region(ioaddr, NE_IO_EXTENT))
         continue;
      /* 检测到一个 I/O 端口地址 */
      if (ne_probe1(dev, ioaddr) == 0)
         return 0;
…………
 
/* from  drivers/net/ne.c */
static int ne_probe1(struct device *dev, int ioaddr)
{
   …………
   /* 检测、确认 I/O 地址;初始化 8390 */
   …………
   /* 自动检测中断号,非常巧妙!! */
   if (dev->irq < 2) {
      autoirq_setup(0); /* 自动检测准备 */
      outb_p(0x50, ioaddr + EN0_IMR);           /* 中断使能 */
      outb_p(0x00, ioaddr + EN0_RCNTLO);
      outb_p(0x00, ioaddr + EN0_RCNTHI);
      outb_p(E8390_RREAD+E8390_START, ioaddr); /* 触发中断 */
      outb_p(0x00, ioaddr + EN0_IMR);          /* 屏蔽中断 */
      dev->irq = autoirq_report(0);     /* 获得刚才产生的中断号 */
      …………
    …………
  /* 登记中断号,中断服务程序为 ei_interrupt
   * 因为 ISA 网卡不能和其他设备共享中断。 */
   int irqval = request_irq(dev->irq, ei_interrupt,
pci_irq_line ? SA_SHIRQ : 0, name, dev);
   if (irqval) {
      printk (" unable to get IRQ %d (irqval=%d).\n", dev->irq, irqval);
      return EAGAIN;
   }
   dev->base_addr = ioaddr;         /* 设置 I/O 地址――已经过确认 */
 
   /* 调用 ethdev_init 初始化 dev 结构 */
   if (ethdev_init(dev)) { /* 该函数下面将分析 */
      printk (" unable to get memory for dev->priv.\n");
      free_irq(dev->irq, NULL); /* 初始化不成功,释放登记的中断号! */
      return -ENOMEM;
   }
   /* 向系统登记 I/O 地址 */
   request_region(ioaddr, NE_IO_EXTENT, name);
   /* 将硬件的物理地址赋给 dev->dev_add */
   for(i = 0; i < ETHER_ADDR_LEN; i++) {
      printk(" %2.2x", SA_prom[i]);
      dev->dev_addr[i] = SA_prom[i];
   }
   printk("\n%s: %s found at %#x, using IRQ %d.\n",
                    dev->name, name, ioaddr, dev->irq);
   …………
   /* dev 结构登记设备打开和关闭函数 */
   dev->open = &ne_open;
   dev->stop = &ne_close;
…………
 
/* from  drivers/net/8390.c  */
int ethdev_init(struct device *dev)
{
   …………
   if (dev->priv == NULL) {
      struct ei_device *ei_local;
      /* 申请私有数据结构空间,用于记录设备的状态等 */
      dev->priv = kmalloc(sizeof(struct ei_device), GFP_KERNEL);
    …………
   dev->hard_start_xmit = &ei_start_xmit;
   dev->get_stats = get_stats;
   dev->set_multicast_list = &set_multicast_list;
   ether_setup(dev);
…………

ether_setup

ether_setup 是一个通用于以太网接口的网络接口设置函数。所有的以太网卡的 device 结构中许多有关的网络接口信息都是通过调 ether_setup 函数统一来 设置。
 
/* from  drivers/net/net_init.c */
void ether_setup(struct device *dev)
{
   int i;
   /* 初始化缓冲队列链表,这是一个双向链表 */
   for (i = 0; i < DEV_NUMBUFFS; i++)   /* DEV_NUMBUFFS=3 */
      skb_queue_head_init(&dev->buffs[i]);
   …………
   /* 一些处理函数的初始化,驱动程序可以不写这些函数了 */
   dev->change_mtu= eth_change_mtu;
   dev->hard_header= eth_header;
   dev->rebuild_header = eth_rebuild_header;
   dev->set_mac_address = eth_mac_addr;
   dev->header_cache_bind = eth_header_cache_bind;
   dev->header_cache_update= eth_header_cache_update;
 
   dev->type                 = ARPHRD_ETHER; /* Ethernet 10Mbps */
   dev->hard_header_len= ETH_HLEN;      /* MAC 层协议头的大小 14 */
   dev->mtu                 = 1500;                /* 最大传输单位   */
   dev->addr_len          = ETH_ALEN;      /* 协议地址长度 4 */
   dev->tx_queue_len    = 100;                   /* 传输队列的长度 */
   memset(dev->broadcast,0xFF, ETH_ALEN);/* 物理地址长度 6 */
 
                                                /* 广播地址有效及支持多点传输 */
   dev->flags         = IFF_BROADCAST|IFF_MULTICAST;
   dev->family      = AF_INET; /* 英特网 IP 协议簇 */
   dev->pa_addr    = 0;               /* 以后用 ifconfig 命令设置 */
   dev->pa_brdaddr= 0;               /* 以后用 ifconfig 命令设置 */
   dev->pa_mask   = 0;               /* 以后用 ifconfig 命令设置 */
   dev->pa_alen     = 4;               /* 协议地址长度 4  */
}
 

2.  “启动初始化模式”的分析

初始化策略

“启动初始化模式”与“模块初始化模式”不同,前者要对所有内核支持的网络设备进行检测和初始化,而后者仅需检测和初始化被装载模块的网络设备。
为了实现在启动时对所有可能存在的设备进行初始化,系统在启动之前将所有内核支持的网络设备的名字及相应的初始化函数都挂在网络设备管理表( dev_base )上。
启动后, net_dev_int ()将依次对网络设备管理表 dev_base 中的每个设备,调用该设备本身的 init_function 进行初始化。
init_function 失败,即该设备不存在或 I/O IRQ 不能获得,则将该设备从 dev_base 去掉。
最后网络设备管理表中剩下的网络接口都是存在的,而且是被初始化过的。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/* 网络设备管理表的初始化 */
/* from  drivers/net/Space.c */
…………
static struct device eth7_dev = {
    "eth7", 0,0,0,0,0xffe0 /* I/O base*/, 0,0,0,0, NEXT_DEV, ethif_probe };
static struct device eth6_dev = {
    "eth6", 0,0,0,0,0xffe0 /* I/O base*/, 0,0,0,0, &eth7_dev, ethif_probe };
…………
static struct device eth0_dev = {
    "eth0", 0, 0, 0, 0, ETH0_ADDR, ETH0_IRQ, 0, 0, 0, &eth1_dev, ethif_probe };
/* 在八个 eth 接口中,只有 eth0 I/O 设为 0 ,让其进行自动检测,其他
* eth 接口的 I/O 都设为 0xffe0 ,不进行检测。 Linux 缺省的内核在启动时
* 只能自动检测到一块 eth 网卡,就是这个原因 */
 
#   undef NEXT_DEV
#   define NEXT_DEV       (&eth0_dev)
#if defined(PLIP) || defined(CONFIG_PLIP)
extern int plip_init(struct device *);
static struct device plip2_dev = {
           "plip2", 0, 0, 0, 0, 0x278, 2, 0, 0, 0, NEXT_DEV, plip_init, };
…………
static struct device plip0_dev = {
    "plip0", 0, 0, 0, 0, 0x3BC, 5, 0, 0, 0, &plip1_dev, plip_init, };
…………
extern int loopback_init(struct device *dev);
struct device loopback_dev = {
    "lo", 0x0, 0x0, 0x0, 0x0, 0, 0, 0, 0, 0, NEXT_DEV, loopback_init };
struct device dev_base = &loopback_dev; /* 关键 */
 

函数调用关系

系统转入核心后, start_kernel 将会创建一个 init 进程,该 init 进程则会通过系统调用 sys_steup 进行所有尚未初始化的设备。 D
evice_setup 不仅要初始化内核支持的字符设备、块设备,也调用 net_dev_init 初始化所有内核支持的且实际存在的网络设备。
net_dev_init 会对每个内核支持的网络设备调用该设备的 init_functions 进行具体的物理设备的初始化工作。整个函数调用关系图如下:
 
 

具体流程

LINUX 启动时,完成了实模式下的系统初始化( arch/i386/boot/setup.S )与保护模式下的核心初始化包括初始化寄存器和数据区 (arch/i386/boot/compressed/head.S) 、核心代码解压缩、页表初始化( arch/i386/kernel/head.S )、初始化 idt gdt ldt 等工作后,系统转入了核心。调用函数 start_kernel 启动核心( init/main.c )后,将继续各方面的初始化工作,其中与网络子系统有关的部分为:调用 sock_init() net/socket.c )初始化网络模块
sock_init ()函数主要做以下动作:
    初始状态设为不支持任何网络协议:
static struct proto_ops *pops[NPROTO];   // #define NPROTO 16
……
for (i = 0; i < NPROTO; ++i) pops[i] = NULL;
    调用 init_netlink() 登记一个网络字符设备(即把 netlink 看作一种字符设备),主设备号为 NETLINK_MAJOR(36) net/netlink.c
register_chrdev(NETLINK_MAJOR,"netlink", &netlink_fops);
for(ct=0;ct<MAX_LINKS;ct++){
          skb_queue_head_init(&skb_queue_rd[ct]);
          netlink_handler[ct]=netlink_err;
}
其中 static int (*netlink_handler[MAX_LINKS])(struct sk_buff *skb) MAX_LINKS 为次设备数,定义为 11
    调用 netlink_attach() ,在 netlink_handler 登记路由设备的回调函数:( net/netlink.c
netlink_attach(NETLINK_ROUTE, netlink_donothing);
其中 netlink_attach 为: {
active_map|=(1<<unit);
netlink_handler[unit]=function;}
    调用 fwchain_init() ,初始化防火墙,设为 FW_ACCEPT
    调用 proto_init() ,执行核心支持的各种网络协议的初始化函数:
void proto_init(void){
/* 核心支持的网络协议在 net/protocols.c 中定义 */
extern struct net_proto protocols[];
struct net_proto *pro;
pro = protocols;
while (pro->name != NULL) {
(*pro->init_func)(pro);
pro++;
}
}
    调用 export_net_symbols() ,向系统登记发布 network  symbols ,以供系统的模块( modules )使用,因为 module 中的函数所调用的外部函数只能是已分布登记( export )的函数:
/* from  net/netsyms.c   */
void export_net_symbols(void)
{
/* 该函数为宏,定义在( include/linux/module.h */
register_symtab(&net_syms);
}
其中 net_syms 静态结构变量初始化为:( net/netsyms.c
static struct symbol_table net_syms = {
#include <linux/symtab_begin.h>
 
/* Socket layer registration */
X(sock_register),
X(sock_unregister),
X(sock_alloc),
X(sock_release),
…………
…………
#include <linux/symtab_end.h>
};
网络协议的初始化竟是在网络设备的初始化之前!
start_kernel 最后将调用 kernel_thread (init, NULL, 0) 创建init进程进行系统配置(其中包括所有设备的初始化工作)。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/*  from  init/main.c  */
static int init(void * unused)
{
…………
/* 创建后台进程bdflush,以不断循环写出文件系统缓冲区中“脏”的内容 */
kernel_thread (bdflush, NULL, 0);
/* 创建后台进程kswapd,专门处理页面换出工作  */
kswapd_setup();
kernel_thread(kswapd, NULL, 0);
…………
/ * 
 *   setup() 函数即系统调用sys_setup()
 *  关于这一点,作者已作过以下试验:在 setup() 调用以前和调用之后,
 *  以及在 sys_setup() 函数内部的开始和结束加入 printk 语句(以使系统
 *  启动时能输出信息)后,重新编译内核。发现用新的内核后,通过
 *  启动时的信息显示,发现 setup() 函数的确就是系统调用 sys_setup()
 * 
 */
setup() ;
…………
}
 
sys_setup 函数 调用 device_setup 对所有的设备进行初始化工作。
/* from  fs/filesystems.c  */
asmlinkage int sys_setup(void)
{
static int callable = 1;
if (!callable)
return -1;
callable = 0;          /* 通过静态变量,限制该函数最多只能被调用一次 */
 
device_setup();      /* 调用 device_setup() ,初始化所有设备 */
…………
}
device_setup 中将对字符设备、块设备、网络设备、 SCSI 设备等进行初始化
/* from  drivers/block/genhd.c  */
void device_setup(void) {
…………
chr_dev_init();              /* 字符设备的初始化 drivers/cha/mem.c       */
blk_dev_init();              /* 块设备的初始化   drivers/block/ll_rw_blk.c */
sti();
#ifdef CONFIG_SCSI
scsi_dev_init();      /* SCSI 设备的初始化 drivers/scsi/scsi.c */
#endif
#ifdef CONFIG_INET
net_dev_init();              /* 网络设备的初始化 net/core/dev.c    */
#endif
…………
}
 
/* from  net/core/dev.c   */
int net_dev_init(void) {
…………
/* 初始化数据包接收队列 */
skb_queue_head_init(&backlog);
/* 网桥必须在其他设备之前初始化 */
#ifdef CONFIG_BRIDGE    
br_init();
#endif
…………
/* 以下进行网络设备初始化检测,如果 dev->init 初始化失败(大多数是
 * 因为设备不存在),则从网络设备链 dev_base 上除去该设备。 */
 
/* dev_base drivers/net/Space.c 中的一个指向 device 结构的静态指针变量,
* 并已初始化为核心支持的所有网络设备的链表,其中包括每个设备的 probe
* 函数指针。 */
dp = &dev_base;
while ((dev = *dp) != NULL)
{
int i;
for (i = 0; i < DEV_NUMBUFFS; i++)  { /* #define DEV_NUMBUFFS 3 */
skb_queue_head_init(dev->buffs + i);
}
if (dev->init && dev->init(dev))
/* 初始化失败,将该设备从链表中删除,并准备初始化下一个 */
*dp = dev->next;
else
dp = &dev->next;  /* 成功,准备初始化下一个 */
}
…………
 

四. 网络设备的打开和关闭

使用网络设备,激活(打开)初始化好了的接口。打开和关闭一个接口是由 shell 命令 ifconfig 调用的,而 ifconfig 则要调用一个通用的设备打开函数 dev_open net/core/dev.c ),相应地还有一个 dev_close 函数。这两个函数提供了独立于设备的操作接口的打开和关闭的功能。
需要调用网络接口 dev open stop 函数,同时它们还需置上 dev->flags IFF_UP 标志。
 
/* from net/core/dev.c */
int dev_open(struct device *dev)
{
       int ret = -ENODEV;
       if (dev->open)
              ret = dev->open(dev);   /* 调用接口的 open 函数 */
       if (ret == 0)                        /* 接口打开成功 */
       {
              dev->flags |= (IFF_UP | IFF_RUNNING); /* 置标志位 */
              /* 初始化有关多点传输的一些状态 */
              dev_mc_upload(dev);
              notifier_call_chain(&netdev_chain, NETDEV_UP, dev);
       }
       return(ret);
}
 
对于网络接口自己的 dev->open 函数一般包括以下几方面内容:
1. 若没有在初始化函数中登记中断号和 I/O 地址,则在设备打开时要进行登记分别用 request_irq request_region 这两个函数进行登记。
2. 若要分配 DMA 通道,则用 request_dma 进行分配登记;
3. 将该设备挂到 irq2dev_map 中。若是使用基于中断的接收数据方式,以后就可以通过中断号直接索引到相应的设备了;
4. 初始化物理设备的寄存器的状态;
5. 设置接口相应 dev 的私有数据结构 (dev->priv) 中的一些域段;
6. 设置 dev 中的 tbusy interrupt start 等域段;
7. 在返回之前嵌入宏 MOD_INC_USE_COUNT
dev->stop 函数则相反,如第六步要改为 MOD_DEC_USE_COUNT
我们看 NE2000 NE2000 dev->open dev->stop 分别对应 ne_open ne_close 。由于 NE2000 驱动程序是在初始化就登记 IRQ I/O 地址的,在这里就不需要登记了。
/* from  drivers/net/ne.c */
static int ne_open(struct device *dev)
{
ei_open(dev);               /* 下面将分析 */
MOD_INC_USE_COUNT;   /* 对应于第 7 项内容 */
return 0;
}
static int ne_close(struct device *dev)
{
if (ei_debug > 1)
printk("%s: Shutting down ethercard.\n", dev->name);
ei_close(dev);        /* 下面将分析 */
MOD_DEC_USE_COUNT;  /* 对应于第 7 项内容 */
return 0;
}
 
/* from  drivers/net/8390.c  */
int ei_open(struct device *dev)
{
struct ei_device *ei_local = (struct ei_device *) dev->priv;
if (ei_local == NULL){ /* 只有没调用 ethdev_init() ,才会出现以下的错误 */
printk(KERN_EMERG "%s: ei_open passed a non-existent device!\n", dev->name);
return -ENXIO;
}
irq2dev_map[dev->irq] = dev;      /* 对应于上面所列内容的第 3 */
NS8390_init (dev, 1);                  /* 下面将分析 */
dev->start = 1;                                   /* 对应于第 6 项,表示接口 UP */
ei_local->irqlock = 0;                  /* 对应于第 5 */
return 0;
}
int ei_close(struct device *dev)
{
NS8390_init(dev, 0);
dev->start = 0;                            /* 对应于第 6 项内容,表示接口 DOWN */
return 0;
}
 
void NS8390_init(struct device *dev, int startp)
{
…………
/* 设置 8390 的各种寄存器的状态 */
…………
dev->tbusy = 0;
dev->interrupt = 0;                      /* 对应于第 6 项内容 */
ei_local->tx1 = ei_local->tx2 = 0;
ei_local->txing = 0;                            /* 对应于第 5 项内容 */
…………
}
 
另外,文件 net/core/dev.c 还提供独立于具体网络设备的操作函数,如
dev_ifsioc (void *arg, unsigned int getset)
它可以处理许多 ioctl SIGNAL :读取和修改接口网络地址(对 TCP/IP 就是 IP 地址)、读取和修改接口的 dev->flags 、读取和设置 MTU 、读取和设置广播地址等等。
 

五. 数据包的传输和接收

当物理网络设备接收到数据时,可通过两种途径解决这个问题。
一种方法是轮询方式,系统每隔一定的时间间隔就去检查一次物理设备,若设备“报告”说有数据到达,就调用读取数据的程序。在 Linux 中,轮询方式可通过定时器实现,但该方法存在一个明显的缺点:不管设备是否有数据,系统总是要固定地花 CPU 时间去查看设备,且可能延迟对一些紧急数据的处理,因为网络设备有数据时可能不能马上得到 CPU 的响应。在这种方式下,设备完全处于一种被动的状态,而 CPU 又负担过重。无论从资源的利用率上还是从效率上看,这种方法都不是最优的。
另一种方法是中断方式,中断方式利用硬件体系结构的中断机制实现设备和系统的应答对话,即当物理设备需要 CPU 处理数据时,设备就发一个中断信号给系统,系统则在收到信号后调用相应的中断服务程序响应对设备中断的处理。中断方式有效地解决了设备与 CPU 的对话交流问题,并将 CPU 从繁重的设备轮询中解脱出来,大大提高了 CPU 的利用率。当前不管是 Linux 平台还是 Windows 平台,它们的网络设备驱动程序几乎都是使用中断方式的。
网络分层有一个问题是,每层的协议在发送数据包时要加协议头和协议尾到原数据中,在收到数据包时则要将本层的协议头和协议尾从数据包中去掉。这使得在不同层协议间传输时,每层都需要知道自己这一层的协议头和协议尾在数据包的哪里。
一种解决方法是在每层都复制缓冲区,但显然效率太低。
Linux 的做法是用一种数据结构 sk_buf f 在不同协议层及网络设备驱动程序之间传送数据。 sk_buff 包括指针和长度域段,允许每个协议层通过标准的函数操作传送的数据包。
首先来分析 sk_buff 这个数据结构。

1. Socket缓冲区及相关操作

网络设备发送与接收数据包用的缓冲区是一个统一的数据结构 sk_buff include/linux/skbuff.h )。对该数据结构,核心提供了一系列低层的操作函数,从而使该数据结构具有网络协议传输需要的通常的缓冲功能和流控制能力,并可方便、灵活地处理数据包首尾的增加和删除。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sk_buff 结构的一个示意图,其中每个 sk_buff 都带有一块数据区,并有四个数据指针指向相应的位置:
unsigned char  *head;
指向被分配的内存空间的首地址;
unsigned char  *data;
指向当前数据包的首地址;
unsigned char  *tail;
指向当前数据包的末地址;
unsigned char  *end;
指向被分配的内存空间的末地址;
unsigned long  len;
当前数据包的大小。
len=skb->tail �C skb->data
unsigned long  truesize
分配到的内存空间大小。
len=skb->end �C skb->head
由于数据包的大小会随着自己在不同协议层间的传送而会不断地变化,故 data tail 指针也将会不断地改变,即依赖于 skb 当前所在的协议层; head end 指针则在内存空间分配后就固定不变。
对缓冲区的操作,核心提供了一个比较完整的函数界面,下面将列出用的最多的几个函数并作分析说明。
/* from  net/core/skbuff.c  */
struct sk_buff  *alloc_skb (unsigned int len, int priority);
struct sk_buff  *dev_alloc_skb (unsigned int len);
申请一个 sk_buff 缓冲区。 alloc_skb 函数分配一个缓冲区并将 skb->data skb->tail 初始化为 skb->head dev_alloc_skb 函数是 alloc_skb 函数的一个快捷方式,它用 priority= GFP_ATOMIC 调用 alloc_skb 并在 skb->data skb->head 之间保留 16 字节的空间。这 16 字节也用来填写硬件头( hardware header )。
void kfree_skb (struct sk_buff *skb, int rw);
void dev_kfree_skb (struct sk_buff *skb, int rw);
释放一个 sk_buff 缓冲区。 kfree_skb 供核心内部调用,驱动程序应该用 dev_kfree_skb ,因为它能正确处理缓冲区加锁。参数 rw 可用 FREE_READ FREE_WRITE 。用于发送的缓冲区应该用 FREE_WRITE ,用于接收的则用 FREE_READ
unsigned char  *skb_put (struct sk_buff *skb, int len);
当有数据要加到缓冲区的尾部时,用于增加 skb->tail skb->len 。返回值是修改之前 skb->tail 指针。
unsigned char  *skb_push (struct sk_buff *skb, int len);
当有数据要加到缓冲区的首部时,用于减少 skb->data 及增大 skb->len 。返回值是修改之后 skb->data
int skb_tailroom (struct sk_buff *skb);
该函数返回在 sk_buff 中可用于 put 的空间大小(尾部空余空间)。若缓冲区被正确分配到空间,驱动程序通常不需要检查缓冲区中空余空间的大小。由于驱动程序在申请空间之前可得到数据包的大小,故只有严重出错的驱动程序才会 put 太多的数据到缓冲区中。
int skb_headroom (struct sk_buff *skb);
类似于 skb_tailroom ,该函数返回可用的 push 的空间大小,即首部空余空间。
void skb_reserve (struct sk_buff *skb, int len);
该函数既增加 skb->data 又增加 skb->tail ,即在首部留出 len 大小的空间。在填充缓冲区之前,可用该函数保留一部分首部空间。许多以太网卡在首部保留 2 字节空间,这样在 14 字节的以太网头的后面, IP 头就能以 16 字节对齐了。
unsigned char  *skb_pull (struct sk_buff *skb, int len);
从数据包的头部剔除数据。它减少 skb->len 并增加 skb->data 。以太网的头就是这样从接收到的数据包中被剔除的。
void skb_trim(struct sk_buff *skb, int len)
从数据包的尾部剔除数据。它将 skb->len 设为 len ,并改变 skb->tail
 

2. 数据包的传输

用户要传输数据时,数据包是沿着网络协议由上往下逐层下传的。数据包将通过 dev_queue_xmit () [net/core/dev.c] 函数传送给网络接口。网络接口的任务就是将数据包传送给硬件,让物理网络设备完成最终的物理传输。从 device 结构中我们可以看到,每个网络接口都应有一个叫 dev->hard_start_xmit 的硬件传输函数指针,通过这个函数指针来完成实际的数据传输的。
硬件传输函数 hard_start_xmit 函数的一般流程如下:
 
1. 通过标志位 tbusy 判断上次数据包的传输是否完成。若 tbusy=0 就做下一步;否则,看上次传输是否已超时,若未超时,就不成功返回,若已超时,则初始化芯片寄存器、置 tbusy=0 ,然后继续下一步;
2. tbusy 标志位打开;
3. 将数据包传给硬件发送;
4. 释放缓冲区 skb
5. 修改接口的一些统计信息。
 
NE2000 的传输函数 dev->hard_start_xmit ei_start_xmit ()
 
/* from drivers/net/8390.c drivers/net/8390.h */
/* 传输函数 */
static int ei_start_xmit(struct sk_buff *skb, struct device *dev)
{
       int e8390_base = dev->base_addr;
       struct ei_device *ei_local = (struct ei_device *) dev->priv;
       int length, send_length, output_page;
      
       /* 若设备忙,就判断上次传输是否已超时 */
       if (dev->tbusy) {   
              /* 读取传输状态寄存器的值 */
              int txsr = inb(e8390_base+EN0_TSR), isr;   /* EN0_TSR 传输状态寄存器(读) */
              int tickssofar = jiffies - dev->trans_start;
             
              /* #define TX_TIMEOUT (20*HZ/100) */
              if (tickssofar < TX_TIMEOUT ||   (tickssofar < (TX_TIMEOUT+5) &&
! (txsr & ENTSR_PTX))) {
                     return 1;  /* 未超时,或超时一点点且发送时出错 */
              }
              /* 已超时 */
              …………
              /* 重新初始化芯片寄存器 */
              ei_reset_8390(dev);
              NS8390_init(dev, 1);
              /* 将开始传输域段置为的当前时间坐标点 */
              dev->trans_start = jiffies;
       }
       …………
       /* 屏蔽网络设备的硬件中断 */
       outb_p(0x00, e8390_base + EN0_IMR);      /* EN0_IMR 中断屏蔽寄存器( WR */
       if (dev->interrupt) {
              /* 正在运行中断服务程序 */
              printk("%s: Tx request while isr active.\n",dev->name);
/* 恢复中断屏蔽,失败返回; EN0_IMR 中断屏蔽寄存器( WR */
              outb_p(ENISR_ALL, e8390_base + EN0_IMR);
              return 1;
       }
       ei_local->irqlock = 1;
       send_length = ETH_ZLEN < length ? length : ETH_ZLEN;
       /* 硬件传输 */
       ei_block_output(dev, length, skb->data, ei_local->tx_start_page);
       ei_local->txing = 1;
       NS8390_trigger_send(dev, send_length, ei_local->tx_start_page);
       /* 设置开始传输时间坐标点,打开“忙”标志 */
       dev->trans_start = jiffies;
       dev->tbusy = 1;
       …………
       ei_local->irqlock = 0;
       /* 恢复中断屏蔽 */
       outb_p(ENISR_ALL, e8390_base + EN0_IMR);/* EN0_IMR 中断屏蔽寄存器( WR */
       /* 释放缓冲区 skb */   
       dev_kfree_skb (skb, FREE_WRITE);
       return 0;
}
 

3. 数据包的接收

由于使用了硬件中断请求机制,当物理网络设备接收到新数据时,它将发送一个硬件中断请求给系统。系统在侦察到有物理设备发出中断请求,就会调用相应的中断服务程序来处理中断请求。在这里,系统首先要知道哪个中断对应哪个中断服务程序。
为了让系统知道网络设备的中断服务程序,一般在网络设备初始化的时候或设备被打开时,要向系统登记中断号及相应的中断服务程序(用 request_irq 这个函数登记)
基于中断方式的设备驱动程序若在设备初始化和设备打开时都没向系统登记中断服务程序,则该设备肯定不能正常工作。
NE2000 的中断号登记是在设备初始化的时候,并登记中断服务程序为 ei_interrupt
一个网络接口的中断服务程序的工作步骤一般有以下几步:
 
1. 确定发生中断的具体网络接口,是 rq2dev_map[irq] 还是 (struct device*) dev_id
2. 打开标志位 dev->interrupt ,表示本服务程序正在被使用;
3. 读取中断状态寄存器,根据寄存器判断中断发生的原因。有两种可能:一种是有新数据包到达;另一种是上次的数据传输已完成。
4. 若是因为有新数据包到达,则调用接收数据包的子函数;
5. 若中断由上次传输引起,则通知协议的上一层、修改接口的统计信息、关闭标志位 tbusy 为下次传输做准备;
6. 关闭标志位 interrupt
 
当中断服务程序明确物理网络设备有数据包收到时,将调用数据接收子程序来完成实际的依赖于硬件的数据接收工作,并在接收完成后通过函数 netif_rx () [net/core/dev.c] 将收到的数据包往上层传。数据接收子程序的内容可以由以下四点来概括:
 
1. 申请 skb 缓冲区给新的数据包存储;
2. 从硬件中读取新到达的数据;
3. 调用 netif_rx (),将新的数据包往网络协议的上一层传送;
4. 修改接口的统计数据。
 
NE2000 中断服务程序 ei_interrupt (),同时我们还将发现 NE2000 的数据接收子程序是 ei_receive ()。
 
/* from drivers/net/8390.c drivers/net/8390.h */
/* 中断服务程序 */
void ei_interrupt(int irq, void *dev_id, struct pt_regs * regs)
{
       struct device *dev = (struct device *)(irq2dev_map[irq]);
       …………       
       e8390_base = dev->base_addr;
       ei_local = (struct ei_device *) dev->priv;
       if (dev->interrupt || ei_local->irqlock) {
              /* 有其他进程运行中断服务程序 */
              return;
       }
       /* 打开中断处理标志,阻止再次进入 */
       dev->interrupt = 1;
       …………       
       /* 读取中断状态寄存器( RD WR */
       while ((interrupts = inb_p(e8390_base + EN0_ISR)) != 0
              && ++nr_serviced < MAX_SERVICE) {
              …………
                     if (interrupts & ENISR_OVER) { /* ENISR_OVER 接收 overrun 标志 */
                     /* 接收超出物理设备的承受能力 */
                     ei_rx_overrun(dev); /* 恢复设备的正确状态 */
              } else if (interrupts & (ENISR_RX+ENISR_RX_ERR)) {
                     /* ENISR_RX 正确接收标志; ENISR_RX_ERR 接收有错标志 */
                     /* 接收到数据包,包括正确接收和接收出错 */
                     ei_receive(dev); /* 从物理设备的缓存中取出数据 */
              }
              if (interrupts & ENISR_TX) {
                     /* 正确发送了数据包, */
                     ei_tx_intr(dev); /* 为下一次传输做准备 */
              } else if (interrupts & ENISR_TX_ERR) {
                     /* 发送数据包的过程中出错 */
                     ei_tx_err(dev); /* 错误处理 */
              }
              …………
       }
      
       …………
       /* 关闭中断处理标志 */
       dev->interrupt = 0;
       return;
}
 
/* 数据接收子程序,被中断服务程序所调用 */
static void ei_receive(struct device *dev)
{
       …………
       /* 申请物理空间 --skb 缓冲区,为读取新的数据包作准备 */
       skb = dev_alloc_skb(pkt_len+2);
       …………
       /* 由于 MAC 头是 14 字节的,为了与 IP 头的 16 字节对齐规则 一致,
 * 特意保留了 2 字节 */
       skb_reserve(skb,2);
       skb->dev = dev;
       /* 腾出逻辑空间给新的数据包 */
       skb_put(skb, pkt_len);   /* Make room */
       /* 从硬件中读取新到达的数据 */
       ei_block_input(dev, pkt_len, skb, current_offset + sizeof(rx_frame));
       skb->protocol=eth_type_trans(skb,dev);
       /* 通过 netif_rx 函数,将收到的数据包往网络协议的上一层传送 */
       netif_rx(skb);
       /* 修改接口的统计数据。 */
       ei_local->stat.rx_packets++;
       …………
}
 

六. 网络设备驱动程序

网络设备(或网络接口)是通过一个数据结构 struct device 来表示的。在系统中,每一个实际存在的物理网络设备都对应于一个 device 结构。而所有这些 device 结构联成一张链表并由一个全局变量指针 dev_base 指向表头,从而使系统能够随时得到每个网络接口的信息。
一个最简单的网络设备驱动程序,应该包括:
 
1. 网络设备的检测及初始化函数,供核心启动初始化时调用
2. 该网络设备的初始化函数,供 register_netdev 调用(可以写成与第 1 项的共用,即用同一个);若是写成 module 兼容方式的,还需写该设备的 init_module cleanup_module 函数;
3. 提供该网络设备的打开和关闭操作。供设备被打开或被关闭时调用(一般用 shell 命令 ifconfig 调用);
4. 提供该网络设备的数据传输函数,负责向硬件发送数据包。当上层协议需要传输数据时,供 dev_queue_xmit 调用;
5. 提供该网络设备的中断服务程序,处理数据传输完毕的善后事宜和数据的接收。当物理网络设备有新数据到达或数据传输完毕时,将向系统发送硬件中断请求,该函数就是用来响应该中断请求的。
 

         }

本文出自 “aban3rd的酒壶” 博客,谢绝转载!

你可能感兴趣的:(linux,职场,网卡,驱动,休闲)