Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ОС_СГTУ new v10.doc
Скачиваний:
148
Добавлен:
12.11.2019
Размер:
4.13 Mб
Скачать

3.3. Процессы в qnx6

В UNIX выделяют следующие типы процессов: системные, демоны и прикладные (пользовательские) [5].

Системные: они являются частью ядра и всегда расположены в оперативной памяти. Они не имеют соответствующих им программ в виде исполняемых файлов и запускаются особым образом при инициализации ядра системы; выполняемые инструкции и данные этих процессов находятся в ядре системы, таким образом, они могут вызывать функции и обращаться к данным не доступным для остальных процессов. Системными процессами UNIX являются: диспетчер свопинга (shed), диспетчер страничного замещения (vhand), диспетчер буферного КЭША (bdfflush), диспетчер памяти ядра (kmademon), к ним относятся процесс init, который является прародителем всех остальных процессов ОС и запускающийся при инициализации системы, хотя он не является частью ядра и запускается из исполняемого файла.

Демоны: это неинтерактивные процессы, которые запускаются обычным образом, путём загрузки в память соответствующих им программ и выполняются в фоновом режиме, они запускаются при инициализации системы и обеспечивают работу различных процессов ОС (система печати, сетевого доступа и т. п.). Они не связаны ни с одним пользовательским сеансом работы и не могут непосредственно управляться пользователем. Большую часть времени они ожидают пока тот или иной процесс не запросит определённую услугу, например, доступ к файловому архиву или печать документов.

Прикладные процессы: к ним относятся все остальные процессы, выполняющиеся в системе. Это процессы порождённые в рамках пользовательского сеанса работы. Они могут выполняться как в интерактивном так и в фоновом режиме. Но в любом случае время жизни ограничено сеансом работы пользователя. При выходе пользователя из системы все его пользовательские процессы будут уничтожены.

В QNX процесс – это выполняющаяся программа. Процесс состоит из образа процесса и метаданных процесса. Образом процесса называется совокупность кода (т.е. инструкций для процессора – выполнение этих инструкций и есть выполнение программы) и данных (ими манипулируют с помощью инструкций) [15]. Метаданные процесса – это информация о процессе, которая хранится в структурах данных ОС и сопровождается ОС. Метаданными являются: информация о физическом размещении кода и данных в оперативной памяти, а также атрибуты процесса, к которым относятся:

  • Идентификатор процесса (Process ID, PID) – уникальный номер, присваиваемый процессу при его порождении ОС.

  • Идентификатор родительского процесса (Parent PID, PPID) – PID процесса, породившего данный процесс, т.е. выполнившего запрос к ОС для создания данного процесса.

  • Реальные идентификаторы владельца и группы (User ID UID и Group ID, GID) – номер, позволяющий механизмам защиты информации от несанкционированного доступа определять, какому пользователю принадлежит процесс и какой группе пользователей принадлежит этот пользователь. Эти идентификаторы присваиваются при регистрации пользователя в системе или командному интерпретатору (login shell), если выполнялась командно-строковая регистрация через утилиту login, или графической оболочкой Photon, если регистрация выполнялась в графическом режиме через утилиту phlogin. Процессы, запускаемые пользователем, наследуют UID и GID той программы, из которой они запускались (т.е. родительского процесса).

  • Эффективные идентификаторы владельца и группы (Effective UID EUID и Effective GID, EGID) – предназначены для повышения гибкости механизмов защиты информации от несанкционированного доступа. Пользователь при наличии соответствующих полномочий может в ходе работы менять эффективные идентификаторы. При этом реальные идентификаторы не меняются.

  • Текущий рабочий каталог – путь (разделенный слэшами список каталогов), который будет автоматически добавляться к относительным именам файлов. Выводится на экран командой pwd.

  • Управляющий терминал (TTY) – терминальная линия (терминал или псевдотерминал) ассоциированный с процессом и, с которым связаны потоки ввода, вывода и ошибок. Если процесс становится процессом – демоном, то он отсоединяется от своей терминальной линии и не имеет ассоциированной терминальной линии. Запуск процесса как фонового (знак «&» в конце командной строки) ‑ не отсоединяет процесс от терминальной линии [17].

  • Маска создания файлов (umask) – атрибуты доступа, которые будут заданы для файла, созданного процессом.

  • Приоритет процесса (prio) – с этим приоритетом создается главный поток процесса (порождаемый функцией main()). Процесс – это статическая субстанция, контейнер.

  • Дисциплина диспетчеризации – также больше относится к потоку.

  • Использование ресурсов процессора (статистика по времени выполнения программы) – включает: время выполнения программы в прикладном контексте (user time – время выполнения инструкций, написанных программистом), время выполнения в контексте ядра (system time – время выполнения инструкций ядра по запросу программы, т.е. системных вызовов), суммарное время выполнения всех дочерних процессов в прикладном контексте, суммарное время выполнения всех дочерних процессов в контексте ядра.

Процесс всегда содержит хотя бы один поток. Для процессов исходный код которых подготовлен на языках С/С++, главным потоком процесса является поток, в котором исполняется функция, текстуально описанная под именем main()[15]. Рассмотрим программу, которая получает информацию о значении своих атрибутов (файл process.c):

#include <stdlib.h> // вызов стандартной библиотеки

#include <sys/resourses.h> // статистическая информация о процессе

int main(int argc, char **argv)

{

struct rusage r_usage; // задается структура последних четырех переметров

printf(“\nProcess Information:\n); // \n – переход на новую строку

printf(“Process name = \t\t%\n”,argv[0]);

// \t – строка, argv[0] – выведем нулевой аргумент (процесс)

printf(“User ID = \t\t<%d>\n”,getuid());

// по аналогии вывод остальной информации

printf(“Effective User ID = \t<%d>\n”,geteuid());

printf(“Group ID = \t\t<%d>\n”,getgid());

printf(“Process Group ID = \t<%d>\n”,getpgid());

printf(“Process ID (PID) = \t<%d>\n”,getpid());

printf(“Parent PID (PPID) = \t<%d>\n”,getppid());

printf(“Process priority = \t<%d>\n”,getprio());

getrusage(RUSAGE_SELF, &r_usage);

printf(‘\t<user time=%d sec, %d microsec >\n’,r_usage, ru_utime,tv_sec,

r_usage, ru_utime,tv_usec);

printf(‘\t<system time=%d sec, %d microsec >\n’,r_usage, ru_stime,tv_sec,

r_usage, ru_stime,tv_usec);

return EXIT_SUCCESS; // завершение работы программы

}

С помощью директивы #include подключаются две библиотки stdlib.h (стандартная библиотека) и sys/resourses.h (статистическая информация о процессе). Программа начинает выполняться с точки входа ‑ функции main().

Параметрами этой функции являются argc – целое значение количества аргументов командной строки при запуске программы и argv – массив, в котором находятся значения этих аргументов. Директива struct rusage задает структуру r_usage для получения информации о статистике выполнения программы. Функция printf() подключаемая через библиотеку stdlib.h обеспечивает вывод на экран сообщений. Управляющий символ \n переводит курсор на новую строку экрана. Переменная argv[0] всегда содержит имя исполняемой программы. Далее в функциях printf() происходит вызов функций для получения соответствующих атрибутов процесса. Функция getrusage(RUSAGE_SELF, &r_usage) возвращает в структуру r_usage информацию о статистике выполнения программы. Оператором return программа завершается и возвращает в создавший ее процесс сообщение EXIT_SUCCESS (имеет значение 0) – успешное завершение.

Предком всех процессов QNX является администратор процессов (процесс procnto) (рис. 39), идентификатор PID которого равен 1. Остальные процессы порождаются в результате вызова соответствующей функции другим процессом, именуемым родительским [15].

Первой из функций пораждения процессов является функция fork() – создает дочерний процесс путем «клонирования» родительского процесса. Действие вызова fork() следующее: порождается дочерний процесс, которому системой присваивается новое уникальное значение PID. Дочерний процесс получает собственные копии файловых дескрипторов, открытых в родительском процессе в точке выполнения fork(). Каждый дескриптор ссылается на тот же файл, который соответствует аналогичному дескриптору родителя. Блокировки файлов (locks), установленные в родительском процессе, наследуются дочерним процессом. При клонировании родительский и дочерний процессы различаются только идентификаторами PID и PPID. Функцию fork() рекомендуется использовать только в однопоточных программах.

Рассмотрим применение функции fork() на примере программы fork.c. Функция fork() возвращает целое число, которое в родительском процессе равно идентификатору PID дочернего процесса, а в дочернем – нулю.

// fork.c

#include <stdlib.h>

#include <sys/syspage.h>

int main(int argc, char **argv, char **env)

// функция возвращает целое значение.

//argc – количство параметров передаваемых функции аргументов,

//argv – сами аргументы, env – сообщения.

{

pid_t pid; // тип данных для хранения идентификатора процесса

char *prefix;

prefix=(char*)malloc(sizeof(char)); // выделение памяти

pid=fork(); // новый процесс, дочерний к родительскому

if (pid==0) sprintf(prefix, ‘child’); // был ли создан дочерний процесс

else sprintf(prefix,”parent’);

printf(“%s Process name =%s\n”,prefix,argv[0]); // вывод информации

printf(“%s PID=%d\n”,prefix,getpid(0));

printf(“%s PPID=%d\n”,prefix,getppid(0));

return EXIT_SUCCESS; // завершение работы программы

}

На рис.44 показано выполнение программы fork.c.

Функция vfork() (виртуальный fork()) – используется как «облегченная» альтернатива паре вызовов fork()-exec(). В отличии от стандартной функции fork(), она не выполняет реального копирования данных, а просто блокирует родительский процесс, пока дочерний не вызовет exec().

Рис.44. Выполнение программы fork.c.

Семейство функций exec() – заменяют образ вызвавшего процесса указанным исполняемым файлом (execl(), execle(), execlp(), execlpe(), execv(), execve(), execvp(), execvpe()). В функциях exec и spawn используются суффиксы: l – список аргументов (определяется через список данных непосредственно в самом вызове); e – список аргументов указывается посредством определения массива переменных; р – относительный путь, если не указан полный путь к файлу программы; v – список аргументов определяется через указатель на массив аргументов. Функции семейства ехес() подменяют исполняемый код текущего процесса исполняемым кодом из другого файла (не изменяя его идентификатор PID, права доступа, внешние ресурсы процесса, а также находящийся в том же адресном пространстве). Поэтому используются эти вызовы непосредственно после fork() для замены копии вызывающего процесса новым.

Программа exec.c иллюстрирует прохождение нового процесса с помощью комбинации вызовов fork() и exec():

#include <stdlib.h>

#include <sys/syspage.h>

int main(int argc, char **argv, char **env)

// функция возвращает целое значение.

// argc – количство параметров передаваемых функции аргументов,

// argv – сами аргументы, env – сообщения.

{

pid_t pid; // тип данных

pid=fork(); // результат выполнения функции, определенной ранее,

If (pid==0) //если pid=0, выполняется фнкция execlp

{ execlp(“process”,”process”, NULL);

// программа завершается неудачей

Perror(“Child”);

Exit(EXIT_FAILURE);

}

Waitpid(0,NULL,0);

Printf(“Parants’s PID =%d\n”,getpid(0));

Printf(“Parants’s PPID =%d\n”,getppid(0));

Return EXIT_SUCCESS;

}

Для запуска программы exec.c из интерпретатора команд sh необходимо набрать имя программы exec и нажать клавишу Enter. После этого интерпретатор команд sh запрашивает администратор процессов Procnto о возможности создания нового процесса exec. Если процесс exec успешно создан, то интерпретатор команд sh блокируется до момента завершения процесса exec. Родительский процесс exec создает дочерний процесс, вызывая функцию fork(). В родительском процессе условие pid==0 не выполняется, так как переменная pid равена идентификатору PID дочернего процесса, а он не может быть равен 0. И затем ожидает завершения дочернего процесса с помощью функции waitpid().

В дочернем процессе условие pid==0 выполняется и с помощью функции execlp(“process”,”process”, NULL) делается попытка загрузить программу process, но путь к ней не указан и функция execlp возвращает ошибку, которая выводится в поток ошибок функцией perror(“Child”). Затем дочерний процесс завершается функцией Exit(EXIT_FAILURE) и возвращает в функцию waitpid() родительского процесса код возврата дочернего процесса. Родительский процесс получает управление, выводит на экран свои идентификаторы PID и PPID и завершается оператором return.

На рис.45 показано выполнение программы exec.c.

Рис.45. Выполнение программы exec.c.

Семейство функций spawn() – сразу порождает дочерний процесс, загрузив указанный исполняемый файл (spawn(), spawnl(), spawnle(), spawnlp(), spawnlpe(), spawnp(), spawnv(), spawnve(), spawnvp(), spawnvpe()) [17]. Суффиксы имеют аналогичное значение, что для описанной выше функции exec(). Это наиболее эффективный способ порождения процессов в QNX Neutrino. Функции семейства spawn () порождают новый процесс с новым идентификатором PID и в новом адресном пространстве. В файле spawn.c представлен пример наиболее простого и быстрого способа порождения нового процесса:

// spawn.c

#include <stdlib.h>

#include <sys/syspage.h>

int main(int argc, char **argv, char **env)

{

spawnl(P_WAIT, “process”,”process” ,NULL);

printf(“Parants’s PID =%d\n”,getpid(0));

printf(“Parants’s PPID =%d\n”,getppid(0));

return EXIT_SUCCESS;

}

Самый простой способ создания процесса - запустить из программного кода дочернюю копию командного интерпретатора, которому затем передать команду запуска процесса. Для этого используется вызов:

int system(const char * command),

где command - текстовая строка, содержащая команду, которую предполагается выполнить ровно в том виде, в котором мы вводим ее командному интерпретатору с консоли.

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

Рассмотрим временные затраты на создание процесса с помощью функции fork() на примере программы p2-1.cc [17]:

//p2-1.cc

#include <stdlib.h>

#include <stdio.h>

#include <inttypes.h>

#include <iostream.h>

#include <sys/neutrino.h>

#include <process.h>

#include <sys/syspage.h>

static double cycle2milisec ( uint64_t ccl ) {

const static double s2m = 1.E+3;

const static uint64_t cps = SYSPAGE_ENTRY( qtime )->cycles_per_sec;

// частота процессора:

return (double)ccl * s2m / (double)cps;

};

struct mbyte {

#pragma pack( 1 )

uint8_t data[ 1024 * 1024 ];

#pragma pack( 4 )

};

int main( int argc, char *argv[] ) {

mbyte *blk = NULL;

if( argc > 1 && atoi( argv[ 1 ] ) > 0 ) {

blk = new mbyte[ atoi( argv[ 1 ] ) ];

};

uint64_t t = ClockCycles();

pid_t pid = fork();

if( pid == -1 ) { perror( "fork" ); exit( EXIT_FAILURE );}

if( pid == 0 ) exit( EXIT_SUCCESS );

if( pid > 0 ) {

waitpid( pid, NULL, WEXITED );

t = ClockCycles() - t;

};

if( blk != NULL ) delete blk;

cout << "Fork time: " << cycle2milisec( t ) << " msec. [" << t << " cycles]" << endl;

exit( EXIT_SUCCESS );

};

Программа начинается с точки входа – функции main(). Предварительно задается структура mbyte, в которой задается блок данных размером один мегабайт. В главном потоке проверяется условие (argc > 1 && atoi( argv[ 1 ]) > 0 ), где функция atoi() преобразует символьную сстроку соответствующую первому параметру из командной строки в целое число ( argv[ 0 ] соответствует имени программы – p2-1). Если это значение больше нуля, то с помощью функции new() и структуры mbyte создается блок данных соответствующего размера. Затем с помощью функции ClockCycles() определяется в переменной t имеющей тип данных uint64_t текущее количество процессорных циклов, котрое необходимо для определения времни затрачиваемого на создание процесса.

Функция fork() создает дочерний процесс. Для дочернего процесса pid = 0 поэтому условие выполняется и дочерний процесс сразу же после проверки условия завершается функцией exit( EXIT_SUCCESS ). В это время родительский процесс ожидает завершения дочерненого порцесса на функции waitpid( pid, NULL, WEXITED ). Как только дочерний процесс завершится в переменной t = ClockCycles() - t определяется количество процессорных затраченных на порожденние процесса. Затем удаляется блок данных blk и осуществляется вывод на экран через поток вывода cout и с помощью функции cycle2milisec( t ), преобразующей процессорные циклы в секунды, время на создание и уничтожение процесса.

На рис.46 показано выполнение программы p2-1.cc.

Рис.46. Выполнение программы p2-1.cc.

При запуске программы со значением параметра в Мбайтах от 1 до 100 наблюдается наблюдается близкая к линенной зависимость времени создания процесса от размера его образа в памяти от 17 до 1170 мсек.

Интересны не только затраты на порождение нового процесса, но и то, насколько «эффективно» сосуществуют параллельные процессы в ОС, насколько быстро происходит переключение контекста с одного процесса на другой [17]. Для оценки этих затрат рассмотрим приложение р5.сс.

// P5.cc

#include <stdlib.h>

#include <inttypes.h>

#include <iostream.h>

#include <unistd.h>

#include <sched.h>

#include <sys/neutrino.h>

int main( int argc, char *argv[] ) {

unsigned long N = 1000;

if( argc > 1 && atoi( argv[ 1 ] ) > 0 ) N = atoi( argv[ 1 ] );

pid_t pid = fork();

if( pid == -1 ) cout << "fork error" << endl, exit( EXIT_FAILURE );

uint64_t t = ClockCycles();

for( unsigned long i = 0; i < N; i++ ) sched_yield();

t = ClockCycles() - t;

delay( 200 );

cout << pid << "\t: cycles - " << t << "; on sched - " << ( t / N ) / 2 << endl;

exit( EXIT_SUCCESS );

};

Программа начинается с точки входа – функции main(). Если при запуске программы в командной строке задано количество повторений цикла, то с помощью функци atoi() оно преобразуется в новое значение переменной N . Функция fork() создает дочерний процесс. И в дочернем и в родительском процессах (их главных потоках) определяются переменные t (своих для каждого процесса). Они имеют тип данных uint64_t и хранят текущее количество процессорных циклов, котрое необходимо для определения времни затрачиваемого на переключение контекста процессов.

В каждом из процессов (главных потоках) запускается N раз for-цикл, где выполняется единственная функция sched_yield(). Она переводит вызвавший ее процесс (главный поток) из состояния выполнения в состяние готовности к выполнению (хотя квант времени выделенный для работы этого потока еще не истек) и запускает передиспетчеризацию потоков.

После завершения цила каждый процесс (главный поток) определяет в своей переменной t = ClockCycles() - t количество процессорных циклов затраченных на выполнение. Делает задержку на 0,2 секунды и выводит на экран через поток вывода cout результаты своей работы. На рис.47 показано выполнение программы p5.cc в перспективе System Profiler.

Таким образом, здесь использована симметричная схема, где два одновременно выполняющихся процесса настолько симметричны и идентичны, что они даже не анализируют PID после выполнения fork(), они только в максимальном темпе «перепасовывают» друг другу активность. Рис. 48 показано взаимодействие двух идентичных процессов: вся их «работа» состоит лишь в том, чтобы как можно быстрее передать управление партнеру.

Рис.47. Выполнение программы p5.cc.

Такая работа возможна при round robin диспетчеризации. На рис. 48 черными стрелками обозначена передача управления от потока к потоку различных процессов. Результат выполнения программы с различными начальными значениями для процессора с частотой 533 МГц, показывает, что время передачи управления составляет 0,2 миллисекунды (600 циклов).

Рис. 48. Симметричное взаимодействие потоков