为您找到与接收数据包和发送数据包相关的共118个结果:
网络通信遵循“有来有回”之原则,意思是网络既有发送包又有接收包,才能保证网络通畅。但是实际遇到的网络不通的故障一般就是只有发送包没有接收包,遇到这样的故障该怎么处理呢?其实这样的故障有几方面的原因,读文网小编给大家仔细剖析:
实战1:DNS转发不当,引发传输死循环
小王最近从单位局域网内的一台工作站中访问Internet网络时,发现网页内容显示速度非常缓慢,而且时不时地还出现无法打开网页的现象。
起初他还以为是自己的工作站系统感染了病毒,可是询问其他同事后,他发现与自己同处一个子网的其他工作站-时速度都很缓慢,这说明该故障与网络病毒有关的可能性不是很大。后来,小王仔细对比检查了几台上网速度不快的工作站配置参数,看到DNS服务器地址参数不尽相同,有的使用的是单位主DNS服务器地址,有的使用备用DNS服务器地址,按道理来说局域网中的两台DNS服务器不应该同时出现故障。小王有点放心不下,赶紧对局域网中的两台服务器DNS服务器配置参数进行了检查,检查结果让其深意外,原来两台DNS服务器都启用了DNS转发功能,不过在设置转发地址时,小王看到IP地址为10.176.6.10的DNS服务器将解析任务转发到10.176.6.11服务器中了,而IP地址为10.176.6.11的DNS服务器却将解析任务又转发给10.176.6.10服务器了,这么一来当主DNS服务器无法完成普通工作站发送过来的解析请求时,它就会自动地将解析任务转发给局域网的备份DNS服务器中,而当单位的两台DNS服务器工作状态都不正常时那么局域网传输通道中就会形成死循环了,要不了多长时间网络传输通道就会被堵塞。找到了故障原因后,小王立即将其中一台DNS服务器的转发地址设置成本地ISP提供的DNS服务器地址,如此一来DNS解析任务就不会发生死循环了;重新修改了DNS转发地址后,小王再一次进行了上网测试,这一次他发现网络访问速度已经恢复了正常。
实战2:网络病毒袭击,引发通道堵塞
由于工作需要,小王准备通过自己工作站的网上邻居窗口去访问同事的一个共享文件,可是在将这个容量不超过10KB的文本文件拖放到本地硬盘中时,他发现网络传输速度异常缓慢,足足等了半个多小时文件传输才算完成。小王的第一感觉就是自己的工作站或者局域网遭受到了网络病毒的攻击。目前网络中各种类型的病毒层出不穷,其中蠕虫病毒对网络的破坏性最强,它常常会充分利用系统漏洞进行自我繁殖复制,同时还会利用网络协议自身的漏洞在网络传输通道中非法传播扩散,最终会导致网络传输通道堵塞。
为了验证自己的工作站系统是否已经感染了蠕虫病毒,小王立即将专业分析工具Sniffer下载下来,利用该工具从连接状态中很快就知道局域网某台工作站系统已经感染了网络蠕虫病毒;当然,要是大家对Sniffer工具的使用不熟悉时,也可以在自己的工作站系统中安装天网防火墙之类的工具,之后按照缺省参数启用该专业防火墙,日后局域网中要是有工作站感染蠕虫病毒不停向网络发送病毒信息时,天网防火墙就能将这些信息源拦截下来,到时查看拦截结果中的IP地址信息就能准确定位蠕虫病毒源。
找到了感染蠕虫病毒的故障工作站后,小王立即来到那台故障工作站现场,要求那台工作站的使用者暂时断开网络连接,并且重新启动系统,同时将系统切换到安全模式状态,之后使用专业的杀毒软件来进行病毒查杀操作。在病毒查杀操作结束后,那台故障工作站再次被连接到局域网中,这一次小王发现Sniffer工具抓取下来的信息并没有什么异常之处,再尝试进行共享访问传输时,那个容量不超过10KB的文本文件很快就被传输完成了,很显然在网络病毒被清除之后,网络传输通道立即被打通了。
在这里需要提醒各位朋友注意的是,现在许多蠕虫病毒存在不少新变种,有时候安装在本地系统中的杀毒软件奈何不了这些变种蠕虫病毒,此时大家可以根据变种蠕虫病毒的名称到网上下载专业的蠕虫病毒查杀工具。为了防止蠕虫病毒再次袭击自己的工作站系统或本地网络,大家还应该到微软公司的官方网站中查找一下是否有针对对应蠕虫病毒的补丁程序,要是有的话必须及时进行下载安装,以增强本地系统或网络的安全“免疫”能力。
浏览量:2
下载量:0
时间:
读文网小编为大家分享了Linux内核数据包处理流程-数据包接收的详细说明,有需要的可以参考下
一、从网卡说起
这并非是一个网卡驱动分析的专门文档,只是对网卡处理数据包的流程进行一个重点的分析。这里以Intel的e100驱动为例进行分析。
大多数网卡都是一个PCI设备,PCI设备都包含了一个标准的配置寄存器,寄存器中,包含了PCI设备的厂商ID、设备ID等等信息,驱动
程序使用来描述这些寄存器的标识符。如下:
CODE:
struct pci_device_id {
__u32 vendor, device; /* Vendor and device ID or PCI_ANY_ID*/
__u32 subvendor, subdevice; /* Subsystem ID's or PCI_ANY_ID */
__u32 class, class_mask; /* (class,subclass,prog-if) triplet */
kernel_ulong_t driver_data; /* Data private to the driver */
};
这样,在驱动程序中,常常就可以看到定义一个struct pci_device_id 类型的数组,告诉内核支持不同类型的
PCI设备的列表,以e100驱动为例:
#define INTEL_8255X_ETHERNET_DEVICE(device_id, ich) {
PCI_VENDOR_ID_INTEL, device_id, PCI_ANY_ID, PCI_ANY_ID,
PCI_CLASS_NETWORK_ETHERNET << 8, 0xFFFF00, ich }
static struct pci_device_id e100_id_table[] = {
INTEL_8255X_ETHERNET_DEVICE(0x1029, 0),
INTEL_8255X_ETHERNET_DEVICE(0x1030, 0),
INTEL_8255X_ETHERNET_DEVICE(0x1031, 3),
……/*略过一大堆支持的设备*/
{ 0, }
};
在内核中,一个PCI设备,使用struct pci_driver结构来描述,
struct pci_driver {
struct list_head node;
char *name;
struct module *owner;
const struct pci_device_id *id_table; /* must be non-NULL for probe to be called */
int (*probe) (struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */
void (*remove) (struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */
int (*suspend) (struct pci_dev *dev, pm_message_t state); /* Device suspended */
int (*resume) (struct pci_dev *dev); /* Device woken up */
int (*enable_wake) (struct pci_dev *dev, pci_power_t state, int enable); /* Enable wake event */
void (*shutdown) (struct pci_dev *dev);
struct device_driver driver;
struct pci_dynids dynids;
};
因为在系统引导的时候,PCI设备已经被识别,当内核发现一个已经检测到的设备同驱动注册的id_table中的信息相匹配时,
它就会触发驱动的probe函数,以e100为例:
/*
* 定义一个名为e100_driver的PCI设备
* 1、设备的探测函数为e100_probe;
* 2、设备的id_table表为e100_id_table
*/
static struct pci_driver e100_driver = {
.name = DRV_NAME,
.id_table = e100_id_table,
.probe = e100_probe,
.remove = __devexit_p(e100_remove),
#ifdef CONFIG_PM
.suspend = e100_suspend,
.resume = e100_resume,
#endif
.driver = {
.shutdown = e100_shutdown,
}
};
这样,如果系统检测到有与id_table中对应的设备时,就调用驱动的probe函数。
驱动设备在init函数中,调用pci_module_init函数初始化PCI设备e100_driver:
static int __init e100_init_module(void)
{
if(((1 << debug) - 1) & NETIF_MSG_DRV) {
printk(KERN_INFO PFX "%s, %s", DRV_DESCRIPTION, DRV_VERSION);
printk(KERN_INFO PFX "%s", DRV_COPYRIGHT);
}
return pci_module_init(&e100_driver);
}
一切顺利的话,注册的e100_probe函数将被内核调用,这个函数完成两个重要的工作:
1、分配/初始化/注册网络设备;
2、完成PCI设备的I/O区域的分配和映射,以及完成硬件的其它初始化工作;
网络设备使用struct net_device结构来描述,这个结构非常之大,许多重要的参考书籍对它都有较为深入的描述,可以参考《Linux设备驱动程序》中网卡驱动设计的相关章节。我会在后面的内容中,对其重要的成员进行注释;
当probe函数被调用,证明已经发现了我们所支持的网卡,这样,就可以调用register_netdev函数向内核注册网络设备了,注册之前,一般会调用alloc_etherdev为以太网分析一个net_device,然后初始化它的重要成员。
除了向内核注册网络设备之外,探测函数另一项重要的工作就是需要对硬件进行初始化,比如,要访问其I/O区域,需要为I/O区域分配内存区域,然后进行映射,这一步一般的流程是:
1、request_mem_region()
2、ioremap()
对于一般的PCI设备而言,可以调用:
1、pci_request_regions()
2、ioremap()
pci_request_regions函数对PCI的6个寄存器都会调用资源分配函数进行申请(需要判断是I/O端口还是I/O内存),例如:
CODE:
int pci_request_regions(struct pci_dev *pdev, char *res_name)
{
int i;
for (i = 0; i < 6; i++)
if(pci_request_region(pdev, i, res_name))
goto err_out;
return 0;
CODE:
int pci_request_region(struct pci_dev *pdev, int bar, char *res_name)
{
if (pci_resource_len(pdev, bar) == 0)
return 0;
if (pci_resource_flags(pdev, bar) & IORESOURCE_IO) {
if (!request_region(pci_resource_start(pdev, bar),
pci_resource_len(pdev, bar), res_name))
goto err_out;
}
else if (pci_resource_flags(pdev, bar) & IORESOURCE_MEM) {
if (!request_mem_region(pci_resource_start(pdev, bar),
pci_resource_len(pdev, bar), res_name))
goto err_out;
}
return 0;
有了这些基础,我们来看设备的探测函数:
static int __devinit e100_probe(struct pci_dev *pdev,
const struct pci_device_id *ent)
{
struct net_device *netdev;
struct nic *nic;
int err;
/*分配网络设备*/
if(!(netdev = alloc_etherdev(sizeof(struct nic)))) {
if(((1 << debug) - 1) & NETIF_MSG_PROBE)
printk(KERN_ERR PFX "Etherdev alloc failed, abort.");
return -ENOMEM;
}
/*设置各成员指针函数*/
netdev->open = e100_open;
netdev->stop = e100_close;
netdev->hard_start_xmit = e100_xmit_frame;
netdev->get_stats = e100_get_stats;
netdev->set_multicast_list = e100_set_multicast_list;
netdev->set_mac_address = e100_set_mac_address;
netdev->change_mtu = e100_change_mtu;
netdev->do_ioctl = e100_do_ioctl;
SET_ETHTOOL_OPS(netdev, &e100_ethtool_ops);
netdev->tx_timeout = e100_tx_timeout;
netdev->watchdog_timeo = E100_WATCHDOG_PERIOD;
netdev->poll = e100_poll;
netdev->weight = E100_NAPI_WEIGHT;
#ifdef CONFIG_NET_POLL_CONTROLLER
netdev->poll_controller = e100_netpoll;
#endif
/*设置网络设备名称*/
strcpy(netdev->name, pci_name(pdev));
/*取得设备私有数据结构*/
nic = netdev_priv(netdev);
/*网络设备指针,指向自己*/
nic->netdev = netdev;
/*PCIy设备指针,指向自己*/
nic->pdev = pdev;
nic->msg_enable = (1 << debug) - 1;
/*将PCI设备的私有数据区指向网络设备*/
pci_set_drvdata(pdev, netdev);
/*激活PCI设备*/
if((err = pci_enable_device(pdev))) {
DPRINTK(PROBE, ERR, "Cannot enable PCI device, aborting.");
goto err_out_free_dev;
}
/*判断I/O区域是否是I/O内存,如果不是,则报错退出*/
if(!(pci_resource_flags(pdev, 0) & IORESOURCE_MEM)) {
DPRINTK(PROBE, ERR, "Cannot find proper PCI device "
"base address, aborting.");
err = -ENODEV;
goto err_out_disable_pdev;
}
/*分配I/O内存区域*/
if((err = pci_request_regions(pdev, DRV_NAME))) {
DPRINTK(PROBE, ERR, "Cannot obtain PCI resources, aborting.");
goto err_out_disable_pdev;
}
/*
* 告之内核自己的DMA寻址能力,这里不是很明白,因为从0xFFFFFFFF来看,本来就是内核默认的32了
* 为什么还要调用pci_set_dma_mask来重复设置呢?可能是对ULL而非UL不是很了解吧。
*/
if((err = pci_set_dma_mask(pdev, 0xFFFFFFFFULL))) {
DPRINTK(PROBE, ERR, "No usable DMA configuration, aborting.");
goto err_out_free_res;
}
SET_MODULE_OWNER(netdev);
SET_NETDEV_DEV(netdev, &pdev->dev);
/*分配完成后,映射I/O内存*/
nic->csr = ioremap(pci_resource_start(pdev, 0), sizeof(struct csr));
if(!nic->csr) {
DPRINTK(PROBE, ERR, "Cannot map device registers, aborting.");
err = -ENOMEM;
goto err_out_free_res;
}
if(ent->driver_data)
nic->flags |= ich;
else
nic->flags &= ~ich;
/*设置设备私有数据结构的大部份默认参数*/
e100_get_defaults(nic);
/* 初始化自旋锁,锅的初始化必须在调用 hw_reset 之前执行*/
spin_lock_init(&nic->cb_lock);
spin_lock_init(&nic->cmd_lock);
/* 硬件复位,通过向指定I/O端口设置复位指令实现. */
e100_hw_reset(nic);
/*
* PCI网卡被BIOS配置后,某些特性可能会被屏蔽掉。比如,多数BIOS都会清掉“master”位,
* 这导致板卡不能随意向主存中拷贝数据。pci_set_master函数数会检查是否需要设置标志位,
* 如果需要,则会将“master”位置位。
* PS:什么是PCI master?
* 不同于ISA总线,PCI总线的地址总线与数据总线是分时复用的。这样做的好处是,一方面
* 可以节省接插件的管脚数,另一方面便于实现突发数据传输。在做数据传输时,由一个PCI
* 设备做发起者(主控,Initiator或Master),而另一个PCI设备做目标(从设备,Target或Slave)。
* 总线上的所有时序的产生与控制,都由Master来发起。PCI总线在同一时刻只能供一对设备完成传输。
*/
pci_set_master(pdev);
/*添加两个内核定时器,watchdog和blink_timer*/
init_timer(&nic->watchdog);
nic->watchdog.function = e100_watchdog;
nic->watchdog.data = (unsigned long)nic;
init_timer(&nic->blink_timer);
nic->blink_timer.function = e100_blink_led;
nic->blink_timer.data = (unsigned long)nic;
INIT_WORK(&nic->tx_timeout_task,
(void (*)(void *))e100_tx_timeout_task, netdev);
if((err = e100_alloc(nic))) {
DPRINTK(PROBE, ERR, "Cannot alloc driver memory, aborting.");
goto err_out_iounmap;
}
/*phy寄存器初始化*/
e100_phy_init(nic);
if((err = e100_eeprom_load(nic)))
goto err_out_free;
memcpy(netdev->dev_addr, nic->eeprom, ETH_ALEN);
if(!is_valid_ether_addr(netdev->dev_addr)) {
DPRINTK(PROBE, ERR, "Invalid MAC address from "
"EEPROM, aborting.");
err = -EAGAIN;
goto err_out_free;
}
/* Wol magic packet can be enabled from eeprom */
if((nic->mac >= mac_82558_D101_A4) &&
(nic->eeprom[eeprom_id] & eeprom_id_wol))
nic->flags |= wol_magic;
/* ack any pending wake events, disable PME */
pci_enable_wake(pdev, 0, 0);
/*注册网络设备*/
strcpy(netdev->name, "eth%d");
if((err = register_netdev(netdev))) {
DPRINTK(PROBE, ERR, "Cannot register net device, aborting.");
goto err_out_free;
}
DPRINTK(PROBE, INFO, "addr 0x%lx, irq %d, "
"MAC addr %02X:%02X:%02X:%02X:%02X:%02X",
pci_resource_start(pdev, 0), pdev->irq,
netdev->dev_addr[0], netdev->dev_addr[1], netdev->dev_addr[2],
netdev->dev_addr[3], netdev->dev_addr[4], netdev->dev_addr[5]);
return 0;
err_out_free:
e100_free(nic);
err_out_iounmap:
iounmap(nic->csr);
err_out_free_res:
pci_release_regions(pdev);
err_out_disable_pdev:
pci_disable_device(pdev);
err_out_free_dev:
pci_set_drvdata(pdev, NULL);
free_netdev(netdev);
return err;
}
执行到这里,探测函数的使命就完成了,在对网络设备重要成员初始化时,有:
netdev->open = e100_open;
指定了设备的open函数为e100_open,这样,当第一次使用设备,比如使用ifconfig工具的时候,open函数将被调用。
二、打开设备
在探测函数中,设置了netdev->open = e100_open; 指定了设备的open函数为e100_open:
CODE:
static int e100_open(struct net_device *netdev)
{
struct nic *nic = netdev_priv(netdev);
int err = 0;
netif_carrier_off(netdev);
if((err = e100_up(nic)))
DPRINTK(IFUP, ERR, "Cannot open interface, aborting.");
return err;
}
大多数涉及物理设备可以感知信号载波(carrier)的存在,载波的存在意味着设备可以工作
据个例子来讲:当一个用户拔掉了网线,也就意味着信号载波的消失。
netif_carrier_off:关闭载波信号;
netif_carrier_on:打开载波信号;
netif_carrier_ok:检测载波信号;
对于探测网卡网线是否连接,这一组函数被使用得较多;
接着,调用e100_up函数启动网卡,这个“启动”的过程,最重要的步骤有:
1、调用request_irq向内核注册中断;
2、调用netif_wake_queue函数来重新启动传输队例;
CODE:
static int e100_up(struct nic *nic)
{
int err;
if((err = e100_rx_alloc_list(nic)))
return err;
if((err = e100_alloc_cbs(nic)))
goto err_rx_clean_list;
if((err = e100_hw_init(nic)))
goto err_clean_cbs;
e100_set_multicast_list(nic->netdev);
e100_start_receiver(nic, 0);
mod_timer(&nic->watchdog, jiffies);
if((err = request_irq(nic->pdev->irq, e100_intr, SA_SHIRQ,
nic->netdev->name, nic->netdev)))
goto err_no_irq;
netif_wake_queue(nic->netdev);
netif_poll_enable(nic->netdev);
/* enable ints _after_ enabling poll, preventing a race between
* disable ints+schedule */
e100_enable_irq(nic);
return 0;
err_no_irq:
del_timer_sync(&nic->watchdog);
err_clean_cbs:
e100_clean_cbs(nic);
err_rx_clean_list:
e100_rx_clean_list(nic);
return err;
}
这样,中断函数e100_intr将被调用;
三、网卡中断
从本质上来讲,中断,是一种电信号,当设备有某种事件发生的时候,它就会产生中断,通过总线把电信号发送给中断控制器,如果中断的线是激活的,中断控制器就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,跳到内存中内核设置的中断处理程序的入口点,进行中断处理。
在内核中断处理中,会检测中断与我们刚才注册的中断号匹配,于是,注册的中断处理函数就被调用了。
当需要发/收数据,出现错误,连接状态变化等,网卡的中断信号会被触发。当接收到中断后,中断函数读取中断状态位,进行合法性判断,如判断中断信号是否是自己的等,然后,应答设备中断——OK,我已经知道了,你回去继续工作吧……
接着,它就屏蔽此中断,然后netif_rx_schedule函数接收,接收函数 会在未来某一时刻调用设备的poll函数(对这里而言,注册的是e100_poll)实现设备的轮询:
CODE:
static irqreturn_t e100_intr(int irq, void *dev_id, struct pt_regs *regs)
{
struct net_device *netdev = dev_id;
struct nic *nic = netdev_priv(netdev);
u8 stat_ack = readb(&nic->csr->scb.stat_ack);
DPRINTK(INTR, DEBUG, "stat_ack = 0x%02X", stat_ack);
if(stat_ack == stat_ack_not_ours || /* Not our interrupt */
stat_ack == stat_ack_not_present) /* Hardware is ejected */
return IRQ_NONE;
/* Ack interrupt(s) */
writeb(stat_ack, &nic->csr->scb.stat_ack);
/* We hit Receive No Resource (RNR); restart RU after cleaning */
if(stat_ack & stat_ack_rnr)
nic->ru_running = RU_SUSPENDED;
e100_disable_irq(nic);
netif_rx_schedule(netdev);
return IRQ_HANDLED;
}
对于数据包的接收而言,我们关注的是poll函数中,调用e100_rx_clean进行数据的接收:
CODE:
static int e100_poll(struct net_device *netdev, int *budget)
{
struct nic *nic = netdev_priv(netdev);
/*
* netdev->quota是当前CPU能够从所有接口中接收数据包的最大数目,budget是在
* 初始化阶段分配给接口的weight值,轮询函数必须接受二者之间的最小值。表示
* 轮询函数本次要处理的数据包个数。
*/
unsigned int work_to_do = min(netdev->quota, *budget);
unsigned int work_done = 0;
int tx_cleaned;
/*进行数据包的接收和传输*/
e100_rx_clean(nic, &work_done, work_to_do);
tx_cleaned = e100_tx_clean(nic);
/*接收和传输完成后,就退出poll模块,重启中断*/
/* If no Rx and Tx cleanup work was done, exit polling mode. */
if((!tx_cleaned && (work_done == 0)) || !netif_running(netdev)) {
netif_rx_complete(netdev);
e100_enable_irq(nic);
return 0;
}
*budget -= work_done;
netdev->quota -= work_done;
return 1;
}
static inline void e100_rx_clean(struct nic *nic, unsigned int *work_done,
unsigned int work_to_do)
{
struct rx *rx;
int restart_required = 0;
struct rx *rx_to_start = NULL;
/* are we already rnr? then pay attention!!! this ensures that
* the state machine progression never allows a start with a
* partially cleaned list, avoiding a race between hardware
* and rx_to_clean when in NAPI mode */
if(RU_SUSPENDED == nic->ru_running)
restart_required = 1;
/* Indicate newly arrived packets */
for(rx = nic->rx_to_clean; rx->skb; rx = nic->rx_to_clean = rx->next) {
int err = e100_rx_indicate(nic, rx, work_done, work_to_do);
if(-EAGAIN == err) {
/* hit quota so have more work to do, restart once
* cleanup is complete */
restart_required = 0;
break;
} else if(-ENODATA == err)
break; /* No more to clean */
}
/* save our starting point as the place we'll restart the receiver */
if(restart_required)
rx_to_start = nic->rx_to_clean;
/* Alloc new skbs to refill list */
for(rx = nic->rx_to_use; !rx->skb; rx = nic->rx_to_use = rx->next) {
if(unlikely(e100_rx_alloc_skb(nic, rx)))
break; /* Better luck next time (see watchdog) */
}
if(restart_required) {
https:// ack the rnr?
writeb(stat_ack_rnr, &nic->csr->scb.stat_ack);
e100_start_receiver(nic, rx_to_start);
if(work_done)
(*work_done)++;
}
}
四、网卡的数据接收
内核如何从网卡接受数据,传统的经典过程:
1、数据到达网卡;
2、网卡产生一个中断给内核;
3、内核使用I/O指令,从网卡I/O区域中去读取数据;
我们在许多网卡驱动中,都可以在网卡的中断函数中见到这一过程。
但是,这一种方法,有一种重要的问题,就是大流量的数据来到,网卡会产生大量的中断,内核在中断上下文中,会浪费大量的资源来处理中断本身。所以,一个问题是,“可不可以不使用中断”,这就是轮询技术,所谓NAPI技术,说来也不神秘,就是说,内核屏蔽中断,然后隔一会儿就去问网卡,“你有没有数据啊?”……
从这个描述本身可以看到,哪果数据量少,轮询同样占用大量的不必要的CPU资源,大家各有所长吧,呵呵……
OK,另一个问题,就是从网卡的I/O区域,包括I/O寄存器或I/O内存中去读取数据,这都要CPU去读,也要占用CPU资源,“CPU从I/O区域读,然后把它放到内存(这个内存指的是系统本身的物理内存,跟外设的内存不相干,也叫主内存)中”。于是自然地,就想到了DMA技术——让网卡直接从主内存之间读写它们的I/O数据,CPU,这儿不干你事,自己找乐子去:
1、首先,内核在主内存中为收发数据建立一个环形的缓冲队列(通常叫DMA环形缓冲区)。
2、内核将这个缓冲区通过DMA映射,把这个队列交给网卡;
3、网卡收到数据,就直接放进这个环形缓冲区了——也就是直接放进主内存了;然后,向系统产生一个中断;
4、内核收到这个中断,就取消DMA映射,这样,内核就直接从主内存中读取数据;
——呵呵,这一个过程比传统的过程少了不少工作,因为设备直接把数据放进了主内存,不需要CPU的干预,效率是不是提高不少?
对应以上4步,来看它的具体实现:
1、分配环形DMA缓冲区
Linux内核中,用skb来描述一个缓存,所谓分配,就是建立一定数量的skb,然后把它们组织成一个双向链表;
2、建立DMA映射
内核通过调用
dma_map_single(struct device *dev,void *buffer,size_t size,enum dma_data_direction direction)
建立映射关系。
struct device *dev,描述一个设备;
buffer:把哪个地址映射给设备;也就是某一个skb——要映射全部,当然是做一个双向链表的循环即可;
size:缓存大小;
direction:映射方向——谁传给谁:一般来说,是“双向”映射,数据在设备和内存之间双向流动;
对于PCI设备而言(网卡一般是PCI的),通过另一个包裹函数pci_map_single,这样,就把buffer交给设备了!设备可以直接从里边读/取数据。
3、这一步由硬件完成;
4、取消映射
dma_unmap_single,对PCI而言,大多调用它的包裹函数pci_unmap_single,不取消的话,缓存控制权还在设备手里,要调用它,把主动权掌握在CPU手里——因为我们已经接收到数据了,应该由CPU把数据交给上层网络栈;
当然,不取消之前,通常要读一些状态位信息,诸如此类,一般是调用
dma_sync_single_for_cpu()
让CPU在取消映射前,就可以访问DMA缓冲区中的内容。
关于DMA映射的更多内容,可以参考《Linux设备驱动程序》“内存映射和DMA”章节相关内容!
OK,有了这些知识,我们就可以来看e100的代码了,它跟上面讲的步骤基本上一样的——绕了这么多圈子,就是想绕到e100上面了,呵呵!
在e100_open函数中,调用e100_up,我们前面分析它时,略过了一个重要的东东,就是环形缓冲区的建立,这一步,是通过
e100_rx_alloc_list函数调用完成的:
CODE:
static int e100_rx_alloc_list(struct nic *nic)
{
struct rx *rx;
unsigned int i, count = nic->params.rfds.count;
nic->rx_to_use = nic->rx_to_clean = NULL;
nic->ru_running = RU_UNINITIALIZED;
/*结构struct rx用来描述一个缓冲区节点,这里分配了count个*/
if(!(nic->rxs = kmalloc(sizeof(struct rx) * count, GFP_ATOMIC)))
return -ENOMEM;
memset(nic->rxs, 0, sizeof(struct rx) * count);
/*虽然是连续分配的,不过还是遍历它,建立双向链表,然后为每一个rx的skb指针分员分配空间
skb用来描述内核中的一个数据包,呵呵,说到重点了*/
for(rx = nic->rxs, i = 0; i < count; rx++, i++) {
rx->next = (i + 1 < count) ? rx + 1 : nic->rxs;
rx->prev = (i == 0) ? nic->rxs + count - 1 : rx - 1;
if(e100_rx_alloc_skb(nic, rx)) { /*分配缓存*/
e100_rx_clean_list(nic);
return -ENOMEM;
}
}
nic->rx_to_use = nic->rx_to_clean = nic->rxs;
nic->ru_running = RU_SUSPENDED;
return 0;
}
CODE:
#define RFD_BUF_LEN (sizeof(struct rfd) + VLAN_ETH_FRAME_LEN)
static inline int e100_rx_alloc_skb(struct nic *nic, struct rx *rx)
{
/*skb缓存的分配,是通过调用系统函数dev_alloc_skb来完成的,它同内核栈中通常调用alloc_skb的区别在于,
它是原子的,所以,通常在中断上下文中使用*/
if(!(rx->skb = dev_alloc_skb(RFD_BUF_LEN + NET_IP_ALIGN)))
return -ENOMEM;
/*初始化必要的成员 */
rx->skb->dev = nic->netdev;
skb_reserve(rx->skb, NET_IP_ALIGN);
/*这里在数据区之前,留了一块sizeof(struct rfd) 这么大的空间,该结构的
一个重要作用,用来保存一些状态信息,比如,在接收数据之前,可以先通过
它,来判断是否真有数据到达等,诸如此类*/
memcpy(rx->skb->data, &nic->blank_rfd, sizeof(struct rfd));
/*这是最关键的一步,建立DMA映射,把每一个缓冲区rx->skb->data都映射给了设备,缓存区节点
rx利用dma_addr保存了每一次映射的地址,这个地址后面会被用到*/
rx->dma_addr = pci_map_single(nic->pdev, rx->skb->data,
RFD_BUF_LEN, PCI_DMA_BIDIRECTIONAL);
if(pci_dma_mapping_error(rx->dma_addr)) {
dev_kfree_skb_any(rx->skb);
rx->skb = 0;
rx->dma_addr = 0;
return -ENOMEM;
}
/* Link the RFD to end of RFA by linking previous RFD to
* this one, and clearing EL bit of previous. */
if(rx->prev->skb) {
struct rfd *prev_rfd = (struct rfd *)rx->prev->skb->data;
/*put_unaligned(val,ptr);用到把var放到ptr指针的地方,它能处理处理内存对齐的问题
prev_rfd是在缓冲区开始处保存的一点空间,它的link成员,也保存了映射后的地址*/
put_unaligned(cpu_to_le32(rx->dma_addr),
(u32 *)&prev_rfd->link);
wmb();
prev_rfd->command &= ~cpu_to_le16(cb_el);
pci_dma_sync_single_for_device(nic->pdev, rx->prev->dma_addr,
sizeof(struct rfd), PCI_DMA_TODEVICE);
}
return 0;
}
e100_rx_alloc_list函数在一个循环中,建立了环形缓冲区,并调用e100_rx_alloc_skb为每个缓冲区分配了空间,并做了
DMA映射。这样,我们就可以来看接收数据的过程了。
前面我们讲过,中断函数中,调用netif_rx_schedule,表明使用轮询技术,系统会在未来某一时刻,调用设备的poll函数:
CODE:
static int e100_poll(struct net_device *netdev, int *budget)
{
struct nic *nic = netdev_priv(netdev);
unsigned int work_to_do = min(netdev->quota, *budget);
unsigned int work_done = 0;
int tx_cleaned;
e100_rx_clean(nic, &work_done, work_to_do);
tx_cleaned = e100_tx_clean(nic);
/* If no Rx and Tx cleanup work was done, exit polling mode. */
if((!tx_cleaned && (work_done == 0)) || !netif_running(netdev)) {
netif_rx_complete(netdev);
e100_enable_irq(nic);
return 0;
}
*budget -= work_done;
netdev->quota -= work_done;
return 1;
}
目前,我们只关心rx,所以,e100_rx_clean函数就成了我们关注的对像,它用来从缓冲队列中接收全部数据(这或许是取名为clean的原因吧!):
CODE:
static inline void e100_rx_clean(struct nic *nic, unsigned int *work_done,
unsigned int work_to_do)
{
struct rx *rx;
int restart_required = 0;
struct rx *rx_to_start = NULL;
/* are we already rnr? then pay attention!!! this ensures that
* the state machine progression never allows a start with a
* partially cleaned list, avoiding a race between hardware
* and rx_to_clean when in NAPI mode */
if(RU_SUSPENDED == nic->ru_running)
restart_required = 1;
/* 函数最重要的工作,就是遍历环形缓冲区,接收数据*/
for(rx = nic->rx_to_clean; rx->skb; rx = nic->rx_to_clean = rx->next) {
int err = e100_rx_indicate(nic, rx, work_done, work_to_do);
if(-EAGAIN == err) {
/* hit quota so have more work to do, restart once
* cleanup is complete */
restart_required = 0;
break;
} else if(-ENODATA == err)
break; /* No more to clean */
}
/* save our starting point as the place we'll restart the receiver */
if(restart_required)
rx_to_start = nic->rx_to_clean;
/* Alloc new skbs to refill list */
for(rx = nic->rx_to_use; !rx->skb; rx = nic->rx_to_use = rx->next) {
if(unlikely(e100_rx_alloc_skb(nic, rx)))
break; /* Better luck next time (see watchdog) */
}
if(restart_required) {
https:// ack the rnr?
writeb(stat_ack_rnr, &nic->csr->scb.stat_ack);
e100_start_receiver(nic, rx_to_start);
if(work_done)
(*work_done)++;
}
}
CODE:
static inline int e100_rx_indicate(struct nic *nic, struct rx *rx,
unsigned int *work_done, unsigned int work_to_do)
{
struct sk_buff *skb = rx->skb;
struct rfd *rfd = (struct rfd *)skb->data;
u16 rfd_status, actual_size;
if(unlikely(work_done && *work_done >= work_to_do))
return -EAGAIN;
/* 读取数据之前,也就是取消DMA映射之前,需要先读取cb_complete 状态位,
以确定数据是否真的准备好了,并且,rfd的actual_size中,也包含了真实的数据大小
pci_dma_sync_single_for_cpu函数前面已经介绍过,它让CPU在取消DMA映射之前,具备
访问DMA缓存的能力*/
pci_dma_sync_single_for_cpu(nic->pdev, rx->dma_addr,
sizeof(struct rfd), PCI_DMA_FROMDEVICE);
rfd_status = le16_to_cpu(rfd->status);
DPRINTK(RX_STATUS, DEBUG, "status=0x%04X", rfd_status);
/* If data isn't ready, nothing to indicate */
if(unlikely(!(rfd_status & cb_complete)))
return -ENODATA;
/* Get actual data size */
actual_size = le16_to_cpu(rfd->actual_size) & 0x3FFF;
if(unlikely(actual_size > RFD_BUF_LEN - sizeof(struct rfd)))
actual_size = RFD_BUF_LEN - sizeof(struct rfd);
/* 取消映射,因为通过DMA,网卡已经把数据放在了主内存中,这里一取消,也就意味着,
CPU可以处理主内存中的数据了 */
pci_unmap_single(nic->pdev, rx->dma_addr,
RFD_BUF_LEN, PCI_DMA_FROMDEVICE);
/* this allows for a fast restart without re-enabling interrupts */
if(le16_to_cpu(rfd->command) & cb_el)
nic->ru_running = RU_SUSPENDED;
/*正确地设置data指针,因为最前面有一个sizeof(struct rfd)大小区域,跳过它*/
skb_reserve(skb, sizeof(struct rfd));
/*更新skb的tail和len指针,也是就更新接收到这么多数据的长度*/
skb_put(skb, actual_size);
/*设置协议位*/
skb->protocol = eth_type_trans(skb, nic->netdev);
if(unlikely(!(rfd_status & cb_ok))) {
/* Don't indicate if hardware indicates errors */
nic->net_stats.rx_dropped++;
dev_kfree_skb_any(skb);
} else if(actual_size > nic->netdev->mtu + VLAN_ETH_HLEN) {
/* Don't indicate oversized frames */
nic->rx_over_length_errors++;
nic->net_stats.rx_dropped++;
dev_kfree_skb_any(skb);
} else {
/*网卡驱动要做的最后一步,就是统计接收计数器,设置接收时间戳,然后调用netif_receive_skb,
把数据包交给上层协议栈,自己的光荣始命也就完成了*/
nic->net_stats.rx_packets++;
nic->net_stats.rx_bytes += actual_size;
nic->netdev->last_rx = jiffies;
netif_receive_skb(skb);
if(work_done)
(*work_done)++;
}
rx->skb = NULL;
return 0;
}
网卡驱动执行到这里,数据接收的工作,也就处理完成了。但是,使用这一种方法的驱动,省去了网络栈中一个重要的内容,就是
“队列层”,让我们来看看,传统中断接收数据包模式下,使用netif_rx函数调用,又会发生什么。
PS:九贱没有去研究过所谓的“零拷贝”技术,不太清楚,它同这种DMA直取方式有何不同?难道是把网卡中的I/O内存直接映射到主内存中,这样CPU就可以像读取主内存一样,读取网卡的内存,但是这要求设备要有好大的I/O内存来做缓冲呀!!^o^,外行了……希望哪位DX提点!
五、队列层
1、软中断与下半部
当用中断处理的时候,为了减少中断处理的工作量,比如,一般中断处理时,需要屏蔽其它中断,如果中断处理时间过长,那么其它中断
有可能得不到及时处理,也以,有一种机制,就是把“不必马上处理”的工作,推迟一点,让它在中断处理后的某一个时刻得到处理。这就
是下半部。
下半部只是一个机制,它在Linux中,有多种实现方式,其中一种对时间要求最严格的实现方式,叫“软中断”,可以使用:
open_softirq()
来向内核注册一个软中断,
然后,在合适的时候,调用
raise_softirq_irqoff()
触发它。
如果采用中断方式接收数据(这一节就是在说中断方式接收,后面,就不用这种假设了),同样也需要软中断,可以调用
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
向内核注册一个名为NET_RX_SOFTIR的软中断,net_rx_action是软中断的处理函数。
然后,在驱动中断处理完后的某一个时刻,调用
raise_softirq_irqoff(NET_RX_SOFTIRQ);
触发它,这样net_rx_action将得到执行。
2、队列层
什么是队列层?通常,在网卡收发数据的时候,需要维护一个缓冲区队列,来缓存可能存在的突发数据,类似于前面的DMA环形缓冲区。
队列层中,包含了一个叫做struct softnet_data:
CODE:
struct softnet_data
{
/*throttle 用于拥塞控制,当拥塞发生时,throttle将被设置,后续进入的数据包将被丢弃*/
int throttle;
/*netif_rx函数返回的拥塞级别*/
int cng_level;
int avg_blog;
/*softnet_data 结构包含一个指向接收和传输队列的指针,input_pkt_queue成员指向准备传送
给网络层的sk_buffs包链表的首部的指针,这个队列中的包是由netif_rx函数递交的*/
struct sk_buff_head input_pkt_queue;
struct list_head poll_list;
struct net_device *output_queue;
struct sk_buff *completion_queue;
struct net_device backlog_dev; /* Sorry. 8) */
};
内核使用了一个同名的变量softnet_data,它是一个Per-CPU变量,每个CPU都有一个。
net/core/dev.c
CODE:
DECLARE_PER_CPU(struct softnet_data,softnet_data);
CODE:
/*
* 网络模块的核心处理模块.
*/
static int __init net_dev_init(void)
{
int i, rc = -ENOMEM;
BUG_ON(!dev_boot_phase);
net_random_init();
if (dev_proc_init()) /*初始化proc文件系统*/
goto out;
if (netdev_sysfs_init()) /*初始化sysfs文件系统*/
goto out;
/*ptype_all和ptype_base是重点,后面会详细分析,它们都是
struct list_head类型变量,这里初始化链表成员*/
INIT_LIST_HEAD(&ptype_all);
for (i = 0; i < 16; i++)
INIT_LIST_HEAD(&ptype_base[i]);
for (i = 0; i < ARRAY_SIZE(dev_name_head); i++)
INIT_HLIST_HEAD(&dev_name_head[i]);
for (i = 0; i < ARRAY_SIZE(dev_index_head); i++)
INIT_HLIST_HEAD(&dev_index_head[i]);
/*
* 初始化包接收队列,这里我们的重点了.
*/
/*遍历每一个CPU,取得它的softnet_data,我们说过,它是一个struct softnet_data的Per-CPU变量*/
for (i = 0; i < NR_CPUS; i++) {
struct softnet_data *queue;
/*取得第i个CPU的softnet_data,因为队列是包含在它里边的,所以,我会直接说,“取得队列”*/
queue = &per_cpu(softnet_data, i);
/*初始化队列头*/
skb_queue_head_init(&queue->input_pkt_queue);
queue->throttle = 0;
queue->cng_level = 0;
queue->avg_blog = 10; /* arbitrary non-zero */
queue->completion_queue = NULL;
INIT_LIST_HEAD(&queue->poll_list);
set_bit(__LINK_STATE_START, &queue->backlog_dev.state);
queue->backlog_dev.weight = weight_p;
/*这里,队列中backlog_dev设备,它是一个伪网络设备,不对应任何物理设备,它的poll函数,指向了
process_backlog,后面我们会详细分析*/
queue->backlog_dev.poll = process_backlog;
atomic_set(&queue->backlog_dev.refcnt, 1);
}
#ifdef OFFLINE_SAMPLE
samp_timer.expires = jiffies + (10 * HZ);
add_timer(&samp_timer);
#endif
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;
}
这样,初始化完成后,在驱动程序中,在中断处理函数中,会调用netif_rx将数据交上来,这与采用轮询技术,有本质的不同:
CODE:
int netif_rx(struct sk_buff *skb)
{
int this_cpu;
struct softnet_data *queue;
unsigned long flags;
/* if netpoll wants it, pretend we never saw it */
if (netpoll_rx(skb))
return NET_RX_DROP;
/*接收时间戳未设置,设置之*/
if (!skb->stamp.tv_sec)
net_timestamp(&skb->stamp);
/*
* 这里准备将数据包放入接收队列,需要禁止本地中断,在入队操作完成后,再打开中断.
*/
local_irq_save(flags);
/*获取当前CPU对应的softnet_data变量*/
this_cpu = smp_processor_id();
queue = &__get_cpu_var(softnet_data);
/*接收计数器累加*/
__get_cpu_var(netdev_rx_stat).total++;
/*接收队列是否已满*/
if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {
if (queue->input_pkt_queue.qlen) {
if (queue->throttle) /*拥塞发生了,丢弃数据包*/
goto drop;
/*数据包入队操作*/
enqueue:
dev_hold(skb->dev); /*累加设备引入计数器*/
__skb_queue_tail(&queue->input_pkt_queue, skb); /*将数据包加入接收队列*/
#ifndef OFFLINE_SAMPLE
get_sample_stats(this_cpu);
#endif
local_irq_restore(flags);
return queue->cng_level;
}
/*
* 驱动程序不断地调用net_rx函数,实现接收数据包的入队操作,当queue->input_pkt_queue.qlen == 0时(?什么情况下设置)
* 则进入这段代码,这里,如果已经被设置拥塞标志的话,则清除它,因为这里将要调用软中断,开始将数据包交给
* 上层了,即上层协议的接收函数将执行出队操作,拥塞自然而然也就不存在了。
*/
if (queue->throttle)
queue->throttle = 0;
/*
* netif_rx_schedule函数完成两件重要的工作:
* 1、将bakclog_dev设备加入“处理数据包的设备”的链表当中;
* 2、触发软中断函数,进行数据包接收处理;
*/
netif_rx_schedule(&queue->backlog_dev);
goto enqueue;
}
/*前面判断了队列是否已满,如果已满而标志未设置,设置之,并累加拥塞计数器*/
if (!queue->throttle) {
queue->throttle = 1;
__get_cpu_var(netdev_rx_stat).throttled++;
}
/*拥塞发生,累加丢包计数器,释放数据包*/
drop:
__get_cpu_var(netdev_rx_stat).dropped++;
local_irq_restore(flags);
kfree_skb(skb);
return NET_RX_DROP;
}
从这段代码的分析中,我们可以看到,当数据被接收后,netif_rx的工作,就是取得当前CPU的队列,然后入队,然后返回,然后中断函数
现调用它,它再把数据包入队……
当队列接收完成后,netif_rx就调用netif_rx_schedule进一步处理数据包,我们注意到:
1、前面讨论过,采用轮询技术时,同样地,也是调用netif_rx_schedule,把设备自己传递了过去;
2、这里,采用中断方式,传递的是队列中的一个“伪设备”,并且,这个伪设备的poll函数指针,指向了一个叫做process_backlog的函数;
netif_rx_schedule函数完成两件重要的工作:
1、将bakclog_dev设备加入“处理数据包的设备”的链表当中;
2、触发软中断函数,进行数据包接收处理;
这样,我们可以猜想,在软中断函数中,不论是伪设备bakclog_dev,还是真实的设备(如前面讨论过的e100),都会被软中断函数以:
dev->poll()
的形式调用,对于e100来说,poll函数的接收过程已经分析了,而对于其它所有没有采用轮询技术的网络设备来说,它们将统统调用
process_backlog函数(我觉得把它改名为pseudo-poll是否更合适一些^o^)。
OK,我想分析到这里,关于中断处理与轮询技术的差异,已经基本分析开了……
继续来看,netif_rx_schedule进一步调用__netif_rx_schedule:
CODE:
/* Try to reschedule poll. Called by irq handler. */
static inline void netif_rx_schedule(struct net_device *dev)
{
if (netif_rx_schedule_prep(dev))
__netif_rx_schedule(dev);
}
CODE:
/* Add interface to tail of rx poll list. This assumes that _prep has
* already been called and returned 1.
*/
static inline void __netif_rx_schedule(struct net_device *dev)
{
unsigned long flags;
local_irq_save(flags);
dev_hold(dev);
/*伪设备也好,真实的设备也罢,都被加入了队列层的设备列表*/
list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
if (dev->quota < 0)
dev->quota += dev->weight;
else
dev->quota = dev->weight;
/*触发软中断*/
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
local_irq_restore(flags);
}
软中断被触发,注册的net_rx_action函数将被调用:
CODE:
/*接收的软中断处理函数*/
static void net_rx_action(struct softirq_action *h)
{
struct softnet_data *queue = &__get_cpu_var(softnet_data);
unsigned long start_time = jiffies;
int budget = netdev_max_backlog;
local_irq_disable();
/*
* 遍历队列的设备链表,如前所述,__netif_rx_schedule已经执行了
* list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
* 设备bakclog_dev已经被添加进来了
*/
while (!list_empty(&queue->poll_list)) {
struct net_device *dev;
if (budget <= 0 || jiffies - start_time > 1)
goto softnet_break;
local_irq_enable();
/*取得链表中的设备*/
dev = list_entry(queue->poll_list.next,
struct net_device, poll_list);
netpoll_poll_lock(dev);
/*调用设备的poll函数,处理接收数据包,这样,采用轮询技术的网卡,它的真实的poll函数将被调用,
这就回到我们上一节讨论的e100_poll函数去了,而对于采用传统中断处理的设备,它们调用的,都将是
bakclog_dev的process_backlog函数*/
if (dev->quota <= 0 || dev->poll(dev, &budget)) {
netpoll_poll_unlock(dev);
/*处理完成后,把设备从设备链表中删除,又重置于末尾*/
local_irq_disable();
list_del(&dev->poll_list);
list_add_tail(&dev->poll_list, &queue->poll_list);
if (dev->quota < 0)
dev->quota += dev->weight;
else
dev->quota = dev->weight;
} else {
netpoll_poll_unlock(dev);
dev_put(dev);
local_irq_disable();
}
}
out:
local_irq_enable();
return;
softnet_break:
__get_cpu_var(netdev_rx_stat).time_squeeze++;
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
goto out;
}
对于dev->poll(dev, &budget)的调用,一个真实的poll函数的例子,我们已经分析过了,现在来看process_backlog,
CODE:
static int process_backlog(struct net_device *backlog_dev, int *budget)
{
int work = 0;
int quota = min(backlog_dev->quota, *budget);
struct softnet_data *queue = &__get_cpu_var(softnet_data);
unsigned long start_time = jiffies;
backlog_dev->weight = weight_p;
/*在这个循环中,执行出队操作,把数据从队列中取出来,交给netif_receive_skb,直至队列为空*/
for (;;) {
struct sk_buff *skb;
struct net_device *dev;
local_irq_disable();
skb = __skb_dequeue(&queue->input_pkt_queue);
if (!skb)
goto job_done;
local_irq_enable();
dev = skb->dev;
netif_receive_skb(skb);
dev_put(dev);
work++;
if (work >= quota || jiffies - start_time > 1)
break;
}
backlog_dev->quota -= work;
*budget -= work;
return -1;
/*当队列中的数据包被全部处理后,将执行到这里*/
job_done:
backlog_dev->quota -= work;
*budget -= work;
list_del(&backlog_dev->poll_list);
smp_mb__before_clear_bit();
netif_poll_enable(backlog_dev);
if (queue->throttle)
queue->throttle = 0;
local_irq_enable();
return 0;
}
这个函数重要的工作,就是出队,然后调用netif_receive_skb()将数据包交给上层,这与上一节讨论的poll是一样的。这也是为什么,
在网卡驱动的编写中,采用中断技术,要调用netif_rx,而采用轮询技术,要调用netif_receive_skb啦!
到了这里,就处理完数据包与设备相关的部分了,数据包将进入上层协议栈
浏览量:2
下载量:0
时间:
QQ文件共享怎么用?下面就由读文网小编来给大家说说QQ文件共享接收和发送文件的详细方法。欢迎大家前来阅读!!!
1、首先,请大家将自己电脑上安装的QQ软件更新到最新版本,然后点击QQ主面板上的文件助手图标,如下图所示。
2、进入QQ文件助手以后,点击文件助手主页面左侧的“我的共享”按钮,开始准备与QQ好友一起共享文件。
3、接下来,点击我的共享页面右侧的“新建共享”按钮,开始创建一个新的共享文件夹,同时与好友一起共享文件。
4、接下来,我们需要填写共享文件夹的名称,这个需要你自己来决定,这里我以学习资料为例向大家讲解,然后我们点击“添加成员”按钮。
5、接下来,在QQ软件界面上的好友列表中选择要添加的好友,如果你想直接添加一个好友分组,直接点击右侧的“+”按钮,当然也可以选择性添加好友,好友最多只能添加100人,然后点击页面右下方的“确定”按钮。
6、接下来,我们需要选择文件放入该共享文件夹,点击页面右侧的“添加新内容”按钮,然后查找自己电脑上的文件将该文件添加进去即可。
7、接下来,页面会显示出“文件已经共享成功”的提示,在使用的过程中需要我们同时保持QQ在线,这样好友就可以及时查看共享的文件啦,也方便好友的下载。
好友给你共享文件
1、如果你被好友成功邀请加入了文件共享的成员,那么QQ会自动弹出文件共享消息和窗口,供我们查看共享文件,我们可以点击“查看共享”按钮来查看共享的文件,如下图所示。
2、在好友的共享选项中,点击就可以看到该共享文件夹,点击之后就可以查看到共享的文件啦,点击“下载”按钮就可以下载该文件啦。
文件共享设置
1、文件共享默认处于开启状态,如果你不想使用该功能的话,我们可以将其关闭,点击如下图所示的“设置”按钮。
2、接下来,我们就会进入QQ文件共享设置选项,在该设置选项中,点击“禁用文件共享功能”,然后保存相应的设置即可。在关闭了QQ文件共享以后,我们将不会接收到来自好友的文件共享,也不会收到任何消息。
总结
以上就是读文网小编为大家整理的“QQ文件共享接收和发送文件的详细方法”相关内容,更多资讯敬请访问读文网玩转QQ的QQ技巧频道!
浏览量:2
下载量:0
时间:
电脑网络本地连接只有发送没有接收,这样就无法访问网络了,该如何解决呢?下面读文网小编就为大家介绍一下这个故障的解决办法,有需要的朋友可以参考下。
1、IP地址,子网掩码和网关是否设置正确!!!
2、用ping命令 ping 网卡 (ping 127.0.0.1),再 ping 自己的IP地址,检查网卡是否有问题,这步检测过后 ,我发现网卡没有问题
3、网线问题<包括网线和水晶头,这个是线路问题,因为12线是用于发送,而36线是接收,要是36线断了的话就会显示这种情况,有仪器测试一下网线应该就可以看出问题了,经过测试我发现是网线问题
4、更换新的网线后,我发现问题解决了。
浏览量:59
下载量:0
时间:
电脑网络本地连接只有发送没有接收数据,这样就无法访问网络了,该如何解决呢?下面读文网小编就为大家介绍一下这个故障的解决办法,有需要的朋友可以参考下。
1、IP地址,子网掩码和网关是否设置正确!!!
2、用ping命令 ping 网卡 (ping 127.0.0.1),再 ping 自己的IP地址,检查网卡是否有问题,这步检测过后 ,我发现网卡没有问题
3、网线问题<包括网线和水晶头,这个是线路问题,因为12线是用于发送,而36线是接收,要是36线断了的话就会显示这种情况,有仪器测试一下网线应该就可以看出问题了,经过测试我发现是网线问题
4、更换新的网线后,我发现问题解决了。
浏览量:2
下载量:0
时间:
无线网络网速比3G或者4G网络快多了,我在家用手机上网,常常有的角落信号很强,有的角落却没有信号,我能否用无线路由器实现不直线插网线,仅通过接收无线信号再发送出去呢?答案是肯定的。下面跟着读文网小编一起来学习吧。
选择好要安装的第二个路由器的位置后,打开路由器设置页面。
点击WAN口介质类型,选择无线WAN。
点击开启扫描功能。
找到第一个无线路由器再前面选择,并确定。
选择安全模式下面的mixed wpa/wpa2-psk,WPA/WPA2加密规则选择AES
最后输入第一个路由器的访问密码,将两个路由器连接起来。
无线路由器如何接收无线信号再发送的相关
浏览量:2
下载量:0
时间:
相信不少朋友在系统连接传真机后却不知道如何去配置传真机,那么XP系统电脑接收传真如何设置呢?读文网小编分享了设置XP系统电脑接收传真的方法,希望可以帮助到大家。
1、单击开始,选择“打印机和传真”。
2、在打开的对话框中,双击传真图标。将启动传真配置向导(如果没有安装调制解调器,可能会先运行调制解调器安装向导,此处从略)。
在“欢迎使用传真配置向导”屏幕上单击“下一步”,会出现“发件人信息”窗口,键入希望在传真封面上显示的信息,然后单击“下一步” 。
在 “选择传真发送或接收设备”屏幕上,单击“请选择传真设备”列表中您想使用的调制解调器和其他选项,然后单击“下一步” 。
3、指定“路由选项”,选择打印机或文件夹来接收传真。
4、单击完成则可以启动“传真控制台”(从传真控制台的工具菜单选择“配置传真”命令可以更改现有传真配置)。
看过“XP系统电脑接收传真如何设置”
浏览量:6
下载量:0
时间:
有的时候我们对一些文件进行操作时,需要把文件放到一个特定的文件夹怎么办才方便快捷呢?下面跟着读文网小编来一起了解下Win7下如何添加“发送到”菜单吧。
1.创建你所想存放文件的文件夹快捷方式。如下图所示
2.在地址栏输入shell:sendto 回车。如下图所示
3.剪切粘贴你所创建的文件夹快捷方式到上一步打开的地址页面下。如下图所示
4.右键你所操作的文件,选择“发送到”你所想存放文件的文件夹(这里你会发现已经成功把想存放文件的文件夹的快捷方式添加到“发送到”中)。如下图所示
5.打开你所想存放文件的文件夹,你会发现刚刚发送的文件已经在此文件夹里面了。如下图所示
以上的“发送到”菜单巧妙小功能是不是真的很便捷呢?特别是在筛选文件的时候可以为你省下大把的时间,有兴趣的同志也可以尝试设置看看哦。
看过“Win7下如何添加“发送到”菜单”
浏览量:6
下载量:0
时间:
Win8开启蓝牙并且连接蓝牙设备以后,会自动在桌面右键菜单的“发送到”中添加蓝牙的选择,这样就可以方便用户利用蓝牙传输文件。不过有些用户没有发送到选项,那么Win8蓝牙如何添加到右键菜单“发送到”选项中呢?下面跟着读文网小编来一起了解下吧。
1、打开C:WindowsSystem32,搜索fsquirt.exe,这时会得到多个搜索结果,选择路径是C:WindowsSystem32的那个,其他的也可以,只不过是英文罢了。
2、在fsquirt.exe上右键-》创建快捷方式,因为无法在当前位置创建快捷方式,所以点“是”把快捷方式放在桌面上;
3、在开始屏幕直接输入shell:sendto。然后点击输入框下面出现的shell:sendto;
4、把创建好的快捷方式复制到刚刚打开的文件夹中,你也可以给快捷方式重命名一下;
5、如果你发现有Bluetooth.btwsendto文件,你可以删除它,不影响使用。完了之后,右键“发送到”里面就有了蓝牙选项;
6、不过界面跟之前W500装原装驱动的时候的有点不一样。
关于Win8把蓝牙添加到右键菜单“发送到”选项中的具体操作方法就介绍完了,这样一来只有右键文件,那么我们就可以利用蓝牙把文件传输到手机上了。
看过“Win8蓝牙如何添加到右键菜单“发送到”选项”
浏览量:2
下载量:0
时间:
有很多人会使用蓝牙功能来发送文件,可是win7无法使用蓝牙设备发送文件,怎么解决呢?今天读文网小编与大家分享下解决win7无法使用蓝牙设备发送文件的具体操作步骤,有需要的朋友不妨了解下。
1.首先,咱们先进入蓝牙功能所在的位置,双击打开计算机,然后在上方的地址栏中输入%windir%system32并单击回车,这样就可以出现如下图中所示的界面了。
2.小编在这里选中的fsquirt.exe便是蓝牙功能了,大家也许从图标就可以看出来了吧,咱们右键点击这个选项,然后在下滑菜单中选择创建快捷方式。
3.接下来大家直接点击键盘上的win键,或者是打开开始菜单,然后在搜索框中输入shell:sendto,在上方的搜索结果中,咱们单击shell:sendto。
4.接下来,咱们将刚刚创建的快捷方式复制到shell:sendto文件夹中,并且将快捷方式重命名为蓝牙设备即可。
浏览量:9
下载量:0
时间:
安装完win7位旗舰版后发现之前的东西都没有了,就连电脑上的iss服务器快捷方式也消失了,那么你知道win7系统怎么将iss服务器发送到桌面吗?下面是读文网小编整理的一些关于win7系统将iss服务器发送到桌面的相关资料,供你参考。
1、我们需要先找到控制面板,点击电脑左下角的“圆球”,然后在弹出的列表中选择“控制面板”;
2、控制面板界面的默认查看方式是“类别”,我们点击“大图标”或者“小图标”,更换查看方式为“类别”;
3、然后我们在所有控制面板项,找到“管理工具”选项,并点击开,进入到管理工具的界面;
4、管理工具页面中有好多管理工具方面的快捷方式,第一个就是我们要找的IIS信息服务管理器;
5、我们右击“IIS服务器”的快捷方式,点击发送到,然后点击再下一级菜单的“桌面快捷方式”选项;
6、我们再次回到桌面,就会看到IIS服务器的快捷方式了,以后直接点击它就可以打开电脑的IIS服务器了。
win7系统将iss服务器发送到桌面的相关
浏览量:3
下载量:0
时间:
对于工作中需要频繁使用邮件传递信息的朋友们,很多时候难免会发错收件人,或者发出去的邮件可能不太完善需要修改。那么你知道Outlook2010怎么撤回已发送邮件吗?下面是读文网小编整理的一些关于Outlook2010撤回已发送邮件的相关资料,供你参考。
第一步:单击outlook面板上的“邮件”或收件箱中的“显示隐藏文件夹列表”按钮。
第二步:单击“已发送的邮件”。
第三步:打开要撤回或替换的邮件。
第四步:选择“文件”→“信息”→“重发或者撤回”选项的“撤回该邮件”,在弹出的“撤回该邮件”对话框中,选择“删除该邮件的未读副本”。若要用其他邮件替换该邮件,请单击“删除未读副本并用新邮件替换”,单击“确定”按钮,然后键入新邮件。(PS:若要替换原邮件,必须发送一封新邮件,否则,将撤回原始邮件。)如下图:
第五步:若要了解对收件人邮件的撤回或替换成功与否,选中“告诉我对每个收件人撤回是成功还是失败”复选框。选中该框后,就会在“收件箱”中列出谁的邮件撤回失败。
注意:使用以上方法必须要满足两个条件:一个是收件人必须登录并使用收件箱或outlook;二是收件人还没有读这封邮件或还未将邮件移出“文件箱”。
Outlook2010的相关
浏览量:2
下载量:0
时间:
台式机想要接收一下手机开的wifi!用什么方法好呢?下面由读文网小编给你做出详细的台式机接收wifi方法介绍!希望对你有帮助!
硬件准备:
电脑必须安装一块无线网卡,建议直接购买USB接口无线网卡。
安装网卡驱动:
确保网卡被电脑正确识别,是使用无线网络的首要条件。在购买无线网卡时,通过会自带一张驱动光盘,直接运行光盘中的可执行文件即可自动完成整个驱动的安装操作。
当无线网卡驱动安装完成后,插入USB无线网卡,并打开“控制面板”,点击“网络和共享中心”项进入。
接着点击“更改适配器设置”项。
在打开的窗口中,点击无线网卡设备,选择“连接/断开”项。
此时就会在状态栏上出现“连接无线网联”的窗口,选择已搜索到的无线网络,点击“连接”按钮。
输入连接密码,点击“连接”按钮。
即可实现连接WiFi网络操作。
看了“台式机如何接收wifi ”文章的还看了:
浏览量:2
下载量:0
时间:
手机开了wifi,台式机要怎么样去接收呢?下面由读文网小编给你做出详细的台式机接收wifi信号方法介绍!希望对你有帮助!
台式电脑无线上网,首先要有无线路由器,然后买张USB或者PCI的无线网卡,装上驱动,连接WIFI,台式电脑就能上网了。具体步骤如下:
1、插上无线网卡后,电脑开机。
2、用无线网卡自带的光盘安装驱动(有些无线网卡支持免驱,直接插上就行)。
3、安装完成后,屏幕右下角网络图标。
4、在网络中找到自己的WIFI名称。
5、输入密码,并连接。
这样台式机就能用无线网卡上网了。
看了“台式机接收wifi信号方法有哪些 ”文章的还看了:
浏览量:37
下载量:0
时间:
台式机想要通过无线接收器来连接wifi,用什么方法好呢?下面由读文网小编给你做出详细的台式机无线接收器连接wifi方法介绍!希望对你有帮助!
思路是使用无线路由器发射wifi,台式机使用无线网卡或是网线上网。
操作如下(以台式机使用网线上网为例):
接线方式:外网接modem的ADSL网口,modem的LAN网口分线到无线路由器的WAN网口,无线路由器的LAN网口分线到电脑网口上(有些不需要modem,可外网直接接无线路由器的WAN网口);
电脑任意打开一个浏览器,输入 192.168.1.1(多数默认,具体详见路由器背面)后enter,进入无线路由器管理后台登陆界面,输入账号或是登陆密码登录进入;
点击无线路由器后台界面左侧的运行向导,进行无线路由器上网参数设置;
设置向导中上网方式一般选择默认后点击下一步;
输入报装网线时,运营商给予的账号和密码,点击下一步;
设置无线网络账号(SSID)和选择无线网络加密方式,设置无线网络密码,点击下一步;
点击完成退出设置向导,即可正常使用网络上网。
备注:台式机网线上网,主要将台式机的IP地址设置为自动获取,且无线路由器的DHCP功能正常开启即可。
浏览量:89
下载量:0
时间:
台式机想要通过无线接收器来上网!该怎么样解决呢?下面由读文网小编给你做出详细的台式机使用无线接收器解决方法介绍!希望对你有帮助!
1、在开始搜索框中输入ncpa.cpl,按回车打开网络连接;
2、打开网络连接,在刚才设立的连接图标上,右击鼠标,选择属性;
3、打开连接属性
在选项标签中,确认提示名称、密码、凭证等这个选项前没有打勾;然后,点击确定;
4、取消勾选“提示名称、密码和证书等”
在连接图标上,右击鼠标,选择建立快捷方式;
5、取消勾选“提示名称、密码和证书等”
在连接图标上,右击鼠标,选择建立快捷方式;
6、创建快捷方式
Windows会问你是否将快捷方式放在桌面上,点是;
7、确定创建快捷方式
回到桌面,在快捷方式图标上,右击鼠标,选择复制;
8、复制快捷方式
接着,再按一下开始按钮,选择所有程序,右击启动,选择打开所有用户;
9、打开所有用户启动文件夹
在新打开的窗口中,于空白处点鼠标右键,选粘贴。
10、粘贴快捷方式 OK,这样设置完成后,下次电脑开机之后,就会自动尝试连接网络了。
看了“台式机如何使用无线接收器 ”文章的还看了:
浏览量:95
下载量:0
时间:
当我的台式机使用无线接收器时,竟然上不了网了,该怎么办呢?下面由读文网小编给你做出详细的台式机使用无线接收器上不了网解决方法介绍!希望对你有帮助!
1、电脑设置IP地址:在桌面上右击“网上邻居”然后单击弹出菜单中的“属性”,在网络连接窗口中右击“本地(或无线)连接”然后再单击弹出菜单中的“属性”,在弹出的对话框的“此连接使用下列项目”中双击“Internet 协议(TCP/IP)(用4.0协议)”
在弹出的Internet 协议(TCP/IP)属性对话框里设置IP.一般地选择“自动获得IP地址”“自动获得DNS服务器地址”。如果不行那就使用“使用下面的IP地址”IP地址:192.168.1.n (n为2--255中的任意一个数)子网掩码:255.255.255.0 默认网关:192.168.1.1 (路由器背面上有)
DNS服务器地址查看一下当地其它上网电脑。(也有IP地址为:192.168.0.n 网关:192.168.0.100)
2、重新搜索一下无线网络信号,选择自己的网络(SSID)输入设定的密钥进行连接。
看了“ 台式机使用无线接收器上不了网如何解决”文章的还看了:
浏览量:2
下载量:0
时间:
自从我的台式机使用了无线接收器后就非常的卡!该怎么办呢?下面由读文网小编给你做出详细的台式机使用无线接收器很卡解决方法介绍!希望对你有帮助!
在确保无线网卡的驱动程序都正确安装的前提下,给出如下建议:
1、无线网卡建议是插到前置的USB面板上,后置的话可能会影响信号的接收;
2、更换更大速率的无线网卡或是使用PCI网卡均可,但有一定几率并不会得到很好改善,建议慎选;
3、更换功率更大的无线路由器,毕竟如果无线路由器距离无线网卡过远,信号经过长距离传播和其他因素干扰,的确是会减弱很多;
4、使用网线直连,这是最稳定的办法。
看了“ 台式机使用无线接收器很卡怎么办”文章的还看了:
浏览量:2
下载量:0
时间: