主要参考了《深入linux内核架构》和《精通Linux内核网络》相关章节
网络访问层。该层主要负责在计算机之间传输信息,与网卡的设备驱动程序直接协作。
前面讲述了Linux内核中网络子系统的结构,现在我们把注意力转向网络实现的第一层,即网络访问层(主机到网络层 / 数据链路层)。该层主要负责在计算机之间传输信息,与网卡的设备驱动程序直接协作。
在内核中,每个网络设备都表示为net_device结构的一个实例。在分配并填充该结构的一个实例之后,必须用net/core/dev.c中的register_netdev函数将其注册到内核。该函数完成一些初始化任务,并将该设备注册到通用设备机制内。这会创建一个sysfs项(参见10.3节)/sys/class/net/
root@meitner # ls -l /sys/class/net
total 0
lrwxrwxrwx 1 root root 0 2008-03-09 09:43 eth0 -> ../../devices/pci0000:00/0000:00:1c.5/
0000:02:00.0/net/eth0
lrwxrwxrwx 1 root root 0 2008-03-09 09:42 lo -> ../../devices/virtual/net/lo
在详细讨论struct net_device的内容之前,先阐述一下内核如何跟踪可用的网络设备,以及如何查找特定的网络设备。照例,这些设备不是全局的,而是按命名空间进行管理的。回想一下,每个命名空间(net实例)中有如下3个机制可用。
net_device结构包含了与特定设备相关的所有信息。该结构的定义有200多行代码,是内核中最庞大的结构。
net_device结构体存储着网络设备的所有信息,每个设备都有这种结构。所有设备的net_device结构放在一个全局变量dev_base所有全局列表中。和sk_buff一样,整体结构相当庞大的。结构体中有一个next指针,用来连接系统中所有网络设备。内核把这些连接起来的设备组成一个链表,并由全局变量dev_base指向链表的第一个元素。
网络设备net_device结构体包含主要设备参数
include\linux\netdevice.h
struct net_device - The DEVICE structure.
Actually, this whole structure is a big mistake. It mixes I/O data with strictly “high-level” data, and it has to know about almost every data structure used in the INET module.
struct net_device {
char name[IFNAMSIZ];
struct hlist_node name_hlist; /* 设备名散列链表的链表元素 */
char *ifalias; // SNMP(简单网络管理协议) 别名
/* I/O相关字段 */
unsigned long mem_end; /* 共享内存结束位置 */
unsigned long mem_start; /* 共享内存起始位置 */
unsigned long base_addr; /* 设备I/O地址 */
unsigned int irq; /* 设备IRQ编号 */
atomic_t carrier_changes;
unsigned long state;
struct list_head dev_list; // The global list of network devices
struct list_head napi_list; // List entry used for polling NAPI devices
struct list_head unreg_list;
struct list_head close_list;
struct list_head ptype_all;
struct list_head ptype_specific;
struct {
struct list_head upper;
struct list_head lower;
} adj_list; // Directly linked devices, like slaves for bonding
netdev_features_t features; // 当前活跃的设备功能
netdev_features_t hw_features; // User-changeable features
netdev_features_t wanted_features; // User-requested features
netdev_features_t vlan_features; // VLAN 设备可继承的特性掩码
/* 封装设备继承的特征掩码,该字段指示硬件能够执行的封装卸载,驱动程序需要适当地设置它们。*/
netdev_features_t hw_enc_features;
netdev_features_t mpls_features;
netdev_features_t gso_partial_features;
int ifindex; /* 接口索引。唯一的设备标识符*/
int group;
struct net_device_stats stats;
atomic_long_t rx_dropped;
atomic_long_t tx_dropped;
atomic_long_t rx_nohandler;
#ifdef CONFIG_WIRELESS_EXT
const struct iw_handler_def *wireless_handlers;
struct iw_public_data *wireless_data;
#endif
const struct net_device_ops *netdev_ops;
const struct ethtool_ops *ethtool_ops;
#ifdef CONFIG_NET_SWITCHDEV
const struct switchdev_ops *switchdev_ops;
#endif
#ifdef CONFIG_NET_L3_MASTER_DEV
const struct l3mdev_ops *l3mdev_ops;
#endif
#if IS_ENABLED(CONFIG_IPV6)
const struct ndisc_ops *ndisc_ops;
#endif
#ifdef CONFIG_XFRM
const struct xfrmdev_ops *xfrmdev_ops;
#endif
const struct header_ops *header_ops;
unsigned int flags;
unsigned int priv_flags;
unsigned short gflags;
unsigned short padded;
unsigned char operstate;
unsigned char link_mode;
unsigned char if_port;
unsigned char dma;
unsigned int mtu; // 网络设备接口的最大传输单元
unsigned int min_mtu;
unsigned int max_mtu;
unsigned short type; // 接口硬件类型
unsigned short hard_header_len; // 硬件接口头长度,最大硬件首部长度
unsigned char min_header_len; // 最小硬件首部长度
unsigned short needed_headroom;
unsigned short needed_tailroom;
/* Interface address info. */
unsigned char perm_addr[MAX_ADDR_LEN];
unsigned char addr_assign_type;
unsigned char addr_len;
unsigned short neigh_priv_len;
unsigned short dev_id;
unsigned short dev_port;
spinlock_t addr_list_lock;
unsigned char name_assign_type;
bool uc_promisc; // 网络设备单播模式
struct netdev_hw_addr_list uc;
struct netdev_hw_addr_list mc;
struct netdev_hw_addr_list dev_addrs;
#ifdef CONFIG_SYSFS
struct kset *queues_kset;
#endif
unsigned int promiscuity; // 网络设备混杂模式
unsigned int allmulti; // 网络设备全组播模式
...
/*
* Cache lines mostly used on receive path (including eth_type_trans())
*/
/* Interface address info used in eth_type_trans() */
unsigned char *dev_addr; // 网络设备接口的MAC地址
...
unsigned char broadcast[MAX_ADDR_LEN]; // 硬件多播地址
...
注册网络设备
每个网络设备都按照如下过程注册。
(1) alloc_netdev分配一个新的struct net_device实例,一个特定于协议的函数用典型值填充该结构。对于以太网设备,该函数是ether_setup。其他的协议(这里不详细介绍)会使用形如XXX_setup的函数,其中XXX可以是fddi(fiber distributed data interface,光纤分布式数据接口)、tr(token ring,令牌环网)、ltalk(指Apple LocalTalk)、hippi(high-performance parallel interface,高
性能并行接口)或fc(fiber channel,光纤通道)。
内核中的一些伪设备在不绑定到硬件的情况下实现了特定的接口,它们也使用了net_device框架。例如,ppp_setup根据PPP协议初始化设备。内核源代码中还可以找到几个XXX_setup函数。
(2) 在struct net_device填充完毕后,需要用register_netdev或register_netdevice注册。这 两 个 函 数 的 区 别 在 于 , register_netdev 可处理用作接口名称的格式串 (有限)。 在net_device->dev中给出的名称可以包含格式说明符%d。在设备注册时,内核会选择一个唯一的数字来代替%d。例如,以太网设备可以指定eth%d,而内核随后会创建设备eth0、eth1……
便 捷 函 数 alloc_etherdev(sizeof_priv) 分 配 一 个 struct net_device 实 例 , 外 加sizeof_priv字节私有数据区。回想前文可知,net_device->priv是一个指针,指向与设备相关联的特定于驱动程序的数据。此外,还调用了上面提到的ether_setup来设置特定于以太网的标准值。
linux2.x版本
如果net_device->init提供了特定于设备的初始化函数,那么内核在进一步处理之前,将先调用该函数。由dev_new_index生成在所属命名空间中唯一标识该设备的接口索引。该索引保存在net_device->ifindex中。在确保所选择的名称尚未使用,而且没有指定自相矛盾的设备特性(所支持特性的列表,请参见
分组到达内核的时间是不可预测的。所有现代的设备驱动程序都使用中断来通知内核(或系统)有分组到达。网络驱动程序对特定于设备的中断设置了一个处理例程,因此每当该中断被引发时(即分组到达),内核都调用该处理程序,将数据从网卡传输到物理内存,或通知内核在一定时间后进行处理。
几乎所有的网卡都支持DMA模式,能够自行将数据传输到物理内存。但这些数据仍然需要解释和处理,这在稍后进行。
当前,内核为分组的接收提供了两个框架。其中一个很早以前就集成到内核中了,因而称为传统方法。但与超高速网络适配器协作时,该API会出现问题,因而网络子系统的开发者已经设计了一种新的API(通常称为NAPI)。我们首先从传统方法开始,因为它比较易于理解。另外,使用旧API的适配器较多,而使用新API的较少。这没有问题,因为其物理传输速度没那么高,不需要新方法。NAPI在稍后讨论。
下图给出了在一个分组到达网络适配器之后,该分组穿过内核到达网络层函数的路径。
下面忽略DMA
因为分组是在中断上下文中接收到的,所以处理例程只能执行一些基本的任务,避免系统(或当前CPU)的其他任务延迟太长时间。
在中断上下文中,数据由3个短函数处理,执行了下列任务。
内 核 在 全 局 定 义 的 softnet_data 数 组 中 管 理 进 出 分 组 的 等 待 队 列 , 数 组 项 类 型 为softnet_data。**为提高多处理器系统的性能,对每个CPU都会创建等待队列,支持分组的并行处理。不心使用显式的锁机制来保护等待队列免受并发访问,因为每个CPU都只修改自身的队列,不会干扰其他CPU的工作。**下文将忽略多处理器相关内容,只考虑单“softnet_data等待队列”,避免过度复杂化。
struct softnet_data
{
...
struct sk_buff_head input_pkt_queue;
...
};
input_pkt_queue使用上文提到的sk_buff_head表头,对所有进入的分组建立一个链表。 netif_rx在结束工作之前将软中断NET_RX_SOFTIRQ标记为即将执行,然后退出中断上下文。
net_rx_action用作该软中断的处理程序。其代码流程图在下图给出。请记住,这里描述的是一个简化的版本。完整版包含了对高速网络适配器引入的新方法,将在下文介绍。
在一些准备任务之后,工作转移到process_backlog,该函数在循环中执行下列步骤。为简化描述,假定循环一直进行,直至所有的待决分组都处理完成,不会被其他情况中断。
(1) __skb_dequeue从等待队列移除一个套接字缓冲区,该缓冲区管理着一个接收到的分组。
(2) ==由netif_receive_skb函数分析分组类型,以便根据分组类型将分组传递给网络层的接收函数(即传递到网络系统的更高一层)。==为此,该函数遍历所有可能负责当前分组类型的所有网络层函数,一一调用deliver_skb。
接下来deliver_skb函数使用一个特定于分组类型的处理程序func,承担对分组的更高层(例如互联网络层)的处理。
netif_receive_skb也处理诸如桥接之类的专门特性,但讨论这些边角情况是不必要的,至少在平均水准的系统中,此类特性都属于边缘情况。
所有用于从底层的网络访问层接收数据的网络层函数都注册在一个散列表中,通过全局数组ptype_base实现。
新的协议通过dev_add_pack增加。各个数组项的类型为struct packet_type,定义如下:
struct packet_type {
__be16 type; /* 这实际上是htons(ether_type)的值。 */
struct net_device *dev;/* NULL在这里表示通配符 */
int (*func) (struct sk_buff *,
struct net_device *,
struct packet_type *,
struct net_device *);
...
void *af_packet_priv;
struct list_head list;
};
type指定了协议的标识符,处理程序会使用该标识符。dev将一个协议处理程序绑定到特定的网卡(NULL指针表示该处理程序对系统中所有网络设备都有效)。
func是该结构的主要成员。它是一个指向网络层函数的指针,如果分组的类型适当,将其传递给该函数。其中一个处理程序就是ip_rcv,用于基于IPv4的协议,在下文讨论。
netif_receive_skb对给定的套接字缓冲区查找适当的处理程序,并调用其func函数,将处理分组的职责委托给网络层,这是网络实现中更高的一层。
如果设备不支持过高的传输率,那么此前讨论的旧式方法可以很好地将分组从网络设备传输到内核的更高层。每次一个以太网帧到达时,都使用一个IRQ来通知内核。这里暗含着“快”和“慢”的概念。 对低速设备来说,在下一个分组到达之前,IRQ的处理通常已经结束。由于下一个分组也通过IRQ通知,如果前一个分组的IRQ尚未处理完成,则会导致问题,高速设备通常就是这样。现代以太网卡的运作高达10 000 Mbit/s,如果使用旧式方法来驱动此类设备,将造成所谓的“中断风暴”。如果在分组等待处理时接收到新的IRQ,内核不会收到新的信息:在分组进入处理过程之前,内核是可以接收IRQ的,在分组的处理结束后,内核也可以接收IRQ,这些不过是“旧闻”而已。为解决该问题,NAPI使用了IRQ和轮询的组合。
当前,几乎所有Linux网络设备驱动程序都支持该技术。NAPI是在2.5/2.6内核中首次引入的,并向后移植到了2.4.20内核。采用NAPI技术时,如果负载很高,网络设备驱动程序将在轮询模式,而不是中断驱动模式下运行。**这意味着,不会在每次接收数据包时都触发中断。相反,驱动程序会将数据包存储在缓冲区,由内核不时地向驱动程序轮询,以取回数据包。**采用NAPI技术,可提高设备在高负载下的性能。从内核3.11起,Linux新增了频繁轮询套接字(Busy Polling on Sockets )的功能,用于那些不惜以提高CPU使用率为代价而尽可能降低延迟的套接字应用程序。
NAPI的另一个优点是可以高效地丢弃分组。如果内核确信因为有很多其他工作需要处理,而导致无法处理任何新的分组,那么网络适配器(网卡)可以直接丢弃分组,无须复制到内核。
只有设备满足如下两个条件时,才能实现NAPI方法。
(1) 设备必须能够保留多个接收的分组,例如保存到DMA环形缓冲区中。下文将该缓冲区称为Rx缓冲区。
(2) 该设备必须能够**禁用用于分组接收的IRQ**。而且,发送分组或其他可能通过IRQ进行的操作,都仍然必须是启用的。
细节如下
假定某个网络适配器此前没有分组到达,但从现在开始,分组将以高频率频繁到达。这就是NAPI设备的情况,如下所述。
如果在新的分组到达时,旧的分组仍然处于处理过程中,工作不会因额外的中断而减速。**虽然对设备驱动程序(和一般意义上的内核代码)来说轮询通常是一个很差的方法,但在这里该方法没有什么不利之处:在没有分组还需要处理时,将停止轮询,设备将回复到通常的IRQ驱动的运行方式。**在没有中断支持的情况下,轮询空的接收队列将不必要地浪费时间,但NAPI并非如此。
内核以循环方式处理链表上的所有设备:内核依次轮询各个设备,如果已经花费了一定的时间来处理某个设备,则选择下一个设备进行处理。此外,某个设备都带有一个相对权重,表示与轮询表中其他设备相比,该设备的相对重要性。较快的设备权重较大,较慢的设备权重较小。由于权重指定了在一个轮询的循环中处理多少分组,这确保了内核将更多地注意速度较快的设备。
与旧的API相比,关键性的变化在于,支持NAPI的设备必须提供一个poll函数。该方法是特定于设备的,在用netif_napi_add注册网卡时指定。调用该函数注册,表明设备可以且必须用新方法处理。
static inline void netif_napi_add(struct net_device *dev,
struct napi_struct *napi,
int (*poll)(struct napi_struct *, int),
int weight);
dev指向所述设备的net_device实例,poll指定了在IRQ禁用时用来轮询设备的函数,weight指定了设备接口的相对权重。
netif_napi_add还需要另一个参数,是一个指向struct napi_struct实例的指针。该结构用于管理轮询表上的设备。其定义如下
struct napi_struct {
struct list_head poll_list;
unsigned long state;
int weight;
int (*poll)(struct napi_struct *, int);
};
轮询表通过一个标准的内核双链表实现,poll_list用作链表元素。weight和poll的语义同上文所述。
请注意,struct napi_struct经常嵌入到一个更大的结构中,后者包含了与网卡有关的、特定于驱动程序的数据。这样在内核使用poll函数轮询网卡时,可用container_of机制获得相关信息。
poll函数需要两个参数:一个指向napi_struct实例的指针和一个指定了“预算”的整数,预算表示内核允许驱动程序处理的分组数目。
我们并不打算处理真实网卡的可能的奇异之处,因此讨论一个伪函数,该函数用于一个需要NAPI的超高速适配器:
static int hyper_card_poll(struct napi_struct *napi, int budget)
{
struct nic *nic = container_of(napi, struct nic, napi);
struct net_device *netdev = nic->netdev;
int work_done;
work_done = hyper_do_poll(nic, budget);
if (work_done < budget) {
netif_rx_complete(netdev, napi);
hcard_reenable_irq(nic);
}
return work_done;
}
从napi_struct的容器获得特定于设备的信息之后,调用一个特定于硬件的方法(这里是hyper_do_poll)来执行所需要的底层操作从网络适配器获取分组,并使用像此前那样使用netif_receive_skb将分组传递到网络实现中更高的层。
hyper_do_poll最多允许处理budget个分组。该函数返回实际上处理的分组的数目。必须区分以下两种情况。
NAPI也需要对网络设备的IRQ处理程序做一些改动。这里仍然不求助于任何具体的硬件,
而介绍针对虚构设备的代码:
static irqreturn_t e100_intr(int irq, void *dev_id)
{
struct net_device *netdev = dev_id;
struct nic *nic = netdev_priv(netdev);
if(likely(netif_rx_schedule_prep(netdev, &nic->napi))) {
hcard_disable_irq(nic);
__netif_rx_schedule(netdev, &nic->napi);
}
return IRQ_HANDLED;
}
假定特定于接口的数据保存在net_device->private中,这是大多数网卡驱动程序使用的方法。
使用辅助函数netdev_priv访问该字段。
现在需要通知内核有新的分组可用。这需要如下二阶段的方法。
在讨论了为支持NAPI驱动程序需要做哪些改动之后,我们来考察一下内核需要承担的职责。
net_rx_action依旧是软中断NET_RX_SOFTIRQ的处理程序。在前一节给出了该函数的一个简化版本。随着有关NAPI的更多细节尘埃落定,现在可以讨论该函数的所有细节了。图12-13给出了其代码流程图。
本质上,内核通过依次调用各个设备特定的poll方法,处理轮询表上当前的所有设备。
设备的权重用作该设备本身的预算,即轮询的一步中可能处理的分组数目。必须确保在这个软中断的处理程序中,不会花费过多时间。
如果如下两个条件成立,则放弃处理。
(1) 处理程序已经花费了超出一个jiffie的时间。
jiffies记录了系统启动以来,经过了多少tick。
一个tick代表多长时间(取决于设备的频率),在内核的CONFIG_HZ中定义。比如CONFIG_HZ=200,则一个jiffies对应5ms时间。所以内核基于jiffies的定时器精度也是5ms。
(2) 所处理分组的总数,已经超过了netdev_budget指定的预算总值。通常,总值设置为300,但可以通过/proc/sys/net/core/netdev_budget修改。
这个预算不能与各个网络设备本身的预算混淆!
在每个轮询步之后,都从全局预算中减去处理的分组数目,如果该预算值下降到0,则退出软中断处理程序。
在轮询了一个设备之后,内核会检查所处理的分组数目,与该设备的预算是否相等。
如果相等,那么尚未获得该设备上所有等待的分组,即代码流程图中work == weight所表示的情况。内核接下来将该设备移动到轮询表末尾,在链表中所有其他设备都处理过之后,继续轮询该设备。显然,这实现了网络设备之间的循环调度。
最后,请注意旧的API是如何在NAPI上实现的。
内核的常规行为,由一个与softnet队列关联的伪网络设备控制,net/core/dev.c中的process_backlog标准函数用作poll方法。
如果没有网络适配器将其自身添加到该队列的轮询表,其中只包含这个伪适配器,那么net_rx_action的行为就是通过对process_backlog的单一调用来处理队列中的分组,而不管分组的来源设备。
套接字队列变空后,网络栈的传统工作方式如下:要么进入休眠状态,等待驱动程序将其他数据加入套接字队列,要么返回(如果它是以非阻断方式运行的)。由于中断和上下文切换,使得延迟增加了。对于愿意以CPU使用率更高换取延迟尽可能低的套接字应用程序,Linux从内核3.11起为其提供了频繁轮询套接字的功能(这种技术最初命名为低延迟套接字轮询,根据Linus的建议而更名为频繁轮询套接字)。==在将数据移交给应用程序方面,频繁轮询采用了更激进的方式。当应用程序请求更多数据,而套接字队列中没有时,网络栈将主动询问设备驱动程序。驱动程序检查新到达的数据,并经网络层(L3)将其交给套接字。==驱动程序可能会发现有其他套接字的数据,进而将这些数据也交给相应的套接字。当轮询调用返回到网络栈时,套接字代码将检查套接字接收队列中是否有未处理的新数据。
要支持频繁轮询,网络驱动程序必须提供频繁轮询方法,并将其作为net_device_ops对象的ndo_busy_poll回调函数。这个驱动程序的ndo_busy_poll回调函数必须将数据包移到网络栈,请参阅方法 ixgbe_low_latency_recv() (rivers/net/ethernet/intelixgbe/ixgbe_main.c )。这个ndo_busy_poll回调函数还必须返回已移到网络栈的数据包数。如果没有将任何数据包移到网络栈,就返回0;如果出现了问题,则返回LL_FLUSH_FAILED或LL_FLUSH_BUSY。如果驱动程序没有设置ndo_busy_poll回调函数,将按正常情况工作,而不会顿繁地轮询它。
网络设备驱动程序的主要任务如下:
接收目的地为当前主机的数据包,并将其传递给网络层(L3 ),之后再将其传递给传输层( L4)。
传输当前主机生成的外出数据包或转发当前主机收到的数据包。对于每个数据包,无论它是接收到的还是发送出去的,都需要在路由子系统中执行一次查找操作。根据路由子系统的查找结果,决定是否应对数据包进行转发以及该从哪个接口发送出去。传输当前主机生成的外出数据包或转发当前主机收到的数据包。
提供低延迟的一个重要组件是频繁轮询。有时候,当驱动程序轮询方法无功而返时,刚好会有新数据到达。这些数据会与返回到网络栈的机会失之交臂。这正是频繁轮询的用武之地。网络栈以可配置的时间间隔轮询驱动程序,从而在新数据包到达后立即获取它们。
支持主动、频繁轮询的设备驱动程序可将延迟降低到接近于硬件延迟。可将频繁轮询同时用于大量套接字,但这无法获得最佳效果。因为在一些套接字上使用频繁轮询时,将降低其他使用相同CPU核心的套接字的速度。图14-1对传统接收流程与启用了频繁轮询的套接字的接收流程进行了比较,其中不断重复的步骤如下。
一种启用频繁轮询的更佳方式是,修改应用程序,使用套接字选项SO_BUSY_POLL,这将设置套接字对象(sock结构实例)的sk_ll_usec。
通过使用这个套接字选项,应用程序可指定要启用频繁轮询的套接字,确保只有这些套接字的CPU使用率更高。其他应用程序和服务中的套接字将继续使用传统接收流程。
推荐将SO_BUSY_POLL的初始值设置为50。sysctl.net.busy_read必须设置为0,而sysctl.net.busy_poll必须根据Documentation/sysctVnet.txt的描述设置。
调整和配置频繁轮询套接字的方式有多种。
对于很多应用程序来说,使用频繁轮询套接字可以降低延迟和抖动,并提高每秒处理的事务数。然而,当系统包含过多的频繁轮询套接字时,会加剧CPU争用,进而影响性能。参数net.core.busy_poll和net.core.busy_read以及套接字选项SO_BUSY_POLL都是可调整的。可以尝试为各种应用程序中的此类设置指定不同的值,以获得最佳结果。
在网络层中特定于协议的函数通知网络访问层处理由套接字缓冲区定义的一个分组时,将发送完成的分组。
当信息从计算机发送出去时,必须注意哪些事项?除了特定协议需要完成的首部和校验和,以及由高层协议实例生成的数据之外,分组的路由是最重要的。(即使计算机只有一个网卡,内核仍然需要区分发送到外部目标的分组和针对环回接口的分组。)
因为该问题只能由更高层的协议实例决定(特别是,如果可以选择到预期目标的路由时),所以设备驱动程序假定高层协议已经做出了决策。在分组可以发送到下一个正确的计算机之前(通常不同于目标计算机,因为除非存在直接的硬件链路,否则IP分组通常通过网关发送),必须确定接收方网卡的硬件地址。这是一个复杂的过程。此时,我们假定已经知道接收方的MAC地址。网络访问层的所需的另一个首部,通常由特定于协议的函数产生。
net/core/dev.c中的dev_queue_xmit用于将分组放置到发出分组的队列上。这里将忽略这个特定于设备的队列的实现,因为它并没有揭示什么网络层的运作机制。只要知道,在分组放置到等待队列上一定的时间之后,分组将发出即可。这是通过特定于适配器的函数hard_start_xmit完成的,在每个net_device结构中都以函数指针的形式出现,由硬件设备驱动程序实现。
总结