Почему до трех — ясно: в тождестве между «потоком» и «нитью» никто не сомневается, поскольку оба термина соответствуют английскому 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 (и следовательно, он должен быть запущен суперпользователем).

Определенным процессам (например, воспроизведению звука) нужно предоставлять процессор вне всякой очереди (упомянутый выше xntpd к таковым не относится). В этих случаях используются атрибуты специальной политики планировщика — SCHED_FIFO (First In-First out scheduling) и SCHED_RR (Round Robin scheduling). Процесс с такими атрибутами ни при каких условиях не может быть прерван обычным. Процессы, планируемые как SCHED_RR и имеющие одинаковый приоритет, периодически все же уступают процессор друг другу, а планируемые как SCHED_FIFO не делают даже этого. Менять политику планировщика позволяет полномочие 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();
}