ГлавнаяarrowСистемное программированиеarrow9. Процессы в UNIX

9. Процессы в UNIX

Процессом в терминологии UNIX являет­ся просто экземпляр выполняемой программы, соответствующий определению за­дачи в других средах. Каждый процесс объединяет код программы, значения дан­ных в переменных программы и более экзотические элементы, такие как значения регистров процессора, стек программы и т.д.
Так как процессы соответствуют выполняемым программам, не следует путать их с программами, которые они выполняют. Несколько процессов могут выпол­нять одну и ту же программу.
Любой процесс UNIX может, в свою очередь, запускать другие процессы. Это придает среде процессов UNIX иерархическую структуру. На вершине дерева процессов находится единственный управляющий процесс, экземпляр очень важной программы init, которая явля­ется предком всех системных и пользовательских процессов.
Система UNIX предоставляет программисту набор системных вызовов для создания процессов и управления ими.
Основным примитивом для создания процессов является системный вызов fork. Он является механизмом, который превращает UNIX в многозадачную систему.
Описание.
#include
#include
pid_t fork(void);
В результате успешного вызова fork ядро создает новый процесс, который яв­ляется почти точной копией вызывающего процесса. Новый процесс выполняет копию той же программы, что и создавший его процесс, при этом все его объекты данных имеют те же самые значения, что и в вызывающем процессе, за одним важным исключением, которое описано ниже.
Созданный процесс называется дочерним процессом (child process), а процесс, осуществивший вызов fork, называется родителем (parent).
После вызова родительский процесс и его вновь созданный потомок выполня­ются одновременно (параллельно), при этом оба процесса продолжают выполнение с оператора, который следует сразу же за вызовом fork.
Вызов fork не имеет аргументов и возвращает идентификатор процесса pid_t.
Значение, возвращаемое родительскому процессу в переменной pid, называ­ется идентификатором процесса (process-id) дочернего процесса. Это число иден­тифицирует процесс в системе аналогично идентификатору пользователя. По­скольку все процессы порождаются при помощи вызова fork, то каждый процесс UNIX имеет уникальный идентификатор процесса.
Вызов fork обретает ценность в сочетании с дру­гими средствами UNIX.
Для смены исполняемой программы можно использовать функции семейства exec. Основное отличие между разными функциями в семействе состоит в способе передачи параметров. В конечном итоге все эти функции выполняют один системный Вызов execve.
Описание.
#include
int execl(const char *path, const char *arg0, ...,
const char *argn, (char*)0);
int execlp(const char *file, const char *arg0, ...,
const char *argn, (char*)0);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
Для семейства вызовов execl аргументы должны быть списком, заканчивающимся NULL. Вызову execl нужно передать полный путь к файлу программы. Вызову execlp нужно только имя файла программы.
Семейству вызовов execv нужно передать массив аргументов. Вызову execv нужно передать полный путь к файлу программы. Вызову execvp нужно только имя файла программы.
Все множество системных вызовов ехес выполняет одну и ту же функцию: они преобразуют вызывающий процесс, загружая новую программу в его простран­ство памяти. Если вызов ехес завершился успешно, то вызывающая программа полностью замещается новой программой, которая запускается с начала. Резуль­тат вызова можно рассматривать как запуск нового процесса, который при этом сохраняет идентификатор вызывающего процесса и по умолчанию наследует фай­ловые дескрипторы.
Важно отметить, что вызов ехес не создает новый подпроцесс, который вы­полняется одновременно с вызывающим, а вместо этого новая программа загру­жается на место старой. Поэтому, в отличие от вызова fork, успешный вызов ехес не возвращает значения.
Все аргументы функции execl являются указателями строк. Первый из них, аргумент path, задает имя файла, содержащего программу, которая будет запуще­на на выполнение. Для вызова execl это должен быть полный путь к программе, абсолютный или относительный. Сам файл должен содержать программу или пос­ледовательность команд оболочки и быть доступным для выполнения. Система определяет, содержит ли файл программу, просматривая его первые байты (обыч­но первые два байта). Если они содержат специальное значение, называемое ма­гическим числом (magic number), то система рассматривает файл как программу. Второй аргумент, arg0, является именем программы или ко­манды, из которого исключен путь к ней. Этот аргумент и оставшееся переменное число аргументов (от arg1 до argn) доступны в вызываемой программе, аналогич­но аргументам командной строки при запуске программы из оболочки. Так как список аргументов имеет про­извольную длину, он должен заканчиваться нулевым указателем для обозначе­ния конца списка.
Другие формы вызова ехес упрощают задание списков параметров запуска загружаемой программы. Вызов execv принимает два аргумента: первый (path в описании применения вызова) является строкой, которая содержит полное имя и путь к запускаемой программе. Второй аргумент (argv) является массивом строк. Первый элемент этого массива указывает на имя запускаемой программы (исключая префикс пути). Оставшиеся элементы указы­вают на все остальные аргументы программы. Так как этот список имеет неопре­деленную длину, он всегда должен заканчиваться нулевым указателем.
Функции execlp и execvp почти эквивалентны функциям execl и execv. Ос­новное отличие между ними состоит в том, что первый аргумент обоих функций execlp и execvp - просто имя программы, не включающее путь к ней. Путь к файлу находится при помощи поиска в каталогах, заданных в переменной среды PATH.
Любая программа может получить доступ к аргументам активизировавшего ее вызова ехес через параметры, передаваемые функции main. Эти параметры могут быть описаны при определении функции main.
Например:
Системные вызовы fork и ехес, объединенные вместе, представляют мощный инструмент для программиста. Благодаря ветвлению при использовании вызова ехес во вновь созданном дочернем процессе программа может выполнять другую программу в дочернем процессе, не стирая себя из памяти.
Созданный при помощи вызова fork дочерний процесс является почти точ­ной копией родительского. Все переменные в дочернем процессе будут иметь те же самые значения, что и в родительском (единственным исключением является значение, возвращаемое самим вызовом fork). Так как данные в дочернем про­цессе являются копией данных в родительском процессе и занимают другое абсо­лютное положение в памяти, важно понимать, что последующие изменения в од­ном процессе не будут затрагивать переменные в другом.
Аналогично все файлы, открытые в родительском процессе, также будут от­крытыми и в потомке. При этом дочерний процесс будет иметь свою копию свя­занных с каждым файлом дескрипторов. Файлы, открытые до вызо­ва fork, остаются тесно связанными в родительском и дочернем процессах. Это обусловлено тем, что указатель чтения/записи для каждого из таких файлов ис­пользуется совместно родительским и дочерним процессами благодаря тому, что он поддерживается системой и существует не только в самом процессе. Следова­тельно, если дочерний процесс изменяет положение указателя в файле, то в роди­тельском процессе он также окажется в новом положении.
Дескрипторы открытых файлов обычно сохраняют свое состояние также во время вызова exec. Файлы, открытые в исходной программе, остаются открытыми, когда совершенно новая программа запускается при помощи вызова exec. Указатели чтения/записи на такие файлы остаются неизменными после вызова.
Есть связанный с файловым дескриптором флаг close-on-exec (за­крывать при вызове exec), который может быть установлен с помощью универ­сальной процедуры fсnt1. Если этот флаг установлен (по умолчанию он сброшен), то файл закрывается при вызове любой функции семейства exec.
Вызов exit используется для завершения процесса, хотя это также про­исходит, когда управление доходит до конца тела функции main или до оператора return в функции main.
Описание.
#include
void exit(int status);
Единственный целочисленный аргумент вызова exit называется статусом за­вершения (exit status) процесса, младшие восемь бит которого доступны родитель­скому процессу при условии, если он выполнил системный вызов wait. При этом возвращаемое вызовом exit значение обычно используется для определения успешного или неудачного завер­шения выполнявшейся процессом задачи. По принятому соглашению, нулевое возвращаемое значение соответствует нормальному завершению, а ненулевое зна­чение говорит о том, что что-то случилось.
Кроме завершения вызывающего его процесса, вызов exit имеет еще несколь­ко последствий: наиболее важным из них является закрытие всех открытых де­скрипторов файлов. Если родительский про­цесс выполнял вызов wait, то его выполнение продолжится. Сочетание вызовов fork и wait наиболее полезно, если дочерний процесс пред­назначен для выполнения совершенно другой программы при помощи вызова ехес.
Возвращаемое значение wait обычно является идентификатором дочернего процесса, который завершил свою работу. Если вызов wait возвращает значение -1, это может означать, что дочерние процессы не существуют, и в этом случае переменная errno будет содержать код ошибки ECHILD.
Вызов wait принимает один аргумент, status, - указатель на целое число. Если указатель равен NULL, то аргумент просто игнорируется. Если же вызову wait передается допустимый указатель, то после возврата из вызова wait пере­менная status будет содержать полезную информацию о статусе завершения процесса. Обычно эта информация будет представлять собой код завершения до­чернего процесса, переданный при помощи вызова exit.
Значение, возвращаемое родительскому процессу при помощи вызова exit, записывается в старшие восемь бит целочисленной переменной status. Чтобы оно имело смысл, младшие восемь бит должны быть равны нулю.
Системный вызов wait позволяет родительскому процессу ожидать заверше­ния любого дочернего процесса. Если нужна большая определен­ность, то можно использовать системный вызов waitpid для ожидания заверше­ния определенного дочернего процесса.
Описание.
#include
#include
pid_t waitpid(pid_t pid, int *status, int options);
Первый аргумент pid определяет идентификатор дочернего процесса, завер­шения которого будет ожидать родительский процесс. Если этот аргумент уста­новлен равным -1, а аргумент options установлен равным 0, то вызов waitpid ведет себя в точности так же, как и вызов wait, поскольку значение -1 соответ­ствует любому дочернему процессу. Если значение pid больше нуля, то родитель­ский процесс будет ждать завершения дочернего процесса с идентификатором процесса равным pid. Во втором аргументе status будет находиться статус до­чернего процесса после возврата из вызова waitpid.
Последний аргумент, options, может принимать константные значения, опре­деленные в файле . Наиболее полезное из них - константа WNOHANG. Задание этого значения позволяет вызывать waitpid в цикле без блокирования процесса, контролируя ситуацию, пока дочерний процесс продолжает выполнять­ся. Если установлен флаг WNOHANG, то вызов waitpid будет возвращать 0 в слу­чае, если дочерний процесс еще не завершился.
Иногда могут возникать такие ситуации:
- в момент завершения дочернего процесса родительский процесс не выпол­няет вызов wait;
- родительский процесс завершается, в то время как один или несколько до­черних процессов продолжают выполняться.
В первом случае завершающийся процесс как бы «теряется» и становится зом­би-процессом (zombie). Зомби-процесс занимает ячейку в таблице, поддерживае­мой ядром для управления процессами, но не использует других ресурсов ядра. В конце концов, он будет освобожден, если его родительский процесс вспомнит о нем и вызовет wait. Тогда родительский процесс сможет прочитать статус за­вершения процесса, и ячейка освободится для повторного использования.
Во вто­ром случае родительский процесс завершается нормально.

Атрибуты процессов



С каждым процессом UNIX связан набор атрибутов, которые помогают системе управлять выполнением и планированием процессов, обеспечивать защиту файловой системы и так далее. Один из атрибутов - это идентификатор процесса, то есть число, которое однозначно идентифицирует процесс. Другие атрибуты простираются от окружения, которое является набором строк, определяемых программистом и находящихся вне области данных, до действующего идентификатора пользователя, определяющего права доступа процесса к файловой системе.
Система присваивает каждому процессу одно неотрицательное число, которое называется идентификатором процесса. В любой момент времени идентификатор процесса является уникальным, хотя после завершения процесса он может использоваться снова для другого процесса. Некоторые идентификаторы процесса зарезервированы системой для особых процессов. Процесс с идентификатором 0, хотя он и называется планировщиком (scheduler), на самом деле является процессом подкачки памяти (swapper). Процесс с идентификатором 1 - это процесс инициализации, выполняющий программу /etc/init. Этот процесс, явно пли неявно, является предком всех других процессов в систе­ме UNIX.
Программа может получить свой идентификатор процесса при помощи следующего системного вызова.
pid = getpid();
Аналогично вызов getppid возвращает идентификатор родителя вызывающего процесса.
ppid = getppid();
Система UNIX позволяет легко помещать процессы в группы. Группы процессов удобны для работы с набором процессов в целом.
Каждая группа процессов (process group) обозначается идентификатором группы процессов (process group-id), имеющим тип pid_t. Процесс, идентификатор ко­торого совпадает с идентификатором группы процессов, считается лидером (leader) группы процессов, и при его завершении выполняются особые действия. Первона­чально процесс наследует идентификатор группы во время вызова fork или exec. Процесс может получить свой идентификатор группы при помощи системно­го вызова getpgrp.
Описание.
pid_t getpgrp(voud);
Процесс может создать новую группу процессов пли присоединиться к суще­ствующей при помощи системного вызова setpgid.
Описание.
int setpgid(pid_t pid, pid_t pgid);
Вызов setpgid устанавливает идентификатор группы процесса с идентифи­катором pid равным pgid. Если pid равен 0, то используется идентификатор вы­зывающего процесса. Если значения идентификаторов pid и pgid одинаковы, то процесс становится лидером группы процессов. В случае ошибки возвращается значение -1. Если идентификатор pgid равен нулю, то в качестве идентификато­ра группы процесса используется идентификатор процесса pid.
Понятие сеанса полезно при работе с фоновыми процессами или процессами-демонами (daemon processes). Процессом-демоном называется просто процесс, не имеющий управляющего терминала. Демон может задать для себя се­анс без управляющего терминала, переместившись в другой сеанс при помощи системного вызова setsid.
Описание.
pid_t setsid(void);
Если вызывающий процесс не является лидером группы процессов, то созда­ется новая группа процессов и новый сеанс, и идентификатор вызывающего процесса станет идентификатором созданного сеанса. Он также не будет иметь управляющего терминала. Процесс-демон теперь будет находиться в странном состоянии, так как он будет единственным процессом в группе процессов, содер­жащейся в новом сеансе, а его идентификатор процесса pid - также идентифика­тором группы и сеанса.
Вызов setsid может завершиться неудачей и возвратит значение -1, если вызывающий процесс уже является лидером группы.
С каждым процессом связан корневой каталог, который используется при поиске абсолютного пути. Так же, как и текущий рабочий каталог, корневым каталогом процесса первоначально является корневой каталог его родительского процесса. Для изменения начала иерархии файловой системы для процесса в ОС UNIX существует системный вызов chroot.
Описание
int chroot(const char *path);

Переменная path указывает на путь, обозначающий каталог. В случае успеш­ного вызова chroot путь path становится начальной точкой при поиске в путях, начинающихся с символа /. В случае неудачи вызов chroot не меняет корне­вой каталог и возвращает значение -1. Для изменения корневого каталога вызы­вающий процесс должен иметь соответствующие права доступа.
С каждым процессом связаны истинные идентификаторы пользователя и груп­пы. Это всегда идентификатор пользователя и текущий идентификатор группы запустившего процесс пользователя.
Действующие идентификаторы пользователя и группы используются для оп­ределения возможности доступа процесса к файлу.
Для получения связанных с процессом идентификаторов пользователя и груп­пы существует несколько системных вызовов.
uid_t getuid(); /*Истинный идентификатор пользователя*/
gid_t getgid(); /*Истинный идентификатор группы*/
uid_t geteuid();/*Действующий идентификатор пользователя*/
gid_t getegid();/*Действующий идентификатор группы*/
Для задания действующих идентификаторов пользователя и группы процесса также существуют системные вызовы.
Описание.
#include
int setuid(uid_t newuid);
int setgid(gid_t newgid);
Процесс, запущенный непривилегированным пользователем, может менять действующие идентификаторы пользователя и группы только на истинные. Обе процедуры возвращают нулевое зна­чение в случае успеха, и -1 в случае неудачи.
 

Hosted by uCoz