Создание многопроцессных программы на языке Си с использованием системных вызовов Unix
Лабораторная работа № 2
Unix-процессы
Цели и задачи: Знакомство с процессной организацией Unix-о подобных систем. Изучение информационных команд отслеживания информации о процессах. Изучение различных типов процессов. Изучение информации о первичном процессе init и уровнях загрузки системы. Создание программы на языке Си с использованием системных вызовов Unix реализующей порождение и замещение процессов, запуск команд Unix из пользовательской программы. Познакомиться с компиляцией программ с использованием компилятора gcc.
Теоретические сведения
Получение информации о процессах в системе
Для получения информации о процессах в системе наиболее часто используются утилиты ps и top. В Linux вся информация о динамике выполнения системы отражается в каталоге /proc, утилиты ps и top собирают данные о запущенных процессах на основании информации находящейся в этом каталоге.
В командной строке наберите:
#ps –AfH | more
и вы получите список всех процессов выполняющихся в системе (Пример 2.1):
Формат команды следующий: ps [PID] [options]
Полное описание команды вы можете узнать - man ps.
Для просмотра иерархии(дерева) процессов можно воспользоваться командой tree.
Опция A – обеспечит вывод всех процессов, f – полную информацию о процессе,ключ H покажет иерархию процессов. “| more “или “| less” обеспечит ‘форматированный’ вывод на экран.
Пример 2.1 Фрагмент результата выполнения команды в ОС Novell Suse Linux 10.0.
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 18:23 ? 00:00:01 init [5]
root 2 1 0 18:23 ? 00:00:00 [ksoftirqd/0]
root 3 1 0 18:23 ? 00:00:00 [events/0]
root 4 1 0 18:23 ? 00:00:00 [khelper]
root 5 1 0 18:23 ? 00:00:00 [kthread]
root 7 5 0 18:23 ? 00:00:00 \_ [kblockd/0]
где:
UID – идентификатор пользователя от имени которого запущен процесс,
PID – уникальный идентификатор процесса,
PPID – идентификатор процесса родителя,
TIME – суммарное время выполнения процесса,
TTY – терминал на котором выполняется данный процесс,
CMD – команда.
Как видно из этого фрагмента (Пример 2.1) во главе процессов находится процесс init, который является первичным процессом в системе. Параметр процесса init (смотрите фрагмент - init [5]) определяет уровень загрузки системы.
Создание многопроцессных программы на языке Си с использованием системных вызовов Unix
Любая программа в Unix-системах выполняется в виде одного (корневого) процесса или набора процессов. В лабораторной работе №1 мы уже рассмотрели, как происходит запуск программы, становимся теперь на том, как это происходит с точки зрения реализации на Си с использованием системных вызовов Unix.
Понятие системного вызова
В любой операционной системе поддерживается некоторый механизм, который позволяет пользовательским программам обращаться за услугами ядра ОС UNIX такие средства называются системными вызовами. Смысл системных вызовов, состоит в том, что для обращения к функциям ядра ОС используются "специальные команды" процессора, при выполнении которых возникает особого рода внутреннее прерывание процессора, переводящее его в режим ядра (в большинстве современных ОС этот вид прерываний называется trap - ловушка). При обработке таких прерываний ядро ОС распознает, что на самом деле прерывание является запросом к ядру со стороны пользовательской программы на выполнение определенных действий, выбирает параметры обращения и обрабатывает его, после чего выполняет "возврат из прерывания", возобновляя нормальное выполнение пользовательской программы. Понятно, что конкретные механизмы возбуждения внутренних прерываний по инициативе пользовательской программы различаются в разных аппаратных архитектурах. Поскольку ОС UNIX стремится обеспечить среду, в которой пользовательские программы могли бы быть полностью мобильны, потребовался дополнительный уровень, скрывающий особенности конкретного механизма возбуждения внутренних прерываний. Этот механизм обеспечивается так называемой библиотекой системных вызовов.
Для пользователя библиотека системных вызовов представляет собой обычную библиотеку заранее реализованных функций системы программирования языка Си. При программировании на языке Си использование любой функции из библиотеки системных вызовов ничем не отличается от использования любой собственной или библиотечной Си-функции. Однако внутри любой функции конкретной библиотеки системных вызовов содержится код, являющийся, вообще говоря, специфичным для данной аппаратной платформы.
Поведение всех программ в системе вытекает из поведения системных вызовов, которыми они пользуются. Сам термин "системный вызов" как раз означает "вызов системы для выполнения действия", т.е. вызов функции в ядре системы. Ядро работает в привилегированном режиме – режим ядра, в котором имеет доступ к системным таблицам, регистрам и портам внешних устройств и диспетчера памяти, к которым обычным программам доступ аппаратно запрещен.
Программно порождение процессов осуществляется с использованием системного вызова fork() – после выполнения этого вызова в системе появляется процесс, который является точной копией процесса, который выдал данный системный вызов. Но для запуска программ одного вызова fork() не достаточно – необходим механизм позволяющий загрузить бинарный код программы в оперативную память. Такой механизм предоставляет функции группы execl(execl, execlp, execle, execv, execvp). Функция данной группы осуществляет подмену кода процесса ее вызвавшего бинарным кодом загружаемой программы(Пример 2.2).
Когда вы в командном интерпретаторе набираете команду Например $ps - фрагмент кода программы запускающий данную команду будет выглядеть примерно так:
Пример 2.2 Запуск команды ps в командном интерпретаторе
main()
int st;
…
if (fork()==0)
execlp("ps" , "ps", 0);
wait(&st);
Остановимся теперь более подробно на функциях порождения и замещения процессов. Для начала дадим несколько утверждений:
o Процесс, который инициировал системный вызов fork(), принято называть родительским процессом (parent process).
o Вновь порожденный процесс принято называть процессом потомком (child process).
o Процесс потомок является почти полной копией родительского процесса
o Процесс родитель и процесс потомок разделяют один и тот же кодовый сегмент. Системный вызов fork() в случае успеха возвращает родительскому процессу идентификатор потомка, а потомку 0. В ситуации, когда процесс не может быть создан функция fork() возвращает -1.
При программировании процессов вам понадобится следующее:
a) Аргументы функции main:
void main(int argc, char *argv[], char *envp[]);
Если вы наберете команду $ a.out a1 a2 a3,где a.out – имя запускаемой вами программы, то функция main программы из файла a.out вызовется с:
argc = 4 /* количество аргументов */
argv[0] = "a.out" argv[1] = "a1"
argv[2] = "a2" argv[3] = "a3"
argv[4] = NULL
По соглашению argv[0] содержит имя выполняемого файла.
b) Информация о так называемом "окружении" (вспомните переменные окружения описанные в лабораторной работе №1) char *envp[], продублированного также в предопределенной переменной extern char **environ;
Окружение состоит из строк вида
"ИМЯПЕРЕМЕННОЙ=значение"
Массив этих строк завершается NULL (как и argv). Для получения значения переменной с именем ИМЯ существует стандартная функция
char *getenv( char *ИМЯ );
Она выдает либо значение, либо NULL если переменной с таким именем нет.
c) Информация об открытых по умолчанию файлов. По умолчанию (неявно) всегда открыты 3 канала:
ВВОД В Ы В О Д
FILE * stdin stdout stderr
соответствует fd 0 1 2
Эти каналы достаются процессу "в наследство" от запускающего процесса и связаны с дисплеем и клавиатурой, если только не были перенаправлены.
Кроме того, программа может сама явно открывать файлы (при помощи системных вызовов open, creat, pipe, fopen и т.п.). Всего программа может одновременно открыть до 20 файлов (считая стандартные каналы), а в некоторых системах и больше (например, 64).
d) Получение информации об уникальном номере процесса потомка и процесса-родителя и др.
Пример 2.3 Системные вызовы для получения идентификаторов процесса
#include <stream.h>
#include <sys/types.h>
#include <unistd.h>
main ( )
{
cout << " Идентификатор текущего процесса PID: " << getpid() <<” \n”;
cout << " Идентификатор родительского процесса- PPID: " << getppid()<<” \n”;
cout << " Идентификатор группы процесса PGID: " << getpgrp()<<” \n”;
cout << " Идентификатор пользователя –real UID: " << getuid() <<” \n”;
cout << " Реальный идентификатор группы пользователя real -GID:" <<
getgid()<<” \n”;
cout << " Эффектифный идентификатор пользователя - UID: " <<
geteuid()<<” \n”;
cout << " Эффектифный идентификатор группы пользователя GID: " <<
getegid()<<” \n”;
exit(0);
}
е) Текущий каталог - достается в наследство от процесса-"родителя", и может быть затем изменен системным вызовом
chdir(char *имя_нового_каталога);
В командном режиме вы можете получить информацию о текущем каталоге, набрав команду $pwd.
Пример 2.4 Порождение процессов
#include <stream.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
void mpinfo()
{
cout << " Идентификатор текущего процесса PID: " << getpid() <<” \n”;
cout << " Идентификатор родительского процесса- PPID: " << getppid()<<” \n”;
cout << " Идентификатор группы процесса PGID: " << getpgrp()<<” \n”;
cout << " Идентификатор пользователя –real UID: " << getuid() <<” \n”;
cout << " Реальный идентификатор группы пользователя real -GID:" <<
getgid()<<” \n”;
cout << " Эффектифный идентификатор пользователя - UID: " <<
geteuid()<<” \n”;
cout << " Эффектифный идентификатор группы пользователя GID: " <<
getegid()<<” \n”;
}
void main()
{
int i,st;
signal(SIGCHLD, SIG_IGN);
for(i=1;i<=3;i++)
fork();
mpinfo();
wait(&st);
exit(0);
}
Рассмотрите как выполняется данная программа(Пример 2.4), определите количество процессов запускаемых данной программой. Попробуйте закомментировать вызов функции wait(&st) – посмотрите что получится.
Итак, программа написана – необходимо ее откомпилировать. Для компиляции программы воспользуйтесь компилятором gcc или g++. Для автоматизации компиляции больших проектов используйте утилиту make.
Компилятор gcc
GNU Compiler Collection (gcc) - это семейство компиляторов с языков C, C++, которые объединены общей технологией и распространяются в рамках проекта GNU. Домашняя страничка компилятора находится по адресу http://www.gnu.org/software/gcc/gcc.html
Этот компилятор является "стандартным" средством для компиляции всех программ, входящих в проект GNU. gcc также является основным компилятором операционной системы Linux - с его помощью компилируется ядро системы.
Примеры компиляции программы main.c
gcc main.c -o main
g++ main.cpp -o main
Ключ “o” задает имя исполняемого файла, если его не указать, то в случае успешной компиляции будет создан исполняемый файл “a.out”
Для запуска программы наберите ./main
Для подготовки программы для отладки используйте ключ “g”
gcc -g main.c -o main
Компиляция программы с множеством исходных файлов
gcc main.c a.c b.c -o multisource
Еще один вариант компиляции программы с множеством исходным файлов
gcc -c main.c
gcc -c a.c
gcc -c b.c
gcc main.o a.o b.o -o multisource