Exec c linux примеры. Изучение создания процесса UNIX. Листинг1. Вывод команды ps

EXEC(2)

НАЗВАНИЕ
exec: execl, execv, execle, execve, execlp, execvp выполнение файла

СИНТАКСИС

Int execl (path, arg0, arg1, ..., argn, (char*) 0) char *path, *arg0, *arg1, ..., *argn; int execv (path, argv) char *path, *argv ; int execle (path, arg0, arg1, ..., argn, (char*) 0, envp) char *path, *arg0, *arg1, ..., *argn, *envp ; int execve (path, argv, envp) char *path, *argv , *envp ; int execlp (file, arg0, arg1, ..., argn, (char*) 0) char *file, *arg0, *arg1, ..., *argn; int execvp (file, argv) char *file, *argv ;

ОПИСАНИЕ
Все формы системного вызова exec превращают вызвавший процесс в новый процесс, который строится из обычного выполняемого файла, называемого в дальнейшем новым выполняемым файлом. Выполняемый файл состоит из заголовка [см. a.out(4)], сегмента команд (.text) и данных. Данные состоят из инициализированной (.data) и неинициализированной (.bss) частей. Если системный вызов exec закончился успешно, то он не может вернуть управление, так как вызвавший процесс уже заменен новым процессом.

При запуске C-программы ее вызывают следующим образом:

Main (argc, argv, envp) int argc; char **argv, **envp;

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

Аргументам системных вызовов группы exec приписан следующий смысл.

Аргумент path указывает на маршрутное имя нового выполняемого файла.

Как и path, аргумент file указывает новый выполняемый файл, но маршрут этого файла определяется в результате просмотра каталогов, переданных через переменную окружения PATH [см. environ(5) ]. Окружение поддерживается shell"ом [см. sh(1) ].

Аргументы arg0, arg1, ..., argn - это указатели на цепочки символов, ограниченные нулевыми байтами. Эти цепочки образуют доступный новому процессу список аргументов. По соглашению, как минимум arg0 должен присутствовать и указывать на цепочку символов, равную path (или последнему компоненту path).

Массив argv содержит указатели на цепочки символов, ограниченные нулевыми байтами. Эти цепочки образуют доступный новому процессу список аргументов. По соглашению, в argv должен присутствовать как минимум первый элемент, указывающий на цепочку символов, равную path (или последнему компоненту path). За последним занятым элементом массива argv должен следовать пустой (нулевой) указатель.

Массив envp содержит указатели на цепочки символов, ограниченные нулевыми байтами. Эти цепочки образуют окружение нового процесса. За последним занятым элементом массива envp должен следовать пустой указатель.

Перед началом выполнения любой программы во внешнюю переменную environ, описание которой выглядит как

Extern char **environ;

помещается адрес массива указателей на цепочки символов, образующие окружение процесса. С помощью этой переменной (как и с помощью аргумента envp функции main) в новом процессе всегда можно получить доступ к окружению, независимо от использовавшегося варианта системного вызова exec. Разница лишь в том, что в случае вызовов execle и execve окружение нового процесса задается явно, а в остальных случаях наследуется у вызвавшего процесса.

Файлы, открытые в вызвавшем процессе, остаются открытыми в новом процессе, за исключением тех, у которых установлен флаг "закрыть при выполнении вызова exec" [см. fcntl(2) ]. Если файл остался открытым, то указатель текущей позиции в файле сохраняется.

Реакция на сигналы сохраняется, за исключением того, что сигналы, которые перехватывались в вызвавшем процессе, вызывают терминирование нового процесса [см. signal(2) ].

В случае, когда реакция на сигналы устанавливалась вызовом sigset(2) и задавалась как SIG_DFL, SIG_IGN или SIG_HOLD, эта реакция наследуется у вызвавшего процесса. Однако, если сигнал перехватывался, то устанавливается реакция SIG_DFL и все полученные, но не обработанные сигналы этого типа откладываются.

Если у нового выполняемого файла установлен бит разрешения переустанавливать действующий идентификатор пользователя [см. chmod(2) ], то действующий идентификатор пользователя нового процесса устанавливается равным идентификатору владельца нового выполняемого файла. Аналогично, если у нового выполняемого файла установлен бит разрешения переустанавливать действующий идентификатор группы, то действующий идентификатор группы нового процесса устанавливается равным идентификатору группы нового выполняемого файла. Реальный идентификатор пользователя и реальный идентификатор группы нового процесса наследуются у вызвавшего процесса.

Присоединенные разделяемые сегменты памяти не наследуются новым процессом [см. shmop(2) ].

У нового процесса выключено профилирование.

Кроме того, новый процесс наследует у процесса, вызвавшего exec, следующие характеристики:

  1. Значение поправки к приоритету [см. nice(2) ].
  2. Идентификатор процесса.
  3. Идентификатор родительского процесса.
  4. Идентификатор группы процессов.
  5. semadj значения [см. semop(2) ].
  6. Идентификатор группы терминала [см. exit(2) ].
  7. Режим трассировки [см. ptrace(2) ].
  8. Время, оставшееся до срабатывания будильника [см. alarm(2) ].
  9. Текущий рабочий каталог.
  10. Корневой каталог.
  11. Маска режима создания файлов [см. umask(2) ].
  12. Ограничение на размер файла [см. ulimit(2) ].
  13. Счетчики времени, потраченного для обслуживания этого процесса (tms_utime, tms_stime, tms_cutime, tms_cstime) [см. times(2) ].
  14. Блокировки доступа к сегментам файлов [см. fcntl(2) и lockf(3C) ].

Системный вызов exec завершается неудачей и управление возвращается, если выполнено хотя бы одно из следующих условий: Компонент маршрута нового выполняемого файла не существует. Компонент маршрута нового выполняемого файла не является каталогом. Один из каталогов, перечисленных в маршруте нового выполняемого файла, не доступен для просмотра. Новый выполняемый файл не является обычным файлом. Нет прав на выполнение нового файла. Права на выполнение нового файла есть, но его заголовок не начинается с допустимого магического числа [см. a.out(4)]. Новый выполняемый файл в настоящий момент открыт для записи некоторым процессом. Новый процесс требует больше памяти, чем позволяет системное ограничение MAXMEM. Суммарная длина списка аргументов превышает системное ограничение, равное 5120 байт. Отсутствует требуемое оборудование. Некорректные адреса в качестве аргументов. Не хватает памяти. К требуемой разделяемой библиотеке не разрешен доступ на выполнение. Попытка непосредственно выполнить разделяемую библиотеку. Во время выполнения вызова exec перехвачен сигнал. Аргумент path указывает на удаленный компьютер, связи с которым в данный момент нет. Компоненты path требуют многократного обращения к удаленным компьютерам.

int execle(char *fname, char *arg0, ..., char *argN, NULL, char *envp)

int execlp(char *fname, char *arg0, ..., char *argN, NULL)

int execlpe(char *fname, char *arg0, ..., char *argN, NULL, char *envp)

int execv(char *fname, char *arg)

int execve(char *fname, char *arg, char *envp)

int execvp(char *fname, char *arg)

int execvpe(char *fname, char *arg, char *envp)

Описание:

Эти функции не определены стандартом ANSI С.

Группа функций exec используется для выполнения другой программы. Эта другая программа, называемая процессом-потомком (child process), загружается поверх программы, содержащей вызов exec. Имя файла, содержащего процесс-потомок, задано с помощью параметра fname. Какие-либо аргументы, передаваемые процессу-потомку, задаются либо с помощью параметров от arg0 до argN, либо с помощью массива arg. Параметр envp должен указывать на строку окруже­ния. (Аргументы, на которые указывает argv в процессе-потомке.)

Если fname не содержит расширения или точки, то поиск сначала производится по имени файла. При неудаче добавляется расширение ЕХЕ и поиск повторяется. При неудаче использует­ся расширение СОМ и поиск опять повторяется. Если же расширение указывается, то осуществля­ется поиск только на предмет точного совпадения. Наконец, если имеется точка, но расширение не указано, то поиск осуществляется по левой части имени файла.

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

Функции, имеющие в качестве суффикса р, ищут процесс-потомок в каталогах, заданных коман­дой PATH. Если же суффикс р отсутствует, то поиск осуществляется только в текущем каталоге.

Если задан суффикс l, то значит, аргументы передаются процессу-потомку индивидуально, а не массивом. Этот метод используется при передаче фиксированного числа аргументов. Следует обратить внимание, что последний аргумент должен быть NULL. (NULL определен в stdio.h .)

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

Суффикс е указывает, что процессу-потомку будет передана одна или более строк окружения. Параметр envp представляет собой массив указателей на строки. Каждая строка, на которую ука­зывает массив, должна иметь следующий вид: переменная_окружения = значение

Последний указатель в массиве должен быть NULL. Если же первый элемент массива является NULL, то процесс-потомок сохраняет то же самое окружение, что и процесс-предок.

Важно помнить, что файлы, открытые при вызове exec, являются также открытыми в програм­ме-потомке.

В случае успеха функция exec не возвращает значения. При неудаче возвращается значение -1, а переменная errno устанавливается равной одному из следующих значений:

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

/* первый файл - родитель */
#include
#include
#include
int main(void )
{
execl("test.exe" , "test.exe" , "hello" , "10" , NULL) ;
return 0 ;
}

/* второй файл - потомок */
#include
#include
int main(int argc, char * argv )
{
printf ("This program is executed with these command line " ) ;
printf ("arguments: " ) ;
printf (argv[ 1 ] ) ;
printf (" %d" , atoi (argv[ 2 ] ) ) ;
return 0 ;
}

Анализ жизненного цикла процесса, запускаемого операционной системой UNIX

Одна из многочисленных обязанностей системного администратора – это обеспечивать правильный запуск программ пользователей. Эта задача усложняется наличием в системе других одновременно выполняющихся программ. По разным причинам эти программы могут не работать, зависать или быть неисправными. Понимание процесса создания, управления и уничтожения этих заданий в операционной системе UNIX® является важнейшим шагом к созданию более надежной системы.

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

Основным компоновочным блоком управления этих программ является процесс. Процесс – это имя, присвоенное программе, выполняемой операционной системой. Если вы знаете команду ps , вам должен быть знаком список процессов, такой как в .

Листинг1. Вывод команды ps
sunbox#ps -ef UID PID PPID C STIME TTY TIME CMD root 0 0 0 20:15:23 ? 0:14 sched root 1 0 0 20:15:24 ? 0:00 /sbin/init root 2 0 0 20:15:24 ? 0:00 pageout root 3 0 0 20:15:24 ? 0:00 fsflush daemon 240 1 0 20:16:37 ? 0:00 /usr/lib/nfs/statd ...

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

Существование родительского PID (PPID), означает, что один процесс создается другим процессом. Исходный процесс, который запускается в системе, называется init , и ему всегда присваивается PID 1. init - это первый действительный процесс, запускаумый ядром при загрузке. Основная задача init запуск всей системы. init и другие процессы с PPID 0 являются процессами ядра.

Использование системного вызова fork

Системный вызов fork(2) создает новый процесс. В показан fork используемый в простом примере C-кода.

Листинг 2. Простое применение fork(2)
sunbox$ cat fork1.c #include #include int main (void) { pid_t p; /* fork returns type pid_t */ p = fork(); printf("fork returned %d\n", p); } sunbox$ gcc fork1.c -o fork1 sunbox$ ./fork1 fork returned 0 fork returned 698

Код в fork1.c просто вызывает fork и отображает целочисленный результат выполения fork через вызов printf . Делается только один вызов, но вывод отображается дважды. Это происходит потому, что новый процесс создается в рамках вызова fork . После вызова возвращаются два отдельных процесса. Это часто называют "вызванный единожды, возвращается дважды."

Возвращаемые fork значения очень интересны. Одно из них - 0; другое – ненулевое значение. Процесс, который получает 0, называется порожденным процессом , а ненулевое значение достается исходному процессу, который является родительским процессом . Вы используете возвращаемые значения, для того чтобы определить, где какой процесс. Поскольку оба процесса возобновляют выполнение в одной и той же области, единственный возможный дифференциатор это возвращаемые значения fork .

Логическим основанием для нулевого и ненулевого возвращаемого значения служит то, что порожденный процесс всегда может вычислить своего родителя с помощью запроса getppid(2) , однако родителю намного сложнее определить всех своих потомков. Таким образом, родитель узнает о своем новом потомке, и потомок при необходимости может отыскать своего родителя.

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

Листинг 3. Более полный пример использования fork
sunbox$ cat fork2.c #include #include int main (void) { pid_t p; printf("Original program, pid=%d\n", getpid()); p = fork(); if (p == 0) { printf("In child process, pid=%d, ppid=%d\n", getpid(), getppid()); } else { printf("In parent, pid=%d, fork returned=%d\n", getpid(), p); } } sunbox$ gcc fork2.c -o fork2 sunbox$ ./fork2 Original program, pid=767 In child process, pid=768, ppid=767 In parent, pid=767, fork returned=768
Листинг 6. Родительский процесс умирает раньше потомка
#include #include int main(void) { int i; if (fork()) { /* Родитель */ sleep(2); _exit(0); } for (i=0; i < 5; i++) { printf("My parent is %d\n", getppid()); sleep(1); } } sunbox$ gcc die1.c -o die1 sunbox$ ./die1 My parent is 2920 My parent is 2920 sunbox$ My parent is 1 My parent is 1 My parent is 1

В этом примере родительский процесс вызывает fork , ждет две секунды и завершается. Порожденный процесс продолжается, распечатывая PID своего родителя в течение пяти секунд. Вы можете видеть, что когда родитель умирает, PPID изменяется на 1. Также интересно возвращение управления командному процессору. Поскольку порожденный процесс выполняется в фоне, как только родитель умирает, управление возвращается к командному процессору.

Потомок умирает раньше родителя

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

Листинг 7. Порожденный процесс умирает раньше родительского
sunbox$ cat die2.c #include #include int main(void) { int i; if (!fork()) { /* Потомок немедленно завершается*/ _exit(0); } /* Родитель ждет около минуты*/ sleep(60); } sunbox$ gcc die2.c -o die2 sunbox$ ./die2 & 2934 sunbox$ ps -ef | grep 2934 sean 2934 2885 0 21:43:05 pts/1 0:00 ./die2 sean 2935 2934 0 - ? 0:00 sunbox$ ps -ef | grep 2934 + Exit 199 ./die2

die2 выполняется в фоновом режиме, используя оператор & , после этого на экран выводится список процессов, отображая только выполняемый процесс и его потомков. PID 2934 – родительский процесс, PID 2935 – процесс, который создается и немедленно завершается. Несмотря на преждевременный выход, порожденный процесс все еще находится в таблице процессов, уже как умерший процесс, который еще называется зомби . Когда через 60 секунд родитель умирает, оба процесса завершаются.

Когда порожденный процесс умирает, его родитель информируется при помощи сигнала, который называется SIGCHLD . Точный механизм всего этого сейчас не имеет значения. Что действительно важно, так это то, что родитель должен как-то узнать о смерти потомка. С момента смерти потомка и до того момента как родитель получает сигнал, потомок находится в состоянии зомби. Зомби не выполняется и не потребляет ресурсов CPU; он только занимает пространство в таблице процессов. Когда родитель умирает, ядро наконец-то может убрать потомков вместе с родителем. Значит, единственный способ избавиться от зомби - это убить родителя. Лучший способ справиться с зомби – гарантировать, что они не окажутся на первом месте. Код в описывает обработчик сигналов, для работы с входящим сигналом SIGCHLD .

Листинг 8. Обработчик сигналов в действии
#include #include #include #include void sighandler(int sig) { printf("In signal handler for signal %d\n", sig); /* wait() это основное для подтверждения SIGCHLD */ wait(0); } int main(void) { int i; /* Установить обработчик сигнала к SIGCHLD */ sigset(SIGCHLD, &sighandler); if (!fork()) { /* Потомок */ _exit(0); } sleep(60); } sunbox$ gcc die3.c -o die3 sunbox$ ./die3 & 3116 sunbox$ In signal handler for signal 18 ps -ef | grep 3116 sean 3116 2885 0 22:37:26 pts/1 0:00 ./die3

Немного сложнее, чем предыдущий пример, поскольку там есть функция sigset , которая устанавливает указатель функции на обработчик сигнала. Всякий раз, когда процесс получает обработанный сигнал, вызывается функция, заданная через sigset . Для сигнала SIGCHLD , приложение должно вызвать функцию wait(3c) для того, чтобы подождать завершения порожденного процесса. Поскольку процесс уже завершен, это необходимо для того, чтобы ядро получило подтверждение о смерти потомков. На самом деле, родителю следовало бы сделать больше, чем просто подтвердить сигнал. Ему следовало бы также очистить данные потомка.

После выполнения die3 , проверяется список процессов. Обработчик сигнала получает значение 18 (SIGCHLD), подтверждение о завершении потомка сделано, и родитель возвращается в состояние ожидания sleep(60) .

Краткие выводы

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

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

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



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: