Управления программами средствами POSIX API 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Управления программами средствами POSIX API



Согласно концепции, заложенной в архитектуру UNIX, компьютер представляет собой совокупность файлов, любые программные и аппаратные ресурсы рассматриваются как файлы. Для файлов реализованы следующие стандартные команды: открыть (open), закрыть (close), прочитать (read), записать (write), изменить значение файлового указателя (lseek) и выполнить файл (exec). Данные команды образуют низкоуровневый прикладной программный интерфейс (API) для работы с файлами. По мере развития языков программирования и операционных систем, развивался и данный API. В настоящее время можно выделить две большие и не совместимые группы прикладных программных интерфейсов – это Win API и POSIX API. Win API разрабатывался и поддерживается компанией Microsoft, он доступен только для семейства ОС MS Windows. По мере развития данной ОС в её состав включались Win16, Win32 и Win64 API. На UNIX/Linux платформах используется POSIX API.

В данном разделе рассматриваются средства POSIX API, связанные с запуском программ на исполнение, управлением данным процессом и определением ресурсов для запускаемых программ.

Процесс – это программа, находящаяся в стадии выполнения. В полном объеме материал, связанный с управлением процессами будет рассмотрен в рамках курса «Параллельные вычисления», сейчас вы познакомитесь лишь с базовыми возможностями по управлению процессом запуска программ и организацией взаимодействия между ними. Важно знать, что процесс является артефактом, сущностью операционной системы (ОС), ему назначаются системные ресурсы, и он обладает определенными свойствами, которые принято называть атрибутами процесса.

Запуск программы на исполнение инициируется пользователем. Но программу на исполнение запускает операционная система. При этом под пользователем может подразумеваться не только человек, но и программная система. Важно, что пользователю в системе назначаются определенные права, определяющие разрешения и запреты на выполнение определенных действий в отношении системных ресурсов. Все ресурсы, с точки зрения ОС – это файлы. Самый важный пользователь в UNIX/Linux –системе имеет имя root. Его ещё называют суперпользователем. Суперпользователь создает всех остальных пользователей в системе и определяет их права. Всем остальным пользователям системы назначаются меньшие права – это основа политики безопасности. Кроме того, некоторые пользователи могут быть объединены в группы.

Важно, что на UNIX/Linux – платформах для того чтобы иметь возможность запускать исполняемые файлы, нужно обладать для это соответствующими правами, заданными в профиле пользователя, от имени которого и осуществляется запуск программ. Кроме того, системная политика позволяет определять такие свойства для пользователей, при которых последний сможет запускать на исполнение лишь строго заданный список программ и не более того. Причем задается и список директорий, в которых может выполняться запуск разрешенных программ.

С каждым процессом связаны определенные ресурсы ОС, это и адресное пространство процесса, и список дескрипторов файлов, связанных с процессом, и другое. Для обеспечения возможности непрерывной работы сразу нескольких процессов, в ядре ОС поддерживаются специальные таблицы – блоки управления процессами (БУП). Как вы уже знаете, на 32-разрядной ОС каждому процессу выделяется адресное пространство размером приблизительно в 4 GB (232 байт), из них примерно 1 GB используется непосредственно для системных нужд: БУП и другое.

Для того чтобы процесс мог выполнять код связанной с ним программы, процессу назначается доступный центральный процессор (ядро центрального процессора). Время ядра центрального процессора (ЦП) разделяется несколькими процессами (разделение времени доступа к ресурсам ядра ЦП), среди которых можно выделить системные и пользовательские процессы. Специальная служба ядра ОС, называемая менеджер процессов, осуществляет работу по выделению квот времени доступа процессов к ресурсам ядра ЦП. Менеджер процессов организует и поддерживает очередь готовых к исполнению процессов. Осуществляется постоянное переключение ЦП между процессами. Процессу необходимо предоставлять процессорное время до тех пор, пока не будет выполнена реализуемая в нем задача. После того, как задача, реализуемая в процесс, будет полностью выполнена, процесс переводится в состояние «завершен» и он выгружается – освобождаются связанные с ним ресурсы.

Существует три способа создать процесс:

  1. запустить программу на исполнение;
  2. изменить процесс, проведя его повторную инициализацию, инициирован системный вызов exec () или ему подобный. Создается новый процесс, полностью замещающий ранее функционировавший процесс;
  3. выполнить ветвление активного процесса посредством системного вызова fork ().

Для различения процессов используется специальный атрибут, называемый идентификатором процесса (pid – Process IDentifier, Process ID). Значение pid определяется операционной системой. Кроме pid, процессу ставится в соответствие и идентификатор родительского процесса (ppid – Parent Process IDentifier).

Запустите терминал (команда konsole) и введите в строку системного приглашения команду ps (process status – состояние процессов). Данная команда выводит в стандартный поток список процессов, загруженных в системе. В случае вызова без параметров, она выводит сокращенный список не фоновых процессов, связанных с текущим терминалом (устройством tty). Вывод по умолчанию осуществляется в три колонки. В первой указывается величина pid отображаемого процесса, во второй – имя связанного с процессом терминала, в третьей – время, затраченное процессом на исполнение, в четвертой идет имя процесса.

Команда ps поддерживает определенный набор параметров. Вот лишь несколько из них:

-e – вывести информацию обо всех процессах;

-d – вывести информацию обо всех процессов, кроме лидеров групп;

-a – вывести информацию обо всех наиболее часто запрашиваемых процессах, то есть обо всех процесса, кроме лидеров групп и процессов, не связанных с терминалом;

-A – такой же результат, как и с параметром –e;

Полную справку по команде ps вы можете прочитать в руководстве, доступном по команде ' map ps '.

Стандартная библиотека языка С включат в себя функции, являющиеся системными вызовами: getpid () и getppid (). Их прототипы имеют следующий вид:

 

#include <sys/types.h> /* POSIX, определяет тип pid_t */

#include <unistd.h>

pid_t getpid(void);

pid_t getppid(void);

 

Функции getpid () и getppid () возвращают значение типа pid_t (сокращение от pid type), являющимся синонимом для целочисленного типа. Определение типа pid_t выполнено в модуле < sys / types. h >. В программах на языке С ++ допускается работать с типом pid_t непосредственно, как с типом int.

Значение pid, возвращаемое функцией getpid (), используется менеджером процессов для идентификации процессов. Это значение используется в программах для различения родительского и дочерних процессов, создаваемых для организации программ, реализующих параллельное выполнение нескольких задач. Например, параллельная работа вычислительного модуля и графического интерфейса пользователя может быть реализована в параллельных процессах, либо в параллельных потоках. Последовательная работа указанных задач не целесообразна.

В самом простом случае можно вывести значения pid в стандартный поток вывода (STDOUT):

 

std::cout << "process id: " << getpid() << std::endl;

 

Пример программы, использующей вызов getpid (), может выглядеть следующим образом.

 

#include <iostream>

#include <sys/types.h>

#include <unistd.h>

#include <cstdlib>

int main() {

pid_t pid = fork();

if(pid < 0) {

   std::cout << "Error: fork() failed! " << std::endl;

   exit(1);

}

if(pid == 0) {

   std::cout << "Hello from child process" << std::endl;

   std::cout << "Current process id of Process: "

   << getpid() << std::endl;

   break;

}

else {

   std::cout << "Hello from parent process" << std::endl;

   std::cout << "Current process id of Process: "

   << getpid() << std::endl;

 

}

return 0;

}

 

Либо, так.

 

#include <iostream>

#include <sys/types.h>

#include <unistd.h>

#include <cstdlib>

int main() {

pid_t pid = fork();

switch(pid) {

   case -1: {

       std::cout << "Error: fork() failed! " << std::endl;

       exit(1);

   }

   case 0: {

       std::cout << "Hello from child process" << std::endl;

       std::cout << "Current process id of Process: "

       << getpid() << std::endl;

       break;

   }

   default:

       std::cout << "Hello from parent process" << std::endl;

       std::cout << "Current process id of Process: "

       << getpid() << std::endl;

 

}

return 0;

}

 

Аналогичным образом может быть реализована программа, выводящая значения идентификатора и родительского процесса.

 

#include <iostream>

#include <sys/types.h>

#include <unistd.h>

int main() {

int pid;

pid = fork(); // "ветвление" - клонирование процесса

if (pid == 0) { // если это дочерний процесс

   std::cout << "\nChild Process id: "

        << getpid();

   std::cout << "\nParent Process with parent id: "

        << getppid() << std::endl;

}

return 0;

}

 

Следующий пример осуществляет более информативный вывод, позволяя оценить «отношения» родительского и дочернего процессов.

 

#include <iostream>

#include <sys/types.h>

#include <unistd.h>

int main() {

std::cout <<"\x1B[1J"; // Очистка экрана

std::cout << "Current process ID: " << getpid() <<std::endl;

std::cout << "Parent process ID: " << getppid()

<< "\n==================================" <<std::endl;

int pid = fork();

std::cout << "Current process ID: " << getpid() <<std::endl;

std::cout << "Parent process ID: " << getppid()

<< "\n==================================" <<std::endl;

if(pid == 0) {

std::cout<<"Children process\nCurrent process id of Process: "

<< getpid() << std::endl;

}

return 0;

}

 

Пояснение: для очистки окна терминала используется команда clear, либо сочетание клавиш ' Ctrl + L '. Очистка окна выполняется за счет вывода на экран определенного количества пустых строк и переводе строки системного приглашения в верхний левый угол экрана. Из программы экран может быть очищен посредством использования управляющих Esc -последовательностей, управляющих состоянием терминала.

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

В приведенных примерах при запуске программы сначала создаются один процесс. После выполнения системного вызова fork () происходит ветвление, получается два процесса: основной, родительский процесс программы и порожденный из него дочерний процесс, связанный с той же программой.

Замечание: вызов fork () вызов является не переносимым, то есть он может быть использован только на POSIX -совместимых платформах. На платформе MS Windows данный вызов не поддерживается.

Системный вызов fork ()является базовым механизмом для создания новых процессов ("вилка" – ветвление, клонирование текущего процесса). В результате успешного выполнения системного вызова fork () создается практически полная копия процесса (программы времени исполнения) – новый процесс, дочерний по отношению к тому, в котором был осуществлен данный вызов. Процесс, в котором был осуществлен вызов функции f ork (), является родительским (parent process) по отношению к своему дочернему процессу. После ветвление оба процесса выполняют одну и ту же программу и находятся в одинаковом «состоянии», то есть выполнение задачи дочернего процесса начинается не с момента запуска программы, а непосредственно с момента, следующего после выполнения вызова fork ().

Замечание: создание нового процесса требует наличия доступных системных ресурсов.

В случае успешного выполнения функция fork () в родительском процессе возвращает значение pid созданного дочернего процесса, в дочернем процессе функция fork () возвращает нулевое значение. В случае ошибки функция fork () возвращает отрицательное значение.

 

#include <iostream>

#include <sys/types.h>

#include <unistd.h>

int main() {

int pid;

static int process_counter = 10;

while(process_counter > 0) {

   pid = fork(); // "ветвление" - клонирование процесса

    --process_counter; // уменьшить значение счетчика

   if (pid < 0) { // Системная ошибка – ветвление невозможно!

       std::cerr << "Can`t create new process!" << std::endl;

       break; 

   }

   if (pid == 0) { // если это дочерний процесс

       std::cout << "\nParent Process id: "

       << getpid();

       std::cout << "\nChild Process with parent id: "

       << getppid() << std::endl;

}

return 0;

}

 

Обратите внимание, количество выполненных процессов превышает начальное значение переменной process_counter. А нужно запустить ровно десять дочерних процессов. Как запустить на исполнение строго заданное количество дочерних процессов? Найдите способ исправить данный программный дефект.

Ниже приведен пример кода, который позволит вам найти ответ на данный вопрос. Но нужно лишь добавить в данный код всего один оператор управления и задача будет решена…

 

#include <iostream>

#include <sys/types.h>

#include <unistd.h>

#include <cstdlib>

int main() {

int pid;

std::cout << "— START --------------------------" << std::endl;

for(int i=0; i<2; i++) {

   pid = fork(); // "ветвление" - клонирование процесса

   if (pid < 0) {

       std::cerr << "Can`t create new process!" << std::endl;

       break; // Системная ошибка – ветвление невозможно!

   }

   if (pid == 0) { // если это дочерний процесс

       std::cout << "\nParent Process id: "

       << getpid();

       std::cout << "\nChild Process with parent id: "

       << getppid() << std::endl;

}

sleep(1);

return 0;

}

 

Выполнение новой программы в созданном процессе осуществляется посредством системного вызова exec (). В результате системного вызова exec () происходит повторная инициализация процесса, приводящая к замене исполняемой в данном процессе программы на новую, указанную в списке параметров данного системного вызова. В языке программирования С имеется целое семейство функций, осуществляющих системный вызов exec ().

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

 

// filename: my_exec.cpp

#include <sys/types.h>

#include <unistd.h>

#include <iostream>

 

int main() {

pid_t pid = getpid();

std::cout << "Process ID: " << pid << std::endl;

std::cout << "I am \'my_exec\' called by execvp()"

<< std::endl;

return 0;

}

 

// filename: exec_demo.cpp

#include <sys/types.h>

#include <unistd.h>

#include <iostream>

#include <cstdlib>

 

int main() {

char *args[] = {"./my_exec", NULL};

pid_t pid = getpid();

std::cout << "Process ID: " << pid << std::endl;

execvp(args[0], args);

std::cout << "Main process exiting" << std::endl;

return 0;

}

 

Результат работы программы exec_demo может иметь следующий вид:

 

Process ID: 13693

Process ID: 13693

I am ‘my_exec’ called by execvp()

 

Как вы можете видеть, после вызова функции execvp () основной процесс запущенной программы был заменен новым процессом, связанным с программой my _ exec.

Детально рассмотрим все шесть вариантов функции exec():

 

#include <unistd.h>

int execl(char *name, char *arg0,... /*NULL*/);

int execv(char *name, char *argv[]);

int execle(char *name, char *arg0,... /*,NULL,

      char *envp[]*/);

int execve(char *name, char *arv[], char *envp[]);

int execlp(char *name, char *arg0,... /*NULL*/);

int execvp(char *name, char *argv[]);

 

Вызов exec () приводит к тому, что переданная в качестве аргумента программа загружается в память вместо текущей, которая вызвала exec (). Происходит повторная инициализация процесса: старые сегменты памяти процесса перезаписываются новой программой.

Суффиксы l, v, p, e в именах функций определяют формат и объем аргументов, а также каталоги, в которых нужно искать загружаемую программу:

· l (list – список). Аргументы командной строки передаются в форме списка arg 0, arg 1.... arg n, NULL. Эту форму используют, если количество аргументов известно;

· v (vector). Аргументы командной строки передаются в форме вектора argv []. Отдельные аргументы адресуются через argv [0], argv [1]... argv [n]. Последний аргумент (argv [n]) должен быть указателем NULL;

· p (path). Обозначенный по имени файл ищется не только в текущем каталоге, но и в каталогах, определенных переменной среды PATH;

· e (environment – среда). Функция ожидает список переменных среды в виде вектора (envp []) и не использует текущей настройки среды.

 

Рассмотрим следующий пример использования функции execl ():

 

#include <iostream>

#include <unistd.h>

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

std::cout << "Running the program: " << argv[0] << std::endl;

execl("/bin/echo" "echo"," ","Hello", "World!", NULL);

return 0;

}

 

Функция execl () принимает список (list) параметров. Первым указана строка, содержащая полный путь расположения команды и сама команда – "/bin/echo", далее следует передаваемый ей список параметров – это список параметров, передаваемых при запуске её функции main ().

 

Аргументы функции main ()

Ранее вам уже пришлось познакомиться с принципом программного управления, предложенным Норбертом Винером, и сегментной организацией адресного пространства приложения, работающего под управлением ОС семейства UNIX / Linux. Вы уже знаете, что работа программы начинается с запуска на исполнение реализованной в ней функции main ().

Стандарт POSIX определяет формат для прототипа функции main (), с передачей управления которой начинается работа любого приложения:

 

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

 

либо равноценный

 

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

 

Данный формат определяет количество и список аргументов функции main (), которые она получает от командного интерпретатора через свой стек. Две формы указания типа второго параметра: char** argv и char* argv [] – эквивалентны.

Командный интерпретатор, при помощи которого осуществляется запуск программы, формирует и передает запускаемой программе аргументы, указанные в командной строке вместе и именем программы. То есть строка, переданная командному интерпретатору, содержащая имя запускаемой программы, это и есть аргументы, передаваемые функции main () запускаемой программы. Даже если вы осуществляете запуск программы из графической среды, имейте в виду, что запуск программы все равно инициируется командным интерпретатором пусть и в скрытой от вас форме. Архитектура большинства операционных систем не предусматривает другого механизма запуска программ, как только с использованием средств командного интерпретатора (командной оболочки). Фактически это означает, что правами на запуск программ пользователя обладает только командный интерпретатор, который осуществляет специальные системные вызовы. Любой запуск программ на исполнение, миную командный интерпретатор, на системах, работающих под управлением многозадачных ОС, невозможен. Ну, если только система не взломана, а это уже совершенно другая тема.

Рассмотрим параметры функции main ():

int argc – (ARGument Count), количество переданных аргументов;

char ** argv – (ARGument Vector), массив строк переданных функции main () аргументов.

Ряд платформ поддерживает ещё и третий, не обязательный параметр – char ** envp – массив строк переменных окружения. В этом случае прототип функции main () принимает следующий вид:

 

int main (int argc, char ** argv, char ** envp);

 

Предложенное именование аргументов функции main () носит рекомендательный характер, но данная рекомендация обеспечивает хорошую читаемость исходного кода программы. В ряде источников встречаются примеры определения параметров функции main (), подобные следующему: int main (int a, char ** b), но такое именование параметров существенно менее информативно. Поэтому, целесообразно придерживаться предложенной системы именования параметров функции main ().

Рассмотрим механизм передачи фактических параметров функции main () со стороны командного интерпретатора на следующем примере:

 

>> cat file1 file2 <Enter>

 

Ввод данных, переедаемых командному интерпретатору, завершается с нажатием клавиши < Enter >. Командный интерпретатор считывает и обрабатывает вводимые строки: выделяет отдельные блоки символов, заключенные в кавычки и лексемы (слова). В данном примере строка ввода включает в себя три лексемы: " cat ", " file 1" и " file 2". Будет сформированы следующие значения аргументов для функции main (): argc = 3, argv [0] = "cat", argv [1] = "file1", argv [2] = "file2" и argv [3] = NULL. Все эти аргументы доступны для чтения в самой программе, но изменять их непосредственно возможности, да и необходимости, нет.

Боле сложные, но и более информативный пример связан с командой создания системного монитора, аналога диспетчера задач, запускаемого в терминале:

 

watch -n 1 'ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%mem | head'

 

Для прерывания работы данной программы нажмите комбинацию клавиш ' Ctrl + Z ' либо ' Ctrl + C '. Как видите, список параметров, передаваемых программе через аргументы функции main (), может быть достаточно сложный.

Рассмотрим следующий пример, демонстрирующий механизм работы с аргументами командной строки.

 

// filename: progr01.cpp

#include <cstdio>

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

int i=1;

printf("Argumen counting: %d\n", argc);

printf("Programs name: %s\n", argv[0]);

if(argv > 1) {

while(argv[i]!= NULL) {

printf("Argment[%d]: %s\n", i, argv[i]);

i++;

}

}

return 0;

}

 

Сохраним этот файл с именем progr 01. cpp и скомпилируем его из командной строки.

>> c++ –o my_program progr01.c

Перепишите данный пример, используя для вывода данных средства объекта std:: cout.

Имя созданной программы – my _ program. Данная программа при запуске выводит сообщение, содержащее информацию о количестве аргументов, с которыми она была запущена, ее название и аргументы командной строки. Имя программы передается в функцию main () в качестве параметра argv [0].

Параметры запуска являются лексемами и разделяются пробельными символами. Разрешается группировать параметры в виде отдельной строки, заключенной в двойные кавычки.

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

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

Под конец данного параграфа рассмотрим следующий пример.

 

#include <sys/types.h>

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

int main() {

if (fork() == 0) {

   execl("/bin/echo", "echo", "this is", "message one",

        (char *) 0);

    perror("exec one failed");

    exit(1);

}

if (fork() == 0) {

   execl("/bin/echo", "echo", "this is", "message two",

        (char *) 0);

   perror("exec two failed");

   exit(2);

}

if (fork() == 0) {

   execl("/bin/echo", "echo", "this is", "message three",

        (char *) 0);

   perror("exec three failed");

   exit(3);

}

printf("Parent program ending\n");

return 0;

}

 

Объясните, что делает данная программа. Постарайтесь сделать это непосредственно до запуска программы, опираясь только на исходный код программы.



Поделиться:


Последнее изменение этой страницы: 2021-06-14; просмотров: 128; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.141.27.244 (0.158 с.)