Изучение создания процесса UNIX. Man execlp (3): запуск файла на исполнение

Анализ жизненного цикла процесса, запускаемого операционной системой 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. Возможность прослеживать дерево процессов для какого-то отдельного процесса, позволяет отследить любое приложение до процесса

Семейство функций exec()

Функции семейства exec() заменяют программу, выполняющуюся в текущем процессе, другой программой. Когда программа вызывает функцию exec() , ее выполнение немедленно прекращается и начинает работу новая программа.

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

Функции, в названии которых присутствует суффикс "p"(execvp() и execlp()), принимают в качестве аргумента имя программы и ищут эту программу в каталогах, определяемых переменном среды PATH . Всем остальным функциям нужно передавать полное путевое имя программы.

Функции, в названии которых присутствует суффикс "v"(execv() , execvp() и execve()), принимают список аргументов программы в виде массива строковых указателей, оканчивающегося NULL -указателем. Функции с суффиксом "l"(execl() , execlp() и execle()) принимают список аргументов переменного размера.

Функции, в названии которых присутствует суффикс "e"(execve() и execle()), в качестве дополнительного аргумента принимают массив переменных среды. Этот массив содержит строковые указатели и оканчивается пустым указателем. Каждая строка должна иметь вид "ПЕРЕМЕННАЯ =значение " .

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

Список аргументов, передаваемых программе, аналогичен аргументам командной строки, указываемым при запуске программы в интерактивном режиме. Их тоже можно получить с помощью параметров argc и argv функции main() . Не забывайте, когда программу запускает интерпретатор команд, первый элемент массива argv будет содержать имя программы, а далее будут находиться переданные программе аргументы. Аналогичным образом следует поступить, формируя список аргументов для функции exec() .

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 ;
}

The exec system call is used to execute a file which is residing in an active process. When exec is called the previous executable file is replaced and new file is executed.

More precisely, we can say that using exec system call will replace the old file or program from the process with a new file or program. The entire content of the process is replaced with a new program.

The user data segment which executes the exec() system call is replaced with the data file whose name is provided in the argument while calling exec().

The new program is loaded into the same process space. The current process is just turned into a new process and hence the process id PID is not changed, this is because we are not creating a new process we are just replacing a process with another process in exec.

If the currently running process contains more than one thread then all the threads will be terminated and the new process image will be loaded and then executed. There are no destructor functions that terminate threads of current process.

PID of the process is not changed but the data, code, stack, heap, etc. of the process are changed and are replaced with those of newly loaded process. The new process is executed from the entry point.

Exec system call is a collection of functions and in C programming language, the standard names for these functions are as follows:

  1. execl
  2. execle
  3. execlp
  4. execv
  5. execve
  6. execvp

It should be noted here that these functions have the same base exec followed by one or more letters. These are explained below:

e: It is an array of pointers that points to environment variables and is passed explicitly to the newly loaded process.

l: l is for the command line arguments passed a list to the function

p: p is the path environment variable which helps to find the file passed as an argument to be loaded into process.

v: v is for the command line arguments. These are passed as an array of pointers to the function.

Why exec is used?

exec is used when the user wants to launch a new file or program in the same process.

Inner Working of exec

Consider the following points to understand the working of exec:

  1. Current process image is overwritten with a new process image.
  2. New process image is the one you passed as exec argument
  3. The currently running process is ended
  4. New process image has same process ID, same environment, and same file descriptor (because process is not replaced process image is replaced)
  5. The CPU stat and virtual memory is affected. Virtual memory mapping of the current process image is replaced by virtual memory of new process image.

Syntaxes of exec family functions:

The following are the syntaxes for each function of exec:

int execl(const char* path, const char* arg, …)
int execlp(const char* file, const char* arg, …)
int execle(const char* path, const char* arg, …, char* const envp)
int execv(const char* path, const char* argv)
int execvp(const char* file, const char* argv)
int execvpe(const char* file, const char* argv, char *const envp)

Description:

The return type of these functions is Int. When the process image is successfully replaced nothing is returned to calling function because the process that called it is no longer running. But if there is any error -1 will be returned. If any error is occurred an errno is set.

  1. path is used to specify the full path name of the file which is to be executes.
  1. arg is the argument passed. It is actually the name of the file which will be executed in the process. Most of the times the value of arg and path is same.
  1. const char* arg in functions execl(), execlp() and execle() is considered as arg0, arg1, arg2, …, argn. It is basically a list of pointers to null terminated strings. Here the first argument points to the filename which will be executed as described in point 2.
  1. envp is an array which contains pointers that point to the environment variables.
  1. file is used to specify the path name which will identify the path of new process image file.
  1. The functions of exec call that end with e are used to change the environment for the new process image. These functions pass list of environment setting by using the argument envp . This argument is an array of characters which points to null terminated String and defines environment variable.

To use the exec family functions, you need to include the following header file in your C program:

#include

Example 1: Using exec system call in C program

Consider the following example in which we have used exec system call in C programming in Linux, Ubuntu: We have two c files here example.c and hello.c:

example.c

#include
#include
#include

{
printf ("PID of example.c = %d\n " , getpid() ) ;
char * args = { "Hello" , "C" , "Programming" , NULL} ;
execv("./hello" , args) ;
printf ("Back to example.c" ) ;
return 0 ;
}

hello.c

PID of example.c = 4733
We are in Hello.c
PID of hello.c = 4733

In the above example we have an example.c file and hello.c file. In the example .c file first of all we have printed the ID of the current process (file example.c is running in current process). Then in the next line we have created an array of character pointers. The last element of this array should be NULL as the terminating point.

Then we have used the function execv() which takes the file name and the character pointer array as its argument. It should be noted here that we have used ./ with the name of file, it specifies the path of the file. As the file is in the folder where example.c resides so there is no need to specify the full path.

When execv() function is called, our process image will be replaced now the file example.c is not in the process but the file hello.c is in the process. It can be seen that the process ID is same whether hello.c is process image or example.c is process image because process is same and process image is only replaced.

Then we have another thing to note here which is the printf() statement after execv() is not executed. This is because control is never returned back to old process image once new process image replaces it. The control only comes back to calling function when replacing process image is unsuccessful. (The return value is -1 in this case).

Difference between fork() and exec() system calls:

The fork() system call is used to create an exact copy of a running process and the created copy is the child process and the running process is the parent process. Whereas, exec() system call is used to replace a process image with a new process image. Hence there is no concept of parent and child processes in exec() system call.

In fork() system call the parent and child processes are executed at the same time. But in exec() system call, if the replacement of process image is successful, the control does not return to where the exec function was called rather it will execute the new process. The control will only be transferred back if there is any error.

Example 2: Combining fork() and exec() system calls

Consider the following example in which we have used both fork() and exec() system calls in the same program:

example.c

#include
#include
#include
int main(int argc, char * argv )
{
printf ("PID of example.c = %d\n " , getpid() ) ;
pid_t p;
p = fork() ;
if (p==- 1 )
{
{
printf ("We are in the parent process" ) ;
}
return 0 ;
}

hello.c:

PID of example.c = 4790
We are in Parent Process
We are in Child Process
Calling hello.c from child process
We are in hello.c
PID of hello.c = 4791

In this example we have used fork() system call. When the child process is created 0 will be assigned to p and then we will move to the child process. Now the block of statements with if(p==0) will be executed. A message is displayed and we have used execv() system call and the current child process image which is example.c will be replaces with hello.c. Before execv() call child and parent processes were same.

It can be seen that the PID of example.c and hello.c is different now. This is because example.c is the parent process image and hello.c is the child process image.