【Linux驱动设备开发详解】14.Linux网络设备架构

1.Linux网络设备驱动的结构

与字符设备和块设备不同,网络设备并不对应于/dev目录下的文件,应用程序最终使用套接字完成与网络设备的接口。
Linux系统对网络设备驱动定义了4个层次,这4个层次为:

  • 网络协议接口层:向网络层协议提供同一的数据包收发接口,无论是IP还是ARP,都是通过dev_queue_xmit()发送数据,通过netif_rx()接收数据
  • 网络设备接口层:向网络协议层提供同一用于描述具体网络设备属性和操作的结构体net_device,此结构体是设备驱动功能层中各函数的容器
  • 设备驱动功能层:这一层的各函数是网络设备接口层net_device数据结构体的具体成员,驱使网络设备硬件完成相应动作的程序,它通过hard_start_xmit()函数启动发送操作,并通过网络设备上的中断触发接收操作
  • 网络设备与媒介层:完成数据包发送和接收的物理实体,包括网络适配器和具体的传输媒介,网络适配器被设备驱动驱动功能层中的函数在物理上驱动

image.png

在设计具体的网络设备驱动程序时,需要完成的主要工作是编写设备驱动功能层的相关函数以填充net_device数据结构的内容并将net_device注册入内核

1.1 网络协议接口层

网络接口协议层最主要的功能是給上层协议提供透明的数据包发送和接收接口。上层ARP协议或IP需要发送数据包时,调用dev_queue_xmit()函数发送该数据包,同时需传递给该函数一个struct sk_buff数据结构的指针。

dev_queue_xmit()函数的原型:

int dev_queue_xmit(struct sk_buff *skb);

上层对数据包的接收也通过向netif_rx()函数传递一个struct sk_buff数据结构的指针来完成。netif_rx()函数的原型为:

int netif_rx(struct sk_buff *skb);

sk_buff含义为套接字缓冲区,定义于include/linux/skbuff.h文件中,用于在Linux网络子系统中的各层之间传递数据,是Linux网络子系统的"中枢神经"。

当发送数据包时,Linux内核的网络处理模块必须建立一个包含要传输的数据包的sk_buff,然后将sk_buff递交到下层,各层在sk_buff中添加不同的协议头直至交给网络设备发送。同样地,当网络设备从网卡接收到数据包后,它必须将接收到数据转换为sk_buff数据结构体并传递给上层,各层剥去相应的协议头直至交给用户。

sk_buff原型:

struct sk_buff {
	union {
		struct {
			/* These two members must be first. */
			struct sk_buff		*next;
			struct sk_buff		*prev;

			union {
				struct net_device	*dev;
				/* Some protocols might use this space to store information,
				 * while device pointer would be NULL.
				 * UDP receive path is one user.
				 */
				unsigned long		dev_scratch;
			};
		};
		struct rb_node		rbnode; /* used in netem, ip4 defrag, and tcp stack */
		struct list_head	list;
	};

	......
	unsigned int		len,
				data_len;
	__u16			mac_len,
				hdr_len;

	.....
	__u32			priority;
	int			skb_iif;
	__u32			hash;
	__be16			vlan_proto;
	__u16			vlan_tci;
	....

	union {
		__be16		inner_protocol;
		__u8		inner_ipproto;
	};

	__u16			inner_transport_header;
	__u16			inner_network_header;
	__u16			inner_mac_header;

	__be16			protocol;
	__u16			transport_header;
	__u16			network_header;
	__u16			mac_header;

	/* private: */
	__u32			headers_end[0];
	/* public: */

	/* These elements must be at the end, see alloc_skb() for details.  */
	sk_buff_data_t		tail;
	sk_buff_data_t		end;
	unsigned char		*head,
				*data;
	...
};

head和end指向缓冲区的头部和尾部,而data和tail指向实际数据的头部和尾部,每一层会在head和data之间填充协议头,或者在tail和end之间添加新的协议数据

image

套接字缓冲区涉及的操作函数:

(1) 分配

分配套接字缓冲区的函数:

// 分配一个套接字缓冲区和一个数据缓冲区, 参数len为数据缓冲区的空间大小, 通常以L1_CACHE_BYTES字节(对于ARM为32) 对齐, 参数priority为内存分配的优先级
struct sk_buff *alloc_skb(unsigned int len, gfp_t priority);
// dev_alloc_skb()函数以GFP_ATOMIC优先级进行skb的分配, 原因是该函数经常在设备驱动的接收中断里被调用
struct sk_buff *dev_alloc_skb(unsigned int len);

(2) 释放

用于释放alloc_skb套接字缓冲区和数据缓冲区的函数有:

void kfree_skb(struct sk_buff *skb);           // 一般在内核内部使用
void dev_kfree_skb(struct sk_buff *skb);        // 用于非中断上下文
void dev_kfree_skb_irq(struct sk_buff *skb);	// 用于中断上下文
void dev_kfree_skb_any(struct sk_buff *skb);	// 在中断或非中断皆可采用(实际是在内部做了判断,分别调用dev_kfree_skb_irq和dev_kfree_skb)

(3) 变更

在缓冲区尾部增加数据

unsigned char *skb_put(struct sk_buff *skb, unsigned int len);

它会导致skb->tail后移len(skb->tail+=len) , 而skb->len会增加len的大小(skb->len+=len) 。 通常, 在设备驱动的接收数据处理中会调用此函数。

在缓冲区开头增加数据

unsigned char *skb_push(struct sk_buff *skb, unsigned int len);

它会导致skb->data前移len(skb->data-=len) , 而skb->len会增加len的大小(skb->len+=len) 。 与该函数的功能完成相反的函数是skb_pull() , 它可以在缓冲区开头移除数据, 执行的动作是skb->len-=len、skb->data+=len。

调整缓冲区的头部

static inline void skb_reserve(struct sk_buff *skb, int len);

它会将skb->data和skb->tail同时后移len, 执行skb->data+=len、 skb->tail+=len。

内核中的使用实例

skb=alloc_skb(len+headspace, GFP_KERNEL);
skb_reserve(skb, headspace);
skb_put(skb,len);
memcpy_fromfs(skb->data,data,len);
pass_to_m_protocol(skb);

先分配一个全新的sk_buff,接着调用skb_reserve() 腾出头部空间, 之后调用skb_put() 腾出数据空间, 然后把数据复制进来, 最后把sk_buff传给协议栈。

1.2 网络设备接口层

net_device结构体在内核中指代一个网络设备,它定义于include/linux/netdevice.h文件中,网络设备程序只需通过net_device的具体成员并注册net_device即可实现硬件操作函数与内核的挂接。

net_device中包含了网络设备的属性描述和操作接口,比如下面这些关键成员:

(1)全局信息

char name[IFNAMESIZE];            // 网络设备的名称

(2)硬件信息

unsigned long mem_end;              // 设备所使用的共享内存的起始地址
unsigned long mem_start;	    // 设备所使用的共享内存的结束地址
unsigned long base_addr;                  // 网络设备I/O基地址
unsigned char irq;			  // 设备使用的中断号
unsigned char if_port;                    // 指定多端口设备使用哪一个端口,比如IF_PORT_10BASE2(同轴电缆)和IF_PORT_10BASET(双绞线)
unsigned char dma;			  // 指定分配给设备的DMA通道

(3)接口信息

unsigned short hard_header_len;                 // 网络设备的硬件头长度,在以太网设备的初始化函数中,该成员被赋值为ETH_HLEN,即14
unsigned short type;                            // 接口的硬件类型
unsigned mtu;                                   // 最大传输单元
unsigned char *dev_addr;                        // 存放设备的硬件地址,驱动可能会提供设置MAC地址的接口,这会导致用户设置的MAC地址等存入该成员
unsigned short flags;                           // 网络接口标志

网络接口标志主要包括以下几种:

IFF_UP:当设备被激活并可以开始发送数据包时,内核设置该标志
IFF_AUTOMEDIA:设备可在多种媒介间切换
IFF_BROADCAST:允许广播
IFF_DEBUG:调试模式,可用于控制printk调用的详细程度
IFF_LOOPBACK:回环
IFF_MULTICAST:允许组播
IFF_NOARP:接口不能执行ARP
IFF_POINTOPOINT:接口连接到点对点链路

(4) 设备操作函数

const struct net_device_ops *netdev_ops;           // 此结构式网络设备的一系列硬件操作的集合
struct net_device_ops {
	int			(*ndo_init)(struct net_device *dev);   
	void			(*ndo_uninit)(struct net_device *dev);
	int			(*ndo_open)(struct net_device *dev);         // 打开网络接口设备,获取设备需要的I/O地址,IRQ,DMA通道等等
	int			(*ndo_stop)(struct net_device *dev);         // 停止网络接口设备
	netdev_tx_t		(*ndo_start_xmit)(struct sk_buff *skb, 
						  struct net_device *dev);   // 启动数据包发送
	netdev_features_t	(*ndo_features_check)(struct sk_buff *skb,
						      struct net_device *dev,
						      netdev_features_t features);
	u16			(*ndo_select_queue)(struct net_device *dev,
						    struct sk_buff *skb,
						    void *accel_priv,
						    select_queue_fallback_t fallback);
	void			(*ndo_change_rx_flags)(struct net_device *dev,
						       int flags);
	void			(*ndo_set_rx_mode)(struct net_device *dev);
	int			(*ndo_set_mac_address)(struct net_device *dev,
						       void *addr);                 // 用于设置设备的MAC地址
	int			(*ndo_validate_addr)(struct net_device *dev);
	int			(*ndo_do_ioctl)(struct net_device *dev,
					        struct ifreq *ifr, int cmd);        // 进行设备特定的I/O控制
	int			(*ndo_set_config)(struct net_device *dev,
					          struct ifmap *map);               // 用于配置接口,也可用于改变设备的I/O地址和中断号
	int			(*ndo_change_mtu)(struct net_device *dev,
						  int new_mtu);
	int			(*ndo_neigh_setup)(struct net_device *dev,
						   struct neigh_parms *);
	void			(*ndo_tx_timeout) (struct net_device *dev);        // 数据包发送超时时调用,需采取重新启动数据包发送过程或重新启动硬件等措施来恢复网络设备到正常状态

	void			(*ndo_get_stats64)(struct net_device *dev,
						   struct rtnl_link_stats64 *storage);
	bool			(*ndo_has_offload_stats)(const struct net_device *dev, int attr_id);
	int			(*ndo_get_offload_stats)(int attr_id,
							 const struct net_device *dev,
							 void *attr_data);
	struct net_device_stats* (*ndo_get_stats)(struct net_device *dev);          // 获取网络设备的状态信息

	int			(*ndo_vlan_rx_add_vid)(struct net_device *dev,
						       __be16 proto, u16 vid);
	int			(*ndo_vlan_rx_kill_vid)(struct net_device *dev,
						        __be16 proto, u16 vid);
	....
};
const struct ethtool_ops *ethtool_ops;      // 成员函数与ethtool各个命令选项对应
const struct header_ops *header_ops;        // 对应于硬件头部操作,主要完成创建硬件头部和从给sk_buff分析出硬件头部等操作

(5) 辅助成员

unsigned long trans_start;                         // 记录最后的数据包开始发送时的时间戳
unsigned long last_rx;				   // 最后一次接收到数据包时的时间戳,这俩个时间戳记录的都是jiffies

NAPI

通常情况下,网络设备以中断方式接收数据包,而poll_conmtroller()则采用纯轮询方式,另外一种数据接收方式是NAPI(New API),其数据接收流程为"接收中断来临->关闭接收中断->以轮询方式接收所有数据包直到收空->开启接收中断->接收中断来临......"

static inline void netif_napi_add(struct net_device *dev,
				  struct napi_struct *napi,
				  int (*poll)(struct napi_struct *,int),         // NAPI要调度执行的轮询函数
			          int weight);                     // 初始化一个NAPI
static inline void netif_napi_del(struct napi_struct *napi);       // 移除一个NAPI
static inline void napi_enable(struct napi_struct *n);               // 使能NAPI调度
static inline void napi_disable(struct napi_struct *n);		     // 禁止NAPI调度
static inline int napi_schedule_prep(struct napi_struct *n);         // 用于检查NAPI是否可以调度
static inline void napi_schedule(struct napi_struct *n);             // 用于调用轮询实例的运行
static inline void napi_complete(structg napi_struct *n);            // NAPI处理完成的时候应该调用

1.3 设备驱动功能层

设备驱动功能层主要是给net_device结构体中的成员(属性和net_device_ops结构体中的函数指针)赋予具体的数值和函数。也就是说设备驱动功能层中的函数是实际的硬件驱动函数。这些函数形如:xxx_open(),xxx_stop(),xxx_tx(),xxx_hard_header()等。

由于网络数据包的接收可由中断触发,所以设备驱动功能层中的另一个主体部分将是中断处理函数,它负责读取硬件上接收到的数据包并传送给上层协议,它负责读取硬件上接收到的数据包并传送给上层协议,因此可能包含xxx_interrupt()和xxx_rx()函数,前者完成中断类型判断的等基本工作,后者完成数据包生成及将其传递给上层等复杂工作。

对于特定的设备,还可以定义相关的私有数据和操作,并封装为一个私有信息结构体xxx_private,让其指针赋值给net_device的私有成员。在xxx_private结构体中可包含设备的页数属性和统计信息等。

在驱动中要用到私有数据的时候,则使用在netdevice.h中定义的接口:

static inline void *netdev_priv(const net_device *dev);

比如驱动dm9000.c的dm9000_probe函数中,使用alloc_etherdev(sizeof(struct board_info))分配网络设备,board_info结构体就成了这个网络设备的私有数据,在其他函数里可以简单地提取这个私有数据,例如:

static int dm9000_start_xmit(struct sk_buff,struct net_device *dev)
{
	unsigned long flags;
	board_info_t *db = netdev_priv(dev);
	...
}

2.网络设备驱动的注册与注销

网络设备驱动注册

int register_netdev(struct net_device *dev);

网络设备驱动注销

void unregister_netdev(struct net_device *dev);

网络设备的驱动的注册和注销都是接收net_device结构体指针为参数,而net_device的生成和成员的赋值可以利用以下宏进行填充:

#define alloc_netdev(sizeof_priv,name,setup)  \
			alloc_netdev_mqs(sizeof_priv,name,setup,1,1)
#define alloc_etherdev(sizeof_priv,alloc_etherdev_mq(sizeof_priv),1)
#define alloc_etherdev_mq(sizeof_priv,count)  alloc_etherdev_msq(sizeof_priv,count,count);

alloc_netdev_mqs()函数的原型:

struct net_device *alloc_netdev_msq(int sizeof_priv,           // 设备私有成员大小
				    const char *name,	       // 设备名
				    void (*setup)(struct net_device *),    // net_device的setup函数指针,net_device指针用于预置net_device成员的值
				    unsigned int txqs,          // 要分配的发送子队列数量
				    unsigned int rxqs);         // 要分配的接收子队列的数量

free_netdev()完成释放alloc_netdev申请的net_device结构体

void free_netdev(struct net_device *dev);

网络设备驱动程序初始化时分配net_device结构体和注册网络设备驱动,net_device结构体的释放和网络设备驱动的注销在设备或驱动被移除时执行:

static int xxx_register(void)
{
    ...
    // 分配net_device结构体并对成员赋值
    xxx_dev = alloc_netdev(sizeof(struct xxx_priv),"sn%d",xxx_init);

    if(xxx_dev = NULL)
    {
	// 分配net_device失败
    }

    // 注册net_device结构体
    if((result = register_netdev(xxx_dev)))
    ...
}

static void xxx_unregister(void)
{
    ...
    // 注销net_device结构体
    unregister_netdev(xxx_dev);
    // 释放net_device结构体
    free_netdev(xxx_dev);
}

3.网络设备的初始化

网络设备的初始化需要完成三个工作:

1.进行硬件上的准备工作,检查网络设备是否存在,如果存在,则检测设备所使用的硬件资源

2.进行软件接口上的准备工作,分配net_device结构体并对其数据和函数指针成员赋值

3.获得设备的私有信息指针并初始化各成员的值。如果私有信息中包括自旋锁或信号量等并发或同步机制,则需对其进行初始化

网络设备初始化函数模板:

void xxx_init(struct net_device *dev)
{
     // 设备的私有信息结构体
     struct xxx_priv *priv;
  
     // 初始化硬件
     xxx_hw_init();      // 探测网络硬件是否存在,先假设存在,访问设备,如果设备的表现与预期一致,则确定设备存在,否则确定设备不存在

     // 初始化以太网设备的公用成员
     ether_setup(dev);

     // 设置设备的成员函数指针
     dev->netdev_ops = &xxx_netdev_ops;
     dev->ethtool_ops = &xxx_ethtool_ops;
     dev->watchdog_timeo = timeout;

     // 取得私有信息,并初始化它
     priv = netdev_priv(dev);
     ...
}

xxx_hw_init()的初始化操作如下:

1.探测网络硬件是否存在,先假设存在,访问设备,如果设备的表现与预期一致,则确定设备存在,否则确定设备不存在

2.探测设备的具体硬件配置,比如网络设备型号

3.申请设备所需要的硬件资源,比如用request_region()函数进行I/O端口的申请等

4.网络设备的打开与释放

网络设备的打开函数需要完成以下工作:

  • 使能设备使用的硬件资源,申请I/O区域、中断和DMA通道等
  • 调用Linux内核提供的netif_start_queue()函数,激活设备发送队列
void netif_start_queue(struct net_device *dev);

网络设备关闭函数需要完成如下工作:

  • 调用Linux内核提供的netif_stop_queue()函数,停止设备传输包
  • 释放设备所使用的I/O区域、中断和DMA资源
void netif_stop_queue(struct net_device *dev);

网络设备打开和释放函数模板:

static int xxx_open(struct net_device *dev)
{
    // 申请端口、IRQ等,类似于fops->open
    ...
    netif_start_queue(dev);
    ...
}

static int xxx_release(struct net_device *dev)
{
    // 释放端口、IRQ等、类似于fops->close
    free_irq(dev->irq,dev);
    ...
    netif_stop_queue(dev);
    ...
}

5.网络设备驱动的数据发送流程

Linux网络子系统在发送数据包时,回调用驱动程序提供的hard_start_transmit()函数,该函数用于启动数据包的发送。在设备初始化时,这个函数指针被初始化以指向设备的xxx_rx()函数。

网络设备驱动发送数据包的流程如下:

  1. 网络设备驱动程序从上层协议传递过来的sk_buff参数获得数据包的有效数据和长度,将有效数据放入临时缓冲区
  2. 对于以太网,如果有效数据的长度小于以太网冲突检测所要求数据帧的最小长度ZTH_ZLEN,则给临时缓冲区的末尾填充0
  3. 设置硬件的寄存器,驱使网络设备进行数据发送操作

网络设备驱动程序的数据包发送函数模板:

int xxx_tx(struct sk_buff *skb,struct net_device *dev)
{
    int len;
    char *data,shortpkt[ETH_ZLEN];
    if(xxx_send_available(...)) {     // 如果队列未满,可以发送
	// 获得有效数据指针和长度
        data = skb->data;
	len = skb->len;
	if(len < ETH_SIZE)
	{
	     // 如果帧长小于以太网最小长度,补0
	     memset(shortpkt,0,ETH_ZLEN);
	     memcpy(shortpkt,skb->data,skb->len);
	     len = ETH_ZLEN;
	     data = shortpkt;
	}
    }

    dev->trans_start = jiffies;     // 记录发送时间戳

    // 设置硬件寄存器,让硬件将数据包发送出去
    if(avail)
    {
         xxx_hw_tx(data,len,dev);
    } 
    else 
    {
	netif_stop_queue(dev);   // 当发送队列为满,或因其他原因来不及发送当前上层传下来的数据包时,则调用此函数组织上层继续向网络设备驱动传递数据包
				 // 当忙于发送的数据包被发送完成后,在以TX结束的中断处理中,应该调用netif_wake_queue()唤醒被阻塞的上层,以启动它继续向网络设备驱动传送数据包
	...
    }
}

当出现数据传输超时时,意味着当前的发送操作失败或硬件已陷入未知状态,此时数据包发送超时处理函数xxx_tx_timeout()将被调用。这个函数也需要调用由Linux内核提供的netif_wake_queue()函数以重新启动设备发送队列,示例代码如下:

void xxx_tx_timeout(struct net_device *dev)
{
     ....
     netif_wake_queue(dev);    // 重新启动设备发送队列
}

netif_wake_queue()和netif_stop_queue()是数据发送流程中要调用的两个重要的函数,分别用于唤醒和阻止上层向下传送数据包,它们的原型定义于include/linux/netdeivce.h,如下:

static inline void netif_wake_queue(struct net_device *dev);
static inline void netif_stop_queue(struct net_device *dev);

6.网络设备驱动的数据接收流程

网络设备接收数据的主要方法是由中断引发设备的中断处理函数,中断处理函数判断中断类型,如果为接收中断,则读取接收到数据,分配sk_buffer数据结构和数据缓冲区,将收到的数据赋值到数据缓冲区,并调用netif_rx()函数将sk_buffer传递给上层协议,以下为网络设备驱动的中断处理函数模板:

// 当设备的中断处理程序判断中断类型为数据包为接收中断
static void xxx_interrrupt(int irq,void *dev_id)
{
     ...
     switch(status &ISQ_EVETN_MASK){
	case IRQ_RECVIVER_EVETN:
	    // 调用xxx_rx函数完成更深入的数据包接收工作
	    xxx_rx(dev);
	    break;
	case 其他类型的中断:
	    ....
     }
}

static void xxx_rx(struct xxx_deivce *dev)
{
     ...
     // 1.从硬件读取到接收数据包的有效数据长度
     length = get_rev_len(...);

     // 2.分配新的套接字缓冲区
     skb = dev_alloc_skb(length + 2);

     skb_reserve(skb,2);     // 对齐
     skb->dev = dev;

     // 3.读取硬件上接收到的数据
     insw(ioaddr + RX_FRRAME_PORT ,skb_put(skb,length),length >> 1);
     if(length & 1 )
     {
	skb->data[length-1] = inw(ioaddr + RX_FRAME_PORT);
     }

     // 4.获取上层协议内容
     skb->protocol = eth_type_trans(skb,dev);

     // 5.将数据包交给上层
     netif_rx(skb);

     // 6.记录接收时间戳
     dev->last_rx = jiffies;
     ....
}

如果是NAPI兼容的设备驱动,则可以通过poll方式接收数据包。在这种情况下,需要为设备驱动提供netif_napi_add()参数的xxx_poll函数,xxx_poll()函数模板:

static int xxx_poll(struct napi_struct *napi,int budget)
{
    int npackets = 0;
	struct sk_buff *skb;
	struct xxx_priv *priv = container_of(napi,struct xxx_priv,napi);
	struct xxx_packet *pkt;

	// 循环读取设备的接收缓冲区,同时读取数据包并提交给上层。
	while(npackets < budget && priv->rx_queue)
	{
		// 从队列中取出数据包
		pkt = xxx_dequeue_buf(dev);

		// 接下来的处理和中断触发的数据包接收一致
		skb = dev_alloc_skb(pkt->datalen + 2);
		...
		skb_reserve(skb,2);
		memcpy(skb_put(skb,pkt->datalen),pkt->data,pkt->datalen);
		skb->dev = dev;
		skb->protocol = eth_type_trans(skb,dev);

	        // 调用netif_recvive_skb,而不是net_rx,将数据包交给上层协议(此处体现出了中断和轮询机制的不同)
		netif_receive_skb(skb);

		// 更改统计数据
		priv->stats.rx_packets++;
		priv->stats.rx_bytes += pkt->datalen;
		xxx_release_buffer(pkt);
		npackets++;
	}
  
	if(npackets < budget)
	{
		napi_complete(napi);     // 宣布一个轮询过程结束
		xxx_enable_rx_init();    // 再次启动网络设备的接收中断
	}
	return npackets;
}

budget是在初始化阶段分配给接口的weight值,xxx_poll函数每次只能接收最多budget个数据包。

NAPI兼容的设备区域以xxx_poll()方式接收数据包,但是仍然需要首次数据包接收中断来触发这个过程。与数据包的中断接收方式不同的是,以轮询方式接收数据库包时,当第一次中断发生后,中断处理成需要禁止设备的数据包接收中断并调度NAPI,示例如下:

static void xxx_interrupt(int irq,void *dev_id)
{
     switch(status &ISQ_EVENT_MASK)
     {
	case ISQ_RECEIVER_EVENT:
	// 获取数据包
	xxx_disable_rx_int(...);     // 禁止接收中断
	napi_schedule(&priv->napi);
	break;
	// 其他类型的中断
     }
}

napi_schedule()被轮询方式驱动的中断程序调用,将设备的poll方法添加到网络层的poll处理队列中,排队并且准备接收数据包,最终触发一个NET_RX_SOFTIRQ软中断,从而通知网络层接收数据包,下图为驱动程序各部分调用关系:

image

7.网络连接状态

网络的连接状态由网络适配器硬件电路通过检测链路上是否由载波来判断网络的连接是否正常。

网络设备驱动可以通过netif_carrier_on()和netif_carrier_off()函数改变设备的连接状态,如果驱动检测到连接状态发生变化,也应该以netif_carrier_on()和netif_carrier_off()函数显式地通知内核。

netif_carrier_ok()可用于检测返回链路上的载波信号是否存在。

这三个函数都接收一个net_device设备结构体指针作为参数,原型如下:

void netif_carrier_on(struct net_device *dev);
void netif_carrier_off(struct net_device *dev);
int netif_carrier_ok(struct net_device *dev);

在网络设备驱动程序中可采取一定的手段来检测和报告链路状态,最常见的方法是采用中断,其次可以设置一个定时器来对链路状态进行周期性的检查。当定时器到期后,在定时器处理函数中读取物理设备的相关寄存器以获得载波状态,从而更新设备的连接状态:

static void xxx_timer(unsigned long data)
{
	struct net_device *dev = (struct net_device*)data;
	u16 link;

	if(!(dev->flags & IFF_UP)
		goto set_timer;

	// 获得物理上的连接状态,xxx_chk_link读取网络适配器硬件的相关寄存器,以获得链路连接状态,具体实现由硬件决定
	if(link = xxx_chk_link(dev))
	{
		if(!(dev->flags & IFF_RUNNING))
		{
			// 显式的通知内核链路正常
			netif_carrier_on(dev);
			dev->flags |= IFF_RUNNING;
			printk(KERN_DEBUG "%s:link down\n",dev->name);
		}
	}
	else
	{
		if(dev->flags & IFF_RUNNING)
		{
			// 显式的通知内核链路失去连接
			netif_carrier_off(dev);
			dev->flags |= ~IFF_RUNNING;
			printk(KERN_DEBUG "%s:link down\n",dev->name);
		}
	}

// 启动新的定时器实现周期性检测的目的,最初启动定时器的的地方一般在设备的打开函数中
set_timer:
	priv->timer.expires = jiffies+1*Hz;
	priv->timer.data = (unsigned long)dev;
	priv->timer.function = &xxx_timer;     // timer handler
	add_timer(&priv->timer);
}

网络设备驱动的打开函数中初始化定时器:

static int xxx_open(struct net_device *dev)
{
	struct xxx_priv *priv = netdev_priv(dev);
	...
	priv->timer.expires = jiffies+1*Hz;
	priv->timer.data = (unsigned long)dev;
	priv->timer.function = &xxx_timer;     // timer handler
	add_timer(&priv->timer);
	...
}
posted @ 2024-06-11 15:48  Emma1111  阅读(508)  评论(0)    收藏  举报