Программирование для Linux. Профессиональный подход.

3.2. Создание процессов.

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

3.2.1. Функция system().

Функция system() определена в стандартной библиотеке языка С и позволяет вызывать из программы системную команду, как если бы она была набрана в командной строке. По сути, эта функция запускает стандартный интерпретатор Bourne shell ( /bin/sh) и передает ему команду на выполнение. Например, программа, представленная в листинге 3.2, вызывает команду ls -l /, отображающую содержимое корневого каталога.

Листинг 3.2. (system.c) Использование функции system().

#include <stdlib.h>

Int main() {

 int return_value;

 return_value = system("ls -l /");

 return return_value;

}

Функция system() возвращает код завершения указанной команды. Если интерпретатор не может быть запущен, возвращается значение 127, а в случае возникновения других ошибок — -1.

Поскольку функция system() запускает интерпретатор команд, она подвержена всем тем ограничениям безопасности, что и системный интерпретатор. Рассчитывать на наличие какой-то конкретной версии Bourne shell не приходится. В большинстве UNIX-систем программа /bin/sh представляет собой символическую ссылку на другой интерпретатор. В Linux — это bash (Bourne-Again SHell), причем в разных дистрибутивах присутствуют разные его версии. Вызов из функции system() программы с привилегиями пользователя root также может иметь неодинаковые последствия в разных системах. Таким образом, лучше создавать процессы с помощью функций fork() и exec().

3.2.2. Функции fork() и exec().

В DOS и Windows API имеется семейство функций spawn(). Они принимают в качестве аргумента имя программы, создают новый экземпляр ее процесса и запускают его. В Linux нет функции, которая делала бы все это за один заход. Вместо этого имеется функция fork(), создающая дочерний процесс, который является точной копией родительского процесса, и семейство функций exec(), заставляющих требуемый процесс перестать быть экземпляром одной программы и превратиться в экземпляр другой программы. Чтобы создать новый процесс, нужно сначала с помощью функции fork() создать копню текущего процесса, а затем с помощью функции exec() преобразовать одну из копий в экземпляр запускаемой программы.

Вызов функции fork().

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

Как же различить между собой оба процесса? Во-первых, дочерний процесс — это новый, только что появившийся в системе процесс, поэтому его идентификатор отличается от идентификатора родительского процесса. Таким образом, программа может вызвать функцию getpid() и узнать, где именно она находится. Но сама функция fork() реализует другой способ: она возвращает разные значения в родительском и дочернем процессах. Родительский процесс получает идентификатор своего потомка, а дочернему процессу возвращается 0. В системе нет процессов с нулевым идентификатором, так что программа легко разбирается в ситуации.

В листинге 3.3 приведен пример ветвления программы с помощью функции fork(). Учтите, что первая часть инструкции if выполняется только в родительском процессе, тогда как ветвь else — только в дочернем.

Листинг 3.3. (fork.c) Ветвление программы с помощью функции fork().

#include <stdio.h>

#include <sys/types.h>

#include <unistd.h>

Int main() {

 pid_t child_pid;

 printf("The main program process ID is %d\n",

  (int)getpid());

 child_pid = fork();

 if (child_pid != 0) {

  printf("This is the parent process, with ID %d\n",

   (int)getpid());

  printf("The child's process ID is %d\n", (int)child_pid);

 } else.

  printf("This is the child process, with ID %d\n",

   (int)getpid());

 return 0;

}

Семейство функций 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().

Совместное использование функций fork() и exec().

Стандартная методика запуска одной программы из другой такова: сначала с помощью функции fork() создается дочерний процесс, затем в нем вызывается функция exec(). Это позволяет главной программе продолжать выполнение в родительском процессе.

Программа, показанная в листинге 3.4, отображает содержимое корневого каталога с помощью команды ls, как и программа в листинге 3.2. Но на этот раз команда ls вызывается не из интерпретатора, а напрямую; ей передаются аргументы -l и /.

Листинг 3.4. (fork-exec.с) Совместное использование функций fork() и exec().

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <unistd.h>

/* Запуск дочернего процесса в виде новой программы. Параметр.

   PROGRAM — это имя вызываемой программы; ее поиск будет.

   осуществляться в каталогах, определяемых переменной среды PATH.

   Параметр ARG_LIST -- это список строковых аргументов,

   передаваемых программе (должен оканчиваться указателем NULL).

   Функция возвращает идентификатор порожденного процесса. */

Int spawn(char* program, char** arg_list) {

 pid_t child_pid;

 /* Создание копии текущего процесса. */

 child_pid = fork();

 if (child_pid != 0).

  /* Это родительский процесс. */

  return child_pid;

 else {

  /* Выполнение указанной программы. */

  execvp(program, arg_list);

  /* Функция execvp() возвращает значение только в случае.

    ошибки. */

  fprintf(stderr, "an error occurred in execvp\n");

  abort();

 }

}

Int main() {

 /* Список аргументов, передаваемых команде ls. */

 char* arg_list[] = {

  "ls", /* argv[0] -- имя программы. */

  "-l",

  NULL /* Список аргументов должен оканчиваться указателем.

          NULL. */

 };

 /* Порождаем дочерний процесс, который выполняет команду ls.

    Игнорируем возвращаемый идентификатор дочернего процесса. */

 spawn("ls", arg_list);

 printf("done with main program\n");

 return 0;

}

3.2.3. Планирование процессов.

Операционная система Linux планирует работу родительских и дочерних процессов независимо друг от друга. Нет гарантии, что один процесс будет запущен раньше другого. и неизвестно, как долго один процесс будет выполняться, прежде чем Linux прервет его работу и передаст управление другому процессу. В частности, к моменту завершения родительского процесса может оказаться, что команда ls еще не выполнена, выполнена частично или уже закончила свою работу.[11] Linux лишь гарантирует, что любой процесс когда-нибудь получит свой "кусочек пирога": ни один процесс не окажется полностью лишенным доступа к процессору.

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

Для запуска программы с ненулевым фактором уступчивости необходимо воспользоваться командой nice -n. Рассмотрим следующий пример:

% nice -n 10 sort input.txt > output.txt.

Здесь активизируется длительная операция сортировки, которая, благодаря пониженному приоритету, не приведет к сильному снижению производительности системы. Изменить фактор уступчивости выполняющегося процесса позволяет команда renice.

Если требуется менять фактор уступчивости программным путем, воспользуйтесь функцией nice(). Ее аргумент — это величина приращения, добавляемая к фактору уступчивости вызывающего процесса. В результате приоритет процесса снижается.

Только программа с привилегиями пользователя root может запускать процессы с отрицательным фактором уступчивости или понижать это значение у выполняющегося процесса. Это означает, что вызывать команды nice и renice с отрицательными аргументами можно, лишь войдя в систему как пользователь root, и только процесс, выполняемый от имени суперпользователя, может передавать функции nice() отрицательное значение. Таким образом, обычные пользователи не могут помешать работать процессам других пользователей и монополизировать системные ресурсы.