135. Процесс: запуск, окружение, память, лимиты, завершение
Прошивка на микроконтроллере — одна программа, которая владеет всем. На компьютере с Linux программа живёт внутри процесса: ядро выдаёт ей память, время процессора и файлы, следит за пределами и убирает за ней после выхода. Эта часть справочника — о том, как программа на C живёт под ядром: процессы, потоки, обмен между ними, библиотеки, ядро изнутри. Начало — с самого процесса: что он получает при запуске, где лежат его данные и что происходит, когда он завершается.
Все числа — из прогонов в контейнере stm-sys:1 (Debian 12, gcc 12,
ядро 6.6): программа _verify/sys135/proc_layout.c печатает адреса своих
же сегментов, считает страничные промахи и читает собственную карту памяти.
Как это называется в документации
Оригинал Перевод process процесс: программа, запущенная ядром, со своей памятью exit status код завершения environment окружение: переменные вида ИМЯ=значениеtext, data, bss сегменты кода, инициализированных и нулевых данных heap, stack куча, стек program break граница кучи, которую двигает brkmemory mapping отображение памяти ( mmap)page fault страничный промах: обращение к странице, которой ещё нет resident set size (RSS) сколько физической памяти занято resource limit предел ресурса ( ulimit)nice value «уступчивость»: чем больше, тем ниже приоритет OOM killer убийца процессов при нехватке памяти
135.1 Завершение: три функции и код возврата
Процесс заканчивается тремя способами, и они не равнозначны:
| Вызов | Что делает |
|---|---|
return из main или exit(n) |
вызывает функции atexit, сбрасывает буферы stdio, закрывает файлы, отдаёт ядру код n |
_exit(n), _Exit(n) |
сразу к ядру: без atexit и без сброса буферов |
| сигнал | процесс убит; кода возврата нет, есть номер сигнала |
Код возврата — младшие 8 бит: оболочка показывает его как $?. Гибель
от сигнала она показывает как $? = 128 + номер: SIGKILL — 137, SIGSEGV — 139.
EXIT_SUCCESS — 0, EXIT_FAILURE — 1. Программа из этого раздела
возвращает первый аргумент:
./proc_layout 7 >/dev/null || echo "rc=$?"
rc=7
Разница между exit и _exit видна, когда вывод идёт не в терминал, а в
канал: stdout тогда буферизован целиком, и _exit теряет всё, что
программа успела «напечатать». Прогон ./proc_layout _exit | cat не
выводит ни одного байта, хотя обычный запуск печатает 14 строк.
Функция, зарегистрированная через atexit, тоже не вызывается. Поэтому
_exit — для потомка после неудачного exec и для обработчиков сигналов,
а в обычном пути — exit.
static void at_exit_hook(void) { puts("atexit: вызвано при exit()"); }
135.2 Что процесс получает при запуске
main принимает три аргумента: число слов командной строки, сами слова
и окружение. Третий аргумент не в стандарте C, но есть везде в Unix;
переносимо окружение читают через extern char **environ и getenv:
int main(int argc, char **argv, char **envp)
Окружение наследуется: потомок получает копию окружения родителя.
Команда env -i запускает программу с пустым окружением, и getenv("PATH")
возвращает NULL:
env -i FOO=bar ./proc_layout
argv0 ./proc_layout argc 1 envp[0] FOO=bar...
PATH через getenv: нет
Переменные, на которые опирается почти всё: PATH (где искать
программы), HOME, LANG и LC_* (язык и кодировка), TERM, PWD. Две
особые — для динамического компоновщика: LD_LIBRARY_PATH добавляет
каталоги с библиотеками, LD_PRELOAD подставляет свою библиотеку раньше
всех (раздел 143). Свои настройки программы принято класть по правилам XDG:
$HOME/.config, $HOME/.cache, $HOME/.local/share.
135.3 Память процесса: сегменты
Программа proc_layout объявляет по переменной в каждом сегменте и
печатает их адреса:
char bss_buf[1 << 20]; /* 1 МБ нулей — в .bss, в файле места не занимает */
char data_buf[1 << 10] = {1}; /* 1 КБ — в .data, лежит в файле */
const char ro_str[] = "только чтение"; /* .rodata */
Команда size считает три сегмента исполняемого файла:
text data bss dec hex filename
4110 1752 1048608 1054470 101706 proc_layout
bss — 1 048 608 байт, а файл proc_layout при этом маленький: нули не
хранятся, ядро выделяет под них страницы при запуске. Это тот же принцип,
что в прошивке (раздел 11): .data копируется из Flash, .bss обнуляется.
Адреса в запущенном процессе (один прогон; при следующем они будут другими — адреса случайны):
text main 0x5ce1f313820f
rodata ro_str 0x5ce1f3139010
data data_buf 0x5ce1f313b0a0
bss bss_buf 0x5ce1f313b4c0
heap malloc100 0x5ce1f78482a0
mmap malloc256K 0x7339f5beb010
stack local 0x7fff44c88dcc
brk sbrk(0) 0x5ce1f7869000
Порядок снизу вверх по адресам: код, константы, данные, нули, куча — все
рядом; стек — далеко вверху, растёт вниз; а 256 КБ из malloc лежат
между ними, в отдельной области. Почему — в следующем подразделе.
Карту памяти процесс может прочитать сам из /proc/self/maps:
631277405000-631277406000 r--p /v/sys135/proc_layout
631277406000-631277407000 r-xp /v/sys135/proc_layout
631277407000-631277408000 r--p /v/sys135/proc_layout
631277408000-631277409000 r--p /v/sys135/proc_layout
631277409000-63127740a000 rw-p /v/sys135/proc_layout
6312aecb6000-6312aecd7000 rw-p [heap]
7fff76efb000-7fff76f1c000 rw-p [stack]
7fff76f52000-7fff76f56000 r--p [vvar]
7fff76f56000-7fff76f58000 r-xp [vdso]
Пять отображений одного файла с разными правами: только код r-xp
исполняемый, только данные rw-p записываемые. Попытка записать в
ro_str — SIGSEGV, и это работа ядра, а не компилятора. [vdso] — кусок
кода ядра, отображённый в процесс: через него gettimeofday и
clock_gettime обходятся без системного вызова.
Карта сокращена: опыт печатает только строки своего файла, кучи, стека и ядра,
строк libc и динамического компоновщика в ней нет. Не видно и мегабайта
bss_buf: в файловое отображение rw-p помещается одна страница с data_buf,
а нули лежат в безымянном отображении rw-p сразу за ним.
135.4 Куча: brk и mmap
Куча в Linux — не один механизм, а два. malloc из glibc берёт маленькие
блоки из области за концом данных, двигая её границу системным вызовом
brk; sbrk(0) возвращает текущую границу — на прогоне она стоит сразу за
кучей. Большие запросы — по умолчанию от 128 КБ (M_MMAP_THRESHOLD) —
malloc выполняет отдельным mmap: поэтому блок 256 КБ оказался по адресу
0x7339…, далеко от кучи, а free такого блока возвращает память ядру
сразу. Маленькие блоки после free остаются у процесса: glibc сдвигает
границу brk вниз, только когда свободен конец кучи больше
M_TRIM_THRESHOLD (по умолчанию 128 КБ), а дыры в середине кучи отдаёт
лишь malloc_trim(0). В опыте 4000 блоков по 1000 байт заняли 3960 КБ
кучи; free всех, кроме последнего, не сдвинул границу ни на байт,
malloc_trim(0) вернул страницы из середины (RSS упал почти на 4 МБ,
граница — лишь на 20 КБ), а free последнего блока опустил границу
до 136 КБ. Порог mmap при этом плавающий: после free большого блока
он поднимается, и следующий такой же блок в 256 КБ пришёл уже из кучи.
Порог и поведение меняются mallopt, а mallinfo показывает статистику.
Стандартный malloc — не единственный: jemalloc, tcmalloc и mimalloc
подменяют его целиком через LD_PRELOAD ради многопоточности и меньшей
фрагментации, а dmalloc — ради поиска ошибок.
135.5 Страницы и страничные промахи
Память выдаётся страницами по 4096 байт (getconf PAGESIZE), и выдаётся
лениво: bss_buf объявлен на 1 МБ, но физической памяти под ним нет,
пока к ней не обратились. Первое обращение к каждой странице — страничный
промах: ядро находит страницу, обнуляет её и подставляет. Программа
считает промахи через getrusage до и после memset на 1 МБ:
minor faults за memset 1 МБ: 256 (страниц по 4 КБ: 256)
Ровно по промаху на страницу. Это minor — страница просто ещё не
выделена. Major промах — когда страницу надо прочитать с диска: файл
или своп. Их числа — столбцы minflt и majflt в getrusage, а RSS в
top — сколько страниц процесса сейчас физически в памяти. VIRT там же —
всё, что процесс зарезервировал, включая нетронутые нули и отображённые
файлы; пугаться VIRT в гигабайты не стоит, смотреть надо на RES.
135.6 Пределы
У процесса есть мягкий и жёсткий предел на каждый ресурс: getrlimit
читает, setrlimit меняет, мягкий можно поднять только до жёсткого.
В контейнере:
RLIMIT_STACK мягкий 8192 КБ
RLIMIT_NOFILE 1048576
Стек 8 МБ — отсюда «падает на большом массиве в main»: массив в 10 МБ
на стеке превышает предел и даёт SIGSEGV. Такой массив кладут в static
или берут из кучи. Остальные пределы:
| Предел | Что ограничивает | При превышении |
|---|---|---|
RLIMIT_CPU |
секунды процессора | SIGXCPU |
RLIMIT_FSIZE |
размер создаваемого файла | SIGXFSZ |
RLIMIT_DATA, RLIMIT_AS |
данные, всё адресное пространство | malloc возвращает NULL |
RLIMIT_STACK |
стек | SIGSEGV |
RLIMIT_CORE |
размер файла core |
0 — файл не создаётся |
RLIMIT_NOFILE |
открытые файлы | open — EMFILE |
RLIMIT_NPROC |
процессов у пользователя | fork — EAGAIN |
Из оболочки то же самое — ulimit -a; ulimit -c unlimited включает
файлы core для посмертной отладки (раздел 138).
135.7 Приоритет, время, /proc
Приоритет. nice от −20 до 19; обычный пользователь может только
повышать число, то есть уступать. Из программы — nice(incr) или
setpriority. Проверка: nice -n 10 sh -c 'cat /proc/$$/stat' показывает
в 19-м поле nice=10.
Время. times и getrusage делят время на пользовательское и
системное — сколько процесс считал сам и сколько за него работало ядро.
Команда time печатает то же самое; /usr/bin/time -v — ещё и
максимальный RSS, промахи, переключения контекста.
/proc. Всё, что ядро знает о процессе, лежит в /proc/<pid>/:
status — состояние и память, maps — карта, fd/ — открытые файлы,
limits — пределы, stat — одна строка с полями для программ.
/proc/self — ссылка на самого себя.
OOM. Когда память кончается, ядро убивает процесс с наибольшим
oom_score; строка в журнале — Out of Memory: Killed process …. Файл
/proc/<pid>/oom_score_adj от −1000 до 1000 говорит ядру, кого убивать
неохотно, а кого первым.
Отсюда неожиданность для программиста на C: malloc в Linux при нехватке
памяти почти никогда не возвращает NULL. Ядро по умолчанию обещает больше,
чем есть (vm.overcommit_memory = 0 отказывает только в явно невозможном
запросе), а страницы выдаёт при первом касании (135.5). В опыте в контейнере
с пределом 128 МБ malloc на 512 МБ вернул адрес, а процесс убили на записи
страниц — код возврата 137, то есть SIGKILL; проверка на NULL тут
не спасает. Строгий учёт — vm.overcommit_memory = 2; для одной программы —
предел RLIMIT_AS (135.6): с ним на 256 МБ malloc на 1 ГБ честно вернул
NULL.
135.8 Грабли
| Симптом | Причина |
|---|---|
| вывод программы пропал, когда её запустили через канал | _exit или сигнал: буфер stdout не сброшен — exit или fflush |
atexit не сработал |
выход через _exit или убит сигналом |
| код возврата 256 «стал нулём» | оболочка видит только младшие 8 бит |
getenv("PATH") вернул NULL |
окружение пустое — запуск из env -i, cron или службы |
SIGSEGV на большом локальном массиве |
стек 8 МБ (RLIMIT_STACK) — массив в static или malloc |
VIRT в гигабайты, а памяти хватает |
резерв, а не занято; смотреть RES |
free не вернул память системе |
дыры в середине кучи остаются у процесса; границу кучи glibc опускает, только когда свободен её конец больше 128 КБ (M_TRIM_THRESHOLD); отдать явно — malloc_trim(0) |
программа убита без причины, в dmesg — Out of Memory |
OOM killer; снизить потребление или oom_score_adj |
135.9 Итоги
exitвызываетatexitи сбрасывает буферы,_exit— нет; код возврата — 8 бит.main(argc, argv, envp); окружение наследуется,env -iего очищает.- Сегменты: text, rodata, data, bss, куча — рядом; стек — вверху;
.bss1 МБ в файле места не занимает. malloc: маленькие блоки —brk, большие (сначала от 128 КБ, порог плавает) — отдельныйmmap;freeмаленьких отдаёт память, только когда свободен конец кучи.- Страница 4096 байт выдаётся при первом обращении: 256 промахов на 1 МБ.
- Пределы
getrlimit/ulimit: стек 8 МБ;nice— только уступать; всё о процессе — в/proc/<pid>/.
Полные исходники
proc_layout.c — адреса сегментов, кучи и стека процесса и порог mmap.
/* Раздел 135: где что лежит в памяти процесса — адреса сегментов, куча, стек, порог mmap. */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <malloc.h>
#include <sys/resource.h>
char bss_buf[1 << 20]; /* 1 МБ нулей — в .bss, в файле места не занимает */
char data_buf[1 << 10] = {1}; /* 1 КБ — в .data, лежит в файле */
const char ro_str[] = "только чтение"; /* .rodata */
static void at_exit_hook(void) { puts("atexit: вызвано при exit()"); }
int main(int argc, char **argv, char **envp)
{
int local = 0;
char *small = malloc(100); /* из кучи (brk) */
char *big = malloc(256 * 1024); /* больше M_MMAP_THRESHOLD (128 К) — отдельное mmap */
printf("text main %p\n", (void *)main);
printf("rodata ro_str %p\n", (void *)ro_str);
printf("data data_buf %p\n", (void *)data_buf);
printf("bss bss_buf %p\n", (void *)bss_buf);
printf("heap malloc100 %p\n", (void *)small);
printf("mmap malloc256K %p\n", (void *)big);
printf("stack local %p\n", (void *)&local);
printf("brk sbrk(0) %p\n", sbrk(0));
printf("argv0 %s argc %d envp[0] %.20s...\n", argv[0], argc, envp[0]);
printf("PATH через getenv: %s\n", getenv("PATH") ? "есть" : "нет");
struct rlimit rl;
getrlimit(RLIMIT_STACK, &rl);
printf("RLIMIT_STACK мягкий %ld КБ\n", (long)(rl.rlim_cur / 1024));
getrlimit(RLIMIT_NOFILE, &rl);
printf("RLIMIT_NOFILE %ld\n", (long)rl.rlim_cur);
atexit(at_exit_hook);
/* page faults: тронуть 1 МБ bss — каждая страница даёт minor fault */
struct rusage ru0, ru1;
getrusage(RUSAGE_SELF, &ru0);
memset(bss_buf, 1, sizeof bss_buf);
getrusage(RUSAGE_SELF, &ru1);
printf("minor faults за memset 1 МБ: %ld (страниц по 4 КБ: %d)\n",
ru1.ru_minflt - ru0.ru_minflt, (int)(sizeof bss_buf / 4096));
if (argc > 1 && strcmp(argv[1], "maps") == 0) { /* карта памяти процесса от ядра */
FILE *f = fopen("/proc/self/maps", "r"); char line[256];
while (fgets(line, sizeof line, f))
if (strstr(line, "proc_layout") || strstr(line, "[heap]") || strstr(line, "[stack]") || strstr(line, "[v"))
printf("maps %s", line);
fclose(f);
}
free(big); free(small);
if (argc > 1 && strcmp(argv[1], "_exit") == 0) _exit(3); /* без atexit и без сброса буферов */
return argc > 1 ? atoi(argv[1]) : 0;
}
Дальше: раздел 136 — fork, exec, wait и сигналы.