[TOC]

mm/util.c

init_user_buckets: 初始化memdup_user的专用内存桶

此代码片段的作用是在内核启动的早期阶段, 创建一个专用的、高性能的内存分配器(一个kmem_buckets实例), 并将其赋给全局指针user_buckets。这个专用的分配器被命名为"memdup_user", 意味着它被内核中的memdup_user()函数和相关函数独占使用, 目的是优化从用户空间复制数据到内核空间时的小块内存分配性能

工作原理:

memdup_user()是一个非常常见的内核函数, 用于安全地从用户空间拷贝一块数据到内核新分配的内存中。这个操作在处理系统调用参数、网络数据包等场景中被频繁调用, 且请求分配的内存大小各不相同。

标准的通用内存分配器kmalloc()虽然功能强大, 但对于这种极其频繁、大小多变的小块内存请求, 其内部的查找和管理开销可能会成为性能瓶颈。

kmem_buckets (内存桶) 则是一种更轻量级、更快的专用分配器, 类似一个内存池(memory pool)

  1. 它内部预先维护了一系列”桶”(bucket), 每个桶中存放着特定大小(通常是2的幂次方)的内存块。
  2. 当请求一个特定大小的内存时, 它会非常迅速地找到能容纳该大小的最小的那个桶, 并从桶中取出一个现成的内存块返回。
  3. 这避免了kmalloc()中相对复杂的slab/slub缓存查找过程, 减少了分配延迟, 并可能因为重复使用相同大小的内存块而提高CPU缓存的命中率。

init_user_buckets函数就是用来创建这样一个专为memdup_user()优化的内存池。

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
/*
* 定义一个静态的、指向 kmem_buckets 结构体的指针 user_buckets.
* "static" 使得此指针仅在当前文件内可见.
* "__ro_after_init" 是一个重要的安全属性, 它告诉内核在初始化阶段完成后,
* 将这个指针变量所在的内存位置标记为只读(Read-Only). 这可以防止它在运行时被意外或恶意地修改.
*/
static kmem_buckets *user_buckets __ro_after_init;

/*
* 静态的初始化函数 init_user_buckets.
* "__init" 属性告诉编译器将此函数的代码放入特殊的 ".init.text" 段.
* 在内核启动过程结束后, 这个段的内存会被释放, 以节省RAM.
* @return: 总是返回 0, 表示初始化成功.
*/
static int __init init_user_buckets(void)
{
/*
* 调用 kmem_buckets_create 函数来创建一个新的内存桶实例.
* - "memdup_user": 为这个内存桶实例指定一个名字, 主要用于调试和跟踪(例如在/proc中显示).
* - 0, 0: 可能代表标志位和最小尺寸等参数, 这里使用默认值.
* - INT_MAX: 指定这个内存桶能够处理的最大分配尺寸. 超过此尺寸的请求将不由它处理.
* - NULL: 可能与NUMA节点相关, NULL表示在当前节点分配内存.
* 函数的返回值(一个指向新创建的内存桶的指针)被赋给全局的 user_buckets 指针.
*/
user_buckets = kmem_buckets_create("memdup_user", 0, 0, INT_MAX, NULL);

return 0;
}
/*
* subsys_initcall 是一个宏, 用于将一个函数注册为内核启动过程中的一个初始化调用.
* 内核启动分为多个阶段(core, postcore, arch, subsys, fs, device, late).
* subsys_initcall() 确保 init_user_buckets 函数在 "subsystem" 这个比较早的阶段被执行,
* 从而保证在后续其他子系统需要使用 memdup_user 时, 这个专用的内存池已经可用.
*/
subsys_initcall(init_user_buckets);

Linux内核内存过量使用(Overcommit)的Sysctl控制

此代码片段的作用是在Linux内核中注册一组sysctl接口, 以便系统管理员可以通过/proc/sys/vm/目录下的文件, 动态地调整内核的内存过量使用(memory overcommit)策略

内存过量使用是现代虚拟内存管理系统的一个核心特性。当一个应用程序请求内存(例如通过malloc())时, 内核并不会立即分配真实的物理RAM, 而是仅仅在进程的虚拟地址空间中做出一个”承诺”。只有当应用程序第一次写入该地址时, 内核才会通过缺页中断(page fault)分配一个物理页帧。过量使用策略允许系统”承诺”出去的虚拟内存总量可以远超实际可用的物理RAM和交换空间(swap)之和。这通常能提高内存利用率, 因为很多程序申请了大量内存但并不会完全使用。然而, 这也带来了风险: 如果所有程序都开始使用它们被承诺的内存, 系统可能会耗尽所有可用内存, 从而触发OOM (Out-Of-Memory) Killer来强制杀死进程。

这组sysctl接口就是用来让管理员根据应用负载的特点, 精确地控制这个”承诺”的风险等级的。

特殊的proc_handler函数

这些函数是当用户读/写对应的/proc/sys/vm/*文件时, 内核调用的处理程序。它们在标准的读写整数值之外增加了额外的逻辑。

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/*
* overcommit_ratio_handler: 处理对 /proc/sys/vm/overcommit_ratio 的写操作.
* 当用户设置了一个新的 overcommit_ratio 时, 它会自动将 overcommit_kbytes 设置为0,
* 因为 ratio 和 kbytes 这两个参数是互斥的, 只能有一个生效.
*/
static int overcommit_ratio_handler(const struct ctl_table *table, int write,
void *buffer, size_t *lenp, loff_t *ppos)
{
int ret;

/* 使用标准的procfs整数处理函数来读/写内核变量. */
ret = proc_dointvec(table, write, buffer, lenp, ppos);
/* 如果写入成功 (ret=0 且 write为真). */
if (ret == 0 && write)
/* 将互斥的 kbytes 参数清零. */
sysctl_overcommit_kbytes = 0;
return ret;
}

/*
* sync_overcommit_as: 一个工作队列函数, 用于强制同步所有CPU上的per-cpu计数器 vm_committed_as.
* vm_committed_as 是一个高性能的计数器, 每个CPU都有一个本地副本, 只是定期同步到全局值.
* 这个函数的作用就是立即进行一次全局同步.
*/
static void sync_overcommit_as(struct work_struct *dummy)
{
percpu_counter_sync(&vm_committed_as);
}

/*
* overcommit_policy_handler: 处理对 /proc/sys/vm/overcommit_memory 的写操作.
* 这是最复杂的处理器, 因为从一个宽松的策略切换到一个严格的策略 (OVERCOMMIT_NEVER) 是一个敏感操作.
*/
static int overcommit_policy_handler(const struct ctl_table *table, int write,
void *buffer, size_t *lenp, loff_t *ppos)
{
struct ctl_table t;
int new_policy = -1;
int ret;

/* 仅在写入时执行特殊逻辑. */
if (write) {
t = *table;
t.data = &new_policy;
/* 先用标准函数读取并验证用户输入的新策略值. */
ret = proc_dointvec_minmax(&t, write, buffer, lenp, ppos);
if (ret || new_policy == -1)
return ret;

/* 根据新策略调整per-cpu计数器的同步频率. 策略越严格, 同步越频繁. */
mm_compute_batch(new_policy);
/*
* 关键逻辑: 如果新策略是 OVERCOMMIT_NEVER (最严格的),
* 我们必须在切换策略之前, 获得一个精确的 vm_committed_as 值.
* schedule_on_each_cpu() 会在每个CPU上调度并执行 sync_overcommit_as,
* 强制所有per-cpu计数器立即同步到全局总数.
*/
if (new_policy == OVERCOMMIT_NEVER)
schedule_on_each_cpu(sync_overcommit_as);
/* 在确保计数准确后, 才正式切换策略. */
sysctl_overcommit_memory = new_policy;
} else {
/* 如果是读操作, 直接使用标准函数. */
ret = proc_dointvec_minmax(table, write, buffer, lenp, ppos);
}

return ret;
}

/*
* overcommit_kbytes_handler: 处理对 /proc/sys/vm/overcommit_kbytes 的写操作.
* 与 ratio_handler 类似, 当用户设置了一个新的 kbytes 值时, 它会自动将 ratio 设置为0.
*/
static int overcommit_kbytes_handler(const struct ctl_table *table, int write,
void *buffer, size_t *lenp, loff_t *ppos)
{
int ret;

ret = proc_doulongvec_minmax(table, write, buffer, lenp, ppos);
if (ret == 0 && write)
sysctl_overcommit_ratio = 0;
return ret;
}

sysctl 表定义与注册

这部分代码定义了将在/proc/sys/vm/下创建哪些文件, 以及它们如何与内核变量和处理函数关联。

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
66
#define OVERCOMMIT_GUESS		0
#define OVERCOMMIT_ALWAYS 1
#define OVERCOMMIT_NEVER 2

int sysctl_overcommit_memory __read_mostly = OVERCOMMIT_GUESS;
static int sysctl_overcommit_ratio __read_mostly = 50;
static unsigned long sysctl_overcommit_kbytes __read_mostly;
int sysctl_max_map_count __read_mostly = DEFAULT_MAX_MAP_COUNT;
unsigned long sysctl_user_reserve_kbytes __read_mostly = 1UL << 17; /* 128MB */
unsigned long sysctl_admin_reserve_kbytes __read_mostly = 1UL << 13; /* 8MB */

/*
* 定义一个 sysctl 表, 它是 struct ctl_table 结构体的数组.
*/
static const struct ctl_table util_sysctl_table[] = {
{
.procname = "overcommit_memory", /* 文件名 */
.data = &sysctl_overcommit_memory, /* 关联的内核变量 */
.maxlen = sizeof(sysctl_overcommit_memory), /* 变量大小 */
.mode = 0644, /* 文件权限 */
.proc_handler = overcommit_policy_handler, /* 指定专用的处理函数 */
.extra1 = SYSCTL_ZERO, /* 用于proc_dointvec_minmax的最小值(0) */
.extra2 = SYSCTL_TWO, /* 用于proc_dointvec_minmax的最大值(2) */
},
{
.procname = "overcommit_ratio",
.data = &sysctl_overcommit_ratio,
.maxlen = sizeof(sysctl_overcommit_ratio),
.mode = 0644,
.proc_handler = overcommit_ratio_handler,
},
{
.procname = "overcommit_kbytes",
.data = &sysctl_overcommit_kbytes,
.maxlen = sizeof(sysctl_overcommit_kbytes),
.mode = 0644,
.proc_handler = overcommit_kbytes_handler,
},
{
.procname = "user_reserve_kbytes", /* 为非root用户保留的内存量 */
.data = &sysctl_user_reserve_kbytes,
.maxlen = sizeof(sysctl_user_reserve_kbytes),
.mode = 0644,
.proc_handler = proc_doulongvec_minmax, /* 使用标准的无符号长整型处理函数 */
},
{
.procname = "admin_reserve_kbytes", /* 为root用户保留的内存量 */
.data = &sysctl_admin_reserve_kbytes,
.maxlen = sizeof(sysctl_admin_reserve_kbytes),
.mode = 0644,
.proc_handler = proc_doulongvec_minmax,
},
};

/*
* 初始化函数, 用于注册上述的 sysctl 表.
*/
static int __init init_vm_util_sysctls(void)
{
/* 调用 register_sysctl_init, 将 util_sysctl_table 注册到 "vm" 子目录下. */
register_sysctl_init("vm", util_sysctl_table);
return 0;
}
/* 使用 subsys_initcall 确保此注册函数在内核启动的 subsystem 阶段被调用. */
subsys_initcall(init_vm_util_sysctls);
#endif /* CONFIG_SYSCTL */