Linux arm64 set_memory_ro/rw函数

2023-09-15 16:27:52

一、函数简介

1.1 简介

// linux-5.4.18/arch/arm64/mm/pageattr.c

int set_memory_ro(unsigned long addr, int numpages)
{
	return change_memory_common(addr, numpages,
					__pgprot(PTE_RDONLY),
					__pgprot(PTE_WRITE));
}

int set_memory_rw(unsigned long addr, int numpages)
{
	return change_memory_common(addr, numpages,
					__pgprot(PTE_WRITE),
					__pgprot(PTE_RDONLY));
}

arm64架构下 set_memory_ro 和 set_memory_rw 用来更改内核内存区域的可读可写属性,只用于更改由 vmalloc 和 vmap分配的内存区间。 两者都是调用 change_memory_common 函数。

1.2 change_memory_common

static int change_memory_common(unsigned long addr, int numpages,
				pgprot_t set_mask, pgprot_t clear_mask)
{
	unsigned long start = addr;
	//计算要更改内存地址的大小
	unsigned long size = PAGE_SIZE*numpages;
	unsigned long end = start + size;
	struct vm_struct *area;
	int i;

	//检查addr是否未对齐到页面边界。如果未对齐,则将start地址调整为前一个页面边界,并相应地更新end
	if (!PAGE_ALIGNED(addr)) {
		start &= PAGE_MASK;
		end = start + size;
		WARN_ON_ONCE(1);
	}

	/*
	 * Kernel VA mappings are always live, and splitting live section
	 * mappings into page mappings may cause TLB conflicts. This means
	 * we have to ensure that changing the permission bits of the range
	 * we are operating on does not result in such splitting.
	 *
	 * Let's restrict ourselves to mappings created by vmalloc (or vmap).
	 * Those are guaranteed to consist entirely of page mappings, and
	 * splitting is never needed.
	 *
	 * So check whether the [addr, addr + size) interval is entirely
	 * covered by precisely one VM area that has the VM_ALLOC flag set.
	 */
	//来查找覆盖指定地址范围的虚拟内存区域area,area由vmalloc or vmap 分配
	area = find_vm_area((void *)addr);
	//如果找不到这样的区域,或者范围的结束地址超过了区域的边界,或者该区域没有设置VM_ALLOC标志,函数将返回错误代码-EINVAL
	if (!area ||
	    end > (unsigned long)area->addr + area->size ||
	    !(area->flags & VM_ALLOC))
		return -EINVAL;

	if (!numpages)
		return 0;

	/*
	 * If we are manipulating read-only permissions, apply the same
	 * change to the linear mapping of the pages that back this VM area.
	 */
	//我手中机器配置没有rodata_full标志
	if (rodata_full && (pgprot_val(set_mask) == PTE_RDONLY ||
			    pgprot_val(clear_mask) == PTE_RDONLY)) {
		for (i = 0; i < area->nr_pages; i++) {
			__change_memory_common((u64)page_address(area->pages[i]),
					       PAGE_SIZE, set_mask, clear_mask);
		}
	}

	/*
	 * Get rid of potentially aliasing lazily unmapped vm areas that may
	 * have permissions set that deviate from the ones we are setting here.
	 */
	//以删除任何可能存在的与当前函数中设置的权限不同的潜在别名延迟取消映射的VM区域
	vm_unmap_aliases();

	//调用__change_memory_common,传入调整后的start地址、大小和set_mask、clear_mask,以对指定的内存范围应用权限更改
	return __change_memory_common(start, size, set_mask, clear_mask);
}

change_memory_common函数是一个用于更改指定内存页面范围权限。

函数change_memory_common接受四个参数:

addr:内存范围的起始地址。
numpages:内存范围中的页面数量。
set_mask:要设置的保护掩码,表示要设置的权限位。
clear_mask:要清除的保护掩码,表示要清除的权限位。

函数首先根据地址addr计算起始地址start和结束地址end,并计算需要更改权限的内存大小size。如果起始地址addr不是页对齐的,会将start调整为页对齐,并重新计算end。同时,会发出一个警告(WARN_ON_ONCE(1))。

接下来,函数检查要操作的内存范围是否完全位于一个由vmalloc(或vmap)创建的虚拟内存区域中。函数调用find_vm_area来查找覆盖指定地址范围的虚拟内存区域。如果找不到这样的区域,或者范围的结束地址超过了区域的边界,或者该区域没有设置VM_ALLOC标志,函数将返回错误代码-EINVAL。这个检查确保指定范围完全被一个具有VM_ALLOC标志的VM区域覆盖,该标志表示该区域完全由页面映射组成,不需要进行分割。

如果numpages为0,表示没有需要更改权限的页,直接返回0。

如果设置了rodata_full标志,并且set_mask或clear_mask指示了只读权限(PTE_RDONLY),函数将遍历VM区域的页面,并调用__change_memory_common,将相同的权限更改应用于页面的线性映射。这一步确保相应的线性映射权限与VM区域权限保持一致。

我手中机器配置没有rodata_full标志:

# CONFIG_RODATA_FULL_DEFAULT_ENABLED is not set
bool rodata_full __ro_after_init = IS_ENABLED(CONFIG_RODATA_FULL_DEFAULT_ENABLED);

在对指定的内存范围应用权限更改之前,函数调用vm_unmap_aliases,以删除任何可能存在的与当前函数中设置的权限不同的潜在别名延迟取消映射的VM区域。

最后,函数调用__change_memory_common,传入调整后的start地址、大小和set_mask、clear_mask,以对指定的内存范围应用权限更改。

1.3 __change_memory_common

/*
 * This function assumes that the range is mapped with PAGE_SIZE pages.
 */
static int __change_memory_common(unsigned long start, unsigned long size,
				pgprot_t set_mask, pgprot_t clear_mask)
{
	//page_change_data结构体用于存储设置和清除的保护掩码
	struct page_change_data data;
	int ret;

	data.set_mask = set_mask;
	data.clear_mask = clear_mask;

	ret = apply_to_page_range(&init_mm, start, size, change_page_range,
					&data);

	flush_tlb_kernel_range(start, start + size);
	return ret;
}

__change_memory_common的函数,用于更改指定内存页面范围的内存权限。

函数__change_memory_common接受四个参数:

start:内存范围的起始地址。
size:内存范围的大小(以字节为单位)。
set_mask:要设置的保护掩码,表示要设置的权限位。
clear_mask:要清除的保护掩码,表示要清除的权限位。

函数调用apply_to_page_range,对init_mm(内核初始化的内存描述符)中指定的内存范围应用给定的函数change_page_range。change_page_range函数被用于处理页范围内的每个页面,并根据data中的保护掩码设置和清除页面的权限。apply_to_page_range函数返回一个整数值,表示操作的结果。

struct page_change_data {
	pgprot_t set_mask;
	pgprot_t clear_mask;
};

static int change_page_range(pte_t *ptep, unsigned long addr, void *data)
{
	struct page_change_data *cdata = data;
	pte_t pte = READ_ONCE(*ptep);

	pte = clear_pte_bit(pte, cdata->clear_mask);
	pte = set_pte_bit(pte, cdata->set_mask);

	set_pte(ptep, pte);
	return 0;
}

名为change_page_range的函数,作为apply_to_page_range函数的参数,函数使用clear_pte_bit宏和set_pte_bit宏分别对pte进行清除和设置操作,根据cdata中的保护掩码指定的权限位。这些宏用于修改页表项的权限位,以实现更改页面权限的目的。

函数调用flush_tlb_kernel_range来刷新内核页表中指定范围的TLB(转换查找缓冲器)。TLB是用于加速虚拟地址到物理地址转换的硬件缓存。刷新TLB确保最新的内存权限设置生效。

二、apply_to_page_range函数

2.1 apply_to_page_range

// linux-5.4.18/mm/memory.c

/*
 * Scan a region of virtual memory, filling in page tables as necessary
 * and calling a provided function on each leaf page table.
 */
int apply_to_page_range(struct mm_struct *mm, unsigned long addr,
			unsigned long size, pte_fn_t fn, void *data)
{
	pgd_t *pgd;
	unsigned long next;
	unsigned long end = addr + size;
	int err;

	if (WARN_ON(addr >= end))
		return -EINVAL;

	pgd = pgd_offset(mm, addr);
	do {
		next = pgd_addr_end(addr, end);
		err = apply_to_p4d_range(mm, pgd, addr, next, fn, data);
		if (err)
			break;
	} while (pgd++, addr = next, addr != end);

	return err;
}
EXPORT_SYMBOL_GPL(apply_to_page_range);

pply_to_page_range的函数,用于扫描虚拟内存的一个区域,并在需要时填充页表,同时在每个叶子页表上调用提供的函数。在这里我们提供的函数是change_page_range,修改page属性。

函数apply_to_page_range接受五个参数:

mm:指向mm_struct结构体的指针,表示进程的内存描述符。 -- 这里传递的是 init_mm
addr:内存范围的起始地址。
size:内存范围的大小(以字节为单位)。
fn:指向函数的指针,该函数将在每个叶子页表上调用。 -- 这里传递的是change_page_range
data:传递给fn函数的数据。

(1)通过调用pgd_offset函数,根据给定的进程内存描述符mm和起始地址addr获取页全局目录项(PGD)的指针,并将其存储在pgd中。

struct mm_struct init_mm = {
	.pgd		= swapper_pg_dir,
};

这里页全局目录项(PGD)的指针就是指内核页表页全局目录项swapper_pg_dir。

(2)使用一个循环来遍历地址范围内的每个页全局目录项。在每次迭代中,通过调用pgd_addr_end函数计算下一个要处理的地址next,根据当前的addr和end。

/*
 * When walking page tables, get the address of the next boundary,
 * or the end address of the range if that comes earlier.  Although no
 * vma end wraps to 0, rounded up __boundary may wrap to 0 throughout.
 */

#define pgd_addr_end(addr, end)						\
({	unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;	\
	(__boundary - 1 < (end) - 1)? __boundary: (end);		\
})

该宏用于计算页全局目录项(PGD)的下一个边界地址。它接受两个参数,addr表示当前地址,end表示范围的结束地址。宏的工作如下:

首先,它将当前地址addr加上页全局目录的大小(PGDIR_SIZE),然后与页全局目录的掩码(PGDIR_MASK)进行按位与操作,得到一个边界地址__boundary。

接下来,宏检查(__boundary - 1 < (end) - 1)是否为真。这里通过使用减一操作来避免了可能发生的边界溢出问题。

如果条件为真,则返回__boundary,否则返回end。

(4)然后,调用apply_to_p4d_range函数,传递进程内存描述符mm、当前的页全局目录项pgd、当前地址addr、下一个地址next、提供的函数fn和数据data。该函数的作用是在页全局目录项范围内递归地处理页中间目录项(P4D)。

2.2 apply_to_p4d_range

static int apply_to_p4d_range(struct mm_struct *mm, pgd_t *pgd,
				     unsigned long addr, unsigned long end,
				     pte_fn_t fn, void *data)
{
	p4d_t *p4d;
	unsigned long next;
	int err;

	p4d = p4d_alloc(mm, pgd, addr);
	if (!p4d)
		return -ENOMEM;
	do {
		next = p4d_addr_end(addr, end);
		err = apply_to_pud_range(mm, p4d, addr, next, fn, data);
		if (err)
			break;
	} while (p4d++, addr = next, addr != end);
	return err;
}

arm64最大为四级页表,p4d一般只有x86_64架构下才有,这里不予讨论。

2.3 apply_to_pud_range

static int apply_to_pud_range(struct mm_struct *mm, p4d_t *p4d,
				     unsigned long addr, unsigned long end,
				     pte_fn_t fn, void *data)
{
	pud_t *pud;
	unsigned long next;
	int err;

	pud = pud_alloc(mm, p4d, addr);
	if (!pud)
		return -ENOMEM;
	do {
		next = pud_addr_end(addr, end);
		err = apply_to_pmd_range(mm, pud, addr, next, fn, data);
		if (err)
			break;
	} while (pud++, addr = next, addr != end);
	return err;
}

函数的执行流程如下:
首先,通过调用pud_alloc函数为给定的P4D分配内存空间,并将返回的PUD指针赋值给pud变量。

进入一个循环,该循环将在PUD范围内进行迭代处理。

在循环的每一次迭代中,首先调用pud_addr_end宏来计算下一个边界地址next。这个宏将根据当前地址addr和范围结束地址end计算出下一个PMD(Page Middle Directory)的边界地址。

然后,调用apply_to_pmd_range函数来对PMD范围内的页面应用函数fn。该函数将处理mm、pud、addr和next之间的页面,并使用fn函数进行处理。

如果没有出现错误,继续进行下一次迭代。在每次迭代中,通过递增pud指针、更新addr为next,并检查addr是否等于end来确定是否继续循环。

当addr等于end时,表示已经处理完整个范围。

这段代码的作用是在给定的PUD范围内对页面应用函数进行处理,通过逐个PMD范围进行迭代,并在每个PMD范围内调用给定的函数进行处理。

2.4 apply_to_pmd_range

static int apply_to_pmd_range(struct mm_struct *mm, pud_t *pud,
				     unsigned long addr, unsigned long end,
				     pte_fn_t fn, void *data)
{
	pmd_t *pmd;
	unsigned long next;
	int err;

	BUG_ON(pud_huge(*pud));

	pmd = pmd_alloc(mm, pud, addr);
	if (!pmd)
		return -ENOMEM;
	do {
		next = pmd_addr_end(addr, end);
		err = apply_to_pte_range(mm, pmd, addr, next, fn, data);
		if (err)
			break;
	} while (pmd++, addr = next, addr != end);
	return err;
}

函数的执行流程如下:
首先,使用BUG_ON宏检查给定的PUD是否是巨页(huge page)。如果是巨页,会引发一个bug检查(bug-on),表示代码中出现了不应该出现的情况。

调用pmd_alloc函数为给定的PUD分配一个PMD,并将返回的PMD指针赋值给pmd变量。如果内存分配失败,函数将返回错误码-ENOMEM。

进入一个循环,该循环将在PMD范围内进行迭代处理。

在循环的每一次迭代中,首先调用pmd_addr_end宏来计算下一个边界地址next。这个宏将根据当前地址addr和范围结束地址end计算出下一个PTE(Page Table Entry)的边界地址。

然后,调用apply_to_pte_range函数来对PTE范围内的页面应用函数fn。该函数将处理mm、pmd、addr和next之间的页面,并使用fn函数进行处理。如果在处理过程中出现错误,将返回一个非零的错误码err。

如果err不为零,表示在处理过程中出现了错误,函数将跳出循环。

如果没有出现错误,继续进行下一次迭代。在每次迭代中,通过递增pmd指针、更新addr为next,并检查addr是否等于end来确定是否继续循环。

当addr等于end时,表示已经处理完整个范围,函数将返回err。

这段代码的作用是在给定的PMD范围内对页面应用函数进行处理,通过逐个PTE范围进行迭代,并在每个PTE范围内调用给定的函数进行处理。

2.5 apply_to_pte_range

static int apply_to_pte_range(struct mm_struct *mm, pmd_t *pmd,
				     unsigned long addr, unsigned long end,
				     pte_fn_t fn, void *data)
{
	pte_t *pte;
	int err;
	spinlock_t *uninitialized_var(ptl);

	pte = (mm == &init_mm) ?
		pte_alloc_kernel(pmd, addr) :
		pte_alloc_map_lock(mm, pmd, addr, &ptl);
	if (!pte)
		return -ENOMEM;

	BUG_ON(pmd_huge(*pmd));

	arch_enter_lazy_mmu_mode();

	do {
		err = fn(pte++, addr, data);
		if (err)
			break;
	} while (addr += PAGE_SIZE, addr != end);

	arch_leave_lazy_mmu_mode();

	if (mm != &init_mm)
		pte_unmap_unlock(pte-1, ptl);
	return err;
}

函数的执行流程如下:

根据mm是否等于&init_mm,选择不同的方式分配PTE。如果mm等于&init_mm,表示当前是内核线程,使用pte_alloc_kernel函数为给定的PMD和地址分配一个PTE;否则,使用pte_alloc_map_lock函数为给定的进程mm、PMD和地址分配一个PTE,并将分配过程中获取的自旋锁地址保存在ptl中。如果无法分配PTE,则返回错误码-ENOMEM。

使用BUG_ON宏检查给定的PMD是否是巨页(huge page)。如果是巨页,会引发一个bug检查(bug-on),表示代码中出现了不应该出现的情况。

调用arch_enter_lazy_mmu_mode函数,进入延迟MMU模式。这个函数是架构相关的,用于在某些架构上进入延迟更新MMU页表的模式。

进入一个循环,该循环将在PTE范围内进行迭代处理。

在循环的每一次迭代中,首先调用给定的函数fn来处理当前的PTE,传递pte、addr和data作为参数。如果在处理过程中出现错误,将返回一个非零的错误码err。

如果err不为零,表示在处理过程中出现了错误,函数将跳出循环。

如果没有出现错误,继续进行下一次迭代。在每次迭代中,通过递增pte指针、更新addr为addr + PAGE_SIZE,并检查addr是否等于end来确定是否继续循环。

完成PTE范围的处理后,调用arch_leave_lazy_mmu_mode函数,离开延迟MMU模式。

如果mm不等于&init_mm,表示当前是用户进程,调用pte_unmap_unlock函数来解除之前映射的PTE页框,并释放自旋锁。这个函数用于解除映射并解锁页表。

三、hook系统调用

set_memory_ro/rw函数只用于更改由 vmalloc 和 vmap分配的内存区间,而系统调用表不是由vmalloc 或者vmap分配的,系统调用表位于内核的只读数据区,在arm64架构中,在Linux 4.6 中 将内核镜像移到vmalloc的区域了,虽然不是由vmalloc 或者vmap分配的,但是是在vmalloc区间,如下所示:

// linux-5.4.18/arch/arm64/mm/mmu.c

static void __init map_kernel_segment(pgd_t *pgdp, void *va_start, void *va_end,
				      pgprot_t prot, struct vm_struct *vma,
				      int flags, unsigned long vm_flags)
{
	phys_addr_t pa_start = __pa_symbol(va_start);
	unsigned long size = va_end - va_start;

	BUG_ON(!PAGE_ALIGNED(pa_start));
	BUG_ON(!PAGE_ALIGNED(size));

	__create_pgd_mapping(pgdp, pa_start, (unsigned long)va_start, size, prot,
			     early_pgtable_alloc, flags);

	if (!(vm_flags & VM_NO_GUARD))
		size += PAGE_SIZE;

	vma->addr	= va_start;
	vma->phys_addr	= pa_start;
	vma->size	= size;
	vma->flags	= VM_MAP | vm_flags;
	vma->caller	= __builtin_return_address(0);

	//添加到vmalloc区间
	vm_area_add_early(vma);
}

而set_memory_ro/rw函数在调用change_memory_common函数时,只是判断内核地址是否在 vmap_area – kernel virtual area 区间,然后检查是否有VM_ALLOC标志,因此我们还是可以通过set_memory_ro/rw函数来更改系统调用表的页表属性。

static int change_memory_common(unsigned long addr, int numpages,
				pgprot_t set_mask, pgprot_t clear_mask)
{
	struct vm_struct *area;
	
	.....
	 /*
	 * Let's restrict ourselves to mappings created by vmalloc (or vmap).
	 * Those are guaranteed to consist entirely of page mappings, and
	 * splitting is never needed.
	 *
	 * So check whether the [addr, addr + size) interval is entirely
	 * covered by precisely one VM area that has the VM_ALLOC flag set.
	 */
	area = find_vm_area((void *)addr);
	if (!area ||
	    end > (unsigned long)area->addr + area->size ||
	    !(area->flags & VM_ALLOC))
		return -EINVAL;
	
	......
	/*
	 * Get rid of potentially aliasing lazily unmapped vm areas that may
	 * have permissions set that deviate from the ones we are setting here.
	 */
	vm_unmap_aliases();

	return __change_memory_common(start, size, set_mask, clear_mask);
}

这样我们可以自己给该地址的vmap_area区间加上VM_ALLOC标志即可,内核镜像是在kernel virtual area 区间的,如下所示:

    area = my_find_vm_area((void *)addr);
    if(!area){
        printk("no find vm area\n");
        return -1;
    }

    area->flags |= VM_ALLOC;

完整的hook代码如下:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kallsyms.h> 
#include <linux/syscalls.h>
#include <linux/vmalloc.h>
#include <asm/unistd.h>
#include <asm/ptrace.h> 

int (*my_set_memory_ro)(unsigned long addr, int numpages);
int (*my_set_memory_rw)(unsigned long addr, int numpages);

struct vm_struct *(*my_find_vm_area)(const void *addr);

static unsigned long *__sys_call_table;

typedef long (*syscall_fn_t)(const struct pt_regs *regs);

#ifndef __NR_mkdirat
#define __NR_mkdirat 34
#endif

//用于保存原始的 mkdir 系统调用
static syscall_fn_t orig_mkdir;

asmlinkage long mkdir_hook(const struct pt_regs *regs)
{
    printk("hook mkdir sys_call\n");

    // return orig_mkdir(regs);

    return 0;
}

static unsigned long addr;

static int __init lkm_init(void)
{
    struct vm_struct *area;

    my_set_memory_ro = (void *)kallsyms_lookup_name("set_memory_ro");
    my_set_memory_rw = (void *)kallsyms_lookup_name("set_memory_rw");

    my_find_vm_area = (void *)kallsyms_lookup_name("find_vm_area");

    __sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table");

    printk("__sys_call_table = %lx\n", __sys_call_table);
    
    //保存原始的系统调用:mkdir
	orig_mkdir = (syscall_fn_t)__sys_call_table[__NR_mkdirat];

    addr = (unsigned long)(__sys_call_table + __NR_mkdirat);

    addr &= PAGE_MASK;

    area = my_find_vm_area((void *)addr);
    if(!area){
        printk("no find vm area\n");
        return -1;
    }

    area->flags |= VM_ALLOC;
    printk("area->addr = %p, area->size = %lx\n", area->addr, area->size);    

	//hook 系统调用表表项:sys_call_table[__NR_mkdirat]
    my_set_memory_rw(addr, 1);
    __sys_call_table[__NR_mkdirat] = (unsigned long)mkdir_hook;
    my_set_memory_ro(addr, 1);

    printk("lkm_init\n");

	return 0;
}

static void __exit lkm_exit(void)
{
	//模块卸载时恢复原来的mkdir系统调用
	my_set_memory_rw(addr, 1);
    __sys_call_table[__NR_mkdirat] = (unsigned long)orig_mkdir;
    my_set_memory_ro(addr, 1);

    printk("lkm_exit\n");
}

module_init(lkm_init);
module_exit(lkm_exit);

MODULE_LICENSE("GPL");

参考资料

Linux 5.4.18

更多推荐

趣解设计模式之《小王与他的Apple商店》

〇、小故事小王开了一个Apple商店,每天销售量都很不错,但是,近期却有一件事让他很苦恼,那就是针对不同的角色用户,商品的售价是各不同的。比如说,对于普通消费者来说,对于最新的Apple产品,都是原价销售的;那么,对于学生消费群体来说,由于每年开学都会有高校折扣的政策,为了减少学生客户群体的购买压力,是在原价的基础上打

C++之智能指针类型转换应用总结(二百二十九)

简介:CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长!优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀人生格言:人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药.更多原创,欢迎关注:Android系统攻城狮1.前言本篇目的:理解C+

CSS选择器练习小游戏

请结合CSS选择器练习小游戏进行阅读(网页的动态效果是没有办法通过静态图片展示的)网址:请点击有些题有多种答案,本文就不一一列出了第一题答案:plate第二题答案:bento第三题答案:#fancy第四题答案:plateapple或者plate>apple第五题答案:#fancypickle第六题答案:.small或者

【校招VIP】产品基础知识之学习渠道

考点介绍:产品经理是一项复合型的工作,需要学习掌握的知识很多,能否通过学习掌握并构建一套属于自己的产品知识体系,是成为一名优秀产品经理的重要条件。系统化学习方法,可以分为以下6个步骤:1、明确的学习目标。2、梳理知识框架,画出知识体系的枝干。3、各个击破,逐步学习掌握分支知识。4、在实践中应用,形成产品思维方式。5、在

WMS仓储管理系统:从集成到面向未来的策略

现代供应链需求对WMS仓储管理系统形成了一系列复杂而又严格的要求,并且面临的挑战日益增多。WMS仓储管理系统需要与不同地区、不同业务以及其他的合作伙伴进行交互,从供应商到制造商,再到运输商和客户,一切都需要与WMS系统进行交互。还有,仓库系统本身不是静态的,它即是物理的,又依赖众多的玩家和渠道,它也在不断进化以满足市场

【Java】JDK8 jvm参数配置及说明

参数说明1.堆内存参数设置-Xms或-XX:InitialHeapSize=n设置堆的初始值指令1:-Xms2g指令2:-XX:InitialHeapSize=2048m-Xmx或-XX:MaxHeapSize=n设置堆区最大值指令1:-Xmx2g指令2:-XX:MaxHeapSize=2048m-XX:NewSize

3D目标检测框架 MMDetection3D环境搭建 docker篇

本文介绍如何搭建3D目标检测框架,使用docker快速搭建MMDetection3D的开发环境,实现视觉3D目标检测、点云3D目标检测、多模态3D目标检测等等。需要大家提前安装好docker,并且docker版本>=19.03。1、下载MMDetection3D源码https://github.com/open-mml

数据结构 第二章作业 线性表 西安石油大学

在顺序表中插入和删除一个结点需平均移动多少个结点?具体的移动次数取决于哪两个因素?在顺序表中插入和删除一个结点时,平均移动的结点数量取决于两个因素:插入/删除位置和当前顺序表的长度。插入/删除位置:如果要在顺序表的开头或末尾进行插入/删除操作,不需要移动其他结点,所以移动的结点数量较少。但是,如果要在顺序表的中间位置进

代理IP和Socks5代理:跨界电商与全球爬虫的关键技术

跨界电商在全球化市场中崭露头角,而代理IP和Socks5代理则成为实现全球市场洞察和数据采集的不可或缺的工具。本文将深入探讨这两种代理技术在跨界电商、爬虫技术和出海战略中的关键作用。引言:介绍跨界电商的崛起和全球市场的机遇与挑战。引出代理IP和Socks5代理作为技术解决方案的重要性。代理IP的应用:多地区数据采集:介

转载—Linux下文件搜索、查找、查看命令

Linux下文件搜索、查找、查看命令1、最强大的搜索命令:find查找各种文件的命令2、在文件资料中查找文件:locate3、搜索命令所在的目录及别名信息:which4、搜索命令所在的目录及帮助文档路径:whereis5、在文件中搜寻字符串匹配的行并输出:grep6、分页显示一个文件或任何输出结果:more7、分页显示

SpringCLoud——Docker的基本介绍

什么是Docker项目部署问题大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题:依赖关系复杂,容易出现兼容性问题开发、测试、生产环境有差异。DockerDocker如何解决依赖的兼容问题的?将应用的LIbs(函数库)、Deps(依赖)、配置与应用一起打包将每个应用放到一个隔离容器去运行,避免互相打扰首先要了解

热文推荐