Системное программирование во FreeBSD


3.1 Процессы и функции ядра

Процесс: Конечная последовательность действий или операций.

В предыдущей главе был описан процесс загрузки для BSD систем. Теперь давайте посмотрим что происходит, когда ядро загружено в память и работает. Как и прочие современные операционные системы, BSD является многопользовательской, многозадачной операционной системой, что означает, что множество пользователей используют ресурсы системы, причем каждый пользователь может иметь множество процессов. Понятие процесс представляет собой абстракцию, которая подрузамевает под собой активность некоторого рода, которая управляется операционной системой.
Этот термин был впервые введен разработчиками ОС Multics, разработанной в 60-х годах, и противопоставлен термину задание.


3.2 Планирование

Процесс - это экземпляр запущенной программы. Например, когда Вы запускаете Netscape на BSD, система создает процесс и начинает его выполнение. Если имеется система с тремя зашедшими(logged - in) пользователями, и каждый запустил Netscape в одно и тоже время, то каждый пользователь получит в распоряжение свою копию Netscape, независящую от других. BSD системы могут поддерживать множество таких
процессов одновременно. Каждый процесс имеет привязанный к нему идентификатор процесса (Process Identifier - PID). Эти процессы используют ресурсы и могут обращаться с устройствами, например с внешним хранилищем.

Иллюзию параллельной работы множества запущенных процессов берет на себя операционная система. Присваивание приоритетов процессам
контролируется специальными планирующими алгоритмами, которые уникальны для ОС.

Операционная система предоставляет каждому процессу на время центральный процессор(CPU). Таким образом, каждый процесс получает в
свое распоряжение промежуток времени, в течение которого он использует процессор. Этот промежуток времени называется slice (англ. - ломтик).
Длина этого slice всецело определяется планирующим алгоритмом ядра.
Для этого алгоритма приспособлены т.н. nice-значения, которые используются для обозначения приоритета иполняющегося процесса. Вот эти значения приоритетов:

Nice-значения Приоритет
-20-0 Высокий приоритет исполнения
0-20 Низкий приоритет исполнения

По умолчанию, nice-значение для всех процессов равно 0. Выполняющийся процесс может повысить свое nice-значение(т.е. понизить свой
приоритет), но только процессы, запущенные от root могут понижать nice (повышать приоритет). BSD предоставляет 2 базовых функции для установки/чтения nice. Вот они:

int getpriority(int which, int who);
int setpriority(int which, int who, int prio);

и

int nice(int incr);

Функция nice устанавливает nice-значение вызывающего процесса в значение incr, передаваемого как параметр. Функция nice() не является гибкой и обернута(wrapped) новыми функциями getpriority() и setpriority(), которые и рекомендуется использовать.

Так как -1 является допустимым значением приоритета, возвращаемое значение getpriority не должно рассматриваться как успех(success) или неудача(failure) - вместо этого, проверяйте, была ли установлена переменная errno. (За информацией о errno см. man(2) errno и пример
программы nice_code.c). setpriority и getpriority могут изменять приоритет других процессов, устанавливая параметры which и who.

Пример программы nice_code.c демонстрирует установку и считывание nice-значения текущего процесса. Если текущий процесс запущен под
root, и процессу будет передано -20, то система перестанет отвечать и зависнет. Это произойдет вследствие того, что процесс установит максимальный приоритет, который только можно установить, и не даст выполняться другим задачам. Будте осторожны и имейте терпение, если
разрешено устанавливать приоритет ниже 0; в зависимости от процессора, отклик системы можен занять до 20 минут. В таких случаях рекомендуется запускать процесс с помощью time:

bash$ time nice_code 10

затем, передайте значение аргумента. Для примера, эта комманда передаст значение ниже 0:

$ time nice_code -1

Это сделает значение большим:

bash$ time nice_code 20

Также попробуйте запустить программу под обычным пользователем и измените приоритет ниже 0. Система должна запретить это; только
процесс, запущенный от root'а, может понижать nice-значение (UNIX системы являются многопользовательскими, и каждый процесс должен
справедливо использовать процессорное время; поэтому только root может изменять приоритет процесса чтобы недопустить монопольного захвата процессора каким-либо пользователем).

3.3 Системные процессы

Понятие защищенного режима было введено в предыдущей главе. Под ним понимается специальный режим работы современных микропроцессоров, который позволяет ОС, помимо многих других вещей, защитить адресное пространство памяти. Исходя из этого, существуют 2 режима выполнения.
Первый из них - в ядре(kernel-land). Это означает, что процесс исполняется внутри адресного пространства ядра и ему доступны команды защищенного режима. Второй - пользовательский(userland), который является всем остальным и не использует операции защищенного режима.

Это обсуждение очень важно, так как любой процесс использует ресурсы обоих режимов. Эти ресурсы могут быть самыми различными: структуры ядра ОС для процесса, выделенная память, открытые файлы и контекст выполнения.

На системе FreeBSD несколько критичных системных процессов помогают ядру в выполнении своих задач. Некоторые из этих процессов целиком реализованы в адресном пространтсве ядра, другие запускаются в userland. Вот эти процессы:

PID Имя
0 swapper
1 init
2 pagedaemon
3 vmdaemon
4 bufdaemon
5 syncer

Все перечисленные выше процессы, за исключением init, реализованы внутри ядра. Это значит что нет бинарных (двоичных) программ для этих
процессов. Они только похожи на процессы userland, и, так как они реализованы в ядре, они используют защищенный режим. Такие архитектурные решения существуют из-за различных причин. Пример: процесс pagedaemon(демон управления виртуальной памятью) - просыпается только тогда, когда в системе мало памяти. Таким образом, если система имеет много свободной памяти, pagedaemon никогда не будет вызван.
Преимущество запуска pagedaemon в userland - возможность использования процессора тогда, когда он действительно необходим. Процесс будет
добавлен в систему и станет учитываться планировщиком. Вычисления планировщика очень небольшие и незначительны по времени.

Процесс init является прародителем всех других процессов. Каждый процесс, за исключением kerneland-процессов, порожден процессом init.
Также, процессы-zombie и все отказавшие процессы - все они порождены init. Он выполняет некоторые административные задачи, включая
отслеживание gettys для системных tty, правильный порядок завершения процессов при окончании работы системы.

3.4 Создание процессов и их идентификаторы.

Как упоминалось выше, когда программа начинает выполнение, то ядро присваивает ей уникальный идентификатор процесса - PID. PID является
положительным числом в диапазоне 0 - PID_MAX. (Для FreeBSD, PID_MAX, который находится в /usr/include/sys/proc.h, равен 99999). Ядро
присваивает PID на основе следующего последовательного свободного PID.
Когда достигнут предел PID_MAX, значения идут заново.

Каждый процесс, котрый запущен на системе, создан другим процессом.
Это происходит с помощью системных вызовов, обсуждаемых в следующей главе. Когда процесс создает новый процесс, оригинал называется
родителем или родительским процессом, а созданный процесс - потомком или дочерним процессом. Родительско-дочерние отношения имеют превосходную аналогию - каждый родитель может иметь несколько детей и родительские процессы происходят от других процессов. Процессы могут получать свой или родительский PID с помощью функции getpid.

Процессы могут быть объединены в группы процессов. Группа процессов имеет свой уникальный идентификатор группы - grpID. Группы процессов были введены в BSD как механизм контроля заданий для оболочки. Вот пример:

bash$ ps -aux | grep root | awk {'print $2'}

Программы ps, grep awk и print объеденены в группу процессов. Это позволяет контролировать комманды ссылаясь на еденичное задание.
Процесс может получить group ID, вызвав getpgrp или getpgid:

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
pid_t getpgrp(void);

Все функции, перечисленные выше, гарантируют успешное выполнение. FreeBSD man страницы строго утверждают, что PID не должен
использоваться для создания униккального имени файла. PID является уникальным только во время создания процесса. Когда процесс завершается, значение PID возвращается в пул неиспользуемых PID, которое затем может быть использовано другим процессом. Простой код proc_ids.c демонстрирует как получить эти значения.

Запуская программу следующим образом:

bash$ sleep 10 | ps -aux | awk{'print $2'} |./proc_ids

А на другом терминале запустив:

bash$ ps -j

Только одна оболочка должна использоваться для запуска этих команд и каждый будет иметь одинаковый PPID (PID родителя) и PGID.

3.5 Процессы их процессов

Процессы могут быть созданы другими процессами, и есть 3 пути сделать это в BSD. Вот они: fork, vfork и rfork. Есть другие вызовы, которые, по сути, являются всего лишь обертками из этих трех функций.

fork

Когда процесс вызывает fork, новый процесс является полной копией родительского:

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

Отличаясь от других вызовов, в случае успеха, fork возвращает два значения - одно для родительского процесса, которое содержит PID дочернего процесса, и второе для дочернего, равное Дочерний процесс получает от родительского следующее:

* терминал
* рабочий каталог
* корневой каталог
* nice - значение
* лимит ресурсов
* текущий, эффективный и сохраненный ID пользователя
* текущий, эффективный и сохраненный ID группы
* ID группы процессов
* дополнительный ID группы
* SUID
* GUID
* сохраненные переменные окружения
* сохраненные дескрипторы файлов и смещения
* флаги close-on-exec
* мода файла(mode)
* настройки обработчиков сигналов(SIG_DFL, SIG_IGN, адреса)
* маска сигналов

Уникальное в дочернем процессе - это его новый PID, PPID имеет значение родительского PID, показатель использования ресурсов установлен в 0, и имеются копии файловых дескрипторов родительского процесса. Порожденный процесс может закрыть файловые дескрипторы,
никак не повлияв на родительский процесс. Если потомок захочет прочитать или записать что-либо в файл, он сохранит файловые смещения
потомка. Это может привести к странным последствиям: непонятному выводу или к краху процессов при одновременном использовании одинаково дескриптора.

После того, как новый процесс создан, порядок выполнения неизвестен.
Наилучший способ контроля - использование семафоров, каналов или сигналов, обсуждаемых далее. С помощью них оба процесса не смогут
порушить друг друга.

wait

Родительский процесс должен собрать информацию о завершении дочерних процессов используя следующие wait-функции:

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
#include <sys/time.h>
#include <sys/resource.h>
pid_t waitpid(pid_t wpid, int *status, int options);
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t wpid, int *status, int options, struct rusage *rusage);


В данном случае параметр options является двоичным значением или одним из следующих:

WNOHANG Не блокироваться при ожидании. Это заставит wait вернуть управление, даже если ни один из дочерних процессов не завершился.
WUNTRACED Используйте, если вы хотите ожидать статуса остановленных процессов.
WLINUXCLONE Используйте, если хотите ожидать kthread из linux_clone.

Когда используется wait4 и waitpid, помните, что параметром wpid является PID ожидаемого процесса. Установив параметр в -1, вы заставите wait ждать завершения всех дочерних процессов. Когда используется функция с rusage, то, если структура не обнулена, будет возвращена информация об использованных ресурсах. Соответственно, если процесс остановлен, то информация rusage не будет доступна. wait функции подрузамевают для родительского процесса, что он захочет узнать статус завершения потомков. Вызвав wait, вызывающий процесс будет заблокирован до тех пор, пока не завершится дочерний процесс, или будет получен сигнал. Этого можно избежать; часто используют WNOHANG, вызывая wait. При успешном завершении, параметр status содержит информацию о завершившемся процессе. Если статус выхода не интересен, то stuas должен содержать NULL. Больше о использовании WNOHANG описано в секции Сигналы.

Если информация о статусе выхода интересна, то определения макросов находятся в /usr/include/sys/wait.h. Их использование - наилучший метод для кросс-платформенной переносимости. Ниже описано 3 из них с объяснением использования:

WIFEXITED(status) Если возвращается true (ненулевое значение), то процесс завершен нормально одним из вызовов exit() или _exit()

WIFSIGNALED(status) Если true, то процесс завершен по сигналу.

WIFSTOPPED(status) Если true, то процесс остановлен и может быть перезапущен. Этот макрос должен использоваться с опцией WUNTRACED или когда дочерний процесс трассируется (с помощью ptrace).

Если есть необходимость, то следующие макросы извлекут оставшуюся информацию:

WEXITSTATUS(status) Используется когда макрос WIFEXITED(status) возвращает true. Он вычисляет младшие 8 бит аргумента, переданного exit() или _exit()

WTERMSIG(status) Используется когда макрос WIFSIGNALED(status) возвращает true. Возвращает номер сигнала, вызвавший завершение процесса.

WCOREDUMP(status) Используется когда макрос WIFSIGNALED(status) возвращает true. Возвращает true если завершенный процесс создал core dump

WSTOPSIG(status) Используется когда макрос WIFSTOPPED(status) возвращает true. Возвращает номер сигнала, который вызвал остановку процесса.

Если родительский процесс никогда не проверяет статус завершения своих дочерних процессов, или, если предок завершается до того, как
завершатся дочерние процессы, init сам соберет статусы завершения.

vfork и rfork

Функция vfork похожа на fork и была реализована в 2.9BSD. Различие между ними заключается в том, что vfork запрещает выполнение
родительского процесса и использует текущий поток родительского процесса для выполнения. Это создано для предотвращения копирования адресного пространства родителя, что является неэффективным.
Использованик vfork не рекомендуется, так как не гарантируется переносимость кода. Для примера, Irix версии 5.x не имеет vfork.

#include <sys/types.h>
#include <unistd>
int vfork(void);

Функция rfork похожа на fork и vfork. Она была впервые раелизована в Plan 9. Ее основная цель - управление созданием процессов и потоков
ядра. FreeBSD/OpenBSD поддерживают ее для моделирования потоков и вызовов Linux clone. rfork является более быстрой и более короткой
функцией, чем fork. Функции можно указать, какие ресурсы сделать общими с дочерним процессом, с помощью операции ИЛИ (OR).

#include <unistd>
int rfork(int flags);

Ресурсы, которые могут быть указаны, следующие:

RFPROC Установите этот флаг когда вы хотите создать новый процесс. По умолчанию, флаг всегда должен быть установлен.

RFNOWAIT Установите это флаг когда дочерний процесс должен быть оторван от родительского. Когда дочерний процесс завершиться, он не
оставит статус выхода для родителя.

RFFDG Установите когда необходима копия таблицы дескрипторов родительского процесса. Если нет, то родитель и потомок разделят между
собой общую таблицу.

RFCFDG Установите, если хотите чистую таблицу файловых дескрипторов дочернего процесса.

RFMEM Используется для указания стека функции, самой функции и ее параметров. Нельзя использовать напрямую из C. Лучший метод -
использование функции rfork_thread:

#include <unistd.h>
int rfork_thread(int flags, void *stack, int (*func)(void *arg), void *arg);


Это создаст процесс с указанным стеком, и вызовет указанную функцию с параметрами. В случае успеха, возвращаемое значение будет PID только для родителя. В случае ошибки, функция вернет -1 и установит errno.

RFSIGSHARE Флаг специфичен для FreeBSD и был использован в эксплоите (переполнение буфера) для FreeBSD 4.3. Флаг позволяет разделять
дочернему процессу сигналы с родителем (разделение структуры sigact).

RFLINUXTHPN Другой, специфичный для FreeBSD, флаг. Заставляет ядро возвращать сигнал SIGUSR1 вместо SIGCHILD по завершении работы
дочернего процесса.

Возвращаемое значение frork похоже на fork. Порожденный процесс получает 0, родитель получает PID дочернего. Одно тонкое разлтчие - rfork засыпает по необходимости до тех пор, пока необходимые ресурсы станут доступны. fork может быть вызван с помощью rfork (флаги
RFFDG|RFPROC). Если вызов rfork заканчивается неудачей, то возвращается -1 и устанавливается errno.

rfork не очень хорошо поддерживается всеми BSD. OpenBSD 1.5 не поддерживает rfork. Рекомендуемый интерфейс для потоков - использование pthreads.

3.6 Выполнение двоичных програм

Процессы были бы бесполезны, если они только ограничивались точной копией родительского процесса. Функция exec используется для замещения образа памяти процесса новым процессом. Возьмем, для примера, запуск команды ls в оболочке. Во-первых, оболочка вызывает fork или vfork (зависит от реализации), а затем вызывает одну из exec-функций. При успешном вызове, exec заменяет образ памяти процесса программой ls (exec не возвращает управление). В случае скриптов, все почти тоже самое, только еще вызывается интерпретатор. Для его вызова применяются начальные символы #!. Пример вызова интерпретатора:

#! inerpreter [arg]

Следующая команда вызывает инерпретатор Perl с аргументом -w:

#!/usr/bin/perl -w

Все вызовы exec перечислены ниже. Базовый синтакис вызова следующий: (запускаемая программа), (аргументы), (если требуется, окружение).

#include <sys/types.h>
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg0, ... const char *argn, NULL);
int execle(const char *path, const char *arg0, ... const char *argn, NULL, const char *environ);
int execlp(const char *file, const char *arg0, ... const char *argn, NULL);
int exect(const char *path, char *const argv[], char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);


ПРЕДУПРЕЖДЕНИЕ: Когда вызывается exec с параметром arg, .... Аргументы должны быть последовательностью NULL-завершенных строк с завершающим символом NULL или 0. Если последовательность не завершается NULL, то вызов функции завершится неудачей. Последовательность является аргументами для запускаемой програмы.

Вызовы exec, содержащие *argv[] - параметр представляет собой NULL-завершенный массив из null-завершенных строк. Массив является списком аргументов для запускаемой програмы.

Короткое руководство

Аргументы в виде массива и последовательности.

массив: exect, execv, execvp последовательность: execl,execle,execlp

Поиск по пути и по файлу.

путь: execl,execle,exect,execv
файл: execlp,execvp

Указанное окружение и унаследованное.

указанное: execle, exect
унаследованное: execl, execlp, execv, execvp

system

#include <stdlib.h>
int system(const char *string);

Другая ключевая функция запуска процессов - system. Аргумент функции посылается прямиком оболочке. Если передается NULL, функция вернет 1, если оболочка доступна, и 0 в противном случае. Будучи вызванной, процесс ждет кода стауса выхода оболочки, который и возвращается
функцией. Когда возвращается -1, то fork или waitpid завершились неудачно. Значение 127 означает, что оболчка не смогла выполнить команду.

Некоторые сигналы, такие как SIGINT и SIGOUT, игнорируются, также может быть проигнорирован SIGCHILD. Существует возможность, что system может порушить вызывающий процесс.

В следующей главе рассказывается о сигналах и управлении процессами, включая использование ресурсов, потоки и лимиты процессов.

Оригинал на английском: http://www.khmere.com/freebsd_book/html/ch03.html

Обновлено: 12.03.2015