Миргородская 7сессия / Операционные системы / %D0%9E%D0%A1_%D0%A1%D0%93%D0%A2%D0%A3%20v5
.pdf3.3.Процессы в QNX6
ВUNIX выделяют следующие типы процессов: системные, демоны и прикладные (пользовательские) [5].
Системные: они являются частью ядра и всегда расположены в опера-
тивной памяти. Они не имеют соответствующих им программ в виде испол-
няемых файлов и запускаются особым образом при инициализации ядра сис-
темы; выполняемые инструкции и данные этих процессов находятся в ядре системы, таким образом, они могут вызывать функции и обращаться к дан-
ным не доступным для остальных процессов. Системными процессами UNIX
являются: диспетчер свопинга (shed), диспетчер страничного замещения
(vhand), диспетчер буферного КЭША (bdfflush), диспетчер памяти ядра
(kmademon), к ним относятся процесс init, который является прародителем всех остальных процессов ОС и запускающийся при инициализации систе-
мы, хотя он не является частью ядра и запускается из исполняемого файла.
Демоны: это неинтерактивные процессы, которые запускаются обыч-
ным образом, путём загрузки в память соответствующих им программ и вы-
полняются в фоновом режиме, они запускаются при инициализации системы и обеспечивают работу различных процессов ОС (система печати, сетевого доступа и т. п.). Они не связаны ни с одним пользовательским сеансом рабо-
ты и не могут непосредственно управляться пользователем. Большую часть времени они ожидают пока тот или иной процесс не запросит определённую услугу, например, доступ к файловому архиву или печать документов.
Прикладные процессы: к ним относятся все остальные процессы, вы-
полняющиеся в системе, как правило, это процесс, порождённые в рамках пользовательского сеанса работы, они могут выполняться как в интерактив-
ном так и в фоновом режиме, но в любом случае время жизни ограничено се-
ансом работы пользователя. При выходе пользователя из системы все его пользовательские процессы будут уничтожены.
111
В 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) – предназначены для повышения гибкости меха-
низмов защиты информации от несанкционированного доступа. Пользователь при наличии соответствующих полномочий может в ходе работы менять эф-
фективные идентификаторы. При этом реальные идентификаторы не меняются.
112
Текущий рабочий каталог – путь (разделенный слэшами список ката-
логов), который будет автоматически добавляться к относительным именам файлов. Выводится на экран командой pwd.
Управляющий терминал (TTY) – терминальная линия (терминал или псевдотерминал) ассоциированный с процессом и, с которым связаны потоки ввода, вывода и ошибок. Если процесс становится процессом – демоном, то он отсоединяется от своей терминальной линии и не имеет ассоциированной терминальной линии. Запуск процесса как фонового – знак «&» в конце ко-
мандной строки - не является достаточным основанием для отсоединения процесса от терминальной линии [17].
Маска создания файлов (umask) – атрибуты доступа, которые будут заданы для файла, созданного процессом.
Приоритет процесса (prio) – с этим приоритетом создается главный поток процесса (порождаемый функцией main()). Процесс – это статическая субстанция, контейнер.
Дисциплина диспетчеризации – также больше относится к потоку.
Использование ресурсов процессора (статистика по времени выпол-
нения программы) – включает: время выполнения программы в прикладном контексте (user time – время выполнения инструкций, написанных програм-
мистом), время выполнения в контексте ядра (system time – время выполне-
ния инструкций ядра по запросу программы, т.е. системных вызовов), сум-
марное время выполнения всех дочерних процессов в прикладном контексте,
суммарное время выполнения всех дочерних процессов в контексте ядра.
Процесс всегда содержит хотя бы один поток. Для процессов исходный код которых подготовлен на языках С/С++, главным потоком процесса яв-
ляется поток, в котором исполняется функция, текстуально описанная под именем main()[15]. Рассмотрим программу, которая получает информацию о значении своих атрибутов (файл process.c):
113
#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 программа
114
завершается и возвращает в создавший ее процесс сообщение
EXIT_SUCCESS (имеет значение 0) – успешное завершение.
Жизненный цикл процесса можно разделить на четыре этапа: создание,
загрузка образа процесса, выполнение процесса и завершение процесса.
Предком всех процессов QNX является администратор процессов
(процесс procnto) (рис. 39), идентификатор PID которого равен 1. Остальные процессы порождаются в результате вызова соответствующей функции дру-
гим процессом, именуемым родительским [15].
Первой из них является функция fork() – создает дочерний процесс пу-
тем «клонирования» родительского процесса. Действие вызова fork() сле-
дующее: порождается дочерний процесс, которому системой присваивается новое уникальное значение PID. Дочерний процесс получает собственные копии файловых дескрипторов, открытых в родительском процессе в точке выполнения fork(). Каждый дескриптор ссылается на тот же файл, который соответствует аналогичному дескриптору родителя. Блокировки файлов
(locks), установленные в родительском процессе, наследуются дочерним процессом. При клонировании родительский и дочерний процессы различа-
ются только идентификаторами PID и PPID. Функцию fork() рекомендуется использовать только в однопоточных программах.
Рассмотрим применение функции fork() на примере программы fork.c.
Функция fork() возвращает целое число, которое в родительском процессе равно идентификатору дочернего процесса, а в дочернем – нулю.
// 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;
115
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.
Рис.44. Выполнение программы fork.c.
Функция vfork() (виртуальный fork()) – используется как «облегченная»
альтернатива паре вызовов fork()-exec(). В отличии от стандартной функции
fork(), она не выполняет реального копирования данных, а просто блокирует
родительский процесс, пока дочерний не вызовет exec().
116
Семейство функций exec() – заменяют образ вызвавшего процесса ука-
занным исполняемым файлом (execl(), execle(), execlp(), execlpe(), execv(),
execve(), execvp(), execvpe()). В функциях exec и spawn используются суффик-
сы: l – список аргументов (определяется через список данных непосредст-
венно в самом вызове); e – список аргументов указывается посредством оп-
ределения массива переменных; р – относительный путь, если не указан пол-
ный путь к файлу программы; v – список аргументов определяется через ука-
затель на массив аргументов. Функции семейства ехес() подменяют испол-
няемый код текущего процесса (не изменяя его идентификатор PID, права
доступа, внешние ресурсы процесса, а также находящийся в том же адресном
пространстве) исполняемым кодом из другого файла. Поэтому используются
эти вызовы непосредственно после fork() для замены копии вызывающего
процесса новым.
Программа exec.c иллюстрирует прохождение нового процесса с по-
мощью комбинации вызовов vfork() и 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;
}
На рис.45 показано выполнение программы exec.c.
117
Рис.45. Выполнение программы exec.c.
Для запуска программы exec.c из интерпретатора команд sh необходи-
мо набрать имя программы exec и нажать клавишу Enter. После этого интер-
претатор команд sh запрашивает администратор процессов Procnto возмож-
ность создания нового процесса exec. Если процесс exec успешно создан, то интерпретатор команд sh блокируется до момента завершения процесса exec.
Родительский процесс exec создает дочерний процесс, вызывая функцию vfork(). В родительском процессе условие pid==0 не выполняется, так как pid
равен PID дочернего процесса, а он не может быть равен 0. И затем ожидает завершения дочернего процесса с помощью функции waitpid().
В дочернем процессе выполняется условие pid==0 и с помощью функ-
ции execlp(“process”,”process”, NULL) делается попытка загрузить програм-
118
му process, но путь к ней не указан и функция execlp возвращает ошибку, ко-
торая выводится в поток ошибок функцией perror(“Child”). Затем дочерний процесс завершается функцией Exit(EXIT_FAILURE) и возвращает в функцию waitpid() родительского процесса код возврата дочернего процесса. Роди-
тельский процесс выводит на экран свои идентификаторы PID и PPID и за-
вершается оператором return.
Семейство функций 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 - текстовая строка, содержащая команду, которую пред-
полагается выполнить ровно в том виде, в котором мы вводим ее командному интерпретатору с консоли.
119
Процесс завершается, если программа выполняет вызов 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;
120