ошибке (естественно, в этом случае возврат происходит только в процесс, выполнивший системный вызов, и новый процесс не создаётся).
Пример программы, порождающей новый процесс, приведён в листинге 2.
Листинг 2. Порождение нового процесса
1.#include <stdio.h>
2.#include <unistd.h>
3.
4.int main(void)
5.{
6.int pid;
7.pid = fork();
8.if (pid == -1)
9.{
10.perror(“fork”); exit(1);
11.}
12.if (pid == 0)
13.{
14./*Эта часть кода выполняется дочерним процессом*/
15.printf(“Потомок\n”);
16.sleep(1);
17.}
18.else
19.{
20./*Эта часть кода выполняется родительским
21.процессом*/
22.printf(“Родитель. Создан потомок %u\n”, pid);
23.sleep(1);
24.}
25.return (0);
26.}
В строках 1-2 подключаются необходимые заголовочные файлы. В данном случае нам требуются функции ввода-вывода (stdio.h) и функции по работе с процессами (unistd.h). Порождение процесса происходит в строке 7, после чего проверяется возвращаемое значение функции fork. Если новый процесс создан, то условие в строке 8 не выполнится, и каждый процесс перейдёт на строку 12. Далее, проверив значение переменной pid, дочерний процесс будет выполнять строки 13-17 (так как в его случае pid = 0), а родительский – строки 19-24. Затем оба процесса завершатся с кодом 0 (строка 25).
Особое внимание следует обратить на строки 16 и 22, в которых родительский и дочерний процессы добровольно переходят в «спящее» состояние на 1 секунду. Для чего это сделано? Ответ прост – для того чтобы оба процесса смогли вывести информацию в поток вывода, и она была отображена на экране терминала. Если этого не сделать, то может возникнуть
66
ситуация, когда один из процессов (в силу алгоритмов планирования) завершит свою работу быстрее, чем другой начнёт выводить информацию в поток вывода. В этом случае завершившийся процесс автоматически закроет все открытые файлы, которыми в нашем случае являются потоки ввода-вывода. В силу того, что файловые дескрипторы у родственных процессов одинаковые, ещё не завершившийся процесс начнёт выдавать информацию в поток, у которого соответствующий файл уже закрыт. И на экран никакой информации выдано не будет.
5.6.2. Запуск новых программ
Очевидно, что простое порождение нового процесса, который бы исполнял точно такой же код, как и родительский процесс, смысла особого не имеет. Важно реализовать возможность выполнять в новом процессе другую программу. Для того чтобы загрузить исполняемый код в адресное пространство50 процесса и настроить его на выполнение новой программы, используются функции семейства exec:
#include <unistd.h>
int execl (const char *path, const char *arg0, ..., const char *argn, char * /*NULL*/);
int execv (const char *path, char *const argv[]); int execle (const char *path,char *const arg0[], ...,
const char *argn, char * /*NULL*/, char const envp[]);
int execve (const char *path, char *const argv[], char *const envp[]);
int execlp (const char *file, const char *arg0, ..., const char *argn, char * /*NULL*/) ;
int execvp (const char *file, char *const argv[]);
На рисунке 6 показано, как связаны между собой приведённые выше функции.
50 Обратите внимание, что вызовы fork и exec никак не связаны между собой. Заменить исполняемую программу можно и в текущем процессе, не порождая новый процесс.
67
Рисунок 6. Связь функций семейства exec
Всё множество системных вызовов exec выполняет одну и ту же функцию: они преобразуют вызывающий процесс, загружая новую программу в его пространство памяти, и настраивают дескриптор процесса так, чтобы загруженная программа начала выполняться с начала. Если вызов exec завершился успешно, то вызывающая программа полностью замещается новой программой. Важно отметить, что вызов exec не создаёт новый процесс, который выполняется одновременно с вызывающим, а вместо этого новая программа загружается на место старой. Поэтому, в отличие от вызова fork, успешный вызов exec не возвращает никакого значения.
Базовой функцией в указанном семействе является вызов execve(), который принимает три параметра:
полный путь к исполняемому файлу, который необходимо загрузить в память и начать выполнять;
список аргументов, которые передаются исполняемому файлу;
список переменных среды окружения нового процесса.
Списки аргументов и переменных среды окружения передаются как массивы указателей на строки, в которых последний элемент содержит NULL (нуль). Такой формат используется для того, чтобы передавать только массивы и не указывать количество элементов в них.
Остальные функции являются надстройками над execve() и позволяют программисту либо указывать параметры в другом виде, либо какие-либо параметры не указывать вообще. Функция execv() позволяет не указывать переменные среды окружения нового процесса, а использовать копию переменных окружения текущего процесса. Аналогично поступают и функции execvp() execl(), execlp(). Функции execle(), execlp() и execl() позволяют указать аргументы, передаваемые новому процессу не как массив, а как перечень параметров функции. Функции execvp() и execlp() позволяют указывать не полный путь к исполняемому файлу, а только его имя. Полный путь будет определён функцией, исходя из значения переменной среды окружение PATH.
Резюмируя, можно сказать, что имена функций определяют перечень и типы параметров, которые они принимают. Буква «v» в имени функции означает, что аргументы, передаваемые новому процессу, задаются как массив
68
указателей на строки с завершающим элементом, содержащим NULL. Буква «l»
– эти аргументы перечисляются как параметры функции. Буква «p» означает, что первым параметром может быть задано только имя исполняемого файла, и полный путь будет сформирован самой функцией.
Пример использования вызова execl приведён в листинге 3.
Листинг 3. Загрузка новой программы в текущий процесс
1. #include <unistd.h>
2.
3.int main(void)
4.{
5.printf (“Запуск программы ls\n”);
6.execl (“/bin/ls”, “ls”, “-l”, (char *)0);
7.perror (“Ошибка вызова execl”);
8.return (1);
9.}
Обратите внимание, что в приведённом примере за вызовом execl следует безусловный вызов функции perror. Это отражает то, что успешный вызов execl прекращает выполнение текущей программы, а в случае возникновения ошибки выполнение текущей программы продолжается. В приведённом примере «старая» программа всегда завершается с кодом возврата 1 (строка 8), что сигнализирует о возникшей ошибке.
Для того чтобы совместить действия по порождению нового процесса и по запуску программы, можно использовать вызов system:
#include <unistd.h>
int system (char * path);
Пример использования вызова system приведён в листинге 4. В данном примере программа выводит на экран фразу «Запуск программы ls» (строка 4), затем запускает интерпретатор sh с параметрами «–c ls» (строка 5). При этом вызов system выполняет следующие действия: порождает новый процесс (fork), загружает туда файл /bin/sh, настраивает его на выполнение команды ls, запускает его и дожидается завершения. Текущий процесс при этом никак не изменяется.
Листинг 4. Запуск программы с использованием system
1.#include <unistd.h>
2.#include <stdio.h>
3.
4.int main(void){
5.printf (“Запуск программы ls\n”);
6.system (“ls”);
7.return (0);
69
Листинг 4. Запуск программы с использованием system
8. }
5.6.3. Создание потока (нити)
По сути, нить – это некоторая функция программы, которая должна выполняться параллельно. «Создать нить» значит сообщить операционной системе, какую из функций программы требуется выполнять отдельно и как при этом производить планирование использования ресурсов.
По стандарту POSIX для создания нити используется функция pthread_create, заголовок которой следующий:
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr, void* (*start)(void *), void *arg)
Первый параметр thread этой функции – это указатель на переменную типа pthread_t, в которую будет записан идентификатор созданной нити. По сути, это аналог PID.
Второй параметр attr задаёт набор некоторых свойств создаваемой нити, описывающей правила планирования исполнения нити, её окружение (стек и т.п.) и поведение нити при завершении (удалять ли информацию сразу или ожидать вызова pthread_join). Если требуется запустить нить с параметрами «по умолчанию», то в качестве этого параметра следует передать NULL.
Параметр start – это указатель на функцию, возвращающую значение типа void* и принимающую в качестве аргумента значение типа void*. Именно эту функцию и начинает выполнять вновь созданная нить, при этом в качестве аргумента этой функции передаётся четвёртый аргумент вызова pthread_create.
Функция pthread_create возвращает нулевое значение в случае успеха и ненулевой код ошибки в случае неудачи.
Пример использования функции pthread_create приведён в листинге 5.
Листинг 5. Запуск двух нитей
1.#include <stdio.h>
2.#include <pthread.h>
3.
4.void * my_thread (void * arg){
5.char ch;
6.ch = *((char *)arg);
7.while (1) printf (“%c”, ch);
8.return (NULL);
9.}
10.
11. int main(void)
70