
учебник
.pdfcout<< "id process PID: " <<getpid() <<"\n";
cout<< "id parent process PPID: " <<getppid()<<" \n"; cout<< "id group process PGID: " <<getpgrp()<<"\n"; cout<< "user real id UID: " <<getuid() <<" \n"; cout<< "real id group user -GID:" <<getgid()<<" \n"; cout<< "effect id user UID: " <<getuid()<<"\n"; cout<< "effect id group GID: " <<getgid()<<"\n";
}
int main()
{
int i,st;
signal(SIGCHLD, SIG_IGN); for(i=1;i<=3;i++)
{
fork();
mpinfo(); cout<<”************”<<”\n”;//
wait(&st);// ожидание завершения процесса exit(0);
}
}
При запуске данной программы мы видим, что у каждого процесса есть несколько типов идентификаторов, основные типы – это реальные и эффективные. Реальный идентификатор пользователя (или группы) сообщает, кто создал процесс, а эффективный идентификатор пользователя (или группы) сообщает, от чьего лица выполняется процесс, если эта информация изменяется. На примере GID и effectid user обычно не меняются, потому что запускаете программу и выполняете ее только вы (1000). Также стоит обратить внимание на идентификаторы PPID (идентификатор процесса родителя) у процессапотомка: это значение равно PID родителя. При этом существует еще один тип идентификаторов IDGROUPPROCESS, родители и потомки относятся к одной группе процессов.
51
3.5 Компиляция программ на языке Си в UNIX и запуск их на исполнение
Для компиляции программ в Linux применяются компиляторы gcc и g++. Для нормальной работы компилятора необходимо, чтобы исходные файлы, содержащие текст программы, имели имена, заканчивающиеся на .c.
В простейшем случае откомпилировать программу можно, запуская компилятор командой
g++ имя_исходного_файла.
Если программа была написана без ошибок, то компилятор создаст исполняемый файл с именем a.out.
Изменить имя создаваемого исполняемого файла можно, задав его с помощью опции -o:
g++ имя_исходного_файла -o имя_исполняемого_файла.
Компилятор g++ имеет несколько сотен возможных опций. Получить информацию о них возможно в UNIX Manual.
Запустить программу на исполнение можно, набрав имя исполняемого файла и нажав клавишу <Enter>.
3.6 Создание процесса в UNIX. Системный вызов fork()
Программное порождение процессов осуществляется с использованием системного вызова fork() – после выполнения этого вызова в системе появляется процесс, который является почти точной копией процесса, выдавшего данный системный вызов. У порожденного процесса по сравнению с родительским процессом (на уровне уже полученных знаний) изменяются значения следующих параметров:
идентификатор процесса – PID;
идентификатор родительского процесса – PPID. Дополнительно может измениться поведение порожденного процесса по отношению к некоторым сигналам.
52
Системный вызов для порождения нового процесса
Прототип системного вызова
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
Описание системного вызова
Процесс, который инициировал системный вызов fork, принято называть родительским процессом (parent process). Вновь порожденный процесс принято называть процессом-ребенком (child process). Процесс-ребенок является почти полной копией родительского процесса. У порожденного процесса по сравнению с родительским изменяются значения следующих параметров:
идентификатор процесса;идентификатор родительского процесса;
время, оставшееся до получения сигнала SIGALRM ;
сигналы, ожидавшие доставки родительскому процессу, не будут доставляться порожденному процессу.Процесс-родитель и процесс-потомок разделяют один и тот же кодовый сегмент. Системный вызов fork() в случае успеха возвращает родительскому процессу идентификатор потомка, а потомку 0. В ситуации, когда процесс не может быть создан, функция fork() возвращает -1.В процессе выполнения системного вызова fork() порождается копия родительского процесса и возвращение из системного вызова будет происходить уже как в родительском, так и в порожденном процессах. Этот системный вызов является единственным, который вызывается один раз, а при успешной работе возвращается два раза (один раз в процессе-родителе и один раз в процессе-ребенке)! После выхода из системного вызова оба процесса продолжают выполнение регулярного пользовательского кода, следующего за системным вызовом.
Для иллюстрации сказанного рассмотрим следующие примеры программ.
53
Программа 3-02. Пример создания нового процесса с одинаковой работой процессов ребенка и родителя
#include <sys/types.h> #include <unistd.h> #include <stdio.h>
int main()
{
pid_t pid, ppid; int a = 0; (void) fork();
/* При успешном создании нового процесса с этого места псевдопараллельно начинают работать два процесса: старый и новый */
/* Перед выполнением следующего выражения значение переменной a в обоих процессах равно 0 */
a = a+1;
/* Узнаем идентификаторы текущего и родительского процесса (в каждом из процессов!!!) */
pid = getpid(); ppid = getppid();
/* Печатаем значения PID, PPID и вычисленное значение переменной a (в каждом из процессов!!!) */
printf("My pid = %d, my ppid = %d, result = %d\n", (int)pid, (int)ppid, a);
return 0;
}
Листинг 3-02. Программа создания процесса
Программа 3-03. Пример создания нового процесса с распечаткой индентификаторов
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h>
54
#include <cstdlib> #include <iostream> #include <wait.h> using namespace std; int main()
{
pid_t pid; int i;
switch(pid=fork()) { case -1: perror("fork"); exit(1);
case 0:
printf(" CHILD: This is child\n"); printf(" CHILD: PID -- %d\n", getpid());
printf(" CHILD: PID my parent %d\n",getppid()); printf(" CHILD: write code return:"); scanf("%d",&i);
printf(" CHILD: exit! \n"); exit(i);
default:
printf("PARENT: This is parent\n"); printf("PARENT: My Pid -- %d\n", getpid()); printf("PARENT: PID my child %d\n",pid); printf("PARENT: I wait my child \n"); wait(0);
printf("PARENT: Code return my child%d\n",WEXITSTATUS(i)); printf("PARENT: exit! \n");
}
}
Листинг 3-03. Программа создания процесса
Вданном случае процесс разделяется на родительский процесс
ина процесс-потомок, после чего процесс-потомок спрашивает код своего завершения и завершается, все это время процесс-родитель ждет его завершения. После завершения процесса-потомка родитель получает код завершения потомка, выводит его и завершается сам.
55
3.7 Завершение процесса. Функция exit()
Существует два способа корректного завершения процесса в программах, написанных на языке Си. Первый способ мы использовали до сих пор: процесс корректно завершался по достижении конца функции main() или при выполнении оператора return в функции main(). Второй способ применяется при необходимости завершить процесс в каком-либо другом месте программы. Для этого используется функция exit() из стандартной библиотеки функций для языка C. При выполнении этой функции происходит сброс всех частично заполненных буферов ввода-вывода с закрытием соответствующих потоков, после чего инициируется системный вызов прекращения работы процесса и перевода его в состояние «закончил исполнение».
Возврата из функции в текущий процесс не происходит и функция ничего не возвращает.
Значение параметра функции exit() – кода завершения процесса – передается ядру операционной системы и может быть затем получено процессом, породившим завершившийся процесс. На самом деле при достижении конца функции main() также неявно вызывается эта функция со значением параметра 0.
Функция для нормального завершения процесса Прототип функции
#include <stdlib.h> void exit(int status);
Параметры функции main() в языке Си. Переменные среды и аргументы командной строки
У функции main() в языке программирования Си существует три параметра, которые могут быть переданы ей операционной системой.
void main(int argc, char *argv[], char *envp[]);
Если вы наберете команду
$ a.out a1 a2 a3, где a.out – имя запускаемой вами программы, то функция main программы из файла a.out вызовется с:
56
argc = 4 /* количество аргументов */ argv[0] = "a.out" argv[1] = "a1" argv[2] = "a2" argv[3] = "a3"
argv[4] = NULL
По соглашению argv[0] содержит имя выполняемого файла.
Первые два параметра при запуске программы на исполнение командной строкой позволяют узнать полное содержание командной строки. Вся командная строка рассматривается как набор слов, разделенных пробелами. Через параметр argc передается количество слов в командной строке, которой была запущена программа. Параметр argv является массивом указателей на отдельные слова. Так, например, если программа была запущена командой
$ ./a.out 12 abcd; то значение параметра argc будет равно 3, argv[0] будет указывать на имя программы — первое слово — «a.out», argv[1]
— на слово «12»0, argv[2] — на слово «abcd». Так как имя программы всегда присутствует на первом месте в командной строке, то argc всегда больше 0, а argv[0] всегда указывает на имя запущенной программы.
Анализируя в программе содержимое командной строки, мы можем предусмотреть ее различное поведение в зависимости от слов, следующих за именем программы. Таким образом, не внося изменений
втекст программы, мы можем заставить ее работать по-разному от запуска к запуску. Например, компилятор gcc, вызванный командой gcc 1.c будет генерировать исполняемый файл с именем a.out, а при вызове командой gcc 1.c –o 1.exe – файл с именем 1.exe.
Третий параметр – envp – является массивом указателей на параметры окружающей среды процесса. Начальные параметры окружающей среды процесса задаются в специальных конфигурационных файлах для каждого пользователя и устанавливаются при входе пользователя
всистему. В дальнейшем они могут быть изменены с помощью специальных команд операционной системы UNIX. Каждый параметр имеет вид: переменная=строка. Такие переменные используются для изменения долгосрочного поведения процессов, в отличие от аргументов командной строки. Например, задание параметра TERM=vt100 может говорить процессам, осуществляющим вывод на экран дисплея, что работать им придется с терминалом vt100. Меняя значение переменной среды TERM, например на TERM=console, мы сообщаем таким процессам, что они должны изменить свое поведение
57

и осуществлять вывод для системной консоли.
Размер массива аргументов командной строки в функции main() мы получали в качестве ее параметра. Так как для массива ссылок на параметры окружающей среды такого параметра нет, то его размер определяется другим способом. Последний элемент этого массива содержит указатель NULL.
3.8 Изменение пользовательского контекста процесса. Семейство функций для системного вызова exec()
Для изменения пользовательского контекста процесса применяется системный вызов exec(), который пользователь не может вызвать непосредственно. Вызов exec() заменяет пользовательский контекст текущего процесса на содержимое некоторого исполняемого файла и устанавливает начальные значения регистров процессора (в том числе устанавливает программный счетчик на начало загружаемой программы). Этот вызов требует для своей работы задания имени исполняемого файла, аргументов командной строки и параметров окружающей среды. Для осуществления вызова программист может воспользоваться одной из шести функций: execlp(), execvp(), execl() и execv(), execle(), execve(), отличающихся друг от друга представлением параметров, необходимых для работы системного вызова exec(). Взаимосвязь указанных выше функций изображена на рисунке 3.3.
Рис. 3.3. Взаимосвязь различных функций для выполнения системного вызова exec()
58
Системный вызов execve() объявляется в заголовочном файле unistd.h: int execve (const char * path, char const * argv[], char * const envp[]);
Все очень просто: системный вызов execve() заменяет текущий образ процесса программой из файла с именем path, набором аргументов argv и окружением envp. Здесь следует только учитывать, что path - это не просто имя программы, а путь к ней. Иными словами, чтобы запустить ls, нужно в первом аргументе указать «/bin/ls».
Массивы строк argv и envp обязательно должны заканчиваться элементом NULL. Кроме того следует помнить, что первый элемент массива argv (argv[0]) - это имя программы или что-либо иное. Непосредственные аргументы программы отсчитываются от элемента с номером 1.
В случае успешного завершения execve() ничего не возвращает, поскольку новая программа получает полное и безвозвратное управление текущим процессом. Если произошла ошибка, то по традиции возвращается -1.
Прототипы функций
#include <unistd.h>
int execlp(const char *file, const char *arg0,
... const char *argN,(char *)NULL)
int execvp(const char *file, char *argv[])
int execl(const char *path, const char *arg0,
... const char *argN,(char *)NULL)
int execv(const char *path, char *argv[])
int execle(const char *path, const char *arg0,
... const char *argN,(char *)NULL, char * envp[])
int execve(const char *path, char *argv[],
59
char *envp[])
Описание функций
Аргумент file является указателем на имя файла, который должен быть загружен. Аргумент path – это указатель на полный путь к файлу, который должен быть загружен.
Аргументы arg0, ..., argN представляют собой указатели на аргументы командной строки. Заметим, что аргумент arg0 должен указывать на имя загружаемого файла. Аргумент argv представляет собой массив из указателей на аргументы командной строки. Начальный элемент массива должен указывать на имя загружаемой программы, а заканчиваться массив должен элементом, содержащим указатель
NULL.
Аргумент envp является массивом указателей на параметры окружающей среды, заданные в виде строк «переменная=строка». Последний элемент этого массива должен содержать указатель NULL. Поскольку вызов функции не изменяет системный контекст текущего процесса, загруженная программа унаследует от загрузившего ее процесса следующие атрибуты:
1.идентификатор процесса;
2.идентификатор родительского процесса;
3.групповой идентификатор процесса;
4.идентификатор сеанса;
5.время, оставшееся до возникновения сигнала SIGALRM ;
6.текущую рабочую директорию;
7.маску создания файлов;
8.идентификатор пользователя;
9.групповой идентификатор пользователя;
10.явное игнорирование сигналов;
11.таблицу открытых файлов (если для файлового дескриптора не устанавливался признак «закрыть файл при выполнении exec()»).
Поскольку системный контекст процесса при вызове exec() остается практически неизменным, большинство атрибутов процесса, доступных пользователю через системные вызовы, после запуска новой программы также не изменяется.
Важно понимать разницу между системными вызовами fork()
60