-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcritmem.html
More file actions
387 lines (364 loc) · 31.6 KB
/
critmem.html
File metadata and controls
387 lines (364 loc) · 31.6 KB
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Критические ресурсы и борьба за них. Память.</title>
</head>
<body>
<p>
Среди ресурсов, необходимых для работы приложения в Unix-подобной системе,
три вида ресурса следует выделить в особую категорию критических ресурсов:
это виртуальная память, описатели открытых файлов и дескрипторы процессов.
Их количество в любой системе всегда ограничено; ни один процесс не может
обойтись без них, независимо от сложности и важности выполняемой им работы.
Другие ресурсы, которые могут быть крайне важными для отдельно взятой
конкретной задачи, тем не менее, не имеют глобального общесистемного
характера; например, к этой категории относится место на диске: при традиционно
принятой политике разделения файловых поддеревьев различного назначения
между разными разделами, не может быть общего критического для всех
дискового раздела.
</p>
<p>
Среди упомянутых трех наиболее важных критических ресурсов — память,
открытые файлы, дескрипторы процессов — мы должны поставить память на первое
место по важности. Причиной этому то, что даже если ОС предупреждает
о исчерпании критического ресурса и дает приложению возможность освободить
ресурсы в темпе, необходимом приложению, то процессы свертки могут потребовать
дополнительного выделения памяти; для процессов и файлов, это значительно
менее вероятно. Причин, по которым память может быть запрошена в процессе
свертки, как минимум две: это создание объектов исключений, в системах
программирования, таких, как C++; и это расщепление страниц памяти, которые
числились в системе общими для нескольких процессов (так называемые
copy-on-write страницы) и которым система должна создавать персональные
для процесса копии, если процесс изменяет такую страницу.
</p>
<p>
Ниже следует ряд демонстрационных тестов, показывающих поведение системы
по выдаче приложению памяти в случае типичной современной Linux-системы.
В тестах на заполнение памяти использовалась следующая программа:</p>
<pre>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int
main( int argc, char *argv[] )
{
size_t sz1;
int cnt;
int fillc;
int ii;
if( argc < 4 ) { fprintf( stderr, "usage\n" ); exit( 1 ); }
cnt = strtol( argv[1], NULL, 0 );
sz1 = strtol( argv[2], NULL, 0 );
fillc = strtol( argv[3], NULL, 0 );
for( ii = 1; ii <= cnt; ++ii ) {
char* p;
printf( "ii=%d... ", ii ); fflush( stdout );
p = malloc( sz1 );
if( !p ) { fprintf( stderr, "failed: ii=%d\n", ii ); exit( 1 ); }
if( fillc != 0 )
memset( p, fillc, sz1 );
printf( "done\n" ); fflush( stdout );
}
printf( "OK\n" );
return 0;
}
</pre>
<p>Тесты выполнялись на AltLinux Junior 1.1, с ядром 2.4.18 взятым
с kernel.org; glibc — glibc-2.2.4-alt2.junior; vm.overcommit_memory
был равен 0.</p>
<p>Функциональность ее прозрачно видна из кода: запуская как ./m N S F,
получаем N циклов по занятию памяти куском размера S, при этом заполняя
значением F. (При F==0, заполнение пропускалось, так как оно будет
игнорироваться ядром.)
</p>
<p>На тестовой системе 256M RAM и 520M свопа. При полном отсутствии
иной нагрузки и запущенных процессов, кроме минимального комплекта демонов,
двух шеллов и одного top, доступный объем составляет около 770M.
Тесты показывают, что максимальный размер области, получаемой одним
вызовом анонимного mmap, равен этому пределу; тем не менее, их количество
не ограничивается реально доступной памятью. Попытка получения за один
раз куска размером 800M провалилась:</p>
<pre>
Пример 1.
execve("./m", ["./m", "100", "800000000", "0"], [/* 30 vars */]) = 0
[...]
old_mmap(NULL, 800002048, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
old_mmap(NULL, 800002048, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
write(2, "failed: ii=1\n", 13) = 13
munmap(0x40016000, 4096) = 0
_exit(1) = ?
</pre>
<p>В противоположность ей, попытка запроса 4 кусков по 550M, что в сумме
составляет значительно больший объем — 2.2G — "успешно" состоялась:</p>
<pre>
Пример 2.
execve("./m", ["./m", "100", "550000000", "0"], [/* 30 vars */]) = 0
[...]
write(1, "ii=1... ", 8) = 8
old_mmap(NULL, 550002688, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4014d000
write(1, "done\n", 5) = 5
write(1, "ii=2... ", 8) = 8
old_mmap(NULL, 550002688, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x60dd3000
write(1, "done\n", 5) = 5
write(1, "ii=3... ", 8) = 8
old_mmap(NULL, 550002688, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x81a59000
write(1, "done\n", 5) = 5
write(1, "ii=4... ", 8) = 8
old_mmap(NULL, 550002688, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
brk(0) = 0x8049928
brk(0x28cceec0) = 0x28cceec0
brk(0x28ccf000) = 0x28ccf000
write(1, "done\n", 5) = 5
write(1, "ii=5... ", 8) = 8
old_mmap(NULL, 550002688, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
</pre>
<p>Попробуем теперь занять память кусками по 500M и тут же ее заполнить.
После первого такого заполнения, от изначальных свободных ~770M остается
примерно ~270M, и второй запрос на 500M не проходит еще до его заполнения:</p>
<pre>
Пример 3.
execve("./m", ["./m", "100", "500000000", "1"], [/* 30 vars */]) = 0
[...]
write(1, "ii=1... ", 8) = 8
old_mmap(NULL, 500002816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4014d000
(в этой точке происходит заполнение памяти. -- netch)
write(1, "done\n", 5) = 5
write(1, "ii=2... ", 8) = 8
old_mmap(NULL, 500002816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
brk(0) = 0x8049928
brk(0x25d1fe40) = 0x8049928
old_mmap(NULL, 2097152, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x5de24000
munmap(0x5de24000, 901120) = 0
munmap(0x5e000000, 147456) = 0
old_mmap(0x5df00000, 32768, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x5df00000
old_mmap(NULL, 500002816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
old_mmap(NULL, 500002816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
write(2, "failed: ii=2\n", 13) = 13
munmap(0x40016000, 4096) = 0
_exit(1) = ?
</pre>
<p>В примере 2, запрос на получение значительно большего объема памяти,
чем есть в системе во всех ресурсах, включая своп, был декларирован системой
(ядро + libc) как успешно выполненный. Неприятности начались бы в тот момент,
когда мы бы захотели использовать полученную таким образом память.
Пример такой неприятности:</p>
<pre>
Пример 4.
execve("./m", ["./m", "100", "780000000", "1"], [/* 30 vars */]) = 0
[...]
write(1, "ii=1... ", 8) = 8
old_mmap(NULL, 780001280, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4014d000
--- SIGTERM (Terminated) ---
+++ killed by SIGTERM +++
</pre>
<p>Здесь, память была декларирована как успешно выделенная, но попытка
ее использования привела к исчерпанию памяти всей системы и, вследствие этого,
получения SIGTERM от компонента ядра, называемого OOM killer и занимающегося
аварийными мерами по освобождению памяти в случае ее глобальной нехватки.
Процессу не было дано какого-то предупредительного сигнала о проблемах с
памятью в системе; не было дано
возможности аккуратно "свернуться", потратив на это, возможно, еще памяти
на короткий период времени; вместо этого, был сразу выдан SIGTERM;
алгоритм работы OOM killer'а содержит выдачу SIGKILL в том случае, если
менее жесткие меры не привели к успеху.</p>
<p>(Заметим, что к такому результату — вышибание процесса
SIGTERM'ом — привела только часть тестов. Другие запуски этого теста с
идентичными или очень близкими параметрами привели к полной блокировке
системы, за исключением bottom half компонент (отработка аппаратных
прерываний). По Alt-SysRq-E, однако,
функционирование системы было восстановлено, ценой отстрела всех процессов,
включая системные (кроме init'а).)</p>
<p>Ряд источников содержит рекомендации использовать для предотвращения
подобных ситуаций системные rlimit'ы (см. описание функций setrlimit,
getrlimit). Мы намеренно игнорировали подобную возможность для проблемы
исчерпания памяти, потому, что устанавливаемые через setrlimit() пределы
действуют только на один процесс; если количество процессов для
пользователя не будет ограничено, то этот предел никак не защитит от
исчерпания памяти; если же предел количества процессов будет установлен
так, что память будет гарантирована от исчерпания, то ресурсы будут
использоваться крайне неэффективно. Это следует из того, что большинство
процессов требуют для своей работы до двух мегабайт реально занятой
виртуальной памяти, а прожорливые процессы встречаются крайне редко и,
как правило, их возникновение вызвано ошибками программирования или
администрирования, а не реальной необходимостью.</p>
<p>Еще более сложный момент, играющий роль даже при честном commit'е, —
выделение памяти под стек. Сейчас в linux невозможность выделить страницу
памяти под стек процесса приводит к немедленной гибели процесса —
потому что нет возможности даже вернуть ему управление с сообщением об ошибке.
</p>
<p>Кто-то может возражать против выводов из описанного эксперимента
на том основании, что выделение памяти таким образом — особенность не ядра,
а glibc. Я не принимаю это возражение по той причине, что рассматривается
намеренно именно тот вариант исполнения, который везде ставится по умолчанию.
Никто не мешает "подрихтовать напильником" все что угодно на основании
наличия исходных текстов и компилятора, но это не может быть предметом
данного разговора.</p>
<p>Поведение, описанное выше, может не составить проблем на типичном
сервере того типа и с теми ролями, на которые изначально позиционировался
Unix и которые составляют роли типичного Internet-сервера (шлюз локальной
сети, сервер ISP). Причиной этому метод реализации сервисов, при котором
на каждый сервис на каждого клиента создается отдельный работающий с ним
процесс, а работа с данными максимально ориентирована на сохранение
данных в случае произвольных сбоев. Например, sendmail обязательно сохраняет
письмо на диске перед тем, как выдать "250 message accepted for delivery"
и до того, как начинает попытки доставки; слет между записью на диск
и отдачей по SMTP подтверждения приведет к дублированию письма, но не к
его потере. В то же время, для сервера базы данных, брокера объектов или
другого приложения, которое не может быть адекватно и эффективно представлено
в виде семейства отдельных процессов, и для которого убиение ядром
без предупреждения приводит к тяжелым последствиям (некорректное закрытие
данных и журналов транзакций, разрывы взаимодействий...), такое поведение
системы глубоко некорректно. Можно сформулировать жесткий принцип:</p>
<p>
<i>Система должна заранее, до попадания в состояние
полного исчерпания памяти, предупредить приложения о нехватке памяти;
система должна дать приложению возможность аккуратно завершить выполнение,
полностью или частично; дать возможность остановить часть выполняемых
работ и освободить тем самым память.
</i>
</p>
<p><i>На сейчас, ни одна из free unix систем не удовлетворяет этому требованию
даже частично:</i><ul>
<li>Нотификация о приближении к состоянию исчерпания памяти отсутствует.</li>
<li>Средства обеспечения гарантированного резерва памяти отсутствуют
или слабы.</li>
<li>Средства обеспечения гарантированного резерва памяти не допускают
резервирование для ситуаций неявного выделения памяти системой (без
явного запроса от приложения), таких, как создание локальной копии
страницы типа copy-on-write, страницы модификации замапленного в память файла.
</li>
</ul></p>
<p>
О ситуации с copy-on-write страницами следует добавить несколько слов особо.
В большинстве случаев, они возникают в результате системного вызова fork(),
который порождает копию вызвавшего процесса (далее называемую дочерним
процессом) с другим идентификатором
процесса (pid). Запуск другой программы в отдельном процессе производится
вызовом fork() и затем вызовом exec() в дочернем процессе.
Так как такая комбинация fork+exec используется в большинстве случаев
порождения нового процесса, то потребовалась оптимизация этого наиболее
частого случая. Вариант, исходно появившийся в BSD, зовется vfork;
при нем, копия памяти не порождается, а родительский процесс останавливается
до момента выполнения дочерним процессом вызова exec(). SysV вариант fork(),
в настоящее время перенесенный всюду, делает исходное адресное пространство
процесса, вызвавшего fork(), общим для обоих процессов, маркируя страницы
признаком copy-on-write. Тот из процессов, кто первый изменил страницу,
получает ее локальную копию. Если fork() был проделан без exec() многократно,
то общая неизмененная копия страницы может быть на три и более процесса...</p>
<p>
Аналогичным по результату механизмом, экономящим память за счет держания
одной общей копии источника, является mmap(). mmap() используется в первую
очередь, в большинстве случаев прозрачно для программиста, при загрузке
shared libraries (shared objects) — разделяемых динамических библиотек;
кроме того, программисту доступно данное средство и в явном виде.
В случае shared libraries, как правило, большинство страниц библиотеки
хранятся в неизменном виде как общие данные; страниц, модифицированных
конкретным процессом, в случае использования PIC при создании библиотеки,
меньшинство.
</p>
<p>
Эти два источника общих страниц — fork и mmap —
приводят к следующим результатам:<ol>
<li>Невозможно точно назвать объем виртуальной памяти, занятой процессом
и не относящейся к другим процессам, то есть такой, чтобы сумма этих объемов
для всех процессов была равна сумме занятой ими памяти.
Можно назвать объем, занятый локальными для данного процесса копиями,
можно назвать объем страниц, разделяемых с еще одним процессом;
можно назвать объем страниц, разделяемых с еще двумя процессами... и так далее,
но никакой однозначной цифры объема памяти, занятой процессом, назвать нельзя.
Процессы, для которых это не выполняется и точный объем однозначно известен,
крайне редки и на общее правило существенно не влияют.
</li>
<li>В большинстве случаев, невозможно точно назвать объем памяти, занятый
одним пользователем. На какого из пользователей записывать память, занятую
общими для всех страницами разделяемой библиотеки? Любое решение, не
учитывающее доли использования, будет недостаточным; решение же, учитывающее
все доли использования ("16 процессов пользователя vasya и 19 пользователя
petya"), становится чрезмерно дорогим за счет накладных расходов на этот учет.
Учет памяти, общей за счет fork, может аналогично спотыкаться в случае
смены процессом uid'ов или gid'ов без выполнения exec().
</li>
</ol>
</p>
<p>
Избавляться от преимуществ разделямых страниц не стоит: это привело бы
к резкому повышению требований к памяти. Проблема в том, как избавиться
от недостатка разделяемых страниц — неконтролируемой процессом затрате
памяти системы и вызванной этим возможности попасть на исчерпание памяти
совершенно неожиданно для себя. Есть как минимум два подхода.</p>
<p>Первый подход реализуется в глубоко BSD'шном стиле — "если есть возможность
сделать что-то просто, но удовлетворительно, или сложно, но хорошо,
то делаем просто". Из коммерческих систем, его использует, например, HP-UX.
В случае исчерпания памяти, у процесса, который не смог получить
свободную страницу памяти, производится ремаппинг части своей памяти
как mmap-отображение файла из /tmp; ядро создает такой файл и вытесняет
на него образ части памяти процесса. Механизм mmap() позволяет
отображение файла в память и свободное вытеснение из памяти неизмененных
страниц, если есть возможность восстановить их по мере необходимости.
Фактически, при этом производится расширение объема виртуальной памяти
системы за счет дискового пространства в /tmp. Существует реализация
подобного механизма для FreeBSD.
</p>
<p>
Второй вариант, оформление идеи которого и стало причиной написания данной
статьи, не реализован, насколько мне известно, нигде, и его реализация
может стать чрезвычайно проблематичной для большинства существующих VM.
Тем не менее, его следует описать, как наиболее соответствующий логике
работы VM с разделяемой памятью. Суть его — в обеспечении резерва страниц,
пригодного для использования ядром в момент разделения страниц по
copy-on-write. Ключевые моменты такого механизма:<ul>
<li>Overcommit (lazy commit) отменяется по умолчанию для всех и исполняется
только в случае явного заказа такого режима выделения (например, для
разреженных массивов).
</li>
<li>Процесс может регулировать размер буфера виртуальной памяти, определенного
как резерв ядра для его страниц. Этот буфер занимает область виртуальной
памяти, равной его размеру и не разрешенной под использование с любыми
другими целями. (Разумеется, реально этот буфер будет виден только
в виде изменений значений счетчиков страниц и размеров областей, что не
влияет на его реальность для контроля возможности операций выделения памяти.)
Только процесс
может определять, какой размер буфера ему необходим для этих целей.
Разумный размер по умолчанию — 5-10% от суммарного размера областей
виртуальной памяти процесса.</li>
<li>При необходимости занять страницу виртуальной памяти под модифицированную
копию ранее разделяемой страницы или страницы lazy commit области,
система пытается получить страницу из общесистемного
пула свободных страниц. При невозможности этого, используется страница
из описываемого здесь резервного пула. Невозможность выделить возникает при
исчерпании системного пула свободных страниц, или же при превышении
каких-либо лимитов; для здесь это имеет только то значение, что следует
переключиться на выделение памяти из резерва процесса.</li>
<li>Процесс заказывает отдачу ему (синхронного) сигнала при снижении
количества страниц в пуле резерва до некоторого граничного значения.
Процесс может по этому сигналу начать операции свёртки и освобождения
памяти.</li>
<li>Процесс может заказать создание личных копий всех разделяемых страниц
в некотором диапазоне его виртуального адресного пространства заранее,
не дожидаясь их модификации в ходе штатной работы. Это является более
аварийным, чем штатным, режимом, но вполне может запрашиваться некоторыми
приложениями. Обязательно регулирование режима такого создания личных
копий: за счет свободной памяти в системе, за счет резерва процесса, до какой
степени за счет резерва процесса.</li>
<li>Процесс может получать полные сведения о состоянии своего адресного
пространства: размеры и типы областей, количество личных и разделяемых
страниц в областях и во всем адресном пространстве. Эта функция —
вспомогательная, но необходимая для обеспечения предыдущих пунктов и для
предсказания возможных проблем в их реализации. Это касается также не только
COW страниц, но и страниц областей, выделенных в режиме lazy commit.</li>
</ul>
</p>
<p>Описанный вариант нигде не реализован и крайне сложен для реализации,
например, на BSD VM. Сейчас и здесь он имеет только теоретическое значение,
и пока что его следует рассматривать только как недостижимый идеал.</p>
<hr>
<p>$Id: critmem.html,v 1.4 2012/01/29 19:38:50 netch Exp $</p>
<p>(C) 2002 Valentin Nechayev. All rights reserved.
<br>
Разрешается полное или частичное копирование, цитирование.
При полном или частичном копировании ссылка на оригинал
обязательна.</p>
</p>
</body>
</html>