![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Процессы, задачи, потоки и нити
Виктор ХименкоНачнем с простого вопроса: сколько понятий Linux фигурирует в заголовке статьи? Как ни удивительно, разные знатоки системы вполне могут назвать в ответ разные числа в диапазоне от одного до трех. Почему до трех — ясно: в тождестве между «потоком» и «нитью» никто не сомневается, поскольку оба термина соответствуют английскому thread. Просто первый из них традиционно использовался в русскоязычной технической литературе, хотя некоторые считают его неудобным из-за многозначности, а второй — более новый и этого недостатка лишен.
Очевидно также, что все перечисленные в заголовке термины имеют отношение к параллельным вычислениям и обозначают вычислительную процедуру, выполняемую вместе с другими такими же. Но на этом ясность кончается, и по простым, казалось бы, вопросам — есть ли разница между процессом и задачей, а также между процессом и нитью (потоком) — среди «профи» единодушия нет. Причем быстро объяснить суть разногласий невозможно: сначала потребуется довольно много внимания уделить управлению процессами (задачами, потоками) в Linux.
Процесс и его атрибуты
Процессы появились в Unix вместе с самой системой, а задачи и нити — значительно позже, поэтому разумно начать с процессов. В классическом варианте каждый процесс представлял собой маленький виртуальный компьютер; процессы были полностью отделены друг от друга и непосредственно взаимодействовали лишь с ядром ОС. Общение их между собой осуществлялось только через файлы и каналы, а с аппаратурой — через файлы символьных и блочных устройств. Со временем в описанную схему пришлось вносить изменения, в результате чего структура процессов стала менее четкой и «прозрачной», но об этом чуть позже.
Список процессов, выполняющихся в данный момент в системе, позволяют увидеть программы top (рис. 1) и ps (с ней мы познакомимся ниже).
Идентификаторы процесса, сессии, группы
Каждый процесс обладает собственным адресным пространством, поэтому один процесс не может испортить данные в другом или, тем более, в ядре ОС. Кроме того, для каждого процесса ядро системы хранит весьма внушительный список различных атрибутов. Чтобы разобраться с ними, проведем несложный эксперимент (рис. 2), для которого нам понадобятся две взаимодействующие программы, работающие достаточно длительное время.
Войдем в систему и зададим команду:
dd if=/dev/random bs=1024 count=1024 | wc -c
Этим мы запустим две программы — dd и wc, причем результат работы первой будет подаваться на вход второй. Программа dd прочтет 1024 блока с псевдоустройства /dev/random, генерирующего криптографически стойкие случайные числа, а wc подсчитает число знаков в образовавшейся последовательности. Поскольку получение таких чисел без специального аппаратного обеспечения — дело долгое, мы успеем поэкспериментировать с нашим процессом. (Блоки должны иметь длину 1024 байта, но это лишь ограничение сверху: dd не ждет заполнения буфера, и в результате в каждом блоке фактически оказывается один—два байта.)
Теперь нажатием +Z приостановим выполнение программ, зададим ту же команду еще раз и посмотрим на происходящее с другого терминала, введя на нем команду ps af, которая выдает на экран список имеющихся процессов (1). Затем нажмем +C, чтобы остановить два процесса, запущенных последними, и снова взглянем на ситуацию с помощью ps (2). Теперь выйдем из системы (logout) и проделаем то же самое в третий раз (3). Мы обнаружим, что все процессы на консоли vc/1 исчезли.
Для каждого процесса команда ps показывает его состояние, или, как часто говорят, статус (Stat — Status: T — приостановлен, R — активен, S — «спит»), числовой идентификатор (PID — Process ID). С помощью отступов и знаков ?\_? нарисовано «генеалогическое древо» процессов: login (команда входа в систему) породила bash (командный интерпретатор), bash породил dd, wc, снова dd и снова wc...
Состояние, идентификатор, ссылка на родительский процесс — это атрибуты, так сказать, бросающиеся в глаза. Однако если задуматься над тем, что произошло в нашем эксперименте, станет ясно, что этим дело не исчерпывается.
Действительно, на консоли vc/1 в общей сложности было запущено шесть процессов: login, bash, два dd и два wc. Что позволило ядру системы определить, какие именно процессы следует приостановить при нажатии +Z, какие остановить при нажатии +C и какие уничтожить при получении команды logout?
Это стало возможным благодаря двум атрибутам — «номеру сессии» (Session ID — SID) и «номеру группы процессов» (Process Group ID — PGID). Все запущенные на определенном терминале процессы входят в одну сессию, при этом некоторые объединены в группы (как dd и wc в нашем примере). В каждый момент терминалом владеет какая-то одна группа процессов, и только ее члены могут считывать с него данные (если это попытается сделать процесс из другой группы, он будет приостановлен). В нашем примере так были считаны нажатия +Z и +C, вызвавшие соответственно приостановку и остановку групп, которые владели в тот момент терминалом. При выходе пользователя из системы останавливаются все процессы сессии.
Права и полномочия
Мы еще вернемся к вопросу о взаимодействии процессов между собой, а сейчас вспомним о том, что им необходимо общаться с внешним миром, причем разные процессы должны иметь разные права. Для этой цели в ядре каждому процессу сопоставлены атрибуты UID (User ID), он же RUID (Real User ID), EUID (Effective User ID), SUID (Saved User ID), FSUID (FileSystem User ID), GID (Group ID), он же RGID (Real Group ID), EGID (Effective Group ID), SGID (Saved Group ID), FSGID (FileSystem Group ID), набор дополнительных (supplementary) групп Groups, а также три набора полномочий (capabilities).
Резонно спросить: не слишком ли здесь много атрибутов? И зачем нужно такое чудовищное их количество? Действительно, в большинстве операций используются только EUID/EGID либо FSUID/FSGID: первая пара — когда операция не касается непосредственно манипуляций с файловой системой, вторая — когда касается. Если UID файла совпадает с FSUID, процесс может делать то, что разрешено владельцу файла; если нет, но GID файла совпадает с FSGID или с идентификатором одной из дополнительных групп, — то, что разрешено группе файла. Если же файл — «посторонний», используются оставшиеся права доступа.
Атрибуты RUID/RGID сохраняют свое значение при запуске программы, даже если содержащий ее файл имеет атрибут SUID (заметим, что атрибут файла SUID — Set User ID — и атрибут процесса SUID — Saved User ID — ничего общего между собой не имеют, разумеется, кроме названия). Это позволяет программам типа passwd (установка пароля) вести себя по-разному при запуске обычным пользователем и суперпользователем.
С помощью атрибутов SUID/SGID можно временно уменьшить уровень привилегий процесса, а затем восстановить его. Для чего это может понадобиться? Представьте себе, например, программу, позволяющую разным пользователям выполнять какие-либо операции с файлами. Для авторизации пользователя такая программа должна иметь доступ к базе авторизации, в которой хранится информация о пользователях. Поэтому она запускается с правами суперпользователя, а после ввода необходимой информации меняет свой EUID, выполняет запрошенные пользователем операции с его правами, а затем возвращает себе права суперпользователя. При этом секретные данные (та же база авторизации) ни в какой момент не становятся доступны пользователю.
Для набора дополнительных групп никаких специальных SGroups или RGroups не предусмотрено. И обычный процесс не может изменить данный набор — для этого ему необходимы соответствующие полномочия.
Полномочиями называются права на выполнение критических операций в системе, таких, например, как изменение прав доступа к файлу, не принадлежащему пользователю с идентификатором FSUID, обращение к «железу» или упомянутая выше модификация набора дополнительных групп. Различаются полномочия действующие (effective) — те, которые процесс сейчас имеет, передаваемые по наследству (inheritable) — те, которыми он вправе наделить порожденный процесс, и разрешенные (permitted) — те, которые он может получить сам (аналогично SUID/SGID для обычных прав).
В настоящее время действующими полномочиями может обладать только процесс с EUID=0, т. е. выполняемый суперпользователем, а разрешенными — процесс с RUID=0 или SUID=0. При запуске программы, файл которой имеет атрибут SUID и принадлежит пользователю с UID=0, процесс получает полномочия, максимально допустимые в системе.
Кстати, ни один процесс в системе не может иметь права и полномочия выше максимального уровня, и любое снижение уровня необратимо: способа «вернуть назад» однажды отобранные привилегии не существует ни для конкретного процесса, ни (естественно) для системы в целом. Это позволяет, например, сделать неудаляемыми и неизменяемыми файлы журналов регистрации событий (log-файлы): присвоив им атрибут append-only, с которым файл нельзя ни удалить, ни изменить (разрешается только дописывать в него новую информацию), вы затем изымаете из системы полномочие CAP_LINUX_ IMMUTABLE. Теперь атрибут не сможет снять никто, включая суперпользователя, и злоумышленнику, сумевшему проникнуть в систему, не удастся уничтожить следы своего пребывания. Такая защита нередко применяется в брандмауэрах (firewalls).
Приоритеты
Многозадачная система немыслима без атрибутов, управляющих планировщиком заданий. Очевидно, что не все процессы равноправны: например, демону xntpd, который синхронизирует часы на машинах сети, требуется для работы не слишком много машинного времени, но очень важно, чтобы это небольшое время выделялось ему по первому требованию. Поэтому каждому процессу планировщик присваивает определенный приоритет. Его значение можно увидеть в выводе программ top и ps, но нельзя изменить.
Для воздействия на приоритет запускаемого процесса служит атрибут под названием nice value, что можно перевести как «степень дружелюбия». Он представляет собой число в диапазоне от -20 до 20, которое тем меньше, чем менее процесс «дружелюбен» (nice) по отношению к другим процессам в системе и, стало быть, чем более высокий приоритет ему должен быть «при прочих равных» назначен. Повысить собственное «дружелюбие», снизив приоритет, может любой процесс, а чтобы стать менее «дружелюбным», процессу требуется полномочие CAP_SYS_NICE (и следовательно, он должен быть запущен суперпользователем).
Обычно необходимости вручную управлять планировщиком заданий не возникает. Однако если вы пытаетесь понять, кто «поставил на колени» ваш сервер, имеет смысл, воспользовавшись командой nice, запустить командный интерпретатор с nice value, скажем, -10 или -12.
Другие атрибуты
Процесс можно ограничить в потреблении системных ресурсов, запретив ему использовать память, процессорное время и т. д. сверх определенной нормы. Для этой цели служит специальный набор атрибутов, точнее, два атрибута: «мягкий лимит» (soft limit) и «жесткий лимит» (hard limit). Мягкий лимит — это текущие ограничения, а жесткий — тот максимум, который процесс (не имеющий полномочия SYS_RESOURCE) может запросить у системы.
Кстати, процесс далеко не всегда занимает ту память, которую он занимает. Что это значит? Проще всего показать на примере. Для этого используем простенькую программу test.c (листинг), которая резервирует 100 Мбайт памяти, создает файл длиной 1 Гбайт (его имя передается программе в качестве параметра) и заканчивает работу по нажатию произвольной клавиши. Запустим десять экземпляров этой программы, создав файлы с именами «0»—«9», и посмотрим на состояние процессов, а также на использование памяти и диска (рис. 3). Как легко убедиться, на машине со 128 Мбайт оперативной памяти при выключенной подкачке смогли разместиться десять процессов по 100 Мбайт, а в файловой системе размером 4 Гбайт — десять файлов по 1 Гбайт. Как это могло произойти? Дело в том, что системный вызов malloc только зарезервировал память: реально она была бы отведена процессу тогда, когда он попытался бы ее использовать. И действительно, в памяти каждый процесс занимает менее 1 Мбайт. То же и с файлами: при размере в 1 Гбайт они, как показывает команда du, выдающая информацию об использовании диска, реально занимают на диске по 4 Кбайт.
Заметим, что для файла существует способ выяснить, сколько он занимает места в действительности, а для процесса нет: можно узнать лишь сколько памяти ему было выделено, но не сколько было реально использовано!
Для организации взаимодействия с файловой системой процессу сопоставлены два каталога — корневой и текущий, а также список открытых файлов и пользовательская маска (umask).
То, что текущий каталог является свойством не системы, а процесса, может удивить разве что знатоков DOS, а для пользователей Windows NT это не является чем-то необычным. Корневой каталог (если он не совпадает с корнем файловой системы) ограничивает множество доступных процессу файлов соответствующим поддеревом: к файлу, лежащему вне поддерева, процесс не может обратиться, даже если на него указывает символическая ссылка (поскольку она будет интерпретирована относительно «корня» процесса — с естественным результатом).
Это часто используется, например, для организации доступа по анонимному ftp: перед тем, как впустить пользователя в систему, ftp-сервер меняет корневой каталог на свой (скажем, /var/state/ftp), и в дальнейшем независимо ни от действий пользователя, ни от ошибок в программе ftpd никакие файлы в самой системе (вне «песочницы» анонимного ftp-сервера) не могут быть повреждены. Как видим, модель безопасности на основе «песочницы», с такой помпой преподнесенная Sun в момент «явления Java народу», весьма давно прозаически используется в Unix, и все ее преимущества и недостатки в общем-то хорошо известны...
Кстати, если процесс «забредет» в подкаталог, длина имени которого превышает 4095 символов, то команда getcwd (получение имени текущего каталога) начнет выдавать несуразные значения, но если при этом имена файлов каталога относительно корня процесса окажутся достаточно короткими, с такими файлами вполне можно будет работать.
С каждым процессом связан набор обработчиков сигналов, от которого зависит, как процесс будет реагировать на сигналы, посылаемые другими процессами, в частности, вырабатываемые системой при таких событиях, как нажатие +Z/+C, отключение терминала, уничтожение ведущего процесса сессии и т. д.
Два атрибута процесса хранятся не в особо защищенной области ядра, а вместе с другими данными программы, поскольку их изменение не угрожает безопасности системы. Это параметры командной строки и переменные окружения. Ну и, конечно, есть множество атрибутов, которые не волнуют никого, кроме разработчиков ядра (типа указателя на следующую задачу, имеющую такое же хэш-значение PID, что и текущая).
Окончание в следующем номере.
ОБ АВТОРЕ
Виктор Хименко, e-mail: khim@mccme.ru
Тестовая программа
#include int main(int argc,char *argv[]) { /* Allocate 100MiB of memory */ char *p=(char *)malloc(100*1024*1024); /* Create file 1GiB in size */ int fd=creat(argv[1],0666); lseek(fd,1024*1024*1024-1,SEEK_SET); write(fd,&fd,1); close(fd); /* Wait */ getchar(); }
Процессы в системе
Рассказ о жизни процессов естественно начать с самого начала — с их появления на свет. Так вот, процессы размножаются... почкованием: системный вызов Linux, создающий новый процесс, называется clone, а дочерний процесс представляет собой почти точную копию родительского. Только далее он выполняет назначенную ему функцию, а исходный процесс — то, что написано в программе после вызова clone. Потом отличий может стать больше, так что пути-дороги процессов способны разойтись достаточно далеко. Но если нам нужно этому воспрепятствовать, вызов clone позволит задать флаги, указывающие, что порожденный процесс будет иметь со своим предком общие:
- адресное пространство (CLONE_VM);
- информацию о файловой системе (CLONE_FS): корневой и текущий каталоги, а также umask;
- таблицу открытых файлов (CLONE_FILES);
- таблицу обработчиков сигналов (CLONE_SIGHAND);
- родителя (CLONE_PARENT) — конечно, в этом случае будет порожден не дочерний, а сестринский процесс.
Нить и задача
Нити, т. е. параллельно выполняемые части одной программы, в стандартной библиотеке поддержки многонитевых программ Linux реализованы просто как процессы, порожденные с указанием флага CLONE_VM, и с точки зрения ядра системы ничем не отличаются от любых других процессов. Однако в некоторых альтернативных реализациях многонитевых библиотек дело обстоит иначе.
Помимо процессов описанного выше вида бывают еще «ущербные», порождаемые с помощью функции kernel_thread для внутренних системных нужд. У них нет параметров командной строки, как правило, они не имеют открытых файлов и т. д. Поскольку, несмотря на свою ущербность, эти процессы все равно фигурируют в списке задач, в литературе иногда различают полноценные процессы, порожденные из «пространства пользователя» (userspace), и задачи, т. е. все процессы, включая внутренние процессы ядра.
Процесс и программа
Вы скажете: все это замечательно, но если новый процесс — всегда копия существующего, то каким образом в системе ухитряются работать разные программы? И откуда берется самая первая из них?
Процессы, выполняющие разные программы, образуются благодаря применению имеющихся в стандартной библиотеке Unix функций «семейства exec»: execl, execlp, execle, execv, execve, execvp. Эти функции отличаются форматом вызова, но в конечном итоге делают одну и ту же вещь: замещают внутри текущего процесса исполняемый код на код, содержащийся в указанном файле. Файл может быть не только двоичным исполняемым файлом Linux, но и скриптом командного интерпретатора, и двоичным файлом другого формата (например, классом java, исполняемым файлом DOS). В последнем случае способ его обработки определяется настраиваемым модулем ядра под названием binfmt_misc.
Таким образом, операция запуска программы, которая в DOS и Windows выполняется как единое целое, в Linux (и в Unix вообще) разделена на две: сначала производится запуск, а потом определяется, какая программа будет работать. Есть ли в этом смысл и не слишком ли велики накладные расходы? Ведь создание копии процесса предполагает копирование весьма значительного объема информации.
Смысл в данном подходе определенно есть. Очень часто программа должна совершить некоторые действия еще до того, как начнется собственно ее выполнение. Скажем, в разбиравшемся выше примере мы запускали две программы, передающие друг другу данные через неименованный канал. Такие каналы создаются системным вызовом pipe; он возвращает пару файловых дескрипторов, с которыми в нашем случае оказались связаны стандартный поток ввода (stdin) программы wc и стандартный поток вывода (stdout) программы dd. Стандартный вывод wc (как, кстати, и стандартный ввод dd, хотя он никак не использовался) связывался с терминалом, а кроме того, требовалось, чтобы командный интерпретатор после выполнения команды не потерял связь с терминалом. Как удалось этого добиться? Да очень просто: сначала были отпочкованы процессы, затем проделаны необходимые манипуляции с дескрипторами файлов и только после этого вызван exec.
Аналогичного результата (как показывает, в частности, пример Windows NT) можно было бы добиться и при запуске программы за один шаг, но более сложным путем. Что же касается накладных расходов, то они чаще всего оказываются пренебрежимо малыми: при создании копии процесса его индивидуальные данные физически никуда не копируются. Вместо этого используется техника, известная под названием copy-on-write (копирование при записи): страницы данных обоих процессов особым образом помечаются, и только тогда, когда один процесс пытается изменить содержимое какой-либо своей страницы, она дублируется.
Листинг 2. Окончание процедуры инициализации ядра Linux
if (execute_command) execve(execute_command,argv_init, envp_init); execve(?/sbin/init?,argv_init,envp_init); execve(?/etc/init?,argv_init,envp_init); execve(?/bin/init?,argv_init,envp_init); execve(?/bin/sh?,argv_init,envp_init); panic(?No init found. Try passing init= option to kernel.?);}
Первый процесс в системе запускается при инициализации ядра. Пожалуй, даже человеку, не умеющему программировать, достаточно будет взглянуть на конец процедуры инициализации ядра Linux (см. листинг 2), чтобы понять, как определяется выполняемая в этом процессе программа: вначале делается попытка «переключить» процесс на файл, указанный в командной строке ядра (есть и такая...), потом на файлы /sbin/init, /etc/init, /bin/init и напоследок на /bin/sh.
Смерть процесса
Рассмотрев рождение процесса, логично будет обсудить и его смерть. Когда процесс закончит работу (нормально или аварийно), он уничтожается, освобождая все использовавшиеся им ресурсы компьютера.
Обратимся еще раз к примеру, рассмотренному выше. Когда мы нажатием +C принудительно завершили выполнение программ dd и wc, соответствующие процессы были уничтожены, и на экране появилось приглашение командного интерпретатора. Пока программы работали, приглашения не было: интерпретатор находился в состоянии ожидания, в которое перешел, послав специальный системный вызов (в действительности таких вызовов существует несколько: wait, waitpid, wait3, wait4). После окончания работы программ вызов вернул управление интерпретатору, и тот выдал на терминал приглашение.
Если родительский процесс по какой-то причине завершится раньше дочернего, последний становится «сиротой» (orphaned process). «Сироты» автоматически «усыновляются» программой init, выполняющейся в процессе с номером 1, которая и принимает сигнал об их завершении.
Если же потомок уже завершил работу, а предок не готов принять от системы сигнал об этом событии, то потомок не исчезает полностью, а превращается в «зомби» (zombie); в поле Stat такие процессы помечаются буквой Z. Зомби не занимает процессорного времени, но строка в таблице процессов остается, и соответствующие структуры ядра не освобождаются. После завершения родительского процесса «осиротевший» зомби на короткое время также становится потомком init, после чего уже «окончательно умирает».
Наконец, процесс может надолго впасть в «сон», который не удается прервать: в поле Stat это обозначается буквой D. Процесс, находящийся в таком состоянии, не реагирует на системные запросы и может быть уничтожен только перезагрузкой системы.
О сигналах
Постойте, но ведь приглашение командного интерпретатора появилось и тогда, когда мы нажали +Z, хотя программы не заканчивали работу, и, следовательно, вызов wait* не мог вернуть управление! Выдача сообщения Stopped (процесс остановлен) и затем приглашения к вводу была реакцией на сигнал CHLD, который ядро посылает при нажатии +Z предкам — в данном случае одному предку — процессов, работающих с терминалом (сами процессы получают свой сигнал).
Сигналы посылаются одними процессами другим с помощью команды, которая носит устрашающее название kill, хотя в общем случае никого не убивает. Все зависит от конкретного сигнала, и практически любой сигнал при необходимости может быть процессом проигнорирован. Исключение составляют KILL, который «без разговоров» уничтожает процесс, и STOP, который его аналогичным образом останавливает.
Правила о том, какой процесс какому имеет право послать сигнал, достаточно сложны. Суперпользователь, очевидно, может посылать сигналы любым процессам, а обычный пользователь — только своим, но здесь есть масса тонкостей: например, нельзя послать сигнал CONT (продолжить выполнение остановленного процесса) своему же процессу, запущенному в другой сессии.
Работа с нитями требует особой техники, поскольку одни сигналы должны «доводиться до сведения» всех нитей, а другие — посылаться индивидуально. В Linux 2.2 это делалось путем довольно хитрых манипуляций со специальной нитью, единственным назначением которой было управление другими нитями. В версии 2.4 ядро может следить за нитями за счет нового флага CLONE_PARENT (таким образом, если одна нить породит другую и закончит работу, то порожденная нить не останется «сиротой») и нескольких специальных правил доставки сигналов, так что надобность в специальной нити отпала.
Компьютерная демонология
Демоном (daemon) в Unix (и в Linux) называется процесс, предназначенный для работы в фоновом режиме без терминала и выполняющий какие-либо действия для других процессов (не обязательно на вашей машине). Обычно демоны тихо занимаются своим делом, и вспоминают о них только в случае каких-либо неполадок в их работе: например, демону начинает недоставать места, и он посылает пользователю сообщение об этом, или демон перестает работать, и вам звонит босс с вопросом, почему у него принтер опять не печатает и когда это прекратится...
На многих машинах демоны, обслуживающие процессы других компьютеров, нужны достаточно редко, так что держать их в памяти постоянно загруженными и транжирить на это ресурсы системы нерационально. Для управления их работой был создан супердемон, которого зовут вовсе не Вельзевулом (в компьютерных демонах вообще мало «демонического» — они ближе демонам Максвелла), а куда скромнее — inetd (что, как вы догадались, является сокращением от Internet daemon).
В конфигурационном файле inetd (/etc/inetd.conf) записано, какой демон обслуживает обращения к какому сервису Internet. Обычно с помощью inetd вызываются программы pop3d, imap4d, ftpd, telnetd (предоставляю читателю определить, какие именно сервисы они обслуживают) и некоторые другие. Эти программы не являются постоянно активными, а значит, не могут считаться демонами в строгом смысле слова, но поскольку они порождаются «полноценным» демоном, их все равно так называют.
Продвинутые средства общения
Процессы посылают друг другу сигналы, передают данные через неименованные и именованные каналы, а также «гнезда». Все это замечательно, но как быть, если один процесс должен передавать другому огромные объемы информации и притом быстро (это нужно, например, при воспроизведении видео)? Могут ли процессы, адресные пространства которых строго разделены, каким-либо образом получить в совместное пользование часть памяти? Да, с помощью временных файлов.
Для передачи обширных массивов данных между процессами служит системный вызов mmap, представляющий собой довольно неожиданное применение страничной виртуальной памяти. Он позволяет, грубо говоря, сказать: «я хочу обращаться к такому-то участку такого-то файла как к оперативной памяти». Данные, которые процесс читает из указанной области памяти, по мере надобности считываются из файла, а те, которые он туда пишет, когда-нибудь попадут на диск. Но процесс сам не работает с диском, этим занимается ядро.
Если два процесса обращаются таким образом к одному и тому же участку одного и того же файла, данные будут переданы непосредственно от одного процесса к другому. Конечно, периодически ядро сбрасывает данные на диск. В некоторых случаях это полезно, но когда mmap обеспечивает только общение процессов между собой, обмен с диском лишь замедляет работу. Для процессов, имеющих общего предка, можно использовать флаг MAP_ANONYMOUS, указывающий, что данные не должны попадать в файл (дескриптор файла тогда никак не используется и может быть любым).
Вызов mmap применяется также для «загрузки в память» исполняемых файлов и библиотек, так что если программа использует 25 библиотек общим объемом во много десятков мегабайт, это вовсе не значит, что она и в памяти будет занимать такое же количество мегабайт.
С помощью временных файлов можно, кроме того, синхронизировать работу процессов, используя возможности системы, предназначенные для работы с рекомендательными (advisory) блокировками файлов. Это позволяют сделать системные вызовы fcntl и его более быстрый и простой вариант flock.
Иногда создавать временные файлы нежелательно, поэтому в Linux включены также функции для общения процессов из Unix SVR4 (Unix System V Release 4). Это shmget — создание области памяти для общения процессов, semget — создание семафора, msgget — создание очереди сообщений. В версии 2.4 к ним добавились еще более мощные функции mq_open, shm_open из SUS2 (Single Unix Specification Version 2).
Получение информации о процессах
Для работы с информацией о процессах, которую выводят на терминал программы ps и top, в Linux используется достаточно необычный механизм: особая файловая система procfs. В большинстве дистрибутивов она монтируется при запуске системы как каталог /proc. Данные о процессе с номером 1 (обычно это /sbin/init) содержатся в подкаталоге /proc/1, о процессе с номером 364 — в /proc/364, и т. д. Все файлы, открытые процессом, представлены в виде символических ссылок в каталоге /proc//fd, а ссылка на корневой каталог процесса хранится как /proc//root.
Со временем у файловой системы procfs появились и другие функции. Например, командой
echo 100000 > /proc/sys/fs/file-maxсуперпользователь может определить, что в системе разрешается открыть до 100 000 файлов, а команда
echo 0 > /proc/sys/kernel/cap-boundотнимет у всех процессов в системе все дополнительные права, т. е. фактически лишит систему понятия «суперпользователь».
Полезную информацию позволяет получить программа lsof, которая выдает список всех файлов, используемых сейчас процессами, включая каталоги, занятые потому, что какой-либо процесс использует их в качестве текущего или корневого; разделяемые библиотеки, загруженные в память; и т. д.
В следующей статье мы поговорим о командном интерпретаторе, его роли в системе и вообще о том, как из отдельных процессов и файлов складывается нечто единое.
ОБ АВТОРЕВиктор Хименко, e-mail: khim@mccme.ru