In Linux virtual memory (VM), there are two orthogonal attributes attached to a particular address range or memory region:

  1. Accessibility – Whether the memory is accessible (readable/writable), corresponding to PROT_READ, PROT_WRITE, etc.
  2. Resident Status – Whether the memory is paged in (i.e., faulted in) and backed by physical memory.

These two attributes combine to form four possible states:

§1. Inaccessible & Non-Resident

This is the initial state — memory is reserved so that the address range cannot be used by other components of the current process. There is no physical memory backing it yet.

1
mmap(..., PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, ...)

This makes the region inaccessible and non-resident — a virtual reservation without committing physical resources.

§2. Accessible & Non-Resident

This is a transient state. The memory is accessible but not yet backed by physical memory. It will be lazily paged in on first write.

One can enter this state either in two steps:

1
2
mmap(..., PROT_NONE, ...);
mprotect(..., PROT_READ | PROT_WRITE);

Or in one go:

1
mmap(..., PROT_READ | PROT_WRITE, ...);

This lazy allocation allows Linux to economize on physical memory. Many programs request large amounts of memory but never use most of it — like travel insurance that often goes unused.

Of course, for this lazy scheme to work, the OS must be able to provide the physical memory when the region is accessed. In other words, the OS has promised to supply this memory on demand, which is why this state is often referred to as committed. The Committed_AS entry in /proc/meminfo reflects this commitment.

§3. Accessible & Resident

This is the final and fully usable state — memory is accessible and backed by physical memory, allowing access with no page faults.

1
2
mmap(..., PROT_READ | PROT_WRITE, ...);
madvise(..., MADV_POPULATE_WRITE);

The MADV_POPULATE_WRITE hint causes the kernel to populate pages immediately, ensuring they’re resident.

§4. Inaccessible & Resident

This is an unusual state. Memory is backed by physical pages but cannot be accessed due to its protection settings.

Normally, if memory is inaccessible, there is no reason for it to remain resident. So this state is almost always transient, where the memory is expected to become accessible soon.

One can create this state by first writing to the memory (to make it resident and dirty), then removing access permissions.

1
2
3
mmap(..., PROT_READ | PROT_WRITE, ...);
memset(..., 0, ...); // to make mem dirty so that they must be preserved
mprotect(..., PROT_NONE);

§Summary

The typical lifecycle of memory goes:

1 (reserved) → 2 (committed) → 3 (resident)

For example, heap memory in the JVM typically progresses from:

  • Reserved → address space claimed
  • Committed → memory is promised to be accessible
  • Pretouched → memory faulted in proactively

The following are some code to study this lifecycle.

§Normal Page

First, we allocate a few pages; since, by default, lazy mapping is used, none of them are resident in physical memory. A page will be backed by physical memory on the first write into that page.

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include <cassert>
#include <cstdio>
#include <cstdlib>

#include <unistd.h>
#include <sys/mman.h>

static void meminfo() {
system("cat /proc/meminfo | grep Committed_AS");
puts("");
}
int main(void) {
// to flush meminfo output
setbuf(stdout, NULL);
constexpr int num_pages = 10;

unsigned char vec[num_pages];
int res;
const size_t PS = sysconf(_SC_PAGESIZE);

puts("Init state");
meminfo();

void *addr = mmap(nullptr, num_pages * PS, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

if (addr == MAP_FAILED) {
perror("mmap");
exit(1);
}

printf("Reserved %d 4K-pages\n", num_pages);
meminfo();

puts("Commit those pages");
if (mprotect(addr, num_pages * PS, PROT_READ|PROT_WRITE) != 0) {
perror("mprotect");
return 1;
}
meminfo();

puts("Still non-resident");
res = mincore(addr, num_pages * PS, vec);
assert(res == 0);

for (int i = 0; i < num_pages; ++i) {
printf("%d", (vec[i] & 1));
}
puts("\n");

puts("Write to the the first 5 pages");
for (int i = 0; i < 5; ++i)
((char *)addr)[i * PS] = 1;

res = mincore(addr, num_pages * PS, vec);
assert(res == 0);

for (int i = 0; i < num_pages; ++i) {
assert((vec[i] & 1) == (i < 5));
printf("%d", (vec[i] & 1));
}
puts("\n");

puts("Write to the rest of pages");
for (int i = 5; i < num_pages; ++i)
((char *)addr)[i * PS] = 1;

res = mincore(addr, num_pages * PS, vec);
assert(res == 0);
for (int i = 0; i < num_pages; ++i)
printf("%d", (vec[i] & 1));
puts("\n");

puts("After MADV_DONTNEED; non-resident");
madvise(addr, num_pages * PS, MADV_DONTNEED);

res = mincore(addr, num_pages * PS, vec);
assert(res == 0);

for (int i = 0; i < num_pages; ++i)
printf("%d", (vec[i] & 1));
puts("\n");

puts("Still committed");
meminfo();

puts("Uncommiting");
void* new_addr = mmap(addr, num_pages * PS, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
assert(new_addr == addr);

meminfo();
res = mincore(addr, num_pages * PS, vec);
assert(res == 0);
for (int i = 0; i < num_pages; ++i)
printf("%d", (vec[i] & 1));

return 0;
}

Output on my box:

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
Init state
Committed_AS: 29309384 kB

Reserved 10 4K-pages
Committed_AS: 29309384 kB

Commit those pages
Committed_AS: 29309424 kB

Still non-resident
0000000000

Write to the the first 5 pages
1111100000

Write to the rest of pages
1111111111

After MADV_DONTNEED; non-resident
0000000000

Still committed
Committed_AS: 29309424 kB

Uncommiting
Committed_AS: 29309384 kB

0000000000

§Large Page

One needs to pre-allocate at least one 2M page.

1
2
3
4
5
$ echo 1 > "/sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages"
$ hugeadm --pool-list
Size Minimum Current Maximum Default
2097152 1 1 1 *
1073741824 0 0 0

The story is the same that physical mem is not used until the first access.

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
#include <cassert>
#include <cstdio>
#include <cstdlib>

#include <unistd.h>
#include <sys/mman.h>

int main(int argc, char **argv)
{
int res;
size_t ps_2m = (1 << 21);
size_t PS = (size_t)sysconf(_SC_PAGESIZE);

unsigned char vec[20];

void *addr;

// Reserve one huge-page -- HugePages_Rsvd in /proc/meminfo is incremented.
// By omitting MAP_NORESERVE, we get the guarantee that a successful mmap entails successful writes to the memory.
// Otherwise, even though the mmap call succeeds, we may get a crash on writing the memory because no huge-page is
// available.
addr = mmap(nullptr, ps_2m * 1,
PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0);
// MAP_NORESERVE|MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0);

if (addr == MAP_FAILED) {
perror("mmap");
exit(1);
}

// huge-page mem has been pre allocated but not tied to our reserved mem yet
res = mincore(addr, 10 * PS, vec);
assert(res == 0);

puts("Reserved but no physical mem is used");
for (int i = 0; i < 10; ++i) {
printf("%d", (vec[i] & 1));
}

puts("\n");

// write to the first byte
puts("Write to the first 1 byte");
((char *)addr)[0] = 1;

// mapping established; in-core
res = mincore(addr, 10 * PS, vec);
assert(res == 0);

for (int i = 0; i < 10; ++i) {
printf("%d", (vec[i] & 1));
}

puts("");

return 0;
}

Output on my box:

1
2
3
4
5
Reserved but no physical mem is used
0000000000

Write to the first 1 byte
1111111111

§References