Linux网络设备分析
潘纲    9811536
浙江大学计算机系    pg@ccnt.zju.edu.cn

[摘要] 在本文中,首先概括了网络设备总体特征和工作原理,接着在分析了一个
重要的数据结构device后,重点剖析了网络设备的整个初始化工作过程;简单地分
析了设备的打开和关闭的操作后,是有关数据包的传输和接收的分析;在最后,本
文对写网络设备驱动程序做了一个总结。以上的每部分的分析,都是在NE2000以太
网卡的基础上进行的。在附录中是一个虚拟的字符设备驱动程序以及写这个程序的
体会,该程序已成功使用过,它是在网络设备分析之前本人做的一个小小的试验。


一. 网络设备概述
在LINUX中,为了简化对设备的管理,所有外围的硬件设备被归结为三类:字符设
备(如键盘、鼠标等)、块设备(如硬盘、光驱、软驱等)和网络设备(也称为网
络接口,network inferface),如以太网卡。在本文中,我们将等效使用"网络设
备"和"网络接口"这两个概念,而对某个具体的网络设备,我们将称之为"物理设备
"或"物理网络设备"。
为了屏蔽网络环境中物理网络设备的多样性,LINUX对所有的物理设备进行抽象并
定义了一个统一的概念,称之为接口(Interface)。所有对网络硬件的访问都是
通过接口进行的,接口提供了一个对所有类型的硬件一致化的操作集合来处理基本
数据的发送和接收。一个网络接口被看作是一个发送和接收数据包(packets)的
实体。对于每个网络接口,都用一个device的数据结构表示,有关该数据结构的具
体内容,将在本文的后面详细介绍。通常,网络设备是一个物理设备如以太网卡,
但软件也可以作为网络设备,如回送设备(loopback)。在内核启动时,通过网络
设备驱动程序,将登记存在的网络设备。设备用标准的支持网络的机制来转递收到
的数据到相应的网络层。所有被发送和接收的包都用数据结构sk_buff表示。这是
一个具有很好的灵活性的数据结构,可以很容易增加或删除网络协议数据包的首部

网络设备作为其中的三类设备之一,它有其非常特殊的地方。它与字符设备及块设
备都有很大的不同:

§  网络接口不存在于Linux的文件系统中,而是在核心中用一个device数据结构表
示的。每一个字符设备或块设备则在文件系统中都存在一个相应的特殊设备文件来
表示该设备,如/dev/hda1、/dev/sda1、/dev/tty1等。网络设备在做数据包发送
和接收时,直接通过接口访问,不需要进行文件的操作;而对字符设备和块设备的
访问都需通过文件操作界面。
§  网络接口是在系统初始化时实时生成的,对于核心支持的但不存在的物理网络
设备,将不可能有与之相对应的device结构。而对于字符设备和块设备,即使该物
理设备不存在,在/dev下也必定有相应的特殊文件与之相对应。且在系统初始化时
,核心将会对所有内核支持的字符设备和块设备进行登记,初始化该设备的文件操
作界面(struct file_operations),而不管该设备在物理上是否存在。

以上两点是网络设备与其他设备之间存在的最主要的不同。然而,它们之间又有一
些共同之处,如在系统中一个网络设备的角色和一个安装的块设备相似。一个块设
备在blk_dev数组及核心其他的数据结构中登记自己,然后根据请求,通过自己的
request_function函数"发送"和"接收"数据块。相似地,为了能与外面世界进行数
据交流,一个网络接口也必须在一个特殊的数据结构中登记自己。
在系统内核中,存在字符设备管理表chardevs和块设备管理表blkdevs,这两张保
存着指向file_operations结构的指针的设备管理表,分别用来描述各种字符驱动
程序和块设备驱动程序。类似地,在内核中也存在着一张网络接口管理表
dev_base,但与前两张表不同,dev_base是指向device结构的指针,因为网络设备
是通过device数据结构来表示的。dev_base实际上是一条device结构链表的表头,
在系统初始化完成以后,系统检测到的网络设备将自动地保存在这张链表中,其中
每一个链表单元表示一个存在的物理网络设备。当要发送数据时,网络子系统将根
据系统路由表选择相应的网络接口进行数据传输,而当接收到数据包时,通过驱动
程序登记的中断服务程序进行数据的接收处理(软件网络接口除外)。以下是网络
设备工作原理图:
 
图一   Linux网络设备工作原理图

每一个具体的网络接口都应该有一个名字,以在系统中能唯一标识一个网络接口。
通常一个名字仅表明该接口的类型。Linux对网络设备命名有以下约定:(其中N为
一个非负整数)
ethN        以太网接口,包括10Mbps和100Mbps;
trN     令牌环接口;
slN         SLIP网络接口;
pppN    PPP网络接口,包括同步和异步;
plipN   PLIP网络接口,其中N与打印端口号相同;
tunlN   IPIP压缩频道网络接口;
nrN         NetROM虚拟设备接口;
isdnN   ISDN网络接口;
dummyN      空设备;
lo      回送网络接口。

二. 重要数据结构--struct device
结构device存储一个网络接口的重要信息,是网络驱动程序的核心。在逻辑上,它
可以分割为两个部分:可见部分和隐藏部分。可见部分是由外部赋值;隐藏部分的
域段仅面向系统内部,它们可以随时被改变。下面我们将对之进行详细的分析和解
剖。

/*  from  include/linux/netdevice.h  */
struct device
{
1.  属性
  char              *name;
设备的名字。如果第一字符为NULL(即'\0'),register_netdev
(drivers/net/net_init.c)将会赋给它一个n最小的可用网络设备名ethn。

  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 (a la 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     /*资源被分配*/
此标志在Linux中没什么用,只是为了与BSD兼容。

  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函数统
一来设置。那么让我们看看它到底会缺省设哪些域段及设为什么值。若你满意这些
缺省设置,那么在写驱动程序时只要调用一下这个函数就可以将这些域段的设置工
作"置之不理了"J,否则,也可在调用该函数之后再改过。

/* 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去掉。这样,最后网络设备管理表中剩下的
网络接口都是存在的,显然也已是被初始化过的。我们看一下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进行所有尚未初始化的设备(有一些设备如内存、PCI等系统已先于
此进行了初始化)。device_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>
};
至此,系统完成了一些有关网络模块的初始化工作。但请注意,目前系统还没涉及
任何有关网络设备的初始化内容(别急 J)。有些特别:网络协议的初始化竟是在
网络设备的初始化之前!!由于本文主要着重于网络设备的分析,故对整个网络子
系统的组成和结构,及网络上层协议(如TCP/IP、SOCKET等)和网络设备之间的缝
合未作深入的代码级分析,望能有后来者弥补这方面的工作(?)。题外话了,言
归正传。
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,那么让我们来看一下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标志。下面是
dev_open的源代码,dev_close函数与以下的类似,但它另外还要释放接口所有的
sk_buff空间。

/* 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;
}
由上容易看到,ne_close几乎就是ne_open在镜中的像。

/* 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、读取和设置广播地址等等。
ifconfig的功能大部分是通过该文件提供的函数实现的。

五. 数据包的传输和接收
当物理网络设备接收到数据时,系统是如何知道并读取数据的呢?当前可通过两种
途径解决这个问题。一种方法是轮询方式,系统每隔一定的时间间隔就去检查一次
物理设备,若设备"报告"说有数据到达,就调用读取数据的程序。在Linux中,轮
询方式可通过定时器实现,但该方法存在一个明显的缺点:不管设备是否有数据,
系统总是要固定地花CPU时间去查看设备,且可能延迟对一些紧急数据的处理,因
为网络设备有数据时可能不能马上得到CPU的响应。在这种方式下,设备完全处于
一种被动的状态,而CPU又负担过重。无论从资源的利用率上还是从效率上看,这
种方法都不是最优的。另一种方法是中断方式,中断方式利用硬件体系结构的中断
机制实现设备和系统的应答对话,即当物理设备需要CPU处理数据时,设备就发一
个中断信号给系统,系统则在收到信号后调用相应的中断服务程序响应对设备中断
的处理。中断方式有效地解决了设备与CPU的对话交流问题,并将CPU从繁重的设备
轮询中解脱出来,大大提高了CPU的利用率。当前不管是Linux平台还是Windows平
台,它们的网络设备驱动程序几乎都是使用中断方式的。故在此我们主要讨论基于
中断方式的网络设备驱动程序。
网络分层引起的一个问题是,每层的协议在发送数据包时要加协议头和协议尾到原
数据中,在收到数据包时则要将本层的协议头和协议尾从数据包中去掉。这使得在
不同层协议间传输时,每层都需要知道自己这一层的协议头和协议尾在数据包的哪
里。一种解决方法是在每层都复制缓冲区,但显然效率太低。Linux的做法是用一
种数据结构sk_buff在不同协议层及网络设备驱动程序之间传送数据。sk_buff 包
括指针和长度域段,允许每个协议层通过标准的函数操作传送的数据包。该数据结
构在整个Linux的网络子系统包括网络设备中扮演了一个十分重要的角色,故我们
在分析数据包的传输和接收之前,首先来看看sk_buff这个数据结构的内容及系统
提供的相关操作。因为对该数据结构的了解将大大有助于对Linux整个网络子系统
的理解。
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 - skb->data;
unsigned long  truesize
分配到的内存空间大小。
len=skb->end - 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的硬件传输函数指针
,Linux正是通过这个函数指针来完成实际的数据传输的。
硬件传输函数hard_start_xmit函数的一般流程如下:

1. 通过标志位tbusy判断上次数据包的传输是否完成。若tbusy=0就做下一步;否
则,看上次传输是否已超时,若未超时,就不成功返回,若已超时,则初始化芯片
寄存器、置tbusy=0,然后继续下一步;
2. 将tbusy标志位打开;
3. 将数据包传给硬件发送;
4. 释放缓冲区skb;
5. 修改接口的一些统计信息。

由上几节对NE2000的分析,我们可以发现,该网络设备的传输函数
dev->hard_start_xmit为ei_start_xmit()。下面我们将继续以NE2000为例,对
网络接口的数据传输过程进行分析。在本文中分析时,作者试图尽量避免依赖于硬
件的那一部分代码,以便加深对整个网络设备管理机制的把握,并试图强调、突出
一般网络设备所共有的东西。

/* 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代码分析知,
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()。下面我们将继续以
NE2000为例,对网络接口的接收过程进行分析。

/* 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. 提供该网络设备的中断服务程序,处理数据传输完毕的善后事宜和数据的接收
。当物理网络设备有新数据到达或数据传输完毕时,将向系统发送硬件中断请求,
该函数就是用来响应该中断请求的。

有关以上的各个函数具体应该做什么事情,以及该如何写、为什么这样写等问题,
请参考本文上面部分的分析和讨论。
若本文所做的工作对那些对Linux网络设备驱动程序感兴趣的人有一点点启示,或
一点点帮助,或能够加快到达深入了解Linux网络子系统的境界,那么作者也就感
到心满意足了。本文中的错误固定不少J,若有兴趣,不妨飞鸽传书pg@ccnt.zju.
edu.cn,让我们一起来讨论、学习。

七. 建议及致谢
1.  建议
2.  致谢
八. 参考文献
[1].David A. Rusling,"The Linux Kernel"Version 0.8-3,1999
[2].Alessandro Rubini,"Linux Device Drivers",O'Reily&Associates,USA
,1998
[3].Michael K. Johnson,"Writing Linux Device Drivers",DECUS '95 in
Washington,1995
[4].Michael K. Johnson and Others,"Linux Kernel Hackers' Guide",
1998
[5].Ori Pomerantz,"Linux Kernel Module Programming Guide",1998
九. 附录--一个虚拟的字符设备驱动程序
下面是本人写的一个虚拟的字符设备的驱动程序,该字符设备简单地实现两个进程
间的数据交流,该两个进程不需要同时在运行。即提供数据的进程只要已对设备写
数据,即使该进程结束后,读数据的进程也可读取数据。
我们的做法是建立两个同一类型的虚拟字符设备,一个是专门接收数据的设备
drgnw,一个是专门提供数据的设备drgnr。同时我们简单地规定,不能同时有两个
进程对设备进行读或对设备进行写。
写这个字符设备驱动程序及建立该设备的基本步骤如下:

1.  确定设备的设备名称和主设备号
我们将这个虚拟的字符设备命名为drgn,主设备号定为30(在2.0.34的内核中还没
有以30作为主设备号的字符设备)。以后我们的这个字符设备的操作函数都将以
"drgn_"打头;

2.  确定编写需要的file_operations中的操作函数及初始化函数,包括:
void drgn_init(void)
static int drgn_open(struct inode * inode,struct file * file)
static void drgn_release(struct inode * inode,struct file * file)
static int drgn_write(struct inode * inode,struct file * file,const char
 * buffer,int count)
static int drgn_read(struct inode * inode , struct file * file,char *
buffer, int count)
static int drgn_ioctl(struct inode * inode, struct file * file,
unsigned int cmd, unsigned long arg)
3.  在drivers/char/mem.c中添加相应语句;
在chr_dev_init函数之前添加drgn_init的原型说明:
void drgn_init(void);
在chr_dev_init函数的return语句之前添加以下语句:
drgn_init(); /* added by XX */
4.  修改drivers/char/Makefile;
找到"L_OBJS   := tty_io.o n_tty.o console.o \"行,将"drgn.o"加到其中。
5.  将该设备私有的*.c,*.h复制到目录drivers/char下。
将drgn.c和drgn.h复制到drivers/char下
6.  重新编译内核;
用命令:make clean;make dep;make zImage
7.  使新内核生效;
8.  在目录/dev下建立相应的特殊文件。
建立两个字符设备的特殊文件"drgnr"和"drgnw":
mknod drgnw c 30 0
mknod drgnr c 30 1
9.  万事已OK!将这里提供的测试程序writeto.c和readfrom.c用gcc编译一下就可
测试使用该设备啦。

/* drgn.h */
#ifdef  KERNEL

#define TRACE_TXT(text) {if(drgn_trace) {console_print(text);
console_print("\n");}}
#define TRACE_CHR(chr) {if(drgn_trace) console_print(chr);}

#define DRGN_READ  1
#define DRGN_WRITE 0

#endif

#define FALSE 0
#define TRUE  1
#define MAX_BUF   120
#define DRGN_TRON  (('M' << 8)|0x01)
#define DRGN_TROFF (('M' << 8)|0x02)

struct drgn_buf
{
   int buf_size;
   char buffer[MAX_BUF];
   struct drgn_buf *link;
};

/* drgn.c */
#define KERNEL

#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/tty.h>
#include <linux/signal.h>
#include <linux/errno.h>
#include <linux/malloc.h>
#include <linux/mm.h>

#include <asm/io.h>
#include <asm/segment.h>
#include <asm/system.h>
#include <asm/irq.h>

#include "drgn.h"

static int drgn_trace;
static int write_busy;
static int read_busy;
static struct drgn_buf * qhead;
static struct drgn_buf * qtail;

static int  drgn_read(struct inode * , struct file * , char * , int );
static int  drgn_write(struct inode * , struct file * , const char *,
int );
static int  drgn_ioctl(struct inode * , struct file * , unsigned int ,
unsigned long );
static int  drgn_open(struct inode *,struct file *);
static void drgn_release(struct inode *,struct file *);
/* extern void console_print(char *);*/

struct file_operations drgn_fops=
{
   NULL,
   drgn_read,
   drgn_write,
   NULL,
   NULL,
   drgn_ioctl,
   NULL,
   drgn_open,
   drgn_release,
   NULL,
   NULL,
   NULL,
   NULL
};

void drgn_init(void)
{
   drgn_trace=TRUE;
   
   if(register_chrdev(30,"drgn",&drgn_fops))
      TRACE_TXT("Cannot register drgn driver as major device 30.")
   else
      TRACE_TXT("Tiny devie driver registered successfully.")

   qhead=0;
   write_busy=FALSE;
   read_busy=FALSE;
 /*  drgn_trace=FALSE;*/
   return;
}

static int drgn_open(struct inode * inode,struct file * file)
{
   TRACE_TXT("drgn_open")

   switch (MINOR(inode->i_rdev))
   {
      case DRGN_WRITE:
         if(write_busy)
            return -EBUSY;
         else{
            write_busy=TRUE;
            return 0;
         }
      case DRGN_READ:
         if(read_busy)
            return -EBUSY;
         else{
            read_busy=TRUE;
            return 0;
         }
      default:
         return -ENXIO;
   }
}

static void drgn_release(struct inode * inode,struct file * file)
{
   TRACE_TXT("drgn_release")

   switch (MINOR(inode->i_rdev))
   {
      case DRGN_WRITE:
         write_busy=FALSE;
         return;
      case DRGN_READ:
         read_busy=FALSE;
         return;
   }
}

static int drgn_write(struct inode * inode,struct file * file,
            const char * buffer,int count)
{
   int i,len;
   struct drgn_buf * ptr;
   
   TRACE_TXT("drgn_write")
   
   if (MINOR(inode->i_rdev)!=DRGN_WRITE)
      return -EINVAL;

   if ((ptr=kmalloc(sizeof(struct drgn_buf),GFP_KERNEL))==0)
      return -ENOMEM;

   len=count < MAX_BUF?count:MAX_BUF;

   if (verify_area(VERIFY_READ,buffer,len))
      return -EFAULT;

   for(i=0;i < count && i<MAX_BUF;++i)
   {
      ptr->buffer[i]=(char) get_user((char*)(buffer+i));
      TRACE_CHR("w")
   }

   ptr->link=0;

   if(qhead==0)
      qhead=ptr;
   else
      qtail->link=ptr;
   qtail=ptr;
   TRACE_CHR("\n")

   ptr->buf_size=i;
   return i;
}

static int drgn_read(struct inode * inode , struct file * file,
             char * buffer, int count)
{
   int i,len;
   struct drgn_buf * ptr;

   TRACE_TXT("drgn_read")

   if(MINOR(inode->i_rdev)!=DRGN_READ)
      return -EINVAL;
   
   if (qhead==0)
      return -ENODATA;

   ptr=qhead;
   qhead=qhead->link;

   len=count < ptr->buf_size?count:ptr->buf_size;

   if (verify_area(VERIFY_WRITE,buffer,len))
      return -EFAULT;

   for (i=0; i<count && i<ptr->buf_size; ++i)
   {
      put_user((char) ptr->buffer[i],(char *)(buffer+i));
      TRACE_CHR("r")
   }

   TRACE_CHR("\n")

   kfree_s(ptr,sizeof(struct drgn_buf));
   return i;
}

static int drgn_ioctl(struct inode * inode, struct file * file,
             unsigned int cmd, unsigned long arg)
{
   TRACE_TXT("drgn_ioctl")

/*   if (cmd==DRGN_TRON){
       drgn_trace=TRUE;
       return 0;
   }
   else
       if (cmd==DRGN_TROFF){
           drgn_trace=FALSE;
           return 0;
       }
       else
           return -EINVAL;*/
   switch(cmd)
   {
      case DRGN_TRON:
         drgn_trace=TRUE;
         return 0;
      case DRGN_TROFF:
         drgn_trace=FALSE;
         return 0;
      default:
         return -EINVAL;
   }
}

/* writeto.c */
#include <stdio.h>

void main(int argc, char * argv[]){
FILE *fp;

if((fp=fopen("/dev/drgnw","w"))==NULL){
  printf("File Open Error!");
  return;
}
if (argc > 1)
  fputs(argv[1],fp);
fclose(fp);
}

/* readfrom.c */
#include <stdio.h>

void main(){
char ss[25]; /*you can change 25 to any number not greater than
MAX_BUF */
FILE * fp;

if((fp=fopen("/dev/drgnr","r"))==NULL){
  printf("File open Error!");
  return;
}
fgets(ss,25,fp);
printf("READ: %s\n",ss);
fclose(fp);
}
GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐