深入理解Linux虚拟内存管理(四)(上)

简介: 深入理解Linux虚拟内存管理(四)

一、启动分析

1、初始化内存

(1)start_kernel

// init/main.c
asmlinkage void __init start_kernel(void)
{
  // ...
  setup_arch(&command_line);
  // ...
  mem_init();
  kmem_cache_sizes_init();
  pgtable_cache_init();
  // ...
}

(2)setup_arch

// arch/i386/kernel/setup.c
void __init setup_arch(char **cmdline_p) {
  // ...
  max_low_pfn = setup_memory();
  paging_init();
  // ...  
}


2.1 setup_memory

    传送门

    这个函数的调用图如图 2.3 所示。它为引导内存分配器初始化自身进行所需信息的获取。它可以分成几个不同的任务。

找到低端内存的 PFN 的起点和终点(min_low_pfn,max_low_pfn),找到高端内存的 PFN 的起点和终点(highstart_pfn,highend_pfn),以及找到系统中最后一页的 PFN。

初始化 bootmem_date 结构以及声明可能被引导内存分配器用到的页面。

标记所有系统可用的页面为空闲,然后为那些表示页面的位图保留页面。

在 SMP 配置或 initrd 镜像存在时,为它们保留页面。


2.2 paging_init

    传送门

    3.6 内核页表

    用于完成页表收尾工作的对应函数是 paging_init()x86 上该函数的调用图如图 3.4 所示。



   系统首先调用 pagetable_init() 初始化对应于 ZONE_DMA 和 ZONE_NORMAL 的所有物理内存所必要的页表。注意在 ZONE_HIGHMEM 中的高端内存不能被直接引用,对其的映射都是临时建立起来的。对于内核所使用的每一个 pgd_t,系统会调用引导内存分配器来分配一个页面给 PGD。


   接下来,pagetable_init() 函数调用 fixrange_init() 建立固定大小地址空间,以映射从虚拟地址空间的尾部即起始于 FIXADDR_START 的空间。映射用于局部高级编程中断控制器(APIC),以及在 FIX_KMAP_BEGIN 和 FIX_KMAP_END 之间 kmap_atomic() 的原子映射。最后,函数调用 fixrange_init() 初始化高端内存映射中 kmap() 函数所需要的页表项。


   在 pagetable_init() 函数返回后,内核空间的页表则完成了初始化,此时静态 PGD(swapper_pg_dir)被载入 CR3 寄存器中,换页单元可以使用静态表。


   paging_init() 接下来的工作是调用 kmap_init() 初始化带有 PAGE_KERNEL 标志位的每个 PTE。最终的工作是调用 zone_sizes_init(),用于初始化所有被使用的管理区结构。

(3)mem_init

    该函数负责消除启动内存分配器和与之相关的结构。


   这个函数的功能相当简单。它负责计算高、低端内存的维度并向用户显示出信息消息。若有需要,还将运行硬盘的最终初始化。在 x86 平台,有关 VM 的主要函数是 free_pages_init()。


   这个函数首先使引导内存分配器调用函数回收自身,UMA 结构中是调用 free_all_bootmem() 函数,NUMA 中是调用 free_all_bootmem_node() 函数。这两个函数都调用内核函数 free_all_bootmem_core() ,但是使用的参数不同。free_all_bootmem_core() 函数原理很简单,它执行如下任务。


对于这个节点上分配器可以识别的所有未被分配的页面:

将它们结构页面上的 PG_reserved 标志位清 0;

将计数器设置为 1;

调用 __free_pages() 以使伙伴分配器(下章将讨论)能建立释放列表。

释放位图使用的所有页面,并将这些页面交给伙伴分配器。

   在这个阶段,伙伴分配器控制了所有在低端内存下的页面。free_all_bootmem() 返回后首先清算保留页面的数量。高端内存页面由 free_pages_init() 负责处理。但此时需要理解的是如何分配和初始化整个 mem_map 序列,以及主分配器如何获取页面。图 5.3 显示了单节点系统中初始化低端内存页面的基本流程。free_all_bootmem() 返回后,ZONE-NORMAL 中的所有页面都为伙伴分配器所有。为了初始化高端内存页面,free_pages_init() 对 highstart_pfn 和 highend_pfn 之间的每一个页面都调用了函数 one_highpage_init() 。此函数简单将 PG_reserved 标志位清 0 ,设置 PG_highmem 标志位,设置计数器为 1 并调用 __free_pages() 将自已释放到伙伴分配器中。这与 free_all_bootmem_core() 操作一样。


   此时不再需要引导内存分配器,伙伴分配器成为系统的主要物理页面分配器。值得注意的是,不仅启动分配器的数据被移除,它所有用于启动系统的代码也被移除了。所有用于启动系统的初始函数声明都被标明为 __init,如下所示:

unsigned long __init free_all_bootmem(void)

    连接器将这些函数都放在 init 区。x86 平台上 free_initmem() 函数可以释放 __init_begin__init_end 的所有页面给伙伴分配器。通过这种方法,Linux 能释放数量很大的启动代码所使用的内存,已不再需要启动代码。


    传送门 mem_init

二、启动内存分配

1、初始化引导内存分配器

(1)init_bootmem

// mm/bootmem.c
// 这是容易混淆的地方。参数 pages 实际上是该节点可寻址内存的 PFN 末端,而不是
// 按名字的意思:页面数。
unsigned long __init init_bootmem (unsigned long start, unsigned long pages) {
  // 如果没有依赖于体系结构的代码,则设置该节点的可寻址最大 PFN。
  max_low_pfn = pages;
  // 如果没有依赖于体系结构的代码,则设置该节点的可寻址最小 PFN。
  min_low_pfn = start;
  // 调用 init_bootmem_core()(见 E.1.3 小节),在那里完成初始化 bootmem_data 的实
  // 际工作。
  return(init_bootmem_core(&contig_page_data, start, 0, pages));
}

(2)init_bootmem_node

// mm/bootmem.c
// 这个函数由 NUMA 体系结构调用,用于初始化特定节点的引导内存分配器数据。
unsigned long __init init_bootmem_node (pg_data_t *pgdat, unsigned long freepfn, 
                    unsigned long startpfn, unsigned long endpfn)
{
  //  仅直接调用 init_bootmem_core()
  return(init_bootmem_core(pgdat, freepfn, startpfn, endpfn));
}

(3)init_bootmem_core

// mm/bootmem.c
static unsigned long __init init_bootmem_core (pg_data_t *pgdat,
  unsigned long mapstart, unsigned long start, unsigned long end)
{
  bootmem_data_t *bdata = pgdat->bdata;
  unsigned long mapsize = ((end - start)+7)/8;

  pgdat->node_next = pgdat_list;
  pgdat_list = pgdat;

  mapsize = (mapsize + (sizeof(long) - 1UL)) & ~(sizeof(long) - 1UL);
  // 内存最低内存分配给 node_bootmem_map , 大小为 mapsize 个页面(struct page)
  bdata->node_bootmem_map = phys_to_virt(mapstart << PAGE_SHIFT);
  bdata->node_boot_start = (start << PAGE_SHIFT);
  bdata->node_low_pfn = end;

  /*
   * Initially all pages are reserved - setup_arch() has to
   * register free RAM areas explicitly.
   */
  // 把所有位图初始化为 1, 即所有内存设置为保留(被占用)
  memset(bdata->node_bootmem_map, 0xff, mapsize);

  return mapsize;
}

(4)总结

   一旦 setup_memory() 确定了可用物理页面的界限,系统将从两个引导内存的初始化函数中选择一个,并以待初始化的节点的起始和终止 PFN 作为调用参数。在 UMA 结构中,init_bootmem() 用于初始化 contig_page_data,而在 NUMA,init_bootmem_node() 则初始化一个具体的节点。这两个函数主要通过调用 init_bootmem_core() 来完成实际工作。


   内核函数首先要把 pgdat_data_t 插入到 pgdat_list 链表中,因为这个节点在函数末尾很快就会用到。然后它记录下该节点的起始和结束地址(该节点与 bootmem_data_t 有关)并且分配一个位图来表示页面的分配情况。位图所需的大小以字节计算,计算公式如下:

image.png  

该位图存放于由 bootmem_data_t→node_boot_start 指向的物理地址处,而其虚拟地址的映射由 bootmem_data_t→node_bootmem_map 指定。由于不存在与结构无关的方式来检测内存中的空洞,整个位图就被初始化为 1 来标志所有页已被分配。将可用页面的位设置为 0 的工作则由与结构相关的代码完成。在 x86 结构中,register_bootmem_low_pages() 通过检测 e820 映射图,并在每一个可用页面上调用 free_bootmem() 函数,将其位设为 1,然后再调用 reserve_bootmem() 为保存实际位图所需的页面预留空间。

2、分配内存

(1)保留大块区域的内存

(a)reserve_bootmem
// mm/bootmem.c
void __init reserve_bootmem (unsigned long addr, unsigned long size)
{
  reserve_bootmem_core(contig_page_data.bdata, addr, size);
}
(b)reserve_bootmem_core
// mm/bootmem.c
/*
 * Marks a particular physical memory range as unallocatable. Usable RAM
 * might be used for boot-time allocations - or it might get added
 * to the free page pool later on.
 */
static void __init reserve_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size)
{
  unsigned long i;
  /*
   * round up, partially reserved pages are considered
   * fully reserved.
   */
  // sidx 是服务页的起始索引。它的值是从请求地址中减去起始地址并除以页大小得到的。   
  unsigned long sidx = (addr - bdata->node_boot_start)/PAGE_SIZE;
  // 末尾索引 eidx 的计算与 sidx 类似,但它的分配是向上取整到最近的页面。这意味着
  // 对保留一页中部分请求将导致整个页都被保留。
  unsigned long eidx = (addr + size - bdata->node_boot_start + 
              PAGE_SIZE-1)/PAGE_SIZE;
  // end 是受本次保留影响的最后 PFN。             
  unsigned long end = (addr + size + PAGE_SIZE-1)/PAGE_SIZE;
  // 检查是否给定了一个非零值。
  if (!size) BUG();
  // 检查起始索引不在节点起点之前。
  if (sidx < 0)
    BUG();
  // 检查末尾索引不在节点末端之后。    
  if (eidx < 0)
    BUG();
  // 检查起始索引不在末尾索引之后。    
  if (sidx >= eidx)
    BUG();
  // 检查起始地址没有超出该启动内存节点所表示的内存范围。   
  if ((addr >> PAGE_SHIFT) >= bdata->node_low_pfn)
    BUG();
  // 检查末尾地址没有超出该启动内存节点所表示的内存范围。   
  if (end > bdata->node_low_pfn)
    BUG();
  // 从 sidx 开始,到 eidx 结束,这里测试和设置启动内存分配图中表示页面已经分
  // 配的位。如果该位已经设置为 1,则打印一条消息:该位被设置了两次。    
  for (i = sidx; i < eidx; i++)
    if (test_and_set_bit(i, bdata->node_bootmem_map))
      printk("hm, page %08lx reserved twice.\n", i*PAGE_SIZE);
}
(c)总结

    由上分析可知:保留内存主要通过函数 reserve_bootmem_core 来实现的,其主要就是把对应页帧号的位图(bootmem_data_t ->node_bootmem_map)设置为 1, 来表示对应的页被占用了。

(2)在启动时分配内存

(a)alloc_bootmem
// include/linux/bootmem.h
// 将 L1 硬件高速缓存页对齐,并在 DMA 中可用的最大地址后开始查找一页。 SMP_CACHE_BYTES 为 64?
#define alloc_bootmem(x) \
  __alloc_bootmem((x), SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS))
// 将 L1 硬件高速缓存页对齐,并从 0 开始查找。  
#define alloc_bootmem_low(x) \
  __alloc_bootmem((x), SMP_CACHE_BYTES, 0)
  
// 将已分配位对齐到一页大小,这样满页将从 DMA 可用的最大地址开始分配。
#define alloc_bootmem_pages(x) \
  __alloc_bootmem((x), PAGE_SIZE, __pa(MAX_DMA_ADDRESS))
// 将已分配位对齐到一页大小,这样满页将从物理地址 0 开始分配。  
#define alloc_bootmem_low_pages(x) \
  __alloc_bootmem((x), PAGE_SIZE, 0)
(b)__alloc_bootmem
// mm/page_alloc.c
pg_data_t *pgdat_list;

// include/linux/mmzone.h
#define for_each_pgdat(pgdat) \
  for (pgdat = pgdat_list; pgdat; pgdat = pgdat->node_next)
// mm/bootmem.c
// 
// size 是请求分配的大小。
// align 是指定的对齐方式,它必须是 2 的幂。目前,一般设置为 SMP_CACHE_BYTES 或 PAGE_SIZE。
// goal 是开始查询的起始地址。
void * __init __alloc_bootmem (unsigned long size, unsigned long align, unsigned long goal)
{
  pg_data_t *pgdat;
  void *ptr;

  // 遍历所有的可用节点,并试着轮流从各个节点开始分配。在 UMA 中,就从
  // contig_page_data 节点开始分配。
  for_each_pgdat(pgdat)
    if ((ptr = __alloc_bootmem_core(pgdat->bdata, size,
            align, goal)))
      return(ptr);

  /*
   * Whoops, we cannot satisfy the allocation request.
   */
  // 如果分配失败,系统将不能启动,故系统瘫痪。   
  printk(KERN_ALERT "bootmem alloc of %lu bytes failed!\n", size);
  panic("Out of memory");
  return NULL;
}

传送门 __alloc_bootmem_core

(c)alloc_bootmem_node
// include/linux/bootmem.h
// 将从请求节点处开始分配,对齐 L1 硬件高速缓存,并从
// ZONE_NORMAL 处开始找到一页 (如,在 ZONE_DMA 末端,其为 MAX_DMA_ADDRESS)。
#define alloc_bootmem_node(pgdat, x) \
  __alloc_bootmem_node((pgdat), (x), SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS))

// 将从请求节点处开始分配,分配页对齐到一页大小,所以全部页面将从 ZONE_NORMAL 中开始分配。
#define alloc_bootmem_pages_node(pgdat, x) \
  __alloc_bootmem_node((pgdat), (x), PAGE_SIZE, __pa(MAX_DMA_ADDRESS))
  
// 将从请求节点处开始分配,分配页对齐到一页大小,所以全部页面将从物理地址 0 处开始分配,
// 这里将使用 ZONE_DMA。  
#define alloc_bootmem_low_pages_node(pgdat, x) \
  __alloc_bootmem_node((pgdat), (x), PAGE_SIZE, 0)
(d)__alloc_bootmem_node
// mm/bootmem.c
// 它所要分配的节点是特定的。
void * __init __alloc_bootmem_node (pg_data_t *pgdat, unsigned long size, unsigned long align, unsigned long goal)
{
  void *ptr;
  // 调用核心函数 __alloc_bootmem_core 来完成分配。
  ptr = __alloc_bootmem_core(pgdat->bdata, size, align, goal);
  // 如果成功则返回一个指针。
  if (ptr)
    return (ptr);

  /*
   * Whoops, we cannot satisfy the allocation request.
   */
  // 否则,若在这里都没有分配内存,系统将不能启动,所以打印一条消息,表示系统瘫痪。   
  printk(KERN_ALERT "bootmem alloc of %lu bytes failed!\n", size);
  panic("Out of memory");
  return NULL;
}
(e)__alloc_bootmem_core

    这是从一个带有引导内存分配器的特定节点中分配内存的核心函数。它非常大,所以将其分解成如下步骤:

  • 函数开始处保证所有的参数都正确。
  • goal 参数为基础计算开始扫描的起始地址。
  • 检查本次分配是否可以使用上次分配的页面以节省内存。
  • 在位图中标记已分配页为 1,并将页中内容清 0
/*
 * We 'merge' subsequent allocations to save space. We might 'lose'
 * some fraction of a page if allocations cannot be satisfied due to
 * size constraints on boxes where there is physical RAM space
 * fragmentation - in these cases * (mostly large memory boxes) this
 * is not a problem.
 *
 * On low memory boxes we get it right in 100% of the cases.
 */

/*
 * alignment has to be a power of 2 value.
 */
// 这是函数的前面部分,它保证参数有效。
// 
// bdata 是要分配结构体的启动内存。
// size 是请求分配的大小。
// align 是分配的对齐方式,它必须是 2 的幂。
// goal 是上面要分配的最可能的合适大小? 以 goal 参数为基础计算开始扫描的起始地址。
static void * __init __alloc_bootmem_core (bootmem_data_t *bdata, 
  unsigned long size, unsigned long align, unsigned long goal)
{
  unsigned long i, start = 0;
  void *ret;
  unsigned long offset, remaining_size;
  unsigned long areasize, preferred, incr;
  // 计算末尾位索引 eidx,它返回可能用于分配的最高页面索引。
  unsigned long eidx = bdata->node_low_pfn - (bdata->node_boot_start >>
              PAGE_SHIFT);
              
  //  如果指定了 0 则调用 BUG()。
  if (!size) BUG();
  //  如果对齐不是 2 的幂,则调用 BUG()。
  if (align & (align-1))
    BUG();
  // 对齐的缺省偏移是 0。
  offset = 0;
  // 如果指定了对齐方式则・・・・
  // 请求的对齐方式与开始节点的对齐方式相同,这里计算偏移。
  if (align &&
      (bdata->node_boot_start & (align - 1UL)) != 0)
  // 要使用的偏移与起始地址低位标记的请求对齐。实际上,这里的 offset 与 align 的一
  // 般值 align 相似。     
    offset = (align - (bdata->node_boot_start & (align - 1UL)));
  offset >>= PAGE_SHIFT;

  /*
   * We try to allocate bootmem pages above 'goal'
   * first, then we try to allocate lower pages.
   */
  // 这一块计算开始的 PFN 从 goal 参数为基础的地址处开始扫描。
  //
  // 如果指定了 goal,且 goal 在该节点的开始地址之后,goal 小于该节点可寻址 PFN,则 ...  
  // 开始的适当偏移是 goal 减去该节点可寻址的内存起点。    
  if (goal && (goal >= bdata->node_boot_start) && 
      ((goal >> PAGE_SHIFT) < bdata->node_low_pfn)) {
    preferred = goal - bdata->node_boot_start;
  } else
  // 如果不是这样,则适当偏移为 0。
    preferred = 0;
    
  // 考虑偏移,调整适当偏移的大小,这样该地址将可以正确对齐。
  preferred = ((preferred + align - 1) & ~(align - 1)) >> PAGE_SHIFT;
  preferred += offset;
  // 本次分配影响到的页面数存放在 areasize 中。
  areasize = (size+PAGE_SIZE-1)/PAGE_SIZE;
  // incr 是要跳过的页面数,如果多于一页,则它们满足对齐请求。
  incr = align >> PAGE_SHIFT ? : 1;

  // 这一块扫描内存,寻找一块足够大的块来满足请求。
  //
  // 如果本次分配不能满足从 goal 开始,则跳到这里的标志,这样将重新扫描该映射图。
restart_scan:
  // 从 preferred 开始,这里线性扫描一块足够大的块来满足请求。这里以 incr 为
  // 步长来遍历整个地址空间以满足大于一页的对齐。如果对齐小于一页,incr 为 1。
  for (i = preferred; i < eidx; i += incr) {
    unsigned long j;
    if (test_bit(i, bdata->node_bootmem_map))
      continue;
  // 扫描下一个 areasize 大小的页面来确定它是否也可以被释放。如果到达可寻
  // 址空间的末端 (eidx) 或者其中的一页已经在使用,则失败。      
    for (j = i + 1; j < i + areasize; ++j) {
      if (j >= eidx)
        goto fail_block;
      if (test_bit (j, bdata->node_bootmem_map))
        goto fail_block;
    }
  // 找到一个空闲块,所以这里记录 start,并跳转到找到的块。   
    start = i;
    goto found;
  fail_block:;
  }
  // 分配失败,所以从开始处重新开始。
  if (preferred) {
    preferred = offset;
    goto restart_scan;
  }
  // 如果再次失败,则返回 NULL,这将导致系统瘫疾。
  return NULL;

  // 这一块测试以确定本次分配可以与前一次分配合并。
found:
  // 检查分配的起点不会在可寻址内存之后。刚才已经检查过,所以这里是多余的。
  if (start >= eidx)
    BUG();

  /*
   * Is the next page of the previous allocation-end the start
   * of this allocation's buffer? If yes then we can 'merge'
   * the previous partial page with this allocation.
   */
  // 如果对齐小于 PAGE_SIZE,前面的页面在其中有空间(last_offset != 0),而且
  // 前面使用的页与本次分配的页相邻,则试着与前一次分配合并。  
  if (align <= PAGE_SIZE
      && bdata->last_offset && bdata->last_pos+1 == start) {
  // 更新用于对 align 请求正确分页的偏移。     
    offset = (bdata->last_offset+align-1) & ~(align-1);
  // 如果偏移现在超过了页面边界,则调用 BUG()。这个条件需要使用一次非常
  // 槽糕的对齐选择。由于一般使用的对齐仅是一个 PAGE_SIZE 的因子,所以不可能在平常
  // 使用。    
    if (offset > PAGE_SIZE)
      BUG();
  // remaining_size 是以前用过的页面中处于空闲的空间。     
    remaining_size = PAGE_SIZE-offset;
// 如果在旧的页面中有足够的空间剩余,这里使用旧的页面,然后更新 bootmem_data 
//
  // 结构来反映这一点。    
    if (size < remaining_size) {
  // 这次分配中用到的页面数现在为 0。    
      areasize = 0;
      // last_pos unchanged
  // 更新 last_offset 为本次分配的末端。     
      bdata->last_offset = offset+size;
  // 计算返回成功分配的虚拟地址。     
      ret = phys_to_virt(bdata->last_pos*PAGE_SIZE + offset +
            bdata->node_boot_start);
    } else {
// 如果不是这样,则这里计算除了这一页外还需要多少页面,并更新 bootmem_data。  
//
  //  remaining_size 是上一次用来满足分配的页面空间。 
      remaining_size = size - remaining_size;
  // 计算还需要多少页面来满足分配请求。      
      areasize = (remaining_size+PAGE_SIZE-1)/PAGE_SIZE;
  // 计算分配开始的地址。     
      ret = phys_to_virt(bdata->last_pos*PAGE_SIZE + offset +
            bdata->node_boot_start);
  // 使用到的最后一页是 start 页面加上满足这次分配的额外页面数量 areasize。            
      bdata->last_pos = start+areasize-1;
  // 已经计算过本次分配的末端。          
      bdata->last_offset = remaining_size;
    }
  // 如果偏移在页尾,则标记为 0。    
    bdata->last_offset &= ~PAGE_MASK;
  } else {
// 如果没有,这里记录本次分配用到的页面和偏移,以便于下次分配时的合并。
//
  // 不发生合并,所以这里记录用到的满足本次分配的最后一页。
    bdata->last_pos = start + areasize - 1;
  // 记录用到的最后一页。   
    bdata->last_offset = size & ~PAGE_MASK;
  // 记录分配的起始虚拟地址。   
    ret = phys_to_virt(start * PAGE_SIZE + bdata->node_boot_start);
  }
  /*
   * Reserve the area now:
   */
// 这一块在位图中标记分配页为 1,并将其内容清 0。
//
  // 遍历本次分配用到的所有页面,在位图中设置 1。如果它们中已经有 1,则发生
  // 了一次重复分配,所以调用 BUG()。   
  for (i = start; i < start+areasize; i++)
    if (test_and_set_bit(i, bdata->node_bootmem_map))
      BUG();
  // 将页面用 0 填充。     
  memset(ret, 0, size);
  // 返回分配的地址。
  return ret;
}
(f)总结

    由上分析可知:启动时分配函数 alloc_bootmemalloc_bootmem_low

alloc_bootmem_pages、alloc_bootmem_low_pages 它们最终会调用 __alloc_bootmem_core 函数,其通过 bdata->node_bootmem_map 位图找到空闲可用的页面。


如果对齐(align)小于 PAGE_SIZE,前面的页面在其中有空间(last_offset != 0),而且前面使用的页与本次分配的页相邻,则试着与前一次分配合并,更新页面(bdata->last_pos)和偏移(bdata->last_offset);


否则不合并,并记录用到的页面(bdata->last_pos)和偏移(bdata->last_offset),以便于下次分配时的合并。


   bdata 的字段意思可参考 ⇒ 5.1 描述引导内存映射

深入理解Linux虚拟内存管理(四)(中):https://developer.aliyun.com/article/1597781

目录
相关文章
|
14天前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
|
23天前
|
Linux 调度
深入理解Linux虚拟内存管理(七)(下)
深入理解Linux虚拟内存管理(七)
35 4
|
23天前
|
存储 Linux 索引
深入理解Linux虚拟内存管理(九)(中)
深入理解Linux虚拟内存管理(九)
19 2
|
23天前
|
Linux 索引
深入理解Linux虚拟内存管理(九)(上)
深入理解Linux虚拟内存管理(九)
25 2
|
23天前
|
Linux
深入理解Linux虚拟内存管理(七)(中)
深入理解Linux虚拟内存管理(七)
23 2
|
23天前
|
机器学习/深度学习 消息中间件 Unix
深入理解Linux虚拟内存管理(九)(下)
深入理解Linux虚拟内存管理(九)
16 1
|
23天前
|
Linux
深入理解Linux虚拟内存管理(七)(上)
深入理解Linux虚拟内存管理(七)
24 1
|
21天前
|
缓存 Linux 调度
Linux服务器如何查看CPU占用率、内存占用、带宽占用
Linux服务器如何查看CPU占用率、内存占用、带宽占用
67 0
|
23天前
|
Linux API
深入理解Linux虚拟内存管理(六)(下)
深入理解Linux虚拟内存管理(六)
13 0
|
23天前
|
Linux
深入理解Linux虚拟内存管理(六)(中)
深入理解Linux虚拟内存管理(六)
18 0