百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 优雅编程 > 正文

详解Linux系统网络技术之关键数据结构解析

sinye56 2025-02-19 13:47 3 浏览 0 评论

Linux网络子系统中存在一些很重要的数据结构,贯穿整个子系统,主要有以下两个:

struct sk_buff:数据封包结构。所有的网络分层都会使用这个结构来存储其报头、有关用户数据,以及协调其他工作的其他内部信息。

struct net_device:在Linux内核中,每种网络设备都用这个数据结构表示,包括软硬件的配置信息。

推荐视频

Linux内核源码分析之网络协议栈架构

网络原理tcp/udp,网络编程epoll/reactor,面试中正经“八股文”

学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂

一、套接字缓冲区:sk_buff结构

这可能是Linux网络代码中最重要的数据结构,表示数据报文。这个结构定义在头文件中,由巨大的变量堆组成,试图满足所有人的所有需求。

这个结构的字段大致分为以下几个类型:

  • 网络层次
  • 通用字段
  • 功能专用
  • 管理函数

在网络系统的不同网络层都会使用这个结构,而当这个结构从一个分层传到另一个分层时,其不同的字段会随之发生改变。如L4层在传递给L3之前会附加一个报头,通用L3到L2之前也会加上自己的报头。附加报头比把数据从一个分层拷贝到另一个分层更有效率。

由于要在一个缓冲区开端新增空间(也就是修改指向缓冲区头部的指针),内核提供了skb_reserve函数来执行这个操作。所以,当缓冲区往下传递给每个网络层时,每层的协议首先要做的就是调用skb_reserve函数为该协议的报头预留空间。

而在缓冲区向上传递给上层网络时,并没有本层报头从缓冲区中删除,二是将直线有效数据的指针向前移到上层的报头位置。

由于网络代码提供了大量的选项性功能,不一定总是需要,如防火墙、多播、连接跟踪等,这些功能都会在sk_buff结构猪附加上字段。因此,sk_buff结构中有许多由C预处理#ifdef指令附加的字段。一般而言,任何引起内核数据结构改变的选项,都不适合编译成一个模块进行动态加载。

sk_buff中的某些字段是为了组织数据结构本身:

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

同时为了迅速找到整个表的头,在表的开端额外增加一个sk_buff_head结构作为一种哑元元素,sk_buff_head结构是:

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

__u32 qlen;
spinlock_t lock;
};

qlen是表中元素的数目,lock是用于防止对表的并发访问。
sk_buff和sk_buff_head结构的前两个元素是相同的,所以同样的函数也可用于操作sk_buff和sk_buff_head二者。
sk_buff结构中的list字段指向表头:

sk_buff中的其他字段:

struct sock *sk:指向拥有此缓冲区的套接字的sock数据结构。当数据在本地产生或者正在由本地进程接收时,就需要这个指针,因为该数据以及套接字相关的信息会由L4层(TCP或UDP)以及用户应用程序使用。当缓冲区只是被转发时,该指针就是NULL。

unsigned int len:这是指缓冲区猪数据区块的大小。这个长度包括主要缓冲区(由head所指)的数据以及一些片段(fragment)的数据。当缓冲区从一个网络层传递给下一个网络层时,其值会发生变化。因为在协议栈中往上移动时,报头会被丢弃。但是往下移动时,报头会被添加进来,len会将协议报头长度算在里面。www.it165.net

unsigned int data_len:与len不同,data_len只计算片段中的数据大小

unsigned int mac_len:MAC报头的大小

atomic_t users:引用计数,或者使用这个sk_buff缓冲区的实例的数目。这个参数的的主要用途是避免这个结构仍在使用时,被另一个实例释放掉。users有时直接使用atimic_inc和atomic_dec函数递增和递减,但在大多数时候,采用skb_get和kfree_skb进行处理。

unsigned int truesize:表示此缓冲区的总大小,包括sk_buff结构本身。当此缓冲区得到所分配的len个字节的数据请求空间时,此字段的初始化由alloc_skb函数设置为len+sizeof(sk_buff)。每当skb->len的值增加时,此字段就会得到更新。

sk_buff_data_t tail
sk_buff_data_t end
unsigned char *head
unsigned char *data

这些字段代表缓冲区的边界以及其中的数据。当每一层为其工作而准备缓冲区时,可能会为了一个报头或更多的数据分配更多的空间。head和end指向已分配空间的开端和尾端,而data和tail指向实际数据的开端和尾端。因此,可以再head和data直接填充报文头,在tail和end之间增加新的数据。

其中tail和end根据系统是否使用
NET_SKBUFF_DATA_USES_OFFSET来决定使用偏移地址还是指针

#ifdef NET_SKBUFF_DATA_USES_OFFSET
typedef unsigned int sk_buff_data_t;
#else
typedef unsigned char *sk_buff_data_t;
#endif

void (*destructor)(struct sk_buff *skb):此函数指针所指的函数在缓冲区被删除时,完成某些工作。当此缓冲区不属于一个套接字时,destructor通常不会被初始化。但若属于一个套接字时,通常被设置为sock_rfree或sock_wfree。这两个函数可用于更新套接字队列猪所持有的内存。

【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,内核,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg,大厂面试题 等)

sk_buff结构中有一些通用字段,和特定内核功能无关:

ktime_t tstamp:对一个已接收的封包才有意义。这是一个时间戳,表示接收包的时间,或者有时用于表示封包预定的传输时间。这个丢按由netif_rx函数调用net_timestamp_check设置,而这个函数在接收每个包后由设备驱动程序调用。

struct net_device *dev:这个字段描述一个网络设备,由dev代表的设备角色依赖于这个封包是即将发出的数据包还是刚被接收的数据包。即若是即将发出的数据包,这个网络设备代表发送设备,否则就是接收设备。在3c59x系列的网卡驱动程序中。vortex_rx函数代码片段为:

有些网络功能允许一些设备按组集合起来代表一个虚拟的接口,有一个虚拟设备驱动程序提供接口服务。当该设备驱动程序被调用时,dev参数会指向此虚拟设备的net_device结构。但该驱动程序会从其组中选择一个特定的物理设备,然后将dev参数指向真实物理设备的net_device结构。因此,这种情况下,在包的处理期间传输设备的指针可能会变化。

sk_buff_data_t transport_header:
sk_buff_data_t network_header:
sk_buff_data_t mac_header:

这些是指向TCP/IP协议栈的协议报头,其值是偏移地址还是指针取决于系统
NET_SKBUFF_DATA_USES_OFFSET是否生效。这些字段分别代表传输层、网络层和链路层的报头位置。当接受一个数据包时,负责处理第n层报头的函数,会从第n-1层接收缓冲区,而该缓冲区的skb->data指向第n层报头的位置,同时将其对应层的指针进行初始化(如,网络层会将network_header字段指向网络层报头位置),因为当下一层进行处理时,skb->data会被设置指向其对应层报头的位置。

unsigned long _skb_refdst:这个字段由路由子系统使用。

char cb[48]:这是一个控制缓冲区(control buffer),也就是每层的私有存储空间。48个字节足以容纳每层所需的私有数据。在每一层都是通过宏进行访问的。如在tcp.h中,TCP使用这个空间来存储一个tcp_skb_cb数据结构:

struct tcp_skb_cb {
union {
struct inet_skb_parm    h4;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
struct inet6_skb_parm    h6;
#endif
} header;    /* For incoming frames        */
__u32        seq;        /* Starting sequence number    */
__u32        end_seq;    /* SEQ + FIN + SYN + datalen    */
__u32        when;        /* used to compute rtt's    */
__u8        flags;        /* TCP header flags.        */
__u8        sacked;        /* State flags for SACK/FACK.    */
#define TCPCB_SACKED_ACKED    0x01    /* SKB ACK'd by a SACK block    */
#define TCPCB_SACKED_RETRANS    0x02    /* SKB retransmitted        */
#define TCPCB_LOST        0x04    /* SKB is lost            */
#define TCPCB_TAGBITS        0x07    /* All tag bits            */
 
#define TCPCB_EVER_RETRANS    0x80    /* Ever retransmitted frame    */
#define TCPCB_RETRANS        (TCPCB_SACKED_RETRANS|TCPCB_EVER_RETRANS)
 
__u32        ack_seq;    /* Sequence number ACK'd    */
};
 
#define TCP_SKB_CB(__skb)    ((struct tcp_skb_cb *)&((__skb)->cb[0]))

在TCP子系统的net/ipv4/tcp_ipv4.c文件中,tcp_v4_rcv函数对这个结构进行了填充:

th = tcp_hdr(skb);
iph = ip_hdr(skb);

TCP_SKB_CB(skb)->seq = ntohl(th->seq);
TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
skb->len - th->doff * 4);
TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
TCP_SKB_CB(skb)->when = 0;
TCP_SKB_CB(skb)->flags = iph->tos;
TCP_SKB_CB(skb)->sacked = 0;

union {
__wsum csum;
struct {
__u16 csum_start;
__u16 csum_offset;
};
};
__u8 ip_summed:2

这两个字段表示校验和(checksum)以及相关联的状态标志

__u8 cloned:1 这个字段置位,表示该结构是另一个sk_buff缓冲区的克隆

__u8 pkt_type:3 这个字段根据报文IP层的目的地址进行设置。可能的取值在include/linux/if_packet.h中。对于Ethernet设备而言,这个参数由eth_type_trans函数进行设置。
#define PACKET_HOST 0 到本机 /* To us */
#define PACKET_BROADCAST 1 广播包 /* To all */
#define PACKET_MULTICAST 2 本机是该接口已注册的多播地址之一 /* To group */
#define PACKET_OTHERHOST 3 到其他主机,若本机转发功能被启用,将转发该包,否则丢弃 /* To someone else */
#define PACKET_OUTGOING 4 这个数据包正在被发送 /* Outgoing of any type */
/* These ones are invisible by user level */
#define PACKET_LOOPBACK 5 传送给回环设备,由于此标志,当处理回环设备时,内核可以跳过一些真实设备所需的操作 /* MC/BRD frame looped back */
#define PACKET_FASTROUTE 6 由Fastroute功能路由封包。2.6内核不再支持Fastroute /* Fastrouted frame */

__u32 priority:这个字段表示正在被传输或转发的数据包QoS服务质量的等级。若包邮本地产生,将由套接字层定义优先级的值。若包在被转发将由ip_forward函数调用rt_tos2priority根据IP报头本身的ToS服务类型字段的值进行定义。 www.it165.net
skb->priority = rt_tos2priority(iph->tos);

__be16 protocol:从链路层来看,就是在下一个较高层使用的协议,如IP、IPv6和ARP等。每种协议都有自己的函数处理例程来处理输入数据包,因此驱动程序使用这个字段来通知上层该使用的处理例程。每个驱动程序都会调用netif_rx来启动上面的网络分层处理例程,所以在该函数调用之前protocol字段必须被初始化。

功能字段

Linux内核是模块化的,所以当内核编译时选择了某些功能,某些字段才会包含在sk_buff数据结构中:

__u8 nfctinfo:3
__u8 nf_trace:1
#if defined(CONFIG_NF_CONNTRACK) || defined(
CONFIG_NF_CONNTRACK_MODULE)

struct nf_conntrack *nfct;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
struct nf_bridge_info *nf_bridge;
#endif
这些参数都是Netfilter架构使用的,关于连接跟踪信息等

#ifdef CONFIG_XFRM
struct sec_path *sp;
#endif
这个由IPsec协议组使用,以记录转换信息。

#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
__u16 tc_verd; /* traffic control verdict */
#endif
#endif
这两个字段由流量控制功能使用

在内核中有许多很短简单的函数来操作sk_buff结构,在linux/skbuff.h和net/core/skbuff.c源文件中,几乎所有函数都有两个版本,类似do_something和__do_something。通常来讲,第一种是封装函数,增加了一些额外的参数合理性检查或在调用第二种函数前加入上锁机制。

skb_put:在tail偏移后面扩展n个字节的空间,但不会超过end偏移的限制空间。返回扩展空间的第一个字节指针

skb_push:在data指针前面扩展n个字节的空间,但不能超过head指针限制。返回新的data指针

skb_pull:将data指针后移n个字节,将来再使用skb_push操作就会将这n个字节的空间内容覆盖,返回新的data指针

skb_reserve:在缓冲区头部保留n个字节的空间,这个操作只允许对空的缓冲区进行操作

分配内存

定义在skbuff.c源文件猪的alloc_skb是分配缓冲区的主要函数。数据缓冲区和sk_buff结构是两个不同的实例,建议一个缓冲区会涉及两次内存分配,分配数据缓冲区和分配sk_buff结构。

alloc_skb通过调研kmem_cache_alloc函数从一个缓存中取得一个sk_buff结构,然后调研kmalloc分配一个数据缓冲区:

在调用kmalloc前,size参数会被SKB_DATA_ALIGN宏进行调整强制对齐。alloc_skb函数会对sk_buff结构中的元素进行参数初始化,其中几个参数的值如下图:

在数据缓冲区的底端的skb_shared_info数据结构主要用于处理一些IP分片。

dev_alloc_skb是由设备驱动程序使用的缓冲区分配函数,应该是在中断模式中执行。此函数是alloc_skb的一个封装函数,为了优化在申请大小上加了16个字节。而且由于此函数是由中断事件处理函数调用的,所以会要求原子操作(GFP_ATOMIC):

struct sk_buff *dev_alloc_skb(unsigned int length)
{
/*
* There is more code here than it seems:
* __dev_alloc_skb is an inline
*/
return __dev_alloc_skb(length, GFP_ATOMIC);
}
static inline struct sk_buff *__dev_alloc_skb(unsigned int length, gfp_t gfp_mask)
{
struct sk_buff *skb = alloc_skb(length + NET_SKB_PAD, gfp_mask);
if (likely(skb))
skb_reserve(skb, NET_SKB_PAD);
return skb;
}

释放内存

kfree_skb和dev_kfree_skb函数是一样的,都是在skb的引用计数为1后,再调用__skb_free函数释放data数据缓冲区和sk_buff结构。

void kfree_skb(struct sk_buff *skb)
{
if (unlikely(!skb))
return;
if (likely(atomic_read(&skb->users) == 1))
smp_rmb();
else if (likely(!atomic_dec_and_test(&skb->users)))
return;
trace_kfree_skb(skb, __builtin_return_address(0));
__kfree_skb(skb);
}

数据预留和对齐

skb_reserve会在缓冲区的头部预留一些空间,通常允许插入一个报头或者强迫数据对齐到某个边界。此函数会同时移动data和tail指针,所以这个函数一般在分配完数据缓冲区后立即调用。

在以太网卡驱动程序drivers/net/3c59x.c源文件中,vortex_rx函数在将任何数据存储到刚分配的缓冲区之前,都会调用下面的命令:

skb_reserve(skb, 2); /* Align IP on 16 byte boundaries */

这样,在将Ethernet帧拷贝到数据缓冲区中后,IP报头将从缓冲区开始按16字节边界对齐。

发送数据时,使用skb_reserve为下层网络保留报头空间的过程:

a.当TCP被请求发送数据时,会根据一些准则如TCP MSS(Max Segment Size),支持分散-聚集IO(Scatter-gather IO)等分配一个缓冲区;

b.TCP会在缓冲区头部调用skb_reserve保留足够的空间,以容纳所有层TCP、IP、链路层的报头。参数MAX_TCP_HEADER是所有层报头的总和,并且要考虑最坏的情况,所以会为每个层预留最大可能的报头。

c.将TCP有效载荷拷贝到缓冲区。TCP有效载荷可能以不同的方式组织,如可以作为多个片段来存储。

d.TCP添加报头

e.TCP层将缓冲区传给IP层,IP层也同样添加报头

f.IP层将IP包传递给链路层,链路层增加链路层帧头。

当缓冲区往下传播经过网络协议栈时,每个协议会将skb->data往下传,并将其报头拷贝进来,然后更新skb->len。

skb_push将一个数据块添加到缓冲区开端,skb_put将一个数据块添加到尾端,这些函数都没有真正将数据添加进缓冲区,只是简单地移动指向头和尾的指针,需要其他函数将数据复制进来。skb_pull将data指针后移,将一个数据块从缓冲区头部删除。

skb_shared_info数据结构

在数据缓冲区尾端的skb_shared_info数据结构,来保持此数据块的附加信息。这个数据结构在标记缓冲区尾部的end指针之后。

/* This data is invariant across clones and lives at
* the end of the header data, ie. at skb->end.
*/
struct skb_shared_info {
unsigned short nr_frags;
unsigned short gso_size;
/* Warning: this field is not always filled in (UFO)! */
unsigned short gso_segs;
unsigned short gso_type;
__be32 ip6_frag_id;
__u8 tx_flags;
struct sk_buff *frag_list;
struct skb_shared_hwtstamps hwtstamps;
 
/*
* Warning : all fields before dataref are cleared in __alloc_skb()
*/
atomic_t dataref;
 
/* Intermediate layers must ensure that destructor_arg
* remains valid until skb destructor */
void * destructor_arg;
/* must be last field, see pskb_expand_head() */
skb_frag_t frags[MAX_SKB_FRAGS];
};

其中dataref代表数据块的用户数,在缓冲区的克隆和拷贝中会用到。nr_frags、frag_list、frags用于处理IP片段。skb_is_nonlinear函数用于检查缓冲区是否为片段,而skb_linearize用于将几个片段压合为一个缓冲区,将引发拷贝从而使性能下降。

sk_buff结构中没有指向skb_shared_info结构的字段,需要通过end指针来访问:

#define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB))

缓冲区的克隆和拷贝

当一个缓冲区需要被特别处理时,可能需要修改sk_buff描述符的内容,但内核不需要完全拷贝sk_buff结构和相关联的数据缓冲区。为了提高效率,内核可以只克隆原始值,也就是拷贝sk_buff结构,然后增加数据缓冲区的引用计数,防止被释放。

当一个数据包需要被传输给多个接受者,如协议处理例程或多个网络分流器时,就需要使用数据包克隆技术。

sk_buff的克隆没有链接到任何表,而且也没有引用套接字的拥有者。skb->cloned字段在克隆的和原来的结构猪都设置为1.克隆的skb->users设置为1,但对数据缓冲区的引用计数会递增,因为又有一个sk_buff结构指向这个数据区。

skb_clone函数可用于检查一个skb缓冲区的克隆状态。

skb_share_check函数检查skb->users引用计数是否为1,当不为1时,则说明这个skb是共享的,克隆一个新的skb返回,同时将原来的skb引用计数减一。否则直接返回原来的skb。

当一个缓冲区被克隆是,数据区块的内容不能修改,因此访问该数据的代码不需要上锁机制。当若函数不仅需要修改sk_buff结构体的字段,还需要修改数据时,就必须连数据缓冲区一起克隆。这种情况有两种选择:若只需要修改skb->head和skb->end区域的数据内容,则可以使用pskb_copy只克隆这个区域。若需要修改分片数据块中的内容,则需要使用skb_copy将分片区域的数据一起克隆。在skb_shared_info结构中也可以包含一个sk_buff结构列表frag_list。

以下分别为pskb_copy和skb_copy函数的操作结果:

常见sk_buff元素队列的管理函数

  • skb_queue_head_init:对skb_buff_head队列头指针进行初始化
  • skb_queue_head、skb_queue_tail:把一个sk_buff添加到队列头或尾
  • skb_dequeue、skb_dequeue_tail:将一个sk_buff从队列头或尾移除
  • skb_queue_purge:把队列变为空队列
  • skb_queue_walk:遍历队列中的每个元素

这类函数必须以原子方式执行,所以会获取sk_buff_head队列头结构的自旋锁

void skb_queue_head(struct sk_buff_head *list, struct sk_buff *newsk)
{
unsigned long flags;
 
spin_lock_irqsave(&list->lock, flags);
__skb_queue_head(list, newsk);
spin_unlock_irqrestore(&list->lock, flags);
}

net_device结构

net_device结构存储特定网络设备的所有信息。不管真实设备如网卡还是虚拟设备如网桥,都有这样一个结构。

net_device这个结构也非常庞大,包括很多特定功能的参数,以及其他来自不同分层的参数,因此这个结构在Linux版本中的变化也很快。

net_device结构的字段大致分为以下几类:

  • 配置字段
  • 统计数据
  • 设备状态
  • 列表管理
  • 流量管理
  • 功能专用
  • 通用字段
  • 函数指针

标识符

net_device有三个标识符

int ifindex:独一无二的ID,当设备以dev_new_index注册时分判给每个设备的。

int iflink:这个字段主要是由虚拟隧道设备使用,可用于标识隧道另一端的真实设备
unsigned short dev_id:目前在zSeries ISA NIC上由IPv6所用,此字段用于区别可由不同OS同时共享的同一种设备的诸多虚拟实例。

配置字段

有些配置字段由内核根据网络设备的种类给定一个默认值,而其他字段留给驱动程序填写。驱动程序可以改变默认值,而有些字段可以再执行期间通过像ifconfig和ip的命令进行改变。而有些字段如base_addr、if_port、dma和irq在设备模块加载时由用户设置的,虚拟设备不会使用这些参数。

char name[IFNAMSIZ] 设备名称,如eth0

unsigned long mem_start

unsigned long mem_end 这些字段描述设备所用的共享内存,用于设备与内恶化通信。其初始化和访问只会在设备驱动程序内进行

unsigned long base_addr 设备自身内存映射到I/O内存的起始地址

unsigned int irq 设备用于与内核通信的中断编号,此值可由多个设备共享。驱动程序使用request_irq函数分配此值,使用free_irq释放

unsigned char if_port 此接口所使用的端口类型

unsigned char dma 设备所使用的DMA通道。为了从内核获取和释放DMA通道,文件定义了request_dma和free_dma。取得DMA通道后,使用enable_dma和disable_dma开启和关闭dma通道。这些函数由ISA设备使用,PCI设备采用其他方法,因而不需要这些函数。有些总线不使用DMA。

unsigned short flags

unsigned short gflags

unsigned short priv_flags

flags字段中的某些位代表网络设备的功能如IFF_MULTICAST,而有些位表示状态的改变如IFF_UP或IFF_RUNNING,在include/linux/if.h中有这些标识的完整列表。设备驱动程序在初始化期间设置这些功能,而这些状态标识由内核管理。ifconfig命令可以查看网络设备的一些状态。

priv_flags用于存储用户空间不可见的标识,目前此字段主要由VLAN和Bridge虚拟设备使用
gflags几乎已不再使用,只是由于兼容性而存在。

这些标识可以通过dev_change_flags函数进行修改

int features 表示网卡当前激活的功能,便于与CPU通信。如能不能对高端内存做DMA或硬件能不能对所有封包做检验和工作。此参数由设备驱动程序初始化。在net_device数据结构定义猪可以找到NETIF_F_XXX特征功能列表。

unsigned int mtu MTU代表最大传输单元,表示设备能处理的帧的最大尺寸。

unsigned short type 设备所属类型,在include/linux/if_arp.h包含可能类型的列表

unsigned short hard_header_len 以字节为单位的设备头的大小,如Ethernet报头为14字节。每个设备头的长度定义在该设备头的头文件中。如对于Ethernet,ETH_HLEN就定义在include/linux/if_ether.h中

unsigned char broadcast[MAX_ADDR_LEN] 链路层广播地址

unsigned char dev_addr[MAX_ADDR_LEN]

unsigned char addr_len 设备链路层地址和长度。addr_len的值与设备的类型相关,如Ethernet的地址为6字节

int promiscuity 混杂模式

接口类型和端口

有些设备有一个以上的连接器(如BNC+RJ45),允许用户根据需求选择其中之一使用,if_port这个参数用于设备设备的端口类型。当设备没有配置选择特定的端口类型,则会选择默认的端口类型。

混杂模式

一个系统可以接收在一条共享光缆上传播的所有帧,而不仅仅是地址直接指定给该系统的帧,则说明其处于混杂模式。

net_device结构的promiscuity计算器,表示设备处于混杂模式中。采用计数器而不是简单标识,是因为多个客户程序可能都会要求混杂模式。因此,请求进入混杂模式就递增这个计数器,退出则递减。除非计数器为0,否则该设备不会退出混杂模式。通过dev_set_promiscuity函数对此字段进行操作。

当promiscuity为非零时,flags的IFF_PROMISC标志位也会设置,并由此接口的函数进行检查。

static int __dev_set_promiscuity(struct net_device *dev, int inc)
{
unsigned short old_flags = dev->flags;
uid_t uid;
gid_t gid;

ASSERT_RTNL();

dev->flags |= IFF_PROMISC;
dev->promiscuity += inc;
if (dev->promiscuity == 0) {
/*
* Avoid overflow.
* If inc causes overflow, untouch promisc and return error.
*/
if (inc < 0)
dev->flags &= ~IFF_PROMISC;
else {
dev->promiscuity -= inc;
printk(KERN_WARNING "%s: promiscuity touches roof, "
"set promiscuity failed, promiscuity feature "
"of device might be broken.\n", dev->name);
return -EOVERFLOW;
}
}
………………
}

统计数据

net_device结构中stats参数用于统计网络数据包信息。struct net_device_stats 结构包含了所有网络设备共有的统计数据,定义在include/linux/netdevice.h头文件中,通过get_stats方法获取网卡状态信息。

struct net_device_stats {
unsigned long rx_packets;
unsigned long tx_packets;
unsigned long rx_bytes;
unsigned long tx_bytes;
unsigned long rx_errors;
unsigned long tx_errors;
unsigned long rx_dropped;
unsigned long tx_dropped;
unsigned long multicast;
unsigned long collisions;
unsigned long rx_length_errors;
unsigned long rx_over_errors;
unsigned long rx_crc_errors;
unsigned long rx_frame_errors;
unsigned long rx_fifo_errors;
unsigned long rx_missed_errors;
unsigned long tx_aborted_errors;
unsigned long tx_carrier_errors;
unsigned long tx_fifo_errors;
unsigned long tx_heartbeat_errors;
unsigned long tx_window_errors;
unsigned long rx_compressed;
unsigned long tx_compressed;
};

无线设备的这些信息通过iw_statistics结构进行保存,通过get_wireless_stats函数获取。

struct iw_statistics
{
__u16 status; /* Status
* - device dependent for now */

struct iw_quality qual; /* Quality of the link
* (instant/mean/max) */
struct iw_discarded discard; /* Packet discarded counts */
struct iw_missed miss; /* Packet missed counts */
};

设备状态

为了控制与NIC之间的交互,每个设备驱动程序都必须维护一些信息。在SMP系统中,内核也必须确保不同CPU对同一个设备并发访问的正确处理。net_device结构猪的以下几个字段就是专门用于这些类型的信息:

unsigned long state;由网络队列子系统使用的一组标识。其索引值是enum netdev_state_t中的常数。个别位的设置和清除都使用函数set_bit和clear_bit函数。这两个函数通常都会被一个更明确的函数封装起来使用。如停止一个设备队列时,子系统调用netif_stop_queue函数:

enum netdev_state_t {
__LINK_STATE_START,
__LINK_STATE_PRESENT,
__LINK_STATE_NOCARRIER,
__LINK_STATE_LINKWATCH_PENDING,
__LINK_STATE_DORMANT,
};

static inline void netif_stop_queue(struct net_device *dev)
{
netif_tx_stop_queue(netdev_get_tx_queue(dev, 0));
}

static inline void netif_tx_stop_queue(struct netdev_queue *dev_queue)
{
if (WARN_ON(!dev_queue)) {
pr_info("netif_stop_queue() cannot be called before register_netdev()\n");
return;
}
20.set_bit(__QUEUE_STATE_XOFF, &dev_queue->state);
}

/* register/unregister state machine */
enum { NETREG_UNINITIALIZED=0,
NETREG_REGISTERED, /* completed register_netdevice */
NETREG_UNREGISTERING, /* called unregister_netdevice */
NETREG_UNREGISTERED, /* completed unregister todo */
NETREG_RELEASED, /* called free_netdev */
NETREG_DUMMY, /* dummy device for NAPI poll */
} reg_state:16;

设备的注册状态

/*
* trans_start here is expensive for high speed devices on SMP,
* please use netdev_queue->trans_start instead.
*/
unsigned long trans_start; /* Time (in jiffies) of last Tx */

这个表示最近的一个帧传输启动的时间。设备驱动程序会在传输前设置此值。如在一段给定的时间后传输没有完成。这个字段用于检测适配卡的问题。在这种情况下,驱动程序通常复位适配卡。

unsigned long last_rx 最后一个封包到达的时间

struct net_device *master 有些协议运行一组设备群集起来作为单一设备。这些协议包括EQL、Bonding、以及流量控制的TEQL队列规则。群组中的一个设备会被选定为所谓的主设备。这个字段是一个指针,指向主设备的net_device结构。若此接口不是这类群组中的成员之一,则此指针是NULL。

spinlock_t xmit_lock

int xmit_lock_owner

xmit_lock锁使驱动程序函数hard_start_smit的访问串行化。每个CPU一次只能对任何给定的一个设备做一次传输。xmit_lock_owner是持有该锁的CPU的ID,对单系统而言这个值总为0,而在SMP系统中该锁没有被取走时,其值为-1。当设备驱动程序支持时,也可以做无锁的传输。

void *atalk_ptr

void *ip_ptr

void *dn_ptr

void *ip6_ptr

void *ec_ptr

void *ax25_ptr

这6个字段是指针,指向特定协议专用的数据结构,而每个结构包含一些该协议私有的参数。如ip_ptr指向一个类型为in_device的数据结构,其中包含各种不同的与IPv4相关的参数。其中有该接口上所配置的IP地址列表。

列表管理

net_device数据结构保存在链表和两个hash表中。

/* device name hash chain */
struct hlist_node name_hlist;

/* device index hash chain */
struct hlist_node index_hlist;

链路层多播

多播是一种把数据传递给多个接收者的机制。多播可以再网络层和链路层中使用。链路层的多播需要在链路层报头猪使用特殊地址和控制信息。Ethernet本身就支持多播。

利用一个特定位把多播地址和其他范围的地址区分开来。当一个接口被要求加入多个多播群组时,则该接口只简单监听所有多播地址。net_device结构猪的flags之一就是用于表示该设备是否监听所有地址,决定何时设置和清除此标志,由allmulti字段控制。每个设备都会为其监听的每个链路层多播地址保存一个dev_mc_list结构的实例。链路层多播地址可以分别用dev_mc_add和dev_mc_delete函数添加或删除。

struct netdev_hw_addr_list mc; /* Multicast mac addresses */指向此设备的dev_mc_list结构列表表头的指针和数目

int allmulti 为非0时,引起此设备监听所有的多播地址。allmulti如promiscuity一样,是一个计数器而不是简单的布尔值。当此变量由0变为非0时,就会调用dev_set_allmulti函数,以指示该端口监听所有的多播地址。当allmulti变为0时,就会发生相反的事情。

流量管理

流量管理的内核配置选项为Device drivers->Networking support->Networking options->QoS and/or fair queueing

net_device结构中的相关字段为:

struct Qdisc *qdisc; 用于管理入口和出口的封包队列
unsigned long tx_queue_len; /* Max frames per queue allowed */队列允许的最大长度

注意,所有队列长度为0的设备都是虚拟设备,虚拟设备依赖相关联的真实设备区做所有的队列化工作,回环设备除外。

通用字段

int watchdog_timeo; /* used by dev_watchdog() */
struct timer_list watchdog_timer; //看门狗定时器

const struct iw_handler_def * wireless_handlers;

struct iw_public_data * wireless_data;

无线设备使用的参数和函数指针

/* delayed register/unregister */
struct list_head todo_list;

网络设备的注册和注销是以两个步骤完成的。todo_list用于处理第二个步骤

相关推荐

程序员:JDK的安装与配置(完整版)_jdk的安装方法

对于Java程序员来说,jdk是必不陌生的一个词。但怎么安装配置jdk,对新手来说确实头疼的一件事情。我这里以jdk10为例,详细的说明讲解了jdk的安装和配置,如果有不明白的小伙伴可以评论区留言哦下...

Linux中安装jdk并配置环境变量_linux jdk安装教程及环境变量配置

一、通过连接工具登录到Linux(我这里使用的Centos7.6版本)服务器连接工具有很多我就不一一介绍了今天使用比较常用的XShell工具登录成功如下:二、上传jdk安装包到Linux服务器jdk...

麒麟系统安装JAVA JDK教程_麒麟系统配置jdk

检查检查系统是否自带java在麒麟系统桌面空白处,右键“在终端打开”,打开shell对话框输入:java–version查看是否自带java及版本如图所示,系统自带OpenJDK,要先卸载自带JDK...

学习笔记-Linux JDK - 安装&amp;配置

前提条件#检查是否存在JDKrpm-qa|grepjava#删除现存JDKyum-yremovejava*安装OracleJDK不分系统#进入安装文件目...

Linux新手入门系列:Linux下jdk安装配置

本系列文章是把作者刚接触和学习Linux时候的实操记录分享出来,内容主要包括Linux入门的一些理论概念知识、Web程序、mysql数据库的简单安装部署,希望能够帮到一些初学者,少走一些弯路。注意:L...

测试员必备:Linux下安装JDK 1.8你必须知道的那些事

1.简介在Oracle收购Sun后,Java的一系列产品就被整合到Oracle官网中,打开官网乍眼一看也不知道去哪里下载,还得一个一个的摸索尝试,而且网上大多数都是一些Oracle收购Sun前,或者就...

Linux 下安装JDK17_linux 安装jdk1.8 yum

一、安装环境操作系统:JDK版本:17二、安装步骤第一步:下载安装包下载Linux环境下的jdk1.8,请去官网(https://www.oracle.com/java/technologies/do...

在Ubuntu系统中安装JDK 17并配置环境变量教程

在Ubuntu系统上安装JDK17并配置环境变量是Java开发环境搭建的重要步骤。JDK17是Oracle提供的长期支持版本,广泛用于开发Java应用程序。以下是详细的步骤,帮助你在Ubuntu系...

如何在 Linux 上安装 Java_linux安装java的步骤

在桌面上拥抱Java应用程序,然后在所有桌面上运行它们。--SethKenlon(作者)无论你运行的是哪种操作系统,通常都有几种安装应用程序的方法。有时你可能会在应用程序商店中找到一个应用程序...

Windows和Linux环境下的JDK安装教程

JavaDevelopmentKit(简称JDK),是Java开发的核心工具包,提供了Java应用程序的编译、运行和开发所需的各类工具和类库。它包括了JRE(JavaRuntimeEnviro...

linux安装jdk_linux安装jdk软连接

JDK是啥就不用多介绍了哈,外行的人也不会进来看我的博文。依然记得读大学那会,第一次实验课就是在机房安装jdk,编写HelloWorld程序。时光飞逝啊,一下过了十多年了,挣了不少钱,买了跑车,娶了富...

linux安装jdk,全局配置,不同用户不同jdk

jdk1.8安装包链接:https://pan.baidu.com/s/14qBrh6ZpLK04QS8ogCepwg提取码:09zs上传文件解压tar-zxvfjdk-8u152-linux-...

运维大神教你在linux下安装jdk8_linux安装jdk1.7

1.到官网下载适合自己机器的版本。楼主下载的是jdk-8u66-linux-i586.tar.gzhttp://www.oracle.com/technetwork/java/javase/downl...

window和linux安装JDK1.8_linux 安装jdk1.8.tar

Windows安装JDK1.8的步骤:步骤1:下载JDK打开浏览器,找到JDK下载页面https://d.injdk.cn/download/oraclejdk/8在页面中找到并点击“下载...

最全的linux下安装JavaJDK的教程(图文详解)不会安装你来打我?

默认已经有了linux服务器,且有root账号首先检查一下是否已经安装过java的jdk任意位置输入命令:whichjava像我这个已经安装过了,就会提示在哪个位置,你的肯定是找不到。一般我们在...

取消回复欢迎 发表评论: