目录

引言

地址绑定

MMU(内存管理单元)

基本硬件

进程如何从磁盘映射到内存

磁盘和内存的映射过程

MMU方案

开始编码

创建我们的缓冲区

/proc/[pid]/pagemap

写 /dev/mem

结论和完整代码清单

DPDK是如何操作的?

推荐文章


引言


理解“了解Linux内核”中有关内存管理的章节中,我认为尝试编写将虚拟内存地址转换为物理地址的程序会很有趣。而且,我想在用户空间上使用它。再进一步,为什么不尝试获取缓冲区的物理地址,转到内存中的该位置,对其进行修改,然后使用虚拟地址查看更改。

尝试在用户空间中完成此任务存在一些问题:

  • 虚拟内存背后的想法是提供连续内存的地址空间。进程的内存很可能存储在不连续的块中。
  • 没有保证page在系统的物理内存中。它可以在交换区或缓存中。可能没有实际地址!
  • 出于安全原因,即使进程的UID为0,进程也无权访问系统的原始内存。

我们可以采用两种方法来获取物理地址:

  1. 给内核添加一个系统调用,给定一个虚拟地址,它将返回物理地址。但是,修改内核违反了从用户空间执行所有操作的规则,因此我们必须将其排除在外。
  2. 将page映射文件用于进程(内核2.6.25中已添加)以获取页面映射到的帧,然后使用该页面查找/dev/mem并修改那里的缓冲区。

使用这种方法,完全有可能在用户空间中将虚拟地址转换为物理地址。但是,要验证我们的翻译是正确的,需要阅读/dev/mem。这确实需要对内核进行一次小的修改(更改配置选项),但稍后会进行更多修改。

内存由大量word或array组成,每个word或array都有与之关联的地址。现在,CPU的工作是从基于内存的程序计数器中获取指令。现在,这些指令可能会导致加载或存储到特定的存储器地址。地址绑定是从一个地址空间映射到另一地址空间的过程。逻辑地址是CPU在执行过程中生成的地址,而物理地址是指存储单元(已加载到内存中的单元)中的位置。请注意,用户仅处理逻辑地址(虚拟地址)。逻辑地址尤其由MMU或地址转换单元进行转换。该过程的输出是适当的物理地址或代码/数据在RAM中的位置。

地址绑定


地址绑定可以通过三种不同的方式完成:

  1. 编译时间–如果您知道在编译期间进程将驻留在内存中,则会生成绝对地址,即在编译过程中将物理地址嵌入到程序的可执行文件中。将可执行文件作为进程加载到内存中的速度非常快。但是,如果生成的地址空间被其他进程所占用,则程序将崩溃,并且有必要重新编译程序以更改地址空间。
  2. 加载时间–如果在编译时不知道进程将驻留在哪里,则将生成可重定位的地址。加载程序将可重定位地址转换为绝对地址。加载程序将主存储器中进程的基地址添加到所有逻辑地址中,以生成绝对地址。在这种情况下,如果进程的基址发生更改,则我们需要再次重新加载进程。
  3. 执行时间-指令在内存中,正在由CPU处理。此时可以分配和/或释放其他内存。如果可以在执行期间将进程从一个内存移动到另一个内存(动态链接-在加载或运行时完成链接),则使用此方法。例如–压实。

MMU(内存管理单元)


虚拟地址和物理地址之间的运行时映射是通过称为MMU的硬件设备完成的。

在内存管理中,操作系统将处理进程并在磁盘和内存之间移动进程以执行。它跟踪可用和已使用的内存。

指令执行周期包括以下步骤:

  1. 从存储器中获取第一条指令,例如ADD A,B
  2. 然后将这些指令解码,即A和B的加法
  3. 然后在某个特定的存储位置进行进一步的加载或存储。

基本硬件

由于主存储器和寄存器内置在处理器中,CPU只能访问它们,因此每条指令都应写入直接访问存储
设备中。

  1. 如果从寄存器访问CPU指令,则由于寄存器内置在CPU中,因此可以在一个CPU时钟周期内完成。
  2. 如果指令驻留在主存储器中,那么它将通过存储器总线进行访问,这将花费大量时间。因此,对此的补救措施是在CPU和主内存之间添加快速内存,即为事务添加缓存。
  3. 现在,我们应该确保流程位于法定地址内。
  4. 合法地址由基址寄存器(拥有最小的物理地址)和限制寄存器(范围的大小)组成。

例如:

基址寄存器= 300040
限制寄存器= 120900
那么合法地址=(300040 + 120900)= 420940(含)。
法定地址=基本寄存器+限制寄存器

进程如何从磁盘映射到内存

  1. 通常,进程以二进制可执行文件的形式驻留在磁盘中。
  2. 因此,执行过程应驻留在主存储器中。
  3. 根据使用的内存管理,进程从磁盘移动到内存。
  4. 进程以就绪队列的形式在磁盘中等待以获取内存。

磁盘和内存的映射过程

正常过程是从输入队列中选择进程并将其加载到内存中。进程执行时,它将访问内存中的数据和指令,一旦完成,它将释放内存,现在内存可用于其他进程。

MMU方案

 CPU ------- MMU ------内存 

  1. CPU将为例如346生成逻辑地址
  2. MMU将为例如:14000生成重定位寄存器(基址寄存器)
  3. 内存中的物理地址位于例如:(346 + 14000 = 14346)

图来源:https://users.dimi.uniud.it/~antonio.dangelo/OpSys/materials/Operating_System_Concepts.pdf
本文由Vaishali Bhatia贡献。如果您喜欢GeeksforGeeks并且愿意做出贡献,那么您也可以使用contribution.geeksforgeeks.org撰写文章或将您的文章邮寄至contribution@geeksforgeeks.org。查看您的文章出现在GeeksforGeeks主页上,并帮助其他Geeks。

开始编码

创建我们的缓冲区


除了通常的malloc()调用之外,创建缓冲区以查找地址的过程还需要另外一步。内核不保证虚拟地址空间中的地址实际映射到内存中的物理地址。它可以存储在交换空间中,某个地方或整个地方的缓存中。为了避免这种可能性,我们可以mlock()用来强制将页面保留在系统的物理内存中。幸运的是,这很简单。只需将mlock()指针传递到缓冲区和缓冲区的大小,它将处理其余的内容。看起来像这样:

void* create_buffer(void) {size_t buf_size = strlen(ORIG_BUFFER) + 1;// Allocate some memory to manipulatevoid *buffer = malloc(buf_size);if(buffer == NULL) {fprintf(stderr, "Failed to allocate memory for buffer\n");exit(1);}// Lock the page in memory// Do this before writing data to the buffer so that any copy-on-write// mechanisms will give us our own page locked in memoryif(mlock(buffer, buf_size) == -1) {fprintf(stderr, "Failed to lock page in memory: %s\n", strerror(errno));exit(1);}// Add some data to the memorystrncpy(buffer, ORIG_BUFFER, strlen(ORIG_BUFFER));return buffer;
}

请注意,锁定后我将数据复制到缓冲区中。这是因为如果缓冲区所在的页面与父进程共享,则OS可能会采用写时复制分页机制。为了强制操作系统提供我们自己的页面,我们将数据锁定后写入缓冲区。

/proc/[pid]/pagemap


页面映射为用户提供了访问空间,以访问内核如何管理进程页面。这是一个二进制文件,因此从中提取信息有些棘手。

从文档1中可以看到,每页有64位信息。我们对页帧号0-54位感兴趣。

我们如何从页面地图中获取给定页面的页面框架号?首先,我们需要确定要映射到页面地图中的偏移量。可以这样进行:

#define PAGEMAP_LENGTH 8
offset = (unsigned long)addr / getpagesize() * PAGEMAP_LENGTH

给定一个地址,我们将其除以页面大小,然后乘以8。为什么是8?每页有64位或8字节的信息。

然后,我们在文件中寻找该位置并读取前7个字节。为什么是7?我们对咬伤0-54感兴趣。总共有55位。因此,我们读取了前7个字节(56位)并清除了位55。位55是我们不关心的软脏标志。

unsigned long get_page_frame_number_of_address(void *addr) {// Open the pagemap file for the current processFILE *pagemap = fopen("/proc/self/pagemap", "rb");// Seek to the page that the buffer is onunsigned long offset = (unsigned long)addr / getpagesize() * PAGEMAP_LENGTH;if(fseek(pagemap, (unsigned long)offset, SEEK_SET) != 0) {fprintf(stderr, "Failed to seek pagemap to proper location\n");exit(1);}// The page frame number is in bits 0-54 so read the first 7 bytes and clear the 55th bitunsigned long page_frame_number = 0;fread(&page_frame_number, 1, PAGEMAP_LENGTH-1, pagemap);page_frame_number &= 0x7FFFFFFFFFFFFF;fclose(pagemap);return page_frame_number;
}

现在我们有了页面帧号,我们可以轻松计算出缓冲区的物理地址,例如2:

physcial_addr = (page_frame_number << PAGE_SHIFT) + distance_from_page_boundary_of_buffer

PAGE_SHIFT内核#define在哪里。对于我的x86_64系统,它定义为12,但这可能因您而异。您应该自己查看内核源代码来确认该值。

写 /dev/mem


现在我们已经确定了物理地址,我们可以继续在内存中找到该位置并对其进行修改。

Linux通过/dev/mem块设备提供对系统内存的直接访问。但是,由于明显的安全隐患,即使是root用户,也无法读取(更不用说写入)该文件了。这是由于CONFIG_STRICT_DEVMEM内核配置选项。作为配置选项,必须在编译时设置它,以便更改它,您必须重新编译内核。

内核的编译和安装会因发行版的不同而有所差异,因此在此不再赘述。如果您已经熟悉该过程,则只需CONFIG_STRICT_DEVMEM=n在配置中设置,重新编译,安装和重新启动即可。希望所有虚拟机都存在,因为这显然会带来巨大的安全漏洞。

假设您的内核已CONFIG_STRICT_DEVMEM禁用,我们可以继续。首先是要知道在哪里寻找/dev/mem我们放入缓冲区的字符串。实际上,这很简单。我们需要寻找的偏移量等于我们上面计算的物理地址。

// Find the difference from the buffer to the page boundary
unsigned int distance_from_page_boundary = (unsigned long)buffer % getpagesize();// Determine how far to seek into memory to find the buffer
uint64_t offset = (page_frame_number << PAGE_SHIFT) + distance_from_page_boundary;

现在让我们打开/dev/mem并寻找我们计算出的偏移量:

int open_memory(void) {// Open the memory (must be root for this)int fd = open("/dev/mem", O_RDWR);if(fd == -1) {fprintf(stderr, "Error opening /dev/mem: %s\n", strerror(errno));exit(1);}return fd;
}void seek_memory(int fd, unsigned long offset) {unsigned pos = lseek(fd, offset, SEEK_SET);if(pos == -1) {fprintf(stderr, "Failed to seek /dev/mem: %s\n", strerror(errno));exit(1);}
}int mem_fd = open_memory();
seek_memory(mem_fd, offset);

快完成了!我们在内部找到了正确的文件描述符,/dev/mem所以现在我们只需要写3。

if(write(mem_fd, NEW_BUFFER, strlen(NEW_BUFFER)) == -1) {fprintf(stderr, "Write failed: %s\n", strerror(errno));
}

请注意,NEW_BUFFER长度必须等于或短于ORIG_BUFFER。就我而言,我将它们定义为相同的长度,因此我不必费心复制NUL终止符。

最后,我们可以从原始缓冲区中读取内容,如果一切正常,我们将看到通过写入更改了缓冲区的内容/dev/mem

printf("Buffer: %s\n", buffer);

结论和完整代码清单


值得注意的是,这只是一个实验。这并不是要依赖的行为。实际上,在我的测试中,我体验了内核在计算偏移量以查找该偏移量并将数据写入该偏移量之间的时间在物理地址周围乱码的情况。底线是:坚持虚拟内​​存;它真的很好。而且,如果您需要从用户空间修改物理内存,请寻找另一种方法。

完整代码清单:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>// ORIG_BUFFER will be placed in memory and will then be changed to NEW_BUFFER
// They must be the same length
#define ORIG_BUFFER "Hello, World!"
#define NEW_BUFFER "Hello, Linux!"// The page frame shifted left by PAGE_SHIFT will give us the physcial address of the frame
// Note that this number is architecture dependent. For me on x86_64 with 4096 page sizes,
// it is defined as 12. If you're running something different, check the kernel source
// for what it is defined as.
#define PAGE_SHIFT 12
#define PAGEMAP_LENGTH 8void* create_buffer(void);
unsigned long get_page_frame_number_of_address(void *addr);
int open_memory(void);
void seek_memory(int fd, unsigned long offset);int main(void) {// Create a buffer with some data in itvoid *buffer = create_buffer();// Get the page frame the buffer is onunsigned int page_frame_number = get_page_frame_number_of_address(buffer);printf("Page frame: 0x%x\n", page_frame_number);// Find the difference from the buffer to the page boundaryunsigned int distance_from_page_boundary = (unsigned long)buffer % getpagesize();// Determine how far to seek into memory to find the bufferuint64_t offset = (page_frame_number << PAGE_SHIFT) + distance_from_page_boundary;// Open /dev/mem, seek the calculated offset, and// map it into memory so we can manipulate it// CONFIG_STRICT_DEVMEM must be disabled for thisint mem_fd = open_memory();seek_memory(mem_fd, offset);printf("Buffer: %s\n", buffer);puts("Changing buffer through /dev/mem...");// Change the contents of the buffer by writing into /dev/mem// Note that since the strings are the same length, there's no purpose in// copying the NUL terminator againif(write(mem_fd, NEW_BUFFER, strlen(NEW_BUFFER)) == -1) {fprintf(stderr, "Write failed: %s\n", strerror(errno));}printf("Buffer: %s\n", buffer);// Clean upfree(buffer);close(mem_fd);return 0;
}void* create_buffer(void) {size_t buf_size = strlen(ORIG_BUFFER) + 1;// Allocate some memory to manipulatevoid *buffer = malloc(buf_size);if(buffer == NULL) {fprintf(stderr, "Failed to allocate memory for buffer\n");exit(1);}// Lock the page in memory// Do this before writing data to the buffer so that any copy-on-write// mechanisms will give us our own page locked in memoryif(mlock(buffer, buf_size) == -1) {fprintf(stderr, "Failed to lock page in memory: %s\n", strerror(errno));exit(1);}// Add some data to the memorystrncpy(buffer, ORIG_BUFFER, strlen(ORIG_BUFFER));return buffer;
}unsigned long get_page_frame_number_of_address(void *addr) {// Open the pagemap file for the current processFILE *pagemap = fopen("/proc/self/pagemap", "rb");// Seek to the page that the buffer is onunsigned long offset = (unsigned long)addr / getpagesize() * PAGEMAP_LENGTH;if(fseek(pagemap, (unsigned long)offset, SEEK_SET) != 0) {fprintf(stderr, "Failed to seek pagemap to proper location\n");exit(1);}// The page frame number is in bits 0-54 so read the first 7 bytes and clear the 55th bitunsigned long page_frame_number = 0;fread(&page_frame_number, 1, PAGEMAP_LENGTH-1, pagemap);page_frame_number &= 0x7FFFFFFFFFFFFF;fclose(pagemap);return page_frame_number;
}int open_memory(void) {// Open the memory (must be root for this)int fd = open("/dev/mem", O_RDWR);if(fd == -1) {fprintf(stderr, "Error opening /dev/mem: %s\n", strerror(errno));exit(1);}return fd;
}void seek_memory(int fd, unsigned long offset) {unsigned pos = lseek(fd, offset, SEEK_SET);if(pos == -1) {fprintf(stderr, "Failed to seek /dev/mem: %s\n", strerror(errno));exit(1);}
}

DPDK是如何操作的?


#define _FILE_OFFSET_BITS 64
#include <errno.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/queue.h>
#include <sys/file.h>
#include <unistd.h>
#include <limits.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <signal.h>
#include <setjmp.h>
#ifdef RTE_EAL_NUMA_AWARE_HUGEPAGES
#include <numa.h>
#include <numaif.h>
#endif#define phys_addr_t   uint64_t
static bool phys_addrs_available = true;
typedef uint64_t rte_iova_t;
#define RTE_BAD_IOVA ((rte_iova_t)-1)
#define RTE_LOG(l, t, fmt...) printf(fmt)
#define PFN_MASK_SIZE   8enum rte_iova_mode {RTE_IOVA_DC = 0,  /* Don't care mode */RTE_IOVA_PA = (1 << 0), /* DMA using physical address */RTE_IOVA_VA = (1 << 1)  /* DMA using virtual address */
};/** Get physical address of any mapped virtual address in the current process.*/
phys_addr_t rte_mem_virt2phy(const void *virtaddr)
{int fd, retval;uint64_t page, physaddr;unsigned long virt_pfn;int page_size;off_t offset;/* Cannot parse /proc/self/pagemap, no need to log errors everywhere */if (!phys_addrs_available)return RTE_BAD_IOVA;/* standard page size */page_size = getpagesize();fd = open("/proc/self/pagemap", O_RDONLY);if (fd < 0) {RTE_LOG(ERR, EAL, "%s(): cannot open /proc/self/pagemap: %s\n",__func__, strerror(errno));return RTE_BAD_IOVA;}virt_pfn = (unsigned long)virtaddr / page_size;offset = sizeof(uint64_t) * virt_pfn;if (lseek(fd, offset, SEEK_SET) == (off_t) -1) {RTE_LOG(ERR, EAL, "%s(): seek error in /proc/self/pagemap: %s\n",__func__, strerror(errno));close(fd);return RTE_BAD_IOVA;}retval = read(fd, &page, PFN_MASK_SIZE);close(fd);if (retval < 0) {RTE_LOG(ERR, EAL, "%s(): cannot read /proc/self/pagemap: %s\n",__func__, strerror(errno));return RTE_BAD_IOVA;} else if (retval != PFN_MASK_SIZE) {RTE_LOG(ERR, EAL, "%s(): read %d bytes from /proc/self/pagemap ""but expected %d:\n",__func__, retval, PFN_MASK_SIZE);return RTE_BAD_IOVA;}/** the pfn (page frame number) are bits 0-54 (see* pagemap.txt in linux Documentation)*/if ((page & 0x7fffffffffffffULL) == 0)return RTE_BAD_IOVA;physaddr = ((page & 0x7fffffffffffffULL) * page_size)+ ((unsigned long)virtaddr % page_size);RTE_LOG(ERR, EAL, "phyaddr %p\n", (void*)physaddr);return physaddr;
}rte_iova_t rte_mem_virt2iova(const void *virtaddr)
{
//  if (rte_eal_iova_mode() == RTE_IOVA_VA)
//      return (uintptr_t)virtaddr;return rte_mem_virt2phy(virtaddr);
}int main()
{int *addr1 = malloc(10);printf("addr1 = %p\n", addr1);rte_iova_t rte_iova = rte_mem_virt2iova(addr1);printf("addr1 = %p, 0x%x\n", addr1, (void*)rte_iova);return 0;
}

推荐文章


将虚拟地址映射到物理地址

在用户空间中将虚拟地址转换为物理地址

用户态进程如何得到虚拟地址对应的物理地址?

操作系统中的逻辑和物理地址

物理和逻辑数据独立

物理和逻辑文件系统

物理层中的设计问题

私有和公共IP地址之间的区别

操作系统中的虚拟内存

虚拟内存| 问题

虚拟内存和缓存内存之间的区别

操作系统中的虚拟机

虚拟机和容器之间的区别

虚拟机类型

创建连接点

抢占优先级与循环调度算法的关系

cd cmd命令

https://www.kernel.org/doc/Documentation/vm/pagemap.txt

https://www.kernel.org/doc/gorman/html/understand/understand005.html

https ://www.blackhat.com/presentations/bh-europe-09/Lineberry/BlackHat-Europe-2009-Lineberry-code-injection-via-dev-mem.pdf

Linux用户空间将虚拟地址转化为物理地址相关推荐

  1. linux c 将虚拟地址转化为物理地址_面试不懂 Linux 内存管理?我用 20 张图给你讲明白...

    微信搜索公众号「 后端技术学堂 」回复「1024」获取50本计算机电子书,回复「学习路线」获取超详细后端技术学习路线思维导图,文章每周持续更新,我们下期见! 大家好,我是柠檬哥. 分享编程学习,助力程 ...

  2. 嵌入式之linux用户空间与内核空间,进程上下文与中断上下文

    文章目录 前言 用户空间与内核空间 内核态与用户态 进程上下文和中断上下文 上下文 原子 进程上下文 中断上下文 进程上下文VS中断上下文 原子上下文 前言 之前在学习嵌入式linux系统的时候,一直 ...

  3. linux用户空间、内核空间

    一.进程理解 进程:资源分配的最小单元,程序在操作系统中运行的实例 线程:最小调度单元 一个进程至少有一个线程或多个线程,一个线程只能属于一个进程,因为进程是最小的资源分配单元,所以线程不存在独立的地 ...

  4. 对linux用户空间DMA的分析(和单片机一样简单)

    一般情况下,对外设的操作包括轮训方式.中断方式,对于数据量很大的情况会用到DMA操作.本文介绍一种在用户空间实现DMA操作的方法来获取AXI总线上的数据,FPGA部分暂时不详细说明,之后会有专题来介绍 ...

  5. Linux内核学习3——虚拟地址转换成物理地址

    这里,我们讲解一下Linux是如何将虚拟地址转换成物理地址的 一.地址转换 在进程中,我们不直接对物理地址进行操作,CPU在运行时,指定的地址要经过MMU转换后才能访问到真正的物理内存. 地址转换的过 ...

  6. linux 内核将两个设备相关联,linux用户空间和内核空间交换数据

    转载地址:http://www.poluoluo.com/server/201107/138420.html 在研究dahdi驱动的时候,见到了一些get_user,put_user的函数,不知道其来 ...

  7. Linux 用户空间和内核空间

    最近在微信群里看到有人提这个问题,然后查了下资料,觉得这篇文章是写得最能让人看懂的,分享给大家. 欢迎大家评论说出自己的见解,让更多的人更容易理解这部分知识. 之前的相关文章 Linux内存,先看这篇 ...

  8. Linux用户空间与内核空间(理解高端内存)

    目录 Linux内核地址映射模型 Linux内核地址空间划分 Linux内核高端内存的由来 Linux内核高端内存的理解 Linux内核高端内存的划分 常见问题 小结 1.虚拟内核空间到物理空间的映射 ...

  9. Linux用户空间与内核空间

    Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据,因为Linux使用的虚拟内存机制,用户空间的数据可能被换出,当内核空间使用用户空间指针时,对应的数 ...

最新文章

  1. 7Papers|斯坦福学者造出机器鸽;港科大等提出学生情绪分析新系统
  2. 第二阶段冲刺第十天,6月9日。
  3. 机器学习中常见的距离公式
  4. 深度学习数字仪表盘识别_【深度学习系列】手写数字识别实战
  5. 在线HTTP请求/响应头转JSON工具
  6. POI报表入门,excel,使用事件模型解析百万数据excel报表
  7. 怎么用python制作随机点名软件_python用tkinter实现一个简易能进行随机点名的界面...
  8. 拓端tecdat|R语言markov switching model马尔可夫转换分析研究水资源
  9. net 去掉第一位和最后一位_2020最后三个月港剧有咩睇?熟女强人首播!
  10. 新装的台式机新装WIN7系统启动时卡在开机动画如何解决?
  11. 对接阿里云天气,获取天气预报数据
  12. python word 表格复制_python实现同一word中的表格分别提取并保存到不同文件下
  13. 黑帽SEO的作弊手法:
  14. 基于CS的脉冲GPR成像技术研究(20111)
  15. 无线耳机哪个品牌好?四大国内蓝牙耳机品牌排行
  16. 微信小程序checkbox的全选以及所有checkbox选中之后的全选
  17. React Native学习资源汇总
  18. 基于三代测序技术的微生物组学研究进展
  19. ZooKeeper - 四字命令解析
  20. cmd命令 从C盘跳到D盘

热门文章

  1. java servlet 注册登录,JSP+JavaBean+Servlet实现用户登录与注册
  2. eclipse中spring配置文件代码提示(全)
  3. JSONOBject的fluentPut(key,value)方法:可链式设置元素
  4. redis学习-redis事务
  5. Java CAS无锁技术深度解析
  6. 基于 YOLOV3 和 OpenCV的目标检测
  7. Idea创建一个springboot多模块项目
  8. Javascript的websocket的使用方法
  9. 基于HttpClient4.0的网络爬虫基本框架(Java实现)
  10. 编写批处理文件编译.Net工程