Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Linux内核学习笔记(三)

30
Jan
2011

Linux内核学习笔记(三)

By Alex
/ in C,Linux
/ tags kernel
0 Comments

Linux使用虚拟内存技术。它是一种位于应用程序内存请求与内存管理单元(MMU,一般是集成于CPU的硬件)硬件之间的抽象层。虚拟内存计数有以下优势:

  1. 多个进程可以同时并发的运行,使用重复的虚拟内存地址
  2. 应用程序所需内存大于物理内存时也可以运行
  3. 程序代码中,只有部分装入内存时,进程也可以执行程序
  4. 进程可以共享库函数或者程序的一份单一的内存映像
  5. 程序在物理内存中的位置可以重新定位
  6. 可以编写机器无关的代码,不用关心物理内存的组织结构

虚拟内存子系统的主要由虚拟地址空间(Virtual address space)组成,进程使用的虚拟内存地址不同于其物理内存地址,内核(提供页表)和MMU负责协调并定位物理地址。

机器的物理内存,除了开辟出一小部分专门用于存放内核映像(内核代码、内核静态数据结构)以外,其它部分通常都由虚拟内存子系统管理,并作以下三个主要用途:

  1. 满足内核对缓冲区、描述符和其它动态内核数据结构的请求
  2. 满足进程对一般内存区域的请求、对文件内存映射的请求
  3. 作为高速缓存的载体,让磁盘等I/O获得更好性能

虚拟内存子系统要解决的一个主要问题是内存碎片,由于内核常常需要物理上连续的内存空间,当碎片化严重时,即使物理内存富余,也可能导致失败。内核内存分配器(KMA)为解决内存碎片问题提供了很好的帮助,当前较好的KMA算法是Solaris发明的Slab。

本文分为以下章节,讲述虚拟内存子系统和相关的内核模块:

  1. 内存管理
  2. 进程地址空间
  3. 页缓存和页回写
内存管理

在内核中分配内存比在用户空间困难,原因包括:

  1. 内核不能像用户空间那样奢侈的使用内存,这是根本原因所在。内核不支持简单的内存分配方式
  2. 内核一般不能睡眠,这导致涉及到换页(潜在的睡眠)的内存分配受限
  3. 内核处理内存分配错误困难,参考第2条
页表

每个进程都有一个页表,用于存储虚拟地址到物理地址,准确的说是页,的映射关系。

进程顶级页面包含一个项,此项的内容是全局共享的,描述内核空间中的虚拟-物理页映射关系。每个进程随时都可能访问内核空间,例如系统调用,这要求随时能够进行内核空间的地址映射。

MMU

内存管理单元,能够通过查找进程的页面,完成从虚拟地址到物理地址的转换。

MMU是由体系结构决定的,因此,页表的结构也和体系结构相关。

页

尽管CPU最小寻址单位通常为字(甚至字节),内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)却把物理页(也称页帧,Page frame)作为管理内存的基本单位——从虚拟内存角度看,页是最小单位,即页表(Page table)的最小条目是一个页。

体系结构不同,页的大小也不同(甚至某些体系结构支持多种页大小)。大部分32位体系结构的页大小为4KB,64位一般支持8KB。大部分Linux系统使用4KB页。

内核使用下面的结构来表示物理页:

/include/linux/mm_types.h
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*
* 页描述符结构体
*
* 每个物理页都对应这样的一个结构,以便内核能够跟踪当前时刻页被用来存放什么东西
* 注意:无法跟踪哪个任务在使用页
*
* 该结构本质上和物理页有关,而不是虚拟页,因此该结构对页的描述是临时的
*/
struct page
{
    //位域标识,该标识存放多种状态,例如是否脏页、是否锁在内存中
    unsigned long flags;
    //存放页的引用计数,如果为-1表示内核没有引用该页,在新的分配中可以使用它,内核代码调用page_count()检查此计数,返回0表示空闲
    atomic_t _count;
    union
    {
        //这个页被映射到了几个进程的地址空间
        atomic_t _mapcount;
        struct
        {
            u16 inuse;
            u16 objects;
        };
    };
    union
    {
        struct
        {
            //一个页可以作为私有数据使用
            unsigned long private;
            /**
             * 该字段目前不用于内核空间
             * 如果当前页用于页缓存,该字段用于访问缓存对应的文件。页缓存用于保存文件的逻辑内容,Linux用它加速磁盘访问
             * 如果当前页是一个匿名页(anonymous page,依赖于swap的用户空间内存)则该字段指向anon_vma结构允许内核快速的找到包含该页的页表
             */
            struct address_space *mapping;
        };
#if USE_SPLIT_PTLOCKS
        spinlock_t ptl;
#endif
        struct kmem_cache *slab;
        struct page *first_page;//指向slab中第一个空闲对象
    };
    union
    {
        pgoff_t index; //对于页缓存中的页,该字段指定了缓存映射的文件的偏移量
        void *freelist;//如果页由slub或者slob分配器管理,则该字段指向空闲对象的列表
    };
    struct list_head lru;
 
#if defined(WANT_PAGE_VIRTUAL)
/**
* 如果不为空,则指向页对应的内核空间虚拟地址,该字段不是很有用,因为地址可以很容易被计算出来
* 某些内存(比如高端内存,high memory,32位系统一般1GB以上)在内核地址空间中,不固定的映射到某个虚拟地址,此时该字段为NULL
*/
void *virtual;
#endif
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
unsigned long debug_flags;
#endif
 
#ifdef CONFIG_KMEMCHECK
void *shadow;
#endif
};

可以看到上述结构的嵌套很复杂,这是出于节约空间的考虑,此结构每增加1B,内核占用内存就会增加若干MB。因为每个物理页都需要这样的一个结构,内存越大,这类结构占用的内存就越多——假定页大小为8KB,内存为4GB,那么page结构占用内核内存就是20MB。

区(Zones)

由于硬件的限制,内核不能按照同样的方式处理所有的内存,例如某些硬件在内存寻址方面存在缺陷:

  1. 某些硬件设备只能对特定内存地址进行DMA(直接内存访问)
  2. 某些体系结构能够寻址的物理地址范围比虚拟地址大的多,结果是,某些内存不能永久性的映射到内核地址空间。例如32位Linux内核把4G虚拟地址中0-3G分配给用户空间,3-4G分配给内核空间——1GB,而x86_32架构支持的物理地址扩展(PAE)可以让物理寻址范围扩大到64G

为应对这些缺陷,内核使用区把相同性质的内存进行分组:

  1. ZONE_DMA:该区的页支持DMA操作,DMA允许硬件绕过CPU直接读写主存。x86_32该区域为物理内存0-16MB
  2. ZONE_DMA32:和上一区类似,但是这些页只能被32位设备访问,某些体系结构中该区比ZONE_DMA更大
  3. ZONE_NORMAL:包含能够正常映射的页
  4. ZONE_HIGHMEM:包含所谓高端内存(High memory),这些内存不能永久的映射到内核地址空间,需要动态映射。x86_32该区域为物理内存896M以上

区的分配和使用依赖于体系结构:

  1. 某些体系结构支持对任何地址进行DMA操作,这些体系结构中ZONE_DMA为空。而x86_32上ISA(Industry Standard Architecture,工业标准体系结构,只支持16位设备)设备只能在物理内存的前16MB进行DMA操作
  2. 某些体系结构支持所有内存的直接映射,这些体系结构中ZONE_HIGHMEM为空,例如x86_64。而x86_32中高于896M的都是高端内存

注意这些内存分区没有物理意义,只是逻辑分组。内核依照分区进行内存分配:

  1. 内存不能跨区分配
  2. 某些分配可以使用多个区,例如一般用途的内存既可以使用ZONE_NORMAL,也可以使用ZONE_DMA

区使用下面的结构表示:

linux/mmzone.h
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
struct zone
{
    // 水位,通过*_wmark_pages(zone)宏访问,该数组持有当前区最小、低、高水位值
    // 内核使用水位为每个区域设置合适的内存消耗基准,水位随着空闲内存的多少而变化
    unsigned long watermark[NR_WMARK];
 
    /*
     * 各区保留的内存的大小
     *
     * 我们不确定分配出去的内存最终是否会被是否,因此,为了防止完全的浪费数GB的内存,我们必须预留低区域中的一些内存
     * 防止地区与内存出现OOM而高区域还有大量的内存可用
     * 该数组在运行时可能被重新计算,如果内核参数sysctl_lowmem_reserve_ratio被调整
     */
    unsigned long lowmem_reserve[MAX_NR_ZONES];
 
    struct per_cpu_pageset __percpu *pageset;
    //该自旋锁防止此结构被并发访问
    spinlock_t lock;
    int all_unreclaimable;
    struct free_area free_area[MAX_ORDER];
 
    ZONE_PADDING (_pad1_)
 
    spinlock_t lru_lock;
    struct zone_lru
    {
        struct list_head list;
    } lru[NR_LRU_LISTS];
 
    struct zone_reclaim_stat reclaim_stat;
 
    unsigned long pages_scanned;
    //区的标志位
    unsigned long flags;
 
    //该区的统计信息
    atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
 
    int prev_priority;
 
    unsigned int inactive_ratio;
 
    ZONE_PADDING (_pad2_)
    wait_queue_head_t * wait_table;
    unsigned long wait_table_hash_nr_entries;
    unsigned long wait_table_bits;
    struct pglist_data *zone_pgdat;
    unsigned long zone_start_pfn;
    unsigned long spanned_pages;
    unsigned long present_pages;
 
    /*
     * 区域的名字,内核启动时初始化,三个区的名字分别为:DMA、Normal、HighMem
     */
    const char *name;
} ____cacheline_internodealigned_in_smp;

这个结构较大,但是系统中只有三个区,因此该结构的实例只有三个。 

按页分配

通过内核提供的接口,我们可以在内核空间进行内存分配和释放。 内核提供了一种请求内存的底层机制,可以用来以页为单位分配内存:

linux/gfp.h
C
1
2
3
4
5
6
7
8
9
10
//分配2^order个连续的物理页,并且返回执行第一个page结构的指针,如果出错返回NULL
struct page * alloc_pages( gfp_t gfp_mask, unsigned int order );
//把页转换为它映射的逻辑地址
void * page_address( struct page *page );
//类似于alloc_pages,但是直接返回第一页的逻辑地址
unsigned long __get_free_pages( gfp_t gfp_mask, unsigned int order );
 
//下面两个函数分配单个页
struct page * alloc_page( gfp_t gfp_mask );
unsigned long __get_free_page( gfp_t gfp_mask );

分配内存后,必须进行错误检查,因为内存分配可能失败。

如果想让返回的页全部填充为0,可以调用: 

C
1
unsigned long get_zeroed_page(unsigned int gfp_mask);

该函数在为用户空间分配页时很有用,可以防止物理内存中的敏感数据被泄漏。

不再需要页时,应当释放之:

C
1
2
3
void __free_pages( struct page *page, unsigned int order );
void free_pages( unsigned long addr, unsigned int order );
void free_page( unsigned long addr );

需要注意的是,只能释放属于自己的页,这要求传递正确的struct page或者地址,传递错误的参数可能导致系统崩溃。 

gfp_mask标志

不管是按页分配,还是下面的按字节分配函数,都有一个标志参数可以设置。该标志参数可以包含多个位域,这些位域都声明在 linux/gfp.h 中声明,可以分为三类:

  1. 行为修饰符(大部分内存分配不需要直接指定):
    标志  说明 
    __GFP_WAIT 内存分配器(allocator)可以睡眠
    __GFP_HIGH 内存分配器可以访问紧急池(emergency pools)
    __GFP_IO 内存分配器可以启动磁盘I/O
    __GFP_FS 内存分配器可以启动文件系统I/O
    __GFP_COLD 内存分配器应该使用缓存中即将淘汰的页(cache cold pages)
    __GFP_NOWARN 内存分配器不打印失败警告
    __GFP_REPEAT 如果分配失败,内存分配器重复尝试分配。注意这次重复尝试也可能失败
    __GFP_NOFAIL 无限制重复尝试,分配不得失败
    __GFP_NORETRY 如果分配失败,绝不重试
    __GFP_NOMEMALLOC 不使用紧急预留区域
    __GFP_HARDWALL 强制hardwall处理器集合范围,即只在允许访问的CPU上分配内存
    __GFP_RECLAIMABLE 指定页是可回收的
    __GFP_COMP 添加混合页元数据,在hugetlb代码内部使用
  2. 区修饰符(指定内存从何处分配,内核默认从ZONE_NORMAL开始):
    标志  说明 
    __GFP_DMA 仅从ZONE_DMA分配
    __GFP_DMA32 仅从ZONE_DMA32分配 
    __GFP_HIGHMEM 从ZONE_HIGHMEM或者ZONE_NORMAL分配。注意该标志不能和__get_free_pages()、kmalloc()使用,原因是这些函数返回逻辑地址,而不是page结构体,而高端内存分配后是没有自动映射到内核地址空间的。只有alloc_pages()才可以使用该标志,它返回page结构体而不是逻辑地址
  3. 类型标志(实际上是结合上面两种标志,更加简单、不容易出错):
    标志 描述 
    GFP_ATOMIC __GFP_HIGH。用于中断处理程序、下半部、持有自旋锁以及其它不能睡眠的地方,例如中断处理程序、软中断、Tasklet
    GFP_NOWAIT 0。类似于上面,但是不会调用紧急内存池,因此增加了内存分配失败的可能性
    GFP_NOIO __GFP_WAIT。可以阻塞,但是不会启动磁盘I/O,该标志用于不能引擎更多磁盘I/O的阻塞性I/O代码中
    GFP_NOFS (__GFP_WAIT | __GFP_IO)。可以阻塞,也可能启动磁盘I/O,但是不会启动文件系统操作。在不能启动另外一个文件系统操作时使用,例如文件系统部分的某些代码中,防止再次调用自身导致死锁
    GFP_KERNEL (__GFP_WAIT | __GFP_IO | __GFP_FS)。常规分配方式,可能会阻塞,用于内核空间睡眠安全的进程上下文中,为了获得足够内存,内核会尽力而为,例如让调用者睡眠、交换页到硬盘
    GFP_USER (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL)。常规分配方式,可能会阻塞,用于用户空间
    GFP_HIGHUSER (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HIGHMEM | __GFP_HIGHMEM)。使用高端内存,用于为用户空间进程分配内存
    GFP_DMA __GFP_DMA。获取支持DMA的内存,一般驱动程序可能使用该标志
kmalloc()

类似于用户空间的内存分配函数 malloc() ,它分配逻辑、物理上都连续的以字节为单位的内核内存:

linux/slab.h
C
1
2
3
4
5
6
7
8
//该函数返回一个内存区域的指针,该区域至少有size大小,并且在物理上是连续的,如果出错则返回null
void * kmalloc(size_t size, gfp_t flags);
 
//用法示例
struct person *p;
p = kmalloc( sizeof(struct person), GFP_KERNEL );
if ( !p )
;
kfree()

该函数用于释放kmalloc()分配的内存:

linux/slab.h
C
1
2
3
void kfree(const void *ptr);
//下面的调用是安全的:
kfree(NULL);

 不得释放:

  1. 已经释放的内存
  2. 不是kmalloc()分配的内存
vmalloc()

该函数与kmalloc()相似,但是只保证分配内存的虚拟地址是连续的,物理地址不必连续 。用户空间malloc()的工作方式也是这样的。该函数可以分配非连续的物理内存块,然后再修正页表,把这些分散的内存映射到逻辑地址空间的连续区域内。

大多数情况下,只有硬件设备需要连续的物理地址,这是因为硬件设备运作于MMC之外,不知道虚拟地址为何物。尽管如此,很多内核代码使用kmalloc(),这是出于性能的考虑——不连续的物理地址需要建立额外的页表项,导致大得多的TLB(Translation lookaside buffer,转译后备缓冲,一种硬件缓冲区,用来缓存虚拟地址到物理地址的映射关系,可以极大提升系统性能,因为大部分内存需要虚拟寻址)抖动。

vmalloc()只在不得已时使用,典型的是获得大块内存,例如模块被动态加载到内核时,使用该函数分配的空间装载内核。

该函数以及相应的释放函数如下:

linux/vmalloc.h
C
1
2
3
4
5
//返回至少size的虚拟连续空闲内存,如果失败返回NULL
//该函数可能睡眠,不得用于中断上下文或者任何不支持阻塞的地方
void * vmalloc(unsigned long size);
//释放由vmalloc()分配的内存
void vfree(const void *addr);
slab层

内核中内存的分配和回收非常频繁。为了提高性能,程序员常常使用空闲链表(free lists), 其中包含特定结构的空闲实例,需要使用时,从中获取一个,用完则放回去,空闲链表相当于对象高速缓冲(对象池),避免不必要的内存分配/回收动作。

这种分散的空闲链表机制难以全局控制,例如当内存紧缺的时候,无法通知这些链表收缩以腾出内存,因为内核根本不知道空闲链表的存在。为解决此问题Linux引入了slab层(即所谓slab分配器),充当通用数据结构缓存层。slab在以下原则之间维持平衡:

  1. 频繁使用的数据结构会导致频繁的内存分配/释放,因此应该缓存之
  2. 频繁的内存分配/释放导致内存碎片,为避免碎片,空闲链表的缓存应当连续存放
  3. 如果分配器知晓对象大小、页大小、总的高速缓存的大小,将有利于决定最佳算法
  4. 如果部分缓存为CPU独占,那么分配/释放可以避免SMP锁
  5. 如果分配器与NUMA(非统一内存存取,被共享的存储器物理上是分布式的)相关,那么它应该从相同的内存节点为请求者分配内存
  6. 可以对存放的对象进行着色,防止多个对象映射到相同的缓存行(cache line,CPU缓存被划分为多个大小固定的行)
slab层的设计

依据对象类型的不同,slab层划分出多个高速缓存组:

  1. 存放进程描述符(struct task_struct)的组
  2. 存放索引节点对象(struct inode)的组
  3. 通用高速缓存组:kmalloc()接口基于该组

上述每个组,会划分为多个slab,每个slab由1-N个物理连续页(一般1个页)构成。每个slab在一个时刻可以是满、空、部分满三种状态,在分配时,优先使用部分满的slab,如果没有部分满slab,则使用空slab,如果空的也没有,则创建新的slab。这种使用策略有利于减少碎片。

高速缓存组使用结构 kmem_cache 表示:

/include/linux/slab_def.h
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct kmem_cache {
/* 1) Per-CPU数据,每次分配/释放时访问 */
    struct array_cache *array[NR_CPUS];
/* 2) 可调整参数,由cache_chain_mutex保护  */
    unsigned int batchcount;
    unsigned int limit;
    unsigned int shared;
 
    unsigned int buffer_size;
    u32 reciprocal_buffer_size;
/* 3) 每次分配/释放时从后端访问 */
 
    unsigned int flags;     /* constant flags */
    unsigned int num;       /* # of objs per slab */
 
/* 4) 缓存增长/收缩 */
    /* 每个slab包含2^gfporder个页 */
    unsigned int gfporder;
 
    /* 控制的GFP标志位 */
    gfp_t gfpflags;
 
    size_t colour;          /* 缓存着色范围 */
    unsigned int colour_off;    /* 着色偏移量 */
    struct kmem_cache *slabp_cache;
    unsigned int slab_size;
    unsigned int dflags;        /* 动态标志位 */
 
    /* 构造函数 */
    void (*ctor)(void *obj);
 
/* 5) 缓存创建/移除 */
    const char *name;//缓存组的名称
    struct list_head next;//下一个缓存组
 
    /*
     * 节点列表(长度一般就是1),该字段必须是最后一个字段
     *
     */
    struct kmem_list3 *nodelists[MAX_NUMNODES];
 
};

注意最后一个字段节点列表,它是 kmem_list3 结构的数组,该结构包含三个链表:slabs_full、slabs_partial、slab_empty,分别表示当前节点满、部分满、空的slab:

/mm/slab.c
C
1
2
3
4
5
6
7
8
9
10
11
12
13
struct kmem_list3 {
    struct list_head slabs_partial;    /* 部分满的slab */
    struct list_head slabs_full;  /* 满的slab */
    struct list_head slabs_free; /* 空闲的slab */
    unsigned long free_objects;
    unsigned int free_limit;
    unsigned int colour_next;    /* Per-node cache coloring */
    spinlock_t list_lock;
    struct array_cache *shared;    /* shared per node */
    struct array_cache **alien;    /* on other nodes */
    unsigned long next_reap;    /* updated without locking */
    int free_touched;        /* updated without locking */
};

这些链表包含所在高速缓存组所有的slab,后者使用slab描述符表示: 

mm/slab.c
C
1
2
3
4
5
6
7
8
struct slab
{
    struct list_head list; /* 数据结构链表,该链表可能是满的、空的或者部分满的 */
    unsigned long colouroff; /* 着色偏移量 */
    void *s_mem; /* 该slab中的第一个对象的指针 */
    unsigned int inuse; /* 该slab已分配的对象数 */
    kmem_bufctl_t free; /* 第一个空闲对象(如果有的话) */
};

slab描述符要么在slab外面另外分配内存存储,要么直接存放在slab的首部。

当缓存空间不足时,内核会调用低级内核页分配函数 kmem_getpages(struct kmem_cache *cachep, gfp_t flags, int nodeid) 为缓存组创建新的slab,后者会则转调 __get_free_pages() 进行内存分配。通过 kmem_freepages() 则可以释放掉slab,它会调用 free_pages() 。

slab层存在的意义就是避免频繁的分配/释放内存,因此只有slab中没有可用空间时,才会分配新的slab;类似的,只有当内存紧缺、高速缓存被显式撤销时,才会释放slab。

slab分配器的接口

下面的函数用于创建新的高速缓存组:

C
1
2
3
4
5
6
7
8
9
10
/**
* 成功时返回指向高速缓存组的指针,否则返回NULL
* 注意该函数不能在中断上下文调用,因为它可能睡眠
*/
struct kmem_cache * kmem_cache_create(
    const char *name, //高速缓存(组)的名称
    size_t size, //缓存中每个元素的大小
    size_t align,//第一个对象的偏移,用来确保在页内进行特定的对齐,默认0(标准对齐)
    unsigned long flags,//标志位集合
    void (*ctor)( void * ) );//高速缓存的构造函数,只有新的页追加到缓存中时,才会调用该函数,内核高速缓存不使用构造函数

要撤销高速缓存组,则可以调用:

C
1
int kmem_cache_destroy(struct kmem_cache *cachep);

该函数常常在模块注销代码中使用,调用前必须保证:组中所有slab都为空;调用过程中、完毕后,不得再使用该缓存。

创建了高速缓存组后,可以调用下面的函数获取或者释放对象:

C
1
2
3
4
5
6
7
8
/**
* 返回指向组中某个对象的指针,如果缓存组中任何slab都没有足够空间,就会触发新的slab的创建
*/
void * kmem_cache_alloc( struct kmem_cache *cachep, gfp_t flags );
/**
* 标注缓存池中的objp对象为空闲
*/
void kmem_cache_free( struct kmem_cache *cachep, void *objp );
在栈上的静态分配

在用户空间,用户栈可以非常大、动态增长。 在内核空间则相反,内核栈固定且很小。内核栈的大小依赖于体系结构和编译时选项。在以前的内核版本中,每个进程都对应一个2页的内核栈,由于32/64位体系结构的页大小分别4/8KB,因此内核栈分别为8/16KB。在2.6版本,引入了一个设置单页内核栈的选项,激活该选项则内核栈只有1页大小。

由于内核栈很小,任何时候在其上进行大量的静态分配(比如大型数组、结构体)都很危险。栈溢出时会导致宕机甚至无声息的数据破坏。使用动态内存分配通常是明智的选择。

高端内存的映射

高端内存不能永久的映射到内核地址空间,因此通过 alloc_pages(__GFP_HIGHMEM, *)  分配的页,可能没有对应的逻辑地址。

在x86架构上,尽管处理器物理寻址范围达4G(启用PAE则64G),然而896M+的内存都是高端内存,高端内存的页一旦被分配,就必须映射到内核的逻辑地址空间上,在x86上内核用于映射高端内存的逻辑地址范围是3-4G。

永久映射

要把一个页映射到内核地址空间,可以调用:

linux/highmem.h
C
1
2
//映射页到逻辑地址,该函数可能会睡眠
void *kmap(struct page *page);

不管是不是高端内存,上述函数都可用:

  1. 如果page属于低端内存,其(已经)映射到的虚拟地址直接作为返回值
  2. 如果page属于高端内存,则会立即永久的映射到一个内核逻辑地址,并返回该地址

由于可供映射的逻辑地址空间有限,因此高端内存不再需要的时候,必须解除映射:

C
1
void kunmap(struct page *page);
临时映射

必须映射高端内存,而当前上下文又不能睡眠时,可以使用内核提供的临时映射(temporary mappings)机制(亦称原子映射,atomic mappings)。内核预留了一些mappings,专供临时映射使用。调用下面的函数可以进行/解除临时映射:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 执行临时映射,该函数不会阻塞。
* 该函数会禁止内核抢占,这是因为mappings对每个CPU来说是唯一的,而内核抢占(调度程序)可能改变任务在哪个CPU上执行
*/
void *kmap_atomic( struct page *page, enum km_type type );
//第二个参数为枚举,说明临时映射的目的
enum km_type
{
    KM_BOUNCE_READ,
    KM_SKB_SUNRPC_DATA,
    KM_SKB_DATA_SOFTIRQ,
    KM_USER0,
    KM_USER1,
    KM_BIO_SRC_IRQ,
    KM_BIO_DST_IRQ,
    KM_PTE0,
    KM_PTE1,
    KM_PTE2,
    KM_IRQ0, KM_IRQ1,
    KM_SOFTIRQ0,
    KM_SOFTIRQ1,
    KM_SYNC_ICACHE,
    KM_SYNC_DCACHE,
    KM_UML_USERCOPY,
    KM_IRQ_PTE,
    KM_NMI,
    KM_NMI_PTE,
    KM_TYPE_NR
};
/**
* 解除临时映射,该函数不会阻塞
*/
void kunmap_atomic( void *kvaddr, enum km_type type );
Per-CPU的分配

可以在SMP机器上使用Per-CPU数据,对于每个CPU,数据具有独特的副本。在2.4中,声明Per-CPU数据的方式是声明长度等于CPU数量的数组,例如:

C
1
unsigned long my_percpu[NR_CPUS];

然后就可以用下面的代码访问之:

C
1
2
3
4
5
int cpu;
cpu = get_cpu(); /* 获得当前CPU并禁止内核抢占 */
/*操控变量*/
my_percpu[cpu]++;
put_cpu(); /* 启用内核抢占 */

注意上述代码中没有锁,这是因为操控的数据对于CPU是专用的,不存在多CPU并发问题。但是需要禁止内核抢占,因为:

  1. 如果当前代码被重新调度到其它CPU,则Per-CPU变量无效,因为它指向的不是当前CPU
  2. 如果另外一个任务抢占当前代码,则可能在同一CPU上访问Per-CPU变量,导致竞态条件 
新的Per-CPU接口

2.6引入了新的接口percpu,可以简化Per-CPU数据的创建、操控:

linux/percpu.h
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//编译时定义Per-CPU变量
DEFINE_PER_CPU( type, name );
//类似上面,某些情况下防止编译器警告
DECLARE_PER_CPU( type, name );
 
//禁止内核抢占,并得到Per-CPU变量的左值
#define get_cpu_var(var) (*({               \
    preempt_disable();              \
    &__get_cpu_var(var); }))
//恢复内核抢占
#define put_cpu_var(var) do {               \
    (void)&(var);                   \
    preempt_enable();               \
} while (0)
 
//获取其它CPU上的Per-CPU数据,注意该函数既不提供锁保护,也不禁止内核抢占
//该宏定义是非SMP的版本,就是简单的获得变量var
#define per_cpu(var, cpu)           (*((void)(cpu), &(var)))  

注意,上述静态编译时声明的Per-CPU数据不能在模块内使用,要在模块中访问Per-CPU数据,需要动态创建:

C
1
2
3
4
5
//给每个CPU分配一个指定类型的对象实例,封装了__alloc_percpu宏,按单字节对齐(给定类型的自然边界)
void *alloc_percpu( type );
//分配对象,size为尺寸,align表示按几个字节进行对齐
void *__alloc_percpu( size_t size, size_t align );
void free_percpu( const void * ); //释放Per-CPU数据
使用Per-CPU数据的原因

使用Per-CPU数据的好处如下:

  1. 减少锁定造成的开销:因为数据不存在并发问题,因此自然不需要加锁
  2. 大大减少缓存失效:失效发生在CPU试图使它们的缓存保持同步时,如果一个CPU需要操作某个数据,而该数据又存放在其它处理器的缓存中,则后者必须清理或者刷出自己的缓存。持续不断的缓存失效称为缓存抖动(thrashing the cache),会对系统性能产生很大影响
进程地址空间

内核除了需要管理自己的内存外,还需要管理用户空间中进程的内存,该内存称为进程地址空间。Linux使用虚拟内存技术管理内存,因此系统中的每一个进程觉得自己可以使用全部物理内存——即使一个进程,其拥有的地址空间也远远大于系统物理内存。

每个进程都在其私有的进程地址空间上运行,在用户态下,进程可以访问进程地址空间的私有栈、数据区、代码区等信息;在内核态下,进程访问内核的数据区、代码区,并使用另外的私有栈(内核栈)。尽管每个进程都有自己的私有地址空间,实际上它们会共享一部分内存内容,这种共享可以由进程显式提出,也可以由内核自动完成以节约内存,比如对于程序、库的副本,尽管有多个进程访问它,只会加载一份在内存

地址空间

进程地址空间由可寻址的虚拟内存组成, 每个进程有多达32或64位平坦的(flat,意味着连续、全部可用)空间。一些OS不提供平坦空间,而是分段式、不连续的,称为段地址空间,现在使用虚拟内存的OS很少使用这种模式了。两个进程即使使用相同的内存地址,也毫不相干。

内存地址是一个数值,其必须在地址空间的范围之内。

进程不一定有权访问其全部虚拟地址空间,地址空间中可以被进程合法访问的部分,称为内存区域(Memory areas),进程可以有多个内存区域,每个区域都是连续的一段虚拟地址区间。进程可以动态的给自己的地址空间添加/减少内存区域。内存区域具有关联的权限,例如可读、可写、可执行,进程必须遵守权限规则。如果进程访问不是内存区域的内存、或者以错误的方式访问,内核将终结进程,提示段错误(Segmentation Fault)。内存区域可以包含以下类型的对象:

  1. 可执行文件代码的内存映射,称为代码段(Text Section)
  2. 可执行文件已初始化的全局变量的内存映射,称为数据段(Data Section)
  3. 包含未初始化全局变量的零页(Zero page,全部存放零的页)的内存映射, 称为Bss Section
  4. 每个共享库(C库、动态库)的代码、数据、Bss段,也被载入进程的地址空间
  5. 任何内存映射文件
  6. 任何共享内存段
  7. 任何匿名(没有映射到实际文件,MAP_ANONYMOUS)的内存映射,比如 malloc() 分配的内存

内存区域不会重叠。可执行代码、已初始化全局变量、未初始化全局变量、共享库的代码和数据、内存映射文件、堆(匿名映射)、栈(用户态栈)都具有独立的区域,这些区域有些在程序通过 exec() 系统调用载入进程时,就会初始化。

内核地址空间

进程拥有完整的虚拟地址空间 —— 不管是32/64位系统。地址空间分为用户、内核两部分。

出于性能的考虑,内核内存映射到任何进程的地址空间。但是,内核地址空间仅仅能由内核代码访问。

对于32位系统来说,Linux将最上面的1G内存用作内核虚拟地址,范围0xc0000000 - 0xffffffff。物理内存完全对应的映射到内核空间,这简化了内存管理。任何0-896M范围的内核虚拟地址,减去0xc0000000的偏移即得到物理地址。

对于64位系统来说,整个地址空间的高半部分,全部留给内核虚拟地址。

内存描述符

内核使用内存描述符来表示进程的地址空间:

linux/mm_types.h
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct mm_struct
{
    /**
     * 下面两个字段都在描述该地址空间中全部内存区域:一个链表形式,一个红黑树形式
     * 这种冗余结构是为了快速遍历的同时,能够快速的搜索
     */
    struct vm_area_struct *mmap; /* 虚拟内存区域的链表 */
    struct rb_root mm_rb; /* 虚拟内存区域(VMA)的红黑树 */
    struct vm_area_struct *mmap_cache; /* 最后使用的虚拟内存区域 */
    unsigned long free_area_cache; /* 地址空间的第一个空洞 */
    pgd_t *pgd; /* 页全局目录 */
    atomic_t mm_users; /* 正在使用该地址空间的进程数,多个线程可能共享一个地址空间 */
    /**
     * 主(线程)引用计数,为0则该结构体可以被撤销
     * 多线程程序中只有主线程会导致该计数增加;进程的线程全部退出后,该计数会变为0
     */
    atomic_t mm_count;
    int map_count; /* 内存区域数量 */
    struct rw_semaphore mmap_sem; /* 内存区域信号量 */
    spinlock_t page_table_lock; /* 页表自旋锁 */
    /**
     * 所有内存描述符(mm_struct)形成的链表,该链表的首元素是init_mm描述符,它代表init进程的地址空间
     * 操作该链表时需要持有mmlist_lock锁
     */
    struct list_head mmlist;
    unsigned long start_code; /* 代码段起始地址 */
    unsigned long end_code; /* 代码段结束地址 */
    unsigned long start_data; /* 数据段起始地址 */
    unsigned long end_data; /* 数据段结束地址 */
    unsigned long start_brk; /* 堆的起始地址 */
    unsigned long brk; /* 堆的结束地址 */
    unsigned long start_stack; /* 栈的起始地址 */
    unsigned long arg_start; /* 命令行参数起始地址 */
    unsigned long arg_end; /* 命令行参数结束地址 */
    unsigned long env_start; /* 环境变量起始地址 */
    unsigned long env_end; /* 环境变量结束地址 */
    unsigned long rss; /* 分配的物理页 */
    unsigned long total_vm; /* VMA总数 */
    unsigned long locked_vm; /* 锁定VMA数量 */
    unsigned long saved_auxv[AT_VECTOR_SIZE]; /* 保存的auxv */
    cpumask_t cpu_vm_mask; /* lazy TLB switch mask */
    mm_context_t context; /* 体系结构特有数据 */
    unsigned long flags; /* 状态标志 */
    int core_waiters; /* thread core dump waiters */
    struct core_state *core_state; /* core dump support */
    spinlock_t ioctx_lock; /* AIO I/O 链表自旋锁*/
    struct hlist_head ioctx_list; /* AIO I/O 链表 */
};
分配内存描述符

内存描述符的指针存放在进程描述符的 task_struct.mm 字段,当前进程的内存描述符可以通过 current -> mm 访问。

fork() 函数利用 copy_mm() 将父进程的内存描述符拷贝给子进程。

内存描述符从slab缓存组中分配:

/kernel/fork.c
C
1
2
//从slab缓存组mm_cachep中分配内存描述符
#define allocate_mm()    (kmem_cache_alloc(mm_cachep, GFP_KERNEL))

一般的每个进程都有自己独特的进程描述符,也就是独立的地址空间。如果父进程希望子进程与自己共享地址空间,可以在调用 clone() 时设置 CLONE_VM 标记,这样的子进程称作线程,指定该标记后,就不需要调用allocate_mm()宏来分配描述符了,只需要将子进程的mm指向父进程的内存描述符:

C
1
2
3
4
5
if (clone_flags & CLONE_VM)
{
    atomic_inc(&current->mm->mm_users);
    tsk->mm = current->mm;
}
撤销内存描述符

进程退出时,内核调用定义在 /kernel/exit.c 中的 exit_mm() 来撤销内存描述符,该函数会:

  1. 执行一些清理工作,更新统计量
  2. 调用 mmput() 减少mm_users计数
  3. 如果mm_users为0则调用 mmdrop() 减少mm_count计数
  4. 如果mm_count为零,说明该内存描述符没人使用了,调用 free_mm() 宏,通过 kmem_cache_free() 把结构体释放,归还slab缓存组
内核线程与内存描述符

内核线程没有进程地址空间,因此其进程描述符的mm字段为空,这是合理的——因为内核线程没有用户上下文。内核线程没有自己的内存描述符、页表。

尽管内核线程没有自己的页表,但是为了访问内核空间,它必须要使用页表。Linux的做法是,让内核线程使用前一个进程的页表:

  1. 当一个进程被调度,获得CPU时,其进程描述符mm字段所指向的地址空间被装载到内存。进程描述符的 active_mm 被更新,指向新的地址空间
  2. 内核线程没有自己的地址空间,因此它被调度时,内核会发现mm为NULL
  3. 这时,内核就会保留刚刚失去CPU的进程的内存描述符,并更新内核线程的 active_mm 使之指向此描述符
  4. 内核线程使用前一个进程的页表,从中查询和内核内存相关的信息。每个进程的页表都有描述内核空间的顶级Entry,此Entry的内容是全局共享的,不存在数据冗余
虚拟内存区域

进程的内存区域在内核中常被称为“虚拟内存区域(VMA)”。 虚拟内存区域是地址空间的连续区间上一个独立内存范围,内核把每个内存区域作为独立对象进行管理,虚拟内存区域使用下面的结构表示:

linux/mm_types.h
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct vm_area_struct
{
    struct mm_struct *vm_mm; /* 关联的内存描述符 */
    //每个虚拟内存区域都对应地址空间内的连续区间,不同虚拟内存区域不会重叠
    unsigned long vm_start; /* 区域首地址(包含) */
    unsigned long vm_end; /* 区域尾地址(排除) */
    struct vm_area_struct *vm_next; /* VMA的链表 */
    pgprot_t vm_page_prot; /* 访问权限 */
    unsigned long vm_flags; /* 标志位 */
    struct rb_node vm_rb; /* 此区域在红黑树中的节点 */
    union
    { /* 关联于 address_space->i_mmap 或者 address_space->i_mmap_nonlinear */
        struct
        {
            struct list_head list;
            void *parent;
            struct vm_area_struct *head;
        } vm_set;
        struct prio_tree_node prio_tree_node;
    } shared;
    struct list_head anon_vma_node; /* 匿名VMA项 */
    struct anon_vma *anon_vma; /* 匿名VMA对象 */
    struct vm_operations_struct *vm_ops; /* VMA操作表 */
    unsigned long vm_pgoff; /* 文件中的偏移量 */
    struct file *vm_file; /* 映射的文件(如果有) */
    void *vm_private_data; /* 私有数据 */
};
VMA标志

flags字段包含若干位标志,其含义如下: 

标志 对VMA及其页面的影响 
VM_READ 区域中的内存页是可读的
VM_WRITE 区域中的内存页是可写的
VM_EXEC 区域中的内存页是可执行的
VM_SHARED 区域中的内存页是被共享的,用于指示此区域包含的映射是否可以在多进程间共享。如果该标志被设置,称为“共享映射”;反之称为“私有映射”
VM_MAYREAD VM_READ标志可以被设置
VM_MAYWRITE VM_WRITE标志可以被设置
VM_MAYEXEC VM_EXEC标志可以被设置
VM_MAYSHARE VM_SHARE标志可以被设置
VM_GROWSDOWN 区域可以向下增长
VM_GROWSUP 区域可以向上增长
VM_SHM 区域被用于共享内存
VM_DENYWRITE 区域映射了不可写文件
VM_EXECUTABLE 区域映射了可执行文件
VM_LOCKED 区域中的页面被锁定
VM_IO 区域映射了一个设备的I/O空间。通常在设备驱动程序执行nmap()函数进行I/O空间映射时才被设置,该标志也表示该区域不得包含在进程的core dump中
VM_SEQ_READ 区域可能被顺序读,提示内核进行有选择的预读(read-ahead),该标志可以通过系统调用 madvise() 设置
VM_RAND_READ 区域可能被随机读,类似上面,作用相反
VM_DONTCOPY 在fork()时,该区域不得拷贝
VM_DONTEXPAND 区域不能通过mremap()增长
VM_RESERVED 区域不得被交换出内存(swapped out),也是由设备驱动在进行映射时设置
VM_ACCOUNT 该区域是一个记账VM对象
VM_HUGETLB 区域使用了hugetlb页面
VM_NONLINEAR 区域是非线性映射的
VMA操作

  vm_area_struct.vm_ops 字段定义了用于操作内存区域函数集合:

linux/mm.h
C
1
2
3
4
5
6
7
8
9
10
11
12
13
struct vm_operations_struct
{
    //当内存区域被加入到一个地址空间时,该函数被调用
    void (*open)( struct vm_area_struct *area );
    //当内存区域从地址空间中移除时,该函数被调用
    void (*close)( struct vm_area_struct * );
    //当访问该内存区域的页,而页不在物理内存中时,页面错误处理器(page fault handler)调用该函数
    int (*fault)( struct vm_area_struct *, struct vm_fault * );
    //当只读页被设置为可修改时,页面错误处理器调用该函数
    int (*page_mkwrite)( struct vm_area_struct *vma, struct vm_fault *vmf );
    //当get_user_pages()失败时,access_process_vm()调用该函数
    int (*access)( struct vm_area_struct *, unsigned long, void *, int, int );
};
实际使用中的内存区域

使用 /proc 文件系统和 pmap 工具可以查看给定进程的内存空间及其包含的内存区域:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pmap $PID
#输出内容:
#开始地址(大小)     权限 (主:次设备号 inode)    文件
myprog[1426]
00e80000 (1212 KB) r-xp (03:01 208530)       /lib/tls/libc-2.5.1.so #C库代码段
00faf000 (12 KB)   rw-p (03:01 208530)       /lib/tls/libc-2.5.1.so #C库数据段
00fb2000 (8 KB)    rw-p (00:00 0)                                   #C库bss段
08048000 (4 KB)    r-xp (03:03 439029)       /root/src/myprog       #程序代码段
08049000 (4 KB)    rw-p (03:03 439029)       /root/src/myprog       #程序数据段
40000000 (84 KB)   r-xp (03:01 80276)        /lib/ld-2.5.1.so       #ld.so的代码段
40015000 (4 KB)    rw-p (03:01 80276)        /lib/ld-2.5.1.so       #ld.so的代码段
4001e000 (4 KB)    rw-p (00:00 0)                                   #ld.so的bss段
bfffe000 (8 KB)    rwxp (00:00 0)            [ stack ]              #栈
mapped: 1340 KB writable/private: 40 KB shared: 0 KB

可以看到,该进程地址空间中被映射的总计1340KB,大约40KB是可写和私有的。如果一个内存范围是共享或不可写的, 那么内核只需要在内存中为文件(backing file)保留一份映射——这是安全的,也是合理的(避免内存浪费)。上面的C库就是不可写的例子。

没有映射文件的内存区域的设备标志为00:00,inode也设置为0,这样的区域属于零页——映射的内容全部是0。

操作内存区域

内核常常需要在VMA上执行操作,这类操作非常频繁。内核在 linux/mm.h 中声明了若干VMA操作辅助函数:

find_vma()

检查某个地址是否包含在某个VMA中:

mm/mmap.c
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 该函数搜索包含此地址的VMA,如果找不到返回NULL
*
* @param mm 内存描述符,指定了进程地址空间
* @param addr 需要寻找的地址
* @return 包含此地址的内存区域
*/
struct vm_area_struct * find_vma( struct mm_struct *mm, unsigned long addr )
{
    struct vm_area_struct *vma = NULL;
    if ( mm )
    {
        /**
         * 由于预期后续还会有更多的调用者查找目标VMA,因此在查找到VMA后,缓存在
         * 内存描述符的mmap_cache字段中
         */
        vma = mm->mmap_cache;
        if ( ! ( vma && vma->vm_end > addr && vma->vm_start <= addr ) ) //如果没有命中缓存
        {
            struct rb_node *rb_node;
            rb_node = mm->mm_rb.rb_node;
            vma = NULL;
            while ( rb_node ) //红黑树遍历
            {
                struct vm_area_struct * vma_tmp;
                vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
                if ( vma_tmp->vm_end > addr ) //判断结束地址大于addr
                {
                    vma = vma_tmp;
                    if ( vma_tmp->vm_start <= addr ) //如果其实地址小于等于addr,则找到,返回
                    break;
                    rb_node = rb_node->rb_left; //找不到,沿着左子节点
                }
                else
                rb_node = rb_node->rb_right; //遍历红黑树,沿着右子节点
            }
            if ( vma ) mm->mmap_cache = vma;
        }
    }
    return vma;
}
find_vma_prev()

工作方式与上面的函数类似, 但是同时返回前一个VMA的指针

linux/mm.h
C
1
2
3
4
5
struct vm_area_struct * find_vma_prev(
    struct mm_struct *mm,
    unsigned long addr,
    struct vm_area_struct **pprev //在此指针中存放前一个VMA
);
find_vma_intersection

该宏用来判断VMA是否和指定的区间交叉,甚至包含该区间:

/include/linux/mm.h
C
1
2
3
4
5
6
7
8
9
10
/* Look up the first VMA which intersects the interval start_addr..end_addr-1,
   NULL if none.  Assume start_addr < end_addr. */
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
{
    struct vm_area_struct * vma = find_vma(mm,start_addr);
 
    if (vma && end_addr <= vma->vm_start)
        vma = NULL;
    return vma;
}
创建地址区间:mmap()/do_mmap()

内核使用 do_mmap() 函数创建一个新的线性地址区间,但是该函数不一定会创建一个新的VMA——如果指定的地址空间与既有VMA相邻,那么将合并为一个VMA,否则创建新的VMA:

linux/mm.h
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//如果有无效参数,返回负数
//如果需要创建新VMA,那么将从slab缓存组中获得一个vm_area_struct实例,并调用vma_link()将其新分配的内存区域加入链表和红黑树
//并更新内存描述符的total_vm字段
unsigned long do_mmap(
    /**
     * 被映射的文件
     * 如果该参数为NULL且offset为0,表示这次映射没有和文件关联,称为“匿名映射(anonymous mapping.)”
     * 如果该参数不为零,则称为“文件映射(file-backed mapping)”
     */
    struct file *file,
    unsigned long addr, //搜索空闲地址的起始点,可选
    unsigned long offset, //文件起始偏移量
    unsigned long len, //映射多长文件内容
    unsigned long prot, //页保护标志:指定映射页的访问权限
    unsigned long flag //映射类型标志:指定类型、改变映射行为
);

页保护标志的取值依赖于体系结构,定义在 asm/mman.h ,通用的取值如下表:

 标志 说明 
PROT_READ 对应权限VM_READ
PROT_WRITE 对应权限VM_WRITE
PROT_EXEC 对应权限VM_EXEC
PROT_NONE 不得访问

映射类型标志定义在asm/mman.h ,取值如下:

标志  说明 
MAP_SHARED 该映射可以共享
MAP_PRIVATE 该映射不得被共享
MAP_FIXED 新的区间必须开始于addr参数指定的位置
MAP_ANONYMOUS 该映射是匿名映射,不和文件关联
MAP_GROWSDOWN 对应VM_GROWSDOWN
MAP_DENYWRITE 对应VM_DENYWRITE
MAP_EXECUTABLE 对应VM_EXECUTABLE
MAP_LOCKED 对应VM_LOCKED
MAP_NORESERVE 不需要为映射保留空间
MAP_POPULATE 填充页表
MAP_NONBLOCK 在I/O操作上不阻塞 

在用户空间可以调用mmap系统调用,间接使用do_mmap()函数,该系统调用如下:

C
1
2
3
4
5
6
7
8
9
10
11
/**
* mmap的第二个版本,原始版本的mmap()调用由POSIX定义,仍然在C库中作为mmap()方法使用,
* 但在内核已经没有对应实现
*/
void * mmap2(void *start,
    size_t length,
    int prot,
    int flags,
    int fd,
    off_t pgoff
);
删除地址区间:munmap()/do_munmap()

do_munmap() 函数用于从特定进程地址空间中删除指定的地址区间:

linux/mm.h
C
1
2
3
4
5
6
7
8
9
//如果成功返回0,否则返回负数作为错误码
int do_munmap(
    //内存描述符,指明地址空间
    struct mm_struct *mm,
    //被删除区间的起始地址
    unsigned long start,
    //被删除区间的长度
    size_t len
);

相应的,系统调用  munmap() 允许进程从自身地址空间删除指定区间:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
//声明
int munmap(void *start, size_t length);
//对应实现,对do_munmap()的简单包装
asmlinkage long sys_munmap( unsigned long addr, size_t len )
{
    int ret;
    struct mm_struct *mm;
    mm = current->mm;
    down_write( &mm->mmap_sem );
    ret = do_munmap( mm, addr, len );
    up_write( &mm->mmap_sem );
    return ret;
}
页表

应用程序操作的是虚拟地址,而CPU直接操作的是物理地址,虚拟地址到物理地址的转换通过页表机制完成。Linux使用三级页表完成地址转换(包括不支持三级页表的体系结构,例如仅支持两级页表或者散列表的体系结构),多级页表可以节约内存空间。在大部分体系结构上,页表的查找和处理是由硬件完成,但是作为前提,内核必须正确的设置页表。页表的基本原理是:将虚拟地址分段(chunk),每段虚拟地址作为索引指向页表(table),而页表项指向下一级页表或者物理页:

  1. 顶级页表称为“页全局目录”(PGD),它是一个pgd_t类型(大部分体系结构上是unsigned long)的数组,该数组的条目指向二级页表的条目
  2. 二级页表称为“页中间目录(PMD)”,它是pmd_t类型的数组,该数组的条目又指向三级页表中的条目
  3. 三级页表就叫做页表,它是页表条目pte_t的数组,页表条目指向物理页

从虚拟地址转换为物理地址的过程如下图所示:

page-table

每个进程都有自己的页表(线程会共享页表),内存描述符的 pgd 字段就指向进程的页全局目录,操作页表时必须持有 page_table_lock 锁。页表对应的结构体依赖于体系结构,定义在相应的 asm/page.h 

TLB

由于每次对虚拟内存中页面的访问都用到页表,因此其搜索性能非常关键。为提升性能,很多体系结构都实现了翻译后备缓冲(Translation lookaside buffer,TLB,也叫快表),TLB是将虚拟地址映射到物理地址的硬件缓存。有了TLB后,处理器都会优先检查TLB,如果缓存命中,则不去搜索页表。

TLB保存了最高频被访问的页表项。

页面缓存和页回写

物理内存的一大优势就是可以作为磁盘或者其它块设备的高速缓存,这是因为磁盘非常慢,常常成为系统的性能瓶颈。使用内存作为缓存后,可以延迟写磁盘的时间,或者避免不必要的读磁盘操作。

Linux内核实现了一种磁盘数据的缓存:页缓存(Page cache),该缓存把磁盘数据存放在物理内存中,以最小化磁盘I/O。为保证数据一致性,内核必须把页缓存中发生变更的数据同步到磁盘上,此过程称为页回写(Writeback)。

页缓存是现代OS不可或缺的组件,因为:

  1. 磁盘访问速度比内存差几个数量级,缓存可以显著提高性能
  2. 某个数据被访问后,该数据或者临近的数据在一定时间内很可能被密集的重复访问,这就是所谓的时间局部性(Temporal locality)。时间局部性意味着页缓存常常具有很高的命中率
缓存手段

页缓存由内存中的物理页组成,其中存放着对应的磁盘物理块,其数据被缓存的磁盘称为后备存储(Backing store)。页缓存的大小是动态变化的,它可能增长以消耗所有空闲内存,或者收缩以减轻内存压力。

当内核开始一个读操作(例如read系统调用)时,会首先检查数据是否存在于页缓存中,如果存在,则直接返回,否则内核需要调度I/O操作,从磁盘读取数据。

相应的,当内核执行写操作时,可能有三种方式和页缓存交互:

  1. 不缓存:即页缓存不去缓存任何写操作,写操作跳过缓存直接写入磁盘,同时让缓存中对应的数据失效
  2. 自动更新缓存:写操作在把数据写入磁盘的同时,更新内存缓存,这种方式称为“Write-through cache”
  3. 回写:这是Linux使用的方式。程序的写操作仅仅写入到缓存中,不更新磁盘。更新操作由回写进程在合适的时候刷出脏页到磁盘
缓存清除

当需要:

  1. 收缩缓存提供内存给其它程序使用时
  2. 将缓存中不重要的数据移除,腾出空间给重要数据缓存时

需要进行缓存回收。Linux的缓存回收策略是:替换非脏页,如果没有足够非脏页,则强制发起回写操作。决定哪些页被替换是最困难的部分,以下算法较为常见:

  1. 最近最少使用(LRU)算法:该算法跟踪每个页面的访问踪迹,以便回收时间戳最老的页面。该策略工作良好是基于这样的假设:如果缓存的数据越久没有被访问,则不太可能在近期被访问。LRU算法对那些仅仅被访问一次(最近访问一次,以后永远不会访问)的文件来说尤其失败
  2. 双链策略(Two-List Strategy):为解决LRU算法的缺陷,Linux对其进行改进,它使用两个列表(而不是LRU那样的单列表):活动列表、非活动列表。活动列表中的页是“热”的,不会被清除;非活动列表中的页则可以被清除。仅当处于非活动列表中的页被访问后,该页才会进入活动列表。两个列表都使用伪LRU(pseudo-LRU)方式管理:条目从尾部加入、头部移除(类似队列)。两个列表的大小保持动态平衡——如果活动列表过大,那么其列表头被移回非活动列表的尾部。双联策略解决了LRU中“仅一次”问题
  3. 双链策略可以泛化为多链策略
Linux页缓存

尽管System V引入的页缓存机制是专用来缓存文件系统数据的,但是Linux的目标是支持任何基于页的数据,Linux的页缓存内容可以来自普通文件系统文件、块设备文件、内存映射文件,等等。注意页缓存不一定缓存整个文件,可能缓存文件中的几个页。

缓存中的一页可以包含多个不连续的物理磁盘块(以x86为例,页大小4KB,而磁盘块一般512B,一个页可以存放8个块,此外文件本身很可能是分散存放在磁盘上的,因此这些块可能不连续),因此,检查某个页中是否包含特定的数据比较困难(否则的话,每个页只需要使用设备名称+块序号即可索引)。

address_space对象

为了避免和文件系统耦合,实现更加通用的页缓存,Linux专门设计了一个新结构管理页缓存中的条目: address_space ,可以认为该对象是虚拟内存区域 vm_area_struct 的物理对应物。打个比方,假设一个文件有10个虚拟内存区域(5个进程分别映射其2次),但是该文件只会对应一个address_space对象。

address_space这个命名具有误导性,可能叫page_cache_entity/physical_pages_of_a_file更加合理。该结构的定义如下:

linux/fs.h
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct address_space
{
    //此映射(当前页缓存条目)关联某种内核对象,一般就是inode,如果不是关联到inode(例如关联到swapper)则该字段为空
    struct inode *host; /* 拥有此映射的inode */
    struct radix_tree_root page_tree; /* 所有页面组成的基数树 */
    spinlock_t tree_lock; /* page_tree的锁 */
    unsigned int i_mmap_writable; /* VM_SHARED计数 */
    /**
     * 优先搜索树,包含此映射中全部共享、私有的映射页面
     * 该树很好的结合了堆和基数树,可以允许内核快速的找到与目标文件关联的映射
     */
    struct prio_tree_root i_mmap;
    struct list_head i_mmap_nonlinear; /* VM_NONLINEAR 链表 */
    spinlock_t i_mmap_lock; /* i_mmap的锁 */
    atomic_t truncate_count; /* 截断计数 */
    unsigned long nrpages; /* 此条目包含的页总数 */
    pgoff_t writeback_index; /* 回写的起始偏移量 */
    struct address_space_operations *a_ops; /* 操作列表 */
    unsigned long flags; /* gfp_mask和错误标记 */
    struct backing_dev_info *backing_dev_info; /* 预读信息 */
    spinlock_t private_lock; /* 私有锁 */
    struct list_head private_list; /* 私有链表 */
    struct address_space *assoc_mapping; /* 相关的缓冲 */
};
address_space操作

address_space对象的 a_ops 字段定义操控页缓存映射的操作:

/include/linux/fs.h
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 定义了管理页缓存的各种行为,例如读取、更新缓存数据
*/
struct address_space_operations
{
    int (*writepage)( struct page *, struct writeback_control * );
    int (*readpage)( struct file *, struct page * );
    int (*sync_page)( struct page * );
    int (*writepages)( struct address_space *, struct writeback_control * );
    int (*set_page_dirty)( struct page * );
    int (*readpages)( struct file *, struct address_space *, struct list_head *, unsigned );
    int (*write_begin)( struct file *, struct address_space *mapping, loff_t pos, unsigned len, unsigned flags,
        struct page **pagep, void **fsdata );
    int (*write_end)( struct file *, struct address_space *mapping, loff_t pos, unsigned len, unsigned copied,
        struct page *page, void *fsdata );
    sector_t (*bmap)( struct address_space *, sector_t );
    int (*invalidatepage)( struct page *, unsigned long );
    int (*releasepage)( struct page *, int );
    int (*direct_IO)( int, struct kiocb *, const struct iovec *, loff_t, unsigned long );
    int (*get_xip_mem)( struct address_space *, pgoff_t, int, void **, unsigned long * );
    int (*migratepage)( struct address_space *, struct page *, struct page * );
    int (*launder_page)( struct page * );
    int (*is_partially_uptodate)( struct page *, read_descriptor_t *, unsigned long );
    int (*error_remove_page)( struct address_space *, struct page * );
};

每个后备存储都实现了自己的 address_space_operations ,例如ext3文件系统的实现如下:

/fs/ext3/inode.c
C
1
2
3
4
5
static const struct address_space_operations ext3_ordered_aops = {
    .readpage        = ext3_readpage,
    .readpages        = ext3_readpages,
    //...
};

这些操作中, readpage()  和 writepage() 最重要,分别对应了页的读写操作。

页读操作的步骤如下:

  1. 内核尝试在页缓存中找到需要的数据:
    1
    struct page *find_get_page(struct address_space *mapping, pgoff_t offset);

    参数mapping指定address_space,参数offset则指页偏移(即被缓存的文件偏移)

  2. 如果搜索的页没有存在页缓存中,上面的函数返回NULL,内核将分配一个新页面,并将之前搜索的页加入页缓存:
    C
    1
    2
    3
    4
    5
    6
    7
    8
    struct page *page;
    int error;
    /* 分配页 */
    page = page_cache_alloc_cold( mapping );
    if (!page){/* 内存分配失败 */}
    /* ... 把新页加到页缓存 */
    error = add_to_page_cache_lru(page, mapping, index, GFP_KERNEL);
    if (error) {/*加入页缓存失败 */}
  3. 然后,所需的数据从磁盘读入,加入页缓存,并返回给用户:
    C
    1
    error = mapping->a_ops->readpage(file, page);

页写操作不太一样,对于文件映射,当页被修改时,仅仅需要设置脏标记: SetPageDirty(page); 。内核会在稍后通过 writepage() 刷出页面修改到磁盘,具体步骤较为复杂,概括起来包括以下步骤:

  1. 搜索页缓存,找到需要的页。如果目标页不在缓存中,则分配一个新的空闲页
    C
    1
    page = __grab_cache_page(mapping, index, &cached_page, &lru_pvec);
  2. 创建一个写请求
    C
    1
    status = a_ops->prepare_write(file, page, offset, offset+bytes);
  3. 将数据从用户空间拷贝到内核空间
    C
    1
    page_fault = filemap_copy_from_user(page, offset, buf, bytes);
  4. 将数据写出到磁盘
    C
    1
    status = a_ops->commit_write(file, page, offset, offset+bytes); 

所有的页I/O操作都需要执行上面的步骤,因此所有页I/O必然通过页缓存进行。内核总是尝试先通过页缓存来满足读请求;对于写操作,页缓存更像是一个存储平台,所有要写出的页都加入到页缓存。

基数(Radix)树

由于内核每次进行页I/O操作都需要检查目标页是否在缓存中存在,因此缓存检索必须足够快。如前面所见,搜索是通过一个address_space和offset进行的。每个address_space都包含一个唯一的radix树,对应字段page_tree。Radix树是二叉树的变体,通过它可以快速的查找需要的页(只需要提供文件偏移量), find_get_page() 、 radix_tree_lookup() 等函数都是通过检索Radix树来工作的。

Radix树的代码位于 lib/radix-tree.c ,使用该树需要包含头文件 linux/radix-tree.h 

旧的页散列表机制

在2.6-的内核中,页缓存通过一个全局散列表而不是Radix树进行检索,该散列表维护系统中的所有页,此散列表的值是散列计算结果相同的缓存条目构成的双向链表。全局散列表有以下缺陷:

  1. 一个全局锁保护此散列表,锁争用导致性能问题
  2. 散列表过大,只有映射当前正在操作的文件的页才相关
  3. 散列查找失败后的性能较差
缓冲区缓存(Buffer Cache)

通过块I/O缓冲,单个的磁盘块也被存入到页缓存中,块I/O缓冲是单个磁盘块的内存映射,是内存页-磁盘块映射的描述符。我们把这一映射称为缓冲缓存(Buffer cache),它作为页缓存的一部分实现。

历史上缓存缓存、页缓存是两个完全不同的缓存,一个磁盘块可能同时出现在这两个缓存中,在2.4它们被合并了,从而避免了两个缓存之间的同步开销和内存浪费。尽管如此,作为块内存映射描述符的buffer_head仍然被内核使用。

Flusher线程

由于页缓存的存在,写操作实际上会被延迟,当页缓存中的数据比后备存储中新时,我们称其为脏数据。内存中积累的脏页必须最终被写回到磁盘。在以下三种情况下,回写发生:

  1. 当空闲内存低于指定阈值时,内核必须回写脏页以释放内存——因为只有干净页才能被回收
  2. 当脏页驻留内存时间超过指定的阈值时,内核将超时的脏页写回磁盘
  3. 当用户进程调用 sync() 、 fsync() 系统调用时,内核按要求执行回写

这三项操作的目的完全不同,在旧内核中它们是由两个独立内核线程分别完成的。在2.6则是由一组内核线程——Flusher线程执行所有这三项操作。

第一项操作的目的是物理内存不足时,释放脏页以获得内存,何时启动该操作由内核参数(sysctl):dirty_background_ratio指定。当空闲内存占比小于此值时,内核调用 wakeup_flusher_threads() 唤醒一个或者多个Flusher,Flusher会调用 bdi_writeback_all () 并开始回写脏页。该函数接受一个参数,用于指定需要回写的页数量,该函数会一直运行,直到满足条件(或者没有脏页):

  1. 指定的最小回写页数量到达
  2. 空闲内存占比大于dirty_background_ratio

对于第二项操作,Flusher会定期唤醒,检查过期的脏页并回写。在系统启动后,定时器被设置,定期唤醒Flusher并执行 wb_writeback() 函数,该函数会把所有变脏超过dirty_expire_interval毫秒的页写出到磁盘。

Flusher线程的代码存放在 mm/page-writeback.c 和 mm/backing-dev.c ,回写相关逻辑位于 fs/fs-writeback.c 

笔记本模式

笔记本模式(Laptop mode)是一种特殊的页回写策略,其目的是最小化硬盘转动的机械行为,允许尽可能长的磁盘停滞,也延迟电池续航。该模式可以通过 /proc/sys/vm/laptop_mode 配置。和传统页回写行为相比,笔记本模式会增加额外判断,以避免主动激活磁盘运行。

多数Linux发行版在计算机接上/拔掉电池时,自动开启/关闭笔记本模式。

Flusher为什么要多线程

如果仅仅使用一个Flusher线程,那么很可能在回写任务繁重时出现阻塞。线程可能阻塞在单个繁忙的设备队列上(队列由等待提交到磁盘的I/O请求构成),导致其它设备的请求队列不能得到即时处理。为避免单个设备队列的拥塞影响整体性能,Flusher使用多线程模式,并且让每个设备对应一个Flusher线程。

← Linux内核学习笔记(二)
Linux内核学习笔记(四) →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • Linux内核编程知识集锦
  • Linux内核学习笔记(二)
  • Linux内核学习笔记(一)
  • Linux内核学习笔记(四)
  • Linux内核学习笔记(五)

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • Bazel学习笔记 37 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2