linux 内核网络协议栈--数据从接收到IP层(二)
此处主要讲的是从数据来到,中断到最终数据包被处理的过程。
首先来介绍一下IO端口访问问题,内核提供了这样一组函数处理: /kernel/io.c中
inb( )、inw( )、inl( )函数
分别从I/O端口读取1、2或4个连续字节。 后缀“b”、“w”、“l”分别代表一个字节(8位)、一个字(16位)以及一个长整型(32 位)。inb_p( )、inw_p( )、inl_p( )
分别从I/O端口读取1、2或4个连续字节,然后执行一条 “空指令” 使CPU暂停。 p 可以理解成pauseoutb( )、outw( )、outl( )
分别向一个I/O端口写入1、2或4个连续字节。outb_p( )、outw_p( )、outl_p( )
分别向一个I/O端口写入1、2或4个连续字节,然后执行一条“空指令”指令使CPU暂停。insb( )、insw( )、insl( )
分别从I/O端口读入以1、2或4个字节为一组的连续字节序列。字节序列的长度由该函数的参数给出。outsb( )、outsw( )、outsl( )
分别向I/O端口写入以1、2或4个字节为一组的连续字节序列。
net_interrupt()
1、当一个中断来到,首先响应 net_interrupt函数
/** The typical workload of the driver:* Handle the network interface interrupts.*/
static irqreturn_t net_interrupt(int irq, void *dev_id) // 注意参数是:中断号和设备id{struct net_device *dev = dev_id;struct net_local *np;int ioaddr, status;int handled = 0;ioaddr = dev->base_addr; // 设备的IO地址 np = netdev_priv(dev); // 得到dev私有数据status = inw(ioaddr + 0); // 从端口读两个字节if (status == 0)goto out;handled = 1;if (status & RX_INTR) {/* Got a packet(s). */net_rx(dev); // 使用这个函数net_rx来获取一个数据包 -----> receive} // 这个函数下面会说到#if TX_RINGif (status & TX_INTR) { // 发送数据/* Transmit complete. */net_tx(dev); // 发送数据使用net_tx ------> transmitnp->stats.tx_packets++; // 计数 netif_wake_queue(dev); // 处理结束,唤醒下一个队列中等待者 }
#endif if (status & COUNTERS_INTR) { /* Increment the appropriate 'localstats' field. */ np->stats.tx_window_errors++; }
out: return IRQ_RETVAL(handled); // 返回中断
}
net_rx()
2、下面需要看一下接收数据包函数net_rx
/* We have a good packet(s), get it/them out of the buffers. */
static void
net_rx(struct net_device *dev) // 所谓接收数据包,其实就是构造skb数据结构 ^_^
{struct net_local *lp = netdev_priv(dev);int ioaddr = dev->base_addr;int boguscount = 10;do { // 下面是循环接收数据么int status = inw(ioaddr); // 获取状态int pkt_len = inw(ioaddr); // 获取包大小if (pkt_len == 0) /* 全部接收 */break; /* 可以结束 */if (status & 0x40) { /* There was an error. */lp->stats.rx_errors++;if (status & 0x20) lp->stats.rx_frame_errors++;if (status & 0x10) lp->stats.rx_over_errors++;if (status & 0x08) lp->stats.rx_crc_errors++;if (status & 0x04) lp->stats.rx_fifo_errors++;} else {/* Malloc up new buffer. */struct sk_buff *skb;lp->stats.rx_bytes+=pkt_len; // 接收的字节数+pkt_lenskb = dev_alloc_skb(pkt_len); // 需要接收多少bytes就分配多少空间给sk_buffif (skb == NULL) { // 需要丢包printk(KERN_NOTICE "%s: Memory squeeze, dropping packet.\n",dev->name);lp->stats.rx_dropped++; // 丢包数++break;}skb->dev = dev; // 现在开始构建skb包/* 'skb->data' points to the start of sk_buff data area. */memcpy(skb_put(skb,pkt_len), (void*)dev->rmem_start, // 注意开始从dev向skb中放入数据,大小pkt_lenpkt_len);/* or */insw(ioaddr, skb->data, (pkt_len + 1) >> 1);netif_rx(skb); // 这个函数很重要,下面会具体说~dev->last_rx = jiffies; // 上一次rx的时间lp->stats.rx_packets++; // 接收包数量++lp->stats.rx_bytes += pkt_len; // 接收字节数+pkt_len}} while (--boguscount);return;}
netif_rx()
3、显然我们知道现在要分析netif_rx函数了
先看几个函数:
local_irq_disable() , local_irq_enable() , local_irq_save() 和 local_irq_restore() 为中断处理函数,
主要是在要进入临界区时禁止中断和在出临界区时使能中断。
local_irq_disable() 和 local_irq_enable() 配对使用;
local_irq_save() 则和 local_irq_restore() 配对使用。
/*** netif_rx - post buffer to the network code* @skb: buffer to post** This function receives a packet from a device driver and queues it for* the upper (protocol) levels to process. It always succeeds. The buffer* may be dropped during processing for congestion control or by the* protocol layers.** return values:* NET_RX_SUCCESS (no congestion)* NET_RX_DROP (packet was dropped)**/// 需要注意的是:这里是非NAPI方式下的函数int netif_rx(struct sk_buff *skb) // 注意接收数据后将数据进行排队,然后给上层协议处理,不过也有可能因为拥塞之类丢包!{struct softnet_data *queue; // 每个cpu结构都有这样一个队列,这样在SMP之间就避免了枷锁操作,提高并发度unsigned long flags;/* if netpoll wants it, pretend we never saw it */if (netpoll_rx(skb)) // 关于netpoll机制以后在讨论return NET_RX_DROP;if (!skb->tstamp.tv64)net_timestamp(skb); // 设置包到达时间/** The code is rearranged so that the path is the most* short when CPU is congested, but is still operating.*/local_irq_save(flags); // 关中断,禁止中断queue = &__get_cpu_var(softnet_data); // 取得当前CPU输入队列(得到CPU参数数据队列 softnet_data)__get_cpu_var(netdev_rx_stat).total++; // 更新当前CPU接收到的帧的数量,包括接收的和丢弃的if (queue->input_pkt_queue.qlen <= netdev_max_backlog) { // 每个CPU都有输入队列的最大长度,如果超过,则丢弃该数据帧if (queue->input_pkt_queue.qlen) { // 如果队列中有元素enqueue:dev_hold(skb->dev); // 网络设备引用值++__skb_queue_tail(&queue->input_pkt_queue, skb); // 将skb添加到队列的末尾(注意这里产生软中断NET_RX_SOFTIRQ,进一步处理包)local_irq_restore(flags); // 开中断 // 同时需要知道:NET_RX_SOFTIRQ 是由net_rx_action函数处理return NET_RX_SUCCESS; // 返回接收数据成功}napi_schedule(&queue->backlog); // 如果qlen=0,说明queue->backlog可能已经当前CPU的poll-list中移除了,要重新加入goto enqueue; // list_add_tail(&n->poll_list, &__get_cpu_var(softnet_data).poll_list);} // 其实就是让后面action中循环能够找到这个设备,,,然后goto到上面重新将包放入队列__get_cpu_var(netdev_rx_stat).dropped++; // 如果上面的没有执行成功,那么丢包数量++local_irq_restore(flags); // 开中断 允许中断kfree_skb(skb); // 因为丢包才能才第到此处,所以将skb free掉 return NET_RX_DROP; // 返回丢包
}
注意一个问题: 上面在将包放进队列的过程中,是关了中断的,完成后开中断,但是在接收包的数据的时候并没有禁止中断,即收包的IRQ是不需要被禁用的。因为将包放入到cpu的等待队列不会耗时太长。这也说明,传统API只能适用与低速设备。
简介:在没有NAPI的时候,都是通过中断系统来处理包的到达,这就才造成一个问题,当有很多很多短包蜂拥到达的时候,中断系统将会忙死,所以为了优化这种情况,加入NAPI,其实采用的是一种轮询方式。非NAPI方式是将数据放进CPU的队列中,而NAPI是有自己的私有队列的,可以说是自己的私有缓冲区!!!
下面来理清一下思路,在内核初始化的时候,对于每个CPU中的softnet_data都初始化了
net_dev_init()
static int __init net_dev_init(void)
{int i, rc = -ENOMEM;BUG_ON(!dev_boot_phase);if (dev_proc_init()) // 不管goto out;if (netdev_kobject_init()) // 不管goto out; INIT_LIST_HEAD(&ptype_all);for (i = 0; i < PTYPE_HASH_SIZE; i++) // 不管INIT_LIST_HEAD(&ptype_base[i]);if (register_pernet_subsys(&netdev_net_ops)) // 不管goto out;if (register_pernet_device(&default_device_ops)) // 不管goto out;/** Initialise the packet receive queues.初始化话数据包的接收队列*/for_each_possible_cpu(i) { // 对于每一个CPU都会进行处理struct softnet_data *queue; // 每个CPU中都有这样一个结构queue = &per_cpu(softnet_data, i); // 获得这个iCPU上面的softnet_data结构skb_queue_head_init(&queue->input_pkt_queue); // 初始化接收数据队列queue->completion_queue = NULL; // 暂无完成INIT_LIST_HEAD(&queue->poll_list); // 初始化设备队列(注意poll_list在处理数据的时候会被遍历)queue->backlog.poll = process_backlog; // 这个很重要!在以后的处理这个设备上的数据的时候使用这个函数,,,看下面queue->backlog.weight = weight_p;}netdev_dma_register(); // 下面忽略dev_boot_phase = 0;open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);hotcpu_notifier(dev_cpu_callback, 0);dst_init();dev_mcast_init();rc = 0;out:return rc;}
process_backlog()
看看process_backlog函数:
static int process_backlog(struct napi_struct *napi, int quota) // 注意需要在下面更详细地说
{int work = 0;struct softnet_data *queue = &__get_cpu_var(softnet_data);unsigned long start_time = jiffies;napi->weight = weight_p;do {struct sk_buff *skb;struct net_device *dev;local_irq_disable();skb = __skb_dequeue(&queue->input_pkt_queue); // 从队里获取一个skbif (!skb) {__napi_complete(napi); // 如果队列已经空了,那么其实就是将napi的poll_list从CPU的那个结构中移除local_irq_enable();break;}local_irq_enable();dev = skb->dev;netif_receive_skb(skb); // 下面处理接收数据(当然这里需要在下面更详细地说)dev_put(dev);} while (++work < quota && jiffies == start_time);// 需要注意的是:退出有两情况:当处理完所有skb 或者 分配时间达到 。return work;
}
struct softnet_data
// 看一下softnet_data结构体struct softnet_data{struct net_device *output_queue; // 网络设备发送队列的头struct sk_buff_head input_pkt_queue; // 接收缓冲区的sk_buff队列struct list_head poll_list; // poll设备队列头struct sk_buff *completion_queue; // 完成发送数据包,等待释放的队列struct napi_struct backlog; // NAPI结构#ifdef CONFIG_NET_DMAstruct dma_chan *net_dma;#endif};
4、放进队列之后该怎么处理呢?是不是要开始处理数据了,net_rx_action现在出现!
注意接收到的数据在两个地方等待net_rx_action来处理:
- 对于非NAPI方式来说,我们需要从CPU的softnet_data->input_pkt_queue中取得数据。
- 对于NAPI方式,前面说过有自己的缓冲区,那么poll函数从设备缓存读取数据。
下面看代码:
net_rx_action()
static void net_rx_action(struct softirq_action *h)
{struct list_head *list = &__get_cpu_var(softnet_data).poll_list; // 获取设备列表unsigned long start_time = jiffies; // 获取当前时间戳int budget = netdev_budget;void *have;local_irq_disable(); // 禁止中断while (!list_empty(list)) { // 对每一个设备进行循环处理一次,看是否有设备等待轮询取得数据struct napi_struct *n;int work, weight;/* If softirq window is exhuasted then punt.** Note that this is a slight policy change from the* previous NAPI code, which would allow up to 2* jiffies to pass before breaking out. The test* used to be "jiffies - start_time > 1".*/if (unlikely(budget <= 0 || jiffies != start_time)) // 保证当前的 POLL 过程的时间不超过一个时间片,这样不至于被软中断占用太多的时间goto softnet_break;local_irq_enable(); // 开中断/* Even though interrupts have been re-enabled, this* access is safe because interrupts can only add new* entries to the tail of this list, and only ->poll()* calls can remove this head entry from the list.*/n = list_entry(list->next, struct napi_struct, poll_list); // 从softnet_data 数据结构中的轮循队列上获得等待轮循的napi_struct结构have = netpoll_poll_lock(n); // 锁定该 struct napi_struct ,并且记录当前调度的CPUweight = n->weight;/* This NAPI_STATE_SCHED test is for avoiding a race* with netpoll's poll_napi(). Only the entity which* obtains the lock and sees NAPI_STATE_SCHED set will* actually make the ->poll() call. Therefore we avoid* accidently calling ->poll() when NAPI is not scheduled.*/work = 0;if (test_bit(NAPI_STATE_SCHED, &n->state)) // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!work = n->poll(n, weight); // !!!这里相当重要!根据weight调用想要的poll函数!// 之前说过如果低非NAPI,那么使用的是初始化时候的即process_backlog函数WARN_ON_ONCE(work > weight); // 如果是NAPI函数,那么就是自己的poll函数处理 // 那么又要返回上面看process_backlog函数(往下看~~~~有重写)budget -= work;local_irq_disable();/* Drivers must not modify the NAPI state if they* consume the entire weight. In such cases this code* still "owns" the NAPI instance and therefore can* move the instance around on the list at-will.*/if (unlikely(work == weight)) {if (unlikely(napi_disable_pending(n)))__napi_complete(n);elselist_move_tail(&n->poll_list, list);}netpoll_poll_unlock(have);}out:local_irq_enable();#ifdef CONFIG_NET_DMA/** There may not be any more sk_buffs coming right now, so push* any pending DMA copies to hardware*/if (!cpus_empty(net_dma.channel_mask)) {int chan_idx;for_each_cpu_mask(chan_idx, net_dma.channel_mask) {struct dma_chan *chan = net_dma.channels[chan_idx];if (chan)dma_async_memcpy_issue_pending(chan);}}#endifreturn;softnet_break:__get_cpu_var(netdev_rx_stat).time_squeeze++;__raise_softirq_irqoff(NET_RX_SOFTIRQ);goto out;}
注意看下面的部分代码:基本的意思就是从CPU这个softnet_data的字段input_pkt_queue队列中不断的取和当前napi_struct相关的数据包,每次获取一个数据包那么就使用函数netif_receive_skb函数处理!这个函数也是非常重要的!下面再说…如果没有的话,那么就是__napi_complete函数将这个napi_struct移除polllist,以免下次被循环到没有数据。
do {struct sk_buff *skb;struct net_device *dev;local_irq_disable();skb = __skb_dequeue(&queue->input_pkt_queue); // 出来一个数据if (!skb) { // 如果是null,那么队列空,移除设备__napi_complete(napi);local_irq_enable();break;}local_irq_enable();dev = skb->dev; // 获取这个包对应的设备netif_receive_skb(skb); // 这个函数最重要!下面分析!!!!!!!!dev_put(dev);
} while (++work < quota && jiffies == start_time);
看netif_receive_skb函数!netif_receive_skb是链路层接收数据报的最后一站!!!
netif_receive_skb()
/*** netif_receive_skb - process receive buffer from network* @skb: buffer to process** netif_receive_skb() is the main receive data processing function.* It always succeeds. The buffer may be dropped during processing* for congestion control or by the protocol layers.** This function may only be called from softirq context and interrupts* should be enabled.** Return values (usually ignored):* NET_RX_SUCCESS: no congestion* NET_RX_DROP: packet was dropped*/
int netif_receive_skb(struct sk_buff *skb) // 注意这个函数可能要被很多人处理,因为可以注册多个协议进行处理
{struct packet_type *ptype, *pt_prev;struct net_device *orig_dev;int ret = NET_RX_DROP;__be16 type;/* if we've gotten here through NAPI, check netpoll */if (netpoll_receive_skb(skb))return NET_RX_DROP;if (!skb->tstamp.tv64)net_timestamp(skb); // 更新时间if (!skb->iif) // 设备的(idx)编号skb->iif = skb->dev->ifindex;orig_dev = skb_bond(skb); // 可以展开成 orig_dev = skb->dev;skb->dev = skb->dev->master;// 不是很懂~ (处理路由聚合问题)if (!orig_dev)return NET_RX_DROP;__get_cpu_var(netdev_rx_stat).total++; // cpu统计skb_reset_network_header(skb); // 网络层头(校准头指针)skb_reset_transport_header(skb); // 传输层头(校准头指针)skb->mac_len = skb->network_header - skb->mac_header; // 注意mac层长度就是网络层的头---->mac层头之间部分!pt_prev = NULL;rcu_read_lock();#ifdef CONFIG_NET_CLS_ACTif (skb->tc_verd & TC_NCLS) {skb->tc_verd = CLR_TC_NCLS(skb->tc_verd);goto ncls;}#endif // 下面类似于协议嗅探器,因为是ETH_p_all类型// 这位部分代码是核心代码哦! 以下的代码用于在协议链上寻找匹配的协议(在ptype_all中找)list_for_each_entry_rcu(ptype, &ptype_all, list) { // 这里需要先理解一下packet_type结构体,goto到下面先看看!!!!if (!ptype->dev || ptype->dev == skb->dev) { // 这个地方在下面有解释if (pt_prev)ret = deliver_skb(skb, pt_prev, orig_dev); // 此处找到的是ETH_P_ALL类型协议(如果有注册)pt_prev = ptype;}}#ifdef CONFIG_NET_CLS_ACTskb = handle_ing(skb, &pt_prev, &ret, orig_dev);if (!skb)goto out;ncls:#endif// 若编译内核时选上BRIDGE,下面会执行网桥模块skb = handle_bridge(skb, &pt_prev, &ret, orig_dev); // 进入桥进行二层处理,如果返回skb == NULL,说明skb 被直接二层转发走了,不用再送网络层了,函数直接返回if (!skb) // 包是否被桥转发走了 ( 具体的后来在分析 )goto out;skb = handle_macvlan(skb, &pt_prev, &ret, orig_dev); // 编译内核时选上MAC_VLAN模块,下面才会执行if (!skb) // 同样如果被vlan消耗,那么无需往上面协议层传递了~!直接退出返回goto out;// 注意哦:如果数据包在上面没有被处理掉,那么说明要传递到上面一层即ip层进行处理 // 注意在I派层处理有两种情况:还要往上面一层即TCP层传递,或者直接ARP处理// 这位部分代码是核心代码哦! 以下的代码用于在协议链上寻找匹配的协(在ptype_base hash表中找)type = skb->protocol;list_for_each_entry_rcu(ptype,&ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) { // 这里面匹配的类型就是ip层一些协议的类型if (ptype->type == type &&(!ptype->dev || ptype->dev == skb->dev)) {if (pt_prev)ret = deliver_skb(skb, pt_prev, orig_dev); // 进行处理~~~~pt_prev = ptype;}}if (pt_prev) {ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);} else {kfree_skb(skb);/* Jamal, now you will not able to escape explaining* me how you were going to use this. :-)*/ret = NET_RX_DROP;}out:rcu_read_unlock();return ret;}
packet_type 结构体看看
struct packet_type
struct packet_type {__be16 type; /* This is really htons(ether_type). */ // 成员保存了二层协议类型,ETH_P_IP、ETH_P_ARP,ETH_P_ALLstruct net_device *dev; /* NULL is wildcarded here */int (*func) (struct sk_buff *, // 成员就是钩子函数了,如 ip_rcv()、arp_rcv()等等struct net_device *,struct packet_type *,struct net_device *);struct sk_buff *(*gso_segment)(struct sk_buff *skb,int features);int (*gso_send_check)(struct sk_buff *skb);void *af_packet_priv;struct list_head list;
};
注意:所有协议的packet_type存放在两条协议链中,ptype_base和ptype_all,ptype_base 为哈希链表,ptype_all为双向链.
系统使用dev_add_pack函数将指定协议类型的packet_type添加到这两个表中。
对于ETH_P_ALL类型的数据报文将在ptype_all表中找到自己对应的packet_type结构。
系统只有创建了一个PF_PACKE类型的socket才会将一个packet_type结构加到ptype_all链表中。
对于ETH_P_IP和ETH_P_ARP可以在ptype_base中找到自己的packet_type结构。
如果协议类型是ETH_P_IP那么func函数就是ip_rcv
如果协议类型是ETH_P_ARP那么func函数就是arp_rcv
OK,现在说说deliver_skb函数:
deliver_skb()
static inline int deliver_skb(struct sk_buff *skb,struct packet_type *pt_prev,struct net_device *orig_dev)
{atomic_inc(&skb->users);return pt_prev->func(skb, skb->dev, pt_prev, orig_dev); // 调用的还是对应的不同协议的func函数
}
最终还是调用了func函数了,下面注意:主要说将数据包传递给ip层进行处理,所以看看 ip_rcv
ip_rcv是怎么和ETH_P_IP关联起来的,这个我们上面说过这个packet_type结构,这个结构是保存不同协议和自己的处理函数func的,那么这个结构体有自己的处理方法:
static struct packet_type arp_packet_type __read_mostly = {.type = cpu_to_be16(ETH_P_ARP),.func = arp_rcv, // 关联上
};
static struct packet_type ip_packet_type __read_mostly = {.type = cpu_to_be16(ETH_P_IP),.func = ip_rcv, // 关联上.gso_send_check = inet_gso_send_check,.gso_segment = inet_gso_segment,.gro_receive = inet_gro_receive,.gro_complete = inet_gro_complete,
};
下面就来看看ip_rcv函数,请看下一篇博客
原文链接:https://blog.csdn.net/shanshanpt/article/details/20377657
linux 内核网络协议栈--数据从接收到IP层(二)相关推荐
- 一文讲解Linux 内核网络协议栈-数据从接收到ip层
[推荐阅读] 一文了解Linux上TCP的几个内核参数调优 一文剖析Linux内核中内存管理 分析linux启动内核源码 此处主要讲的是从数据来到,中断到最终数据包被处理的过程. 0:首先来介绍一下I ...
- linux内核网络协议栈--数据包的网卡缓冲区(二十四)
程序员可能关心的基本网卡知识 网卡相关介绍:http://www.linuxidc.com/Linux/2012-12/77132.htm 一.什么是网卡? 它是主机的网络设备,本身是LAN(局域网) ...
- linux内核网络协议栈--数据包的接收过程(二十)
本文将介绍在Linux系统中,数据包是如何一步一步从网卡传到进程手中的. 本文只讨论以太网的物理网卡,不涉及虚拟设备,并且以一个UDP包的接收过程作为示例. 本示例里列出的函数调用关系来自于kerne ...
- linux内核网络协议栈--数据包的接收过程(二十二)
与其说这篇文章分析了网卡驱动中中数据包的接收,还不如说基于Kernel:2.6.12,以e100为例,对网卡驱动编写的一个说明.当然,对数据包的接收说的很清楚. 一.从网卡说起 这并非是一个网卡驱动分 ...
- linux内核网络协议栈--数据包的发送过程(二十一)
继上一篇介绍了数据包的接收过程后,本文将介绍在Linux系统中,数据包是如何一步一步从应用程序到网卡并最终发送出去的. socket层 +-------------+| Application |+- ...
- linux内核网络协议栈--数据包的接收流程(二十三)
网卡在接受数据包时会产生中断,即当 有一个以太网帧到来时,网卡向内核产生一次中断: CPU收到中断信号后,执行中断处理程序,中断处理程序会设置 缓冲区地址.DMA 地址等信息: 网卡通过DMA 方式将 ...
- linux内核网络协议栈--数据接收流程图(五)
各层主要函数以及位置功能说明: 1)sock_read:初始化msghdr{}的结构类型变量msg,并且将需要接收的数据存放的地址传给msg.msg_iov->iov_base. net/soc ...
- linux内核网络协议栈--数据包的skb桥转发蓝图(二十六)
话不多说,先看一张桥转发时函数调用的一个基本蓝图. 这张图中,简单的展示了,数据的接收和发送,其中还包括netfilet的钩子点所处的位置. 需要说明的是: 1.我们先暂时忽略数据包从一开始是怎么从驱 ...
- linux内核网络协议栈--数据包的网卡驱动收发包过程(二十五)
网卡 网卡工作在物理层和数据链路层,主要由PHY/MAC芯片.Tx/Rx FIFO.DMA等组成,其中网线通过变压器接PHY芯片.PHY芯片通过MII接MAC芯片.MAC芯片接PCI总线 PHY芯片主 ...
最新文章
- 强强联合!Papers with Code携手arXiv,上传论文、提交代码一步到位
- 基础知识《十》unchecked异常和checked异常
- maven各个属性参数详解
- C语言实用算法系列之学生管理系统_对整个结构体操作_选择排序_提取排序规则
- mysql 锁问题 (相同索引键值或同一行或间隙锁的冲突)
- python小程序_小会计的实用Python小程序(三):人民币大写金额转换器
- CART决策树算法的Python实现(注释详细)
- 圆孔夫琅禾费衍射MATLAB程序,模拟夫琅禾费衍射的matlab源代码
- 一名软件测试工程师的日常
- 网络狂飙2(netspeeder2) v2.0 游戏版 怎么用
- js中的深拷贝和浅拷贝
- Stay hungry. Stay foolish.
- 高级查询组件下拉框联动(三)
- 设计模式与软件体系结构复习资料——设计模式
- 系统自带测试软件,Windows7自带软件测试RAID系统
- Tomcat到底是个啥?
- 常见颜色RGB颜色值
- Java工程师培训课(十六【新的领域】)
- laravel导出Excel表格提示内存超出
- Android Studio安装步骤