零拷贝技术研究与实现

嵌入式实时以太网驱动:LwIP零拷贝Netif实现 以太网网络接口(Netif)是嵌入式系统接入TCP/IP协议栈的关键抽象层,其性能直接决定通信实时性与资源开销。在ARM Cortex-M等资源受限平台,传统Netif常因内存动态分配、数据包拷贝和中断延迟过高而难以满足硬实时要求。EthernetNetif_RSF通过零拷贝(Zero-Copy)、静态内存池与DMA描述符环等机制,在LwIP 2.1.x基础上重构Netif实现,确保μs级确定性响应。该方案深度适配STM32系列MCU与主流PHY芯片,广泛应用于工业网关、运动控制及IEEE 1588时间同步 阅读详情
零拷贝技术研究与实现
作者:梁健(firstdot)
E-MAIL:firstdot@163.com

感谢王超、史晓龙的共同研究与大力帮助
一.基本概念
零拷贝(zero-copy)基本思想是:数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现CPU的零参与,彻底消除CPU在这方面的负载。实现零拷贝用到的最主要技术是DMA数据传输技术和内存区域映射技术。如图1所示,传统的网络数据报处理,需要经过网络设备到操作系统内存空间,系统内存空间到用户应用程序空间这两次拷贝,同时还需要经历用户向系统发出的系统调用。而零拷贝技术则首先利用DMA技术将网络数据报直接传递到系统内核预先分配的地址空间中,避免CPU的参与;同时,将系统内核中存储数据报的内存区域映射到检测程序的应用程序空间(还有一种方式是在用户空间建立一缓存,并将其映射到内核空间,类似于linux系统下的kiobuf技术),检测程序直接对这块内存进行访问,从而减少了系统内核向用户空间的内存拷贝,同时减少了系统调用的开销,实现了真正的“零拷贝”。


图1 传统数据处理与零拷贝技术之比较
二.实现
在redhat7.3上通过修改其内核源码中附带的8139too.c完成零拷贝的试验,主要想法是:在8139too网卡驱动模块启动时申请一内核缓存,并建立一数据结构对其进行管理,然后试验性的向该缓存写入多个字符串数据,最后通过proc文件系统将该缓存的地址传给用户进程;用户进程通过读proc文件系统取得缓存地址并对该缓存进行地址映射,从而可以从其中读取数据。哈哈,为了偷懒,本文只是对零拷贝思想中的地址映射部分进行试验,而没有实现DMA数据传输(太麻烦了,还得了解硬件),本试验并不是一个IDS产品中抓包模块的一部分,要想真正在IDS中实现零拷贝,除了DMA外,还有一些问题需考虑,详见本文第三节的分析。以下为实现零拷贝的主要步骤,详细代码见附录。

步骤一:修改网卡驱动程序
a.在网卡驱动程序中申请一块缓存:由于在linux2.4.X内核中支持的最大可分配连续缓存大小为2M,所以如果需要存储更大量的网络数据报文,则需要分配多块非连续的缓存,并使用链表、数组或hash表来对这些缓存进行管理。

#define PAGES_ORDER 9
unsigned long su1_2
su1_2 = __get_free_pages(GFP_KERNEL,PAGES_ORDER);

b.向缓存中写入数据:真正IDS产品中的零拷贝实现应该是使用DMA数据传输把网卡硬件接收到的包直接写入该缓存。作为试验,我只是向该缓存中写入几个任意的字符串,如果不考虑DMA而又想向缓存中写入真正的网络数据包,可以在8139too.c的rtl8139_rx_interrupt()中调用netif_rx()后插入以下代码:

//put_pkt2mem_n++; //包个数
//put_mem(skb->data,pkt_size);
其中put_pkt2mem_n变量和put_mem函数见附录。

c.把该缓存的物理地址传到用户空间:由于在内核中申请的缓存地址为虚拟地址,而在用户空间需要得到的是该缓存的物理地址,所以首先要进行虚拟地址到物理地址的转换,在linux系统中可以使用内核虚拟地址减3G来获得对应的物理地址。把缓存的地址传到用户空间需要在内核与用户空间进行少量数据传输,这可以使用字符驱动、proc文件系统等方式实现,在这里采用了proc文件系统方式。

int read_procaddr(char *buf,char **start,off_t offset,int count,int *eof,void *data)
{
    sprintf(buf,"%u/n",__pa(su1_2));
    *eof = 1;
    return 9;
}
create_proc_read_entry("nf_addr",0,NULL,read_procaddr,NULL);

步骤二:在用户程序中实现对共享缓存的访问
a.读取缓存地址:通过直接读取proc文件的方式便可获得。

char addr[9];
int fd_procaddr;
unsigned long ADDR;
fd_procaddr = open("/proc/nf_addr",O_RDONLY);
read(fd_procaddr,addr,9);
ADDR = atol(addr);

b.把缓存映射到用户进程空间中:在用户进程中打开/dev/mem设备(相当于物理内存),使用mmap把网卡驱动程序申请的缓存映射到自己的进程空间,然后就可以从中读取所需要的网络数据包了。

char *su1_2;
int fd;
fd=open("/dev/mem",O_RDWR);    
su1_2 = mmap(0,PAGES*4*1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, ADDR);

三.分析
    零拷贝中存在的最关键问题是同步问题,一边是处于内核空间的网卡驱动向缓存中写入网络数据包,一边是用户进程直接对缓存中的数据包进行分析(注意,不是拷贝后再分析),由于两者处于不同的空间,这使得同步问题变得更加复杂。缓存被分成多个小块,每一块存储一个网络数据包并用一数据结构表示,本试验在包数据结构中使用标志位来标识什么时候可以进行读或写,当网卡驱动向包数据结构中填入真实的包数据后便标识该包为可读,当用户进程对包数据结构中的数据分析完后便标识该包为可写,这基本解决了同步问题。然而,由于IDS的分析进程需要直接对缓存中的数据进行入侵分析,而不是将数据拷贝到用户空间后再进行分析,这使得读操作要慢于写操作,有可能造成网卡驱动无缓存空间可以写,从而造成一定的丢包现象,解决这一问题的关键在于申请多大的缓存,太小的缓存容易造成丢包,太大的缓存则管理麻烦并且对系统性能会有比较大的影响。

四.附录
a.    8139too.c中加入的代码

/*add_by_liangjian for zero_copy*/
#include <linux/wrapper.h>
#include <asm/page.h>
#include <linux/slab.h>
#include <linux/proc_fs.h>
#define PAGES_ORDER 9
#define PAGES 512
#define MEM_WIDTH    1500
/*added*/

/*add_by_liangjian for zero_copy*/
struct MEM_DATA
{
    //int key;
    unsigned short width;/*缓冲区宽度*/
    unsigned short length;/*缓冲区长度*/
    //unsigned short wtimes;/*写进程记数,预留,为以后可以多个进程写*/
    //unsigned short rtimes;/*读进程记数,预留,为以后可以多个进程读*/
    unsigned short wi;/*写指针*/
    unsigned short ri;/*读指针*/
} * mem_data;
struct MEM_PACKET
{
    unsigned int len;
    unsigned char packetp[MEM_WIDTH - 4];/*sizeof(unsigned int) == 4*/
};
unsigned long su1_2;/*缓存地址*/
/*added*/

/*add_by_liangjian for zero_copy*/
//删除缓存
void del_mem()
{
    int pages = 0;
    char *addr;
    addr = (char *)su1_2;
    while (pages <=PAGES -1)
    {
        mem_map_unreserve(virt_to_page(addr));
        addr = addr + PAGE_SIZE;
        pages++;
    }
    free_pages(su1_2,PAGES_ORDER);    
}    
void init_mem()
/********************************************************
*                  初始化缓存
*       输入:   aMode:    缓冲区读写模式:  r,w        *
*       返回:   00:     失败                        *
*               >0:     缓冲区地址                  *
********************************************************/
{
    int i;
    int pages = 0;
    char *addr;
    char *buf;
    struct MEM_PACKET * curr_pack;
    
    su1_2 = __get_free_pages(GFP_KERNEL,PAGES_ORDER);
    printk("[%x]/n",su1_2);
    addr = (char *)su1_2;
    while (pages <= PAGES -1)
    {
        mem_map_reserve(virt_to_page(addr));//需使缓存的页面常驻内存
        addr = addr + PAGE_SIZE;
        pages++;
    }
    mem_data = (struct MEM_DATA *)su1_2;
    mem_data[0].ri = 1;
          mem_data[0].wi = 1;
          mem_data[0].length = PAGES*4*1024 / MEM_WIDTH;
          mem_data[0].width = MEM_WIDTH;
    /* initial su1_2 */
    for(i=1;i<=mem_data[0].length;i++)
    {
        buf = (void *)((char *)su1_2 + MEM_WIDTH * i);
        curr_pack = (struct MEM_PACKET *)buf;
        curr_pack->len = 0;
    }    
}
int put_mem(char *aBuf,unsigned int pack_size)
/****************************************************************
*                 写缓冲区子程序                                *
*       输入参数    :   aMem:   缓冲区地址                      *
*                       aBuf:   写数据地址                      *
*       输出参数    :   <=00 :  错误                            *
*                       XXXX :  数据项序号                      *
*****************************************************************/
{
    register int s,i,width,length,mem_i;
    char *buf;
    struct MEM_PACKET * curr_pack;

    s = 0;
    mem_data = (struct MEM_DATA *)su1_2;
    width  = mem_data[0].width;
    length = mem_data[0].length;
    mem_i  = mem_data[0].wi;
    buf = (void *)((char *)su1_2 + width * mem_i);

    for (i=1;i<length;i++){
        curr_pack = (struct MEM_PACKET *)buf;
            if  (curr_pack->len == 0){
                    memcpy(curr_pack->packetp,aBuf,pack_size);
                    curr_pack->len = pack_size;;
                s = mem_i;
            mem_i++;
                    if  (mem_i >= length)
                        mem_i = 1;
                mem_data[0].wi = mem_i;
                break;
            }
            mem_i++;
            if  (mem_i >= length){
                    mem_i = 1;
                    buf = (void *)((char *)su1_2 + width);
            }
            else buf = (char *)su1_2 + width*mem_i;
        }

    if(i >= length)
            s = 0;
    return s;
}
// proc文件读函数
int read_procaddr(char *buf,char **start,off_t offset,int count,int *eof,void *data)
{
    sprintf(buf,"%u/n",__pa(su1_2));
    *eof = 1;
    return 9;
}
/*added*/

在8139too.c的rtl8139_init_module()函数中加入以下代码:
/*add_by_liangjian for zero_copy*/
    put_pkt2mem_n = 0;
    init_mem();
    put_mem("data1dfadfaserty",16);
    put_mem("data2zcvbnm",11);
    put_mem("data39876543210poiuyt",21);
    create_proc_read_entry("nf_addr",0,NULL,read_procaddr,NULL);
/*added */    

在8139too.c的rtl8139_cleanup_module()函数中加入以下代码:
/*add_by_liangjian for zero_copy*/
    del_mem();
    remove_proc_entry("nf_addr",NULL);
/*added*/    

b.用户空间读取缓存代码

#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#define PAGES 512
#define MEM_WIDTH 1500
struct MEM_DATA
{
    //int key;
    unsigned short width;/*缓冲区宽度*/
    unsigned short length;/*缓冲区长度*/
    //unsigned short wtimes;/*写进程记数,预留,为以后可以多个进程写*/
    //unsigned short rtimes;/*读进程记数,预留,为以后可以多个进程读*/
    unsigned short wi;/*写指针*/
    unsigned short ri;/*读指针*/
} * mem_data;

struct MEM_PACKET
{
    unsigned int len;
    unsigned char packetp[MEM_WIDTH - 4];/*sizeof(unsigned int) == 4*/
};

int get_mem(char *aMem,char *aBuf,unsigned int *size)
/****************************************************************
*                 读缓冲区子程序                                *
*       输入参数    :   aMem:   缓冲区地址                      *
*                       aBuf:   返回数据地址, 其数据区长度应大于*
*                               缓冲区宽度                      *
*       输出参数    :   <=00 :  错误                            *
*                       XXXX :  数据项序号                      *
*****************************************************************/
{
    register int i,s,width,length,mem_i;
    char     *buf;
    struct MEM_PACKET * curr_pack;

    s = 0;
    mem_data = (void *)aMem;
    width  = mem_data[0].width;
    length = mem_data[0].length;
    mem_i  = mem_data[0].ri;
    buf = (void *)(aMem + width * mem_i);

    curr_pack = (struct MEM_PACKET *)buf;
    if  (curr_pack->len != 0){/*第一个字节为0说明该部分为空*/
            memcpy(aBuf,curr_pack->packetp,curr_pack->len);
            *size = curr_pack->len;
            curr_pack->len = 0;
            s = mem_data[0].ri;
            mem_data[0].ri++;
            if(mem_data[0].ri >= length)
                    mem_data[0].ri = 1;
            goto ret;
        }
    
    for (i=1;i<length;i++){
            mem_i++;/*继续向后找,最糟糕的情况是把整个缓冲区都找一遍*/
            if  (mem_i >= length)
                mem_i = 1;
            buf = (void *)(aMem + width*mem_i);
            curr_pack = (struct MEM_PACKET *)buf;
            if  (curr_pack->len == 0)
                    continue;
            memcpy(aBuf,curr_pack->packetp,curr_pack->len);
            *size = curr_pack->len;
            curr_pack->len = 0;
            s = mem_data[0].ri = mem_i;
            mem_data[0].ri++;
            if(mem_data[0].ri >= length)
            mem_data[0].ri = 1;
            break;
        }

    ret:
    return s;
}

int main()
{
    char *su1_2;
    char receive[1500];
    int i,j;
    int fd;
    int fd_procaddr;
    unsigned int size;
    char addr[9];
    unsigned long ADDR;
    
    j = 0;
    /*open device 'mem' as a media to access the RAM*/
    fd=open("/dev/mem",O_RDWR);    
    fd_procaddr = open("/proc/nf_addr",O_RDONLY);
    read(fd_procaddr,addr,9);
    ADDR = atol(addr);
    close(fd_procaddr);
    printf("%u[%8lx]/n",ADDR,ADDR);
    /*Map the address in kernel to user space, use mmap function*/
    su1_2 = mmap(0,PAGES*4*1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, ADDR);
    perror("mmap");
    while(1)
    {
        bzero(receive,1500);
        i = get_mem(su1_2,receive,&size);
        if (i != 0)
        {
            j++;
            printf("%d:%s[size = %d]/n",j,receive,size);
        }    
        else
        {
            printf("there have no data/n");
            munmap(su1_2,PAGES*4*1024);
            close(fd);
            break;
        }
    }
    while(1);
}

五.参考文献
1.CHRISTIAN KURMANN, FELIX RAUCH ,THOMAS M. STRICKER.
Speculative Defragmentation - Leading Gigabit Ethernet to True Zero-Copy Communication
2.ALESSANDRO RUBINI,JONATHAN CORBET.《LINUX DEVICE DRIVERS 2》,O’Reilly & Associates 2002.
3.胡希明,毛德操.《LINUX 内核源代码情景分析》,浙江大学出版社 2001


关于作者:梁健,华北计算技术研究所在读硕士研究生,研究方向:信息安全。论文开题为《基于系统调用分析的主机异常入侵检测与防御》。对IDS有两年多的研究经验,熟悉linux内核,熟悉linux c/c++编程、win32 API编程,对网络和操作系统安全感兴趣。
零拷贝网络传输技术研究与实践 作者:禅与计算机程序设计艺术 1.简介 在现代计算机系统中,无论是多核CPU、GPU还是FPGA等加速处理器,都逐渐成为数据中心中主流的计算平台之一。为了提高数据中心内的网络性能和资源利用率,越来越多的厂商在各种网络协议层面上进行了改进。其中最主要的是对TCP/IP协议栈中的传输层(传输控制协议/互联网 阅读详情

相关推荐

mac搭建ios自动化环境之WebDriverAgent配置

一、WebDriverAgent 1、简单介绍: WebDriverAgent是facebook为苹果公司开发的一款自动化测试工具,github上有下载资源;另外appium安装后也会自动安装自带的WebDriverAgent;可以用自带的,也可以从github下载并替换自带的; 2、不同appium版本自带WebDriverAgent默认路径不同 1)appium 1.10.1之前版本: npm方式安装: /usr/local/lib/node_modules/appium/node_modules/a

qq_30237715的博客 2765

零拷贝的理解

在内核空间到用户空间的零拷贝(Kernel Space to User Space Zero-Copy)场景中,数据从内核空间传递到用户空间时,采用一些技术手段来减少或避免数据的实际复制。COW的基本原理是在需要复制数据时,先共享原始数据的副本,只有在写入操作发生时,才进行实际的复制。文件描述符传递是一种在进程之间传递打开文件的文件描述符的机制,允许接收进程访问传递的文件。零拷贝技术的应用可以显著提高数据传输的效率,减少了数据在系统内存之间的冗余复制操作,特别是在高性能网络应用中,零拷贝技术被广泛采用。

Alaskan_Husky的博客 2212

模电仿真软件:MultSim14.3下载与安装

在setup文件夹中,找到【Install.exe】文件,然后右键单击选择【以管理员身份运行】(这一步超级重要,否则可能会出现权限不足导致安装失败)!解压完成后,打开解压出来的文件夹,找到并进入【setup】文件夹(这一步很重要,不要进错文件夹)。4、在弹出的安装向导中,仔细阅读许可协议后,选择【我接受】,然后点击【下一步】继续安装过程。首先我们需要将下载好的安装包解压到本地文件夹,确保有足够的磁盘空间(建议至少5GB以上)!按照安装向导的提示,点击【下一步】继续(保持默认设置即可,除非你有特殊需求)

weixin_44264994的博客 2343

SylixOS网卡驱动优化

SylixOS网卡驱动优化

PCSean的博客 919

软件设计师---计算机系统

软件设计师---计算机系统

我从不打没有准备的仗! 3万+

9.16华为OD机试真题 新系统 - 矩阵螺旋遍历 (Java/Py/C/C++/Js/Go)

9.16华为OD机试真题 新系统 - 矩阵螺旋遍历 (Java/Py/C/C++/Js/Go)

算法大师! 657

(转载)零拷贝技术研究与实现

零拷贝技术研究与实现发布日期:2003-11-24文摘内容: 文摘出处:http://www.xfocus.net/创建时间:2003-11-21 更新时间:2003-11-23文章属性:原创文章提交:firstdot (firstdot_at_163.com)零拷贝技术研究与实现作者:梁健(firstdot)E-MAIL:firstdot@163.com感谢王超、史晓龙的共同研究与大力帮

Kendiv's Box 2710

Linux系统中u-boot启动流程分析(详解)

代码部分,具体如下;

嵌入式技术开发 1993

LWIP开源协议栈

LWIPLwIP源代码文件目录解析LwIP为用户提供三组API接口,sequential API(Netconn API) 为普通的、顺序的程序提供了使用lwIP栈的方法。依赖操作系统,所有操作都需要协议栈去处理,应用程序与协议栈通信,通过发送消息方式进行,因此这种方式会造成频繁的任务切换,速度相比RAW API慢了许多。在操作系统上推荐使用本API。BSD Socket API 建立在sequential API之上的,封装出的一套BSD Socket API。

技术容我懒 1002

在MATLAB中将C语言代码转化为可执行的mexw64文件

这样,您可以将高效的C语言算法集成到MATLAB环境中,以便更好地利用MATLAB的功能和特性。请记住,在使用mex转换C语言代码之前,您需要正确编写和测试C语言代码,并确保它与MATLAB的mex接口兼容。MATLAB是一种功能强大的数值计算软件,它提供了一个机制,可以将C语言代码转化为MATLAB可执行的mexw64文件。在上述命令中,"-output"选项指定生成的mex文件的名称,"myfunction"是您选择的名称,"myfunction.c"是要编译的C语言源文件。步骤1:编写C语言代码。

CodeRoarX的博客 1428

AI 时代嵌入式工程师的能力模型与学习路线:全系列收官

AI 时代嵌入式工程师的能力模型与学习路线:全系列收官

weixin_42162924的博客 40

(转载)零拷贝技术研究与实现

零拷贝技术研究与实现作者:梁健(firstdot)E-MAIL:firstdot@163.com一.基本概念零拷贝(zero-copy)基本思想是:数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现CPU的零参与,彻底消除CPU在这方面的负载。实现零拷贝用到的最主要技术是DMA数据传输技术和内存区域映射技术。如图1所示,传统的网络数据报处理,需要经过网络设备到操作

峥嵘岁月 6056

Kappa函数:测量一致性并计算置信区间的R语言包

Kappa函数是一种常用的统计量,用于衡量两个评估者之间的一致性。在R语言中,有一个名为"vcd"的包提供了计算Kappa函数以及相应的置信区间的功能。本文将介绍如何使用"vcd"包来计算Kappa函数的值,并进一步计算置信区间。Kappa函数的值介于-1和1之间,值越接近1表示评估者之间的一致性越高,值越接近-1表示评估者之间的一致性越低,值接近0表示评估者之间的一致性接近于随机。通过以上步骤,我们成功地使用R语言中的"vcd"包计算了Kappa函数的值和置信区间。接下来,我们可以使用"vcd"包中的。

2301_79331230的博客 1877

[嵌入式开发]零拷贝技术研究与实现

零拷贝技术研究与实现   作者:梁健(firstdot)   E-MAIL:firstdot@163.com   感谢王超、史晓龙的共同研究与大力帮助   一.基本概念   零拷贝(zero-copy)基本思想是:数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现CPU的零参与,彻底消除CPU在这方面的负载。实现零拷贝用到的最主要技术是D

xjbclz的专栏 666

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

(转载)零拷贝技术研究与实现收藏

 零拷贝技术研究与实现作者:梁健(firstdot)E-MAIL:firstdot@163.com一.基本概念零拷贝(zero-copy)基本思想是:数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现CPU的零参与,彻底消除CPU在这方面的负载。实现零拷贝用到的最主要技术是DMA数据传输技术和内存区域映射技术。如图1所示,传统的网络数据报处理,需要经过

hanwei_1049的专栏 624

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

浅析Linux中的零拷贝技术

本文探讨Linux中主要的几种零拷贝技术以及零拷贝技术适用的场景。为了迅速建立起零拷贝的概念,我们拿一个常用的场景进行引入: 引文## 在写一个服务端程序时(Web Server或者文件服务器),文件下载是一个基本功能。这时候服务端的任务是:将服务端主机磁盘中的文件不做修改地从已连接的socket发出去,我们通常用下面的代码完成: while((n = read(diskfd, buf, ...

u012501054的博客 219

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

零拷贝:原理与实现

I/O 或输入/输出通常意味着CPU) 与如磁盘、鼠标、键盘等)之间的读写。在深入研究零拷贝之前,有必要指出包括磁盘设备和其他块导向设备)和之间的区别。。同时,。套接字接口背后发生的事情是发送方和接收方计算机创建自己的套接字,并设置发送或接收文件的连接。如今,越来越多的 Web 应用程序已经实现了从 CPU 绑定到 I/O 绑定的转变,这意味着 I/O 的性能通常是这些应用程序的瓶颈。。数据必须,而。这时零拷贝就派上用场了。零拷贝的主要原理是尽可能地消除或。

在技术的广袤天地里,本博客如精准罗盘。剖析前沿科技,深掘代码奥秘,以精炼笔触,带您穿越复杂技术迷宫,速达知识彼岸。 1412
上一篇: Win2000,XP下忽略驱动数字签名
下一篇: 应用和驱动之间用事件通讯的办法(转载)
iiprogram
博客等级 码龄25年 1068粉丝 1515原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值