资源描述
Linux网络设备分析
[摘要] 在本文中,首先概括了网络设备总体特征和工作原理,接着在分析了一个重要的数据结构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为最小的可用以太网设备号注 在2.0.34版本的内核中,只有以太网设备的缺省名是在register_netdev中赋值的。对于其他网络设备,一般在其他地方就赋以缺省值,而无需register_netdev处理。如PLIP,在plip.c中就预定了3个PLIP设备plip0、plip1和plip2。若启动时或装载模块时若无指定参数传入,则会依次对三个设备试图进行初始化:
for (i=0; i < 3; i++) { /* from drivers/net/plip.c */
if (register_netdev(&dev_plip[i]) == 0)
devices++;
}
;然后,网络设备自己的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); /* 初始化不成功,释放登记的中断号! */
展开阅读全文