Программное порождение процессов

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

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

Оригинальное и очень изящное решение для создания процессов принято в Unix. Для создания процесса здесь служит системная функция fork(), которая даже не имеет аргументов! Эта функция по существу раздваивает процесс (fork - вилка), в результате в ОС оказываются два почти идентичных процесса. Единственно различие между ними для ОС - их различные идентификаторы, под которыми они зарегистрированы в системе. Различие этих процессов для выполняемой программы в том, что процесс-родитель получает как результат функции fork()значение идентификатора нового - дочернего процесса, а дочерний процесс в качестве числа, возвращаемого функцией, получает нуль. Поэтому в программе, общей хотя бы на участке порождения дочернего процесса, анализом кода возврата функции можно решить, какой процесс в анализируемый момент обслуживает программа - родительский или дочерний. Дочерний процесс, как правило, распознав ситуацию и обнаружив себя, использует вызов запуска другой исполняемой программы. Имеется несколько вариантов системных функций такого запуска. Особенностью их в Unix является то, что они не создают новый процесс, но просто позволяют процессу сменить управляющую им программу.

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

 

#include <stdio.h>

int main()

{int rc, k;

printf("Parent Proccess\n");

rc=fork();

if (!rc) {execl("child1.exe",0); }

printf("For Child Process:\n"); printf("PID=%d \n", rc);

for (k=0; k<10; k++)

{printf("I am Parent (My K=%d)\n", k); usleep(2000000); }

printf("Parent ended\n"); return 0;

}

Листинг 7.2.1а. Программа proces.c для Unix

 

#include <stdio.h>

int main()

{int k;

printf("Demonstration processes, Child Proccess\n");

for (k=0; k<30; k++)

{printf(" ------- I am Child ... (child=%d)\n", k); sleep(1); }

printf("Child ended\n");

return 0;

}

Листинг 7.2.1b. Программа child1.c для Unix

 

Программа proces.c для Unix после сообщения о начале работы процесса-родителя вызывает функцию разветвления fork(), возвращающую значение в переменнуюrc. Далее анализируется эта переменная rc, при нулевом ее значении вызывается функция замены программы execl(), которая использована в виде

execl("child1.exe",0).

Эта функция имеет переменное число аргументов, и последний из них должен иметь значение 0. В более общей форме эта функция позволяет задать аргументы для командной строки программы процесса. После выполнения вызова execl("child1.exe",0) дочерний процесс управляется программой child1.exe, а исходная программа не имеет к нему уже никакого отношения.

Родительский же процесс использует свою программу proces.exe дальше участка анализа: выдает на экран сообщение об идентификаторе дочернего процесса

For Child Process PID=номер

и переходит к циклу выдачи на экран сообщений о своем очередном шаге. После каждого из этих циклов производится приостановка выполнения функцией usleep(). Последняя функция требует задания времени приостановки в микросекундах (а функции приостановки с параметром в миллисекундах в Unix нет!).

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

BOOL CreateProcess(LPCTSTR pNameModule, LPCTSTR pCommandLine,

SECURITY_ATTRIBUTES *pProcessAttr,

SECURITY_ATTRIBUTES *pThreadAttr, BOOL InheritFlag,

DWORD CreateFlag, LPVOID penv, LPCTSTR pCurrDir,

STARTUPINFO *pstartinfo,

PROCESS_INFORMATION *pProcInfo).

Здесь тип LPCTSTR задает дальний указатель (адрес в модели FLAT) для текстовой строки, а DWORD аналогичен типу unsigned LONG. Заметим, что параметры pProcessAttr и pThreadAttr относятся к средствам расширенной защиты в Windows NT и в большинстве приложений не используются, что указывается заданием соответствующего аргумента равным NULL.

Параметр pNameModule является указателем на строку текста – на имя файла запускаемой программы (файла с расширением exe), а параметр pCommandLine – указателем на текст командной строки запуска программы. С формальной стороны эти два аргумента частично дублируют дрѓг друга, практически їаще всего используеђся лишь один из них.

Если паѐаметр pNameModule задан равным NULL, а используется параметр pCommandLine (т.е. он задан не равным NULL), то из текста параметра pCommandLine извлекается первое слово – последовательность символов до первого пробельного символа. Это слово используется как имя файла запускаемой программы, причем при отсутствии в нем расширения к нему автоматически приписывается расширение EXE. Это имя вначале используется для поиска исполняемого файла в том же каталоге, где находится EXE-файл процесса, вызывающего функцию CreateProcess(). При не обнаружении требуемого файла, его поиск продолжается в текущем каталоге вызывающего процесса, потом (при еще одной неудаче) – в системном каталоге Windows, затем в основном каталоге Windows и, наконец, в последовательности каталогов, перечисленных в переменной окружения PATH.

Параметр pNameModuleпозволяет задавать полное имя запускаемой программы или ее имя в текущем каталоге, причем обязательно с явно присутствующим в тексте расширением EXE. Если при этом дополнительно присутствует и ненулевой параметр pNameModule, то текст последнего используется как последовательность аргументов для командной строки. При нулевом pNameModule параметр CommandLine, задаваемый указателем pCommandLine, кроме имени программы задает после него и аргументы командной строки. Таким образом, параметр pCommandLine дает больше возможностей, но использование одного параметра pNameModule – проще. Параметр InheritFlag используется для задания наследования объектов дочерним процессом и в нашем изложении использоваться не будет (полагается равным FALSE, т.е. равным 0). Параметр pCurrDirотносится к дополнительным возможностям функции создания и задает новый текущий каталог для запускаемого процесса. В ближайших примерах он всегда будет задаваться нулевым указателем, т.е. его возможности не будут использоваться. Параметр penvзадает указатель на блок памяти, хранящий строки окружения, в простейших и наиболее частых ситуациях он равен NULL.

Заметим, что кроме одной текстовой строки аргументов в ОС, начиная с Unix – родоначальницы всех современных ОС, представляется возможность применения так называемого окружения (environment) процесса. Окружение процесса (назы­ваемое также средой процесса) – это специальная текстовая область, служащая для передачи данных создаваемому процессу. В Unix она предназначена в первую очередь для передачи настроек программы от операционной системы или командной оболочки. В иных ОС окружение используется достаточно редко, но сама такая возможность оставлена. Чтобы не использовать передаваемую информацию для окружения, следует просто задать в качестве аргумента env нулевой указатель.

ПараметрCreateFlag задает флаги создания и, кроме достаточно тонких нюансов, определяет класс приоритета создаваемого процесса. Обычное значение этого флага задается константой NORMAL_PRIORITY_CLASS.

Два последних параметра задают адреса двух специальных структур данных при создании процесса. Структура STARTINFO соответствует структуре описания сессии (сеанса) в OS/2 и содержит 18 полей, определяющих в первую очередь характеристики окна для создаваемого процесса. В простейших случаях все поля могут быть заполнены нулями, кроме поля с именем cb, в котором должна быть записана длина экземпляра этой структуры в байтах.

Структура PROCESS_INFORMATION, задаваемая адресом в последнем параметре, содержит четыре поля для возврата учетной информации из ОС после создания нового процесса. Описывается она в заголовочном файле как

typedef struct

{HANDLE hProcess;

HANDLE hThread;

DWORD dwProcessId;

DWORD dwThreadId;

} PROCESS_INFORMATION;

Наиболее значимыми в ней является поле hProcess, возвращающее хэндл созданного процесса, и поле dwProcessId, возвращающее идентификатор (уникальный номер) созданного процесса.

Возвращаемое функцией CreateProcess значение типа BOOL позволяет вызывающей программе решить, удачно ли прошел запуск – удалось ли создать процесс. При успешном создании возвращается значение TRUE, иначе FALSE, численно равное нулю.

Следующий пример, представляемый программами в листингах 7.2.2а и 7.2.2b, демонстрирует простейшее использование функции создания процесса в Windows. Получаемые из этих исходных текстов программы должны обязательно строиться как консольные приложения, в частности, в интегрированной системе разработки Borland/Inprise языка C выбором опции Console.

#include <windows.h>

#include <stdio.h>

int main()

{int k;

DWORD rc;

STARTUPINFO si;

PROCESS_INFORMATION pi;

printf("Demonstration processes, Main Proccess\n");

memset(&si, 0, sizeof(STARTUPINFO)); si.cb=sizeof(si);

rc=CreateProcess(NULL, "child1.exe", NULL, NULL, FALSE,

NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi);

if (!rc)

{printf("Error create Process, codeError = %ld\n", GetLastError());

getchar(); return 0; }

printf("For Child Process:\n");

printf("hProcess=%d hThread=%d ProcessId=%ld ThreadId=%ld\n",

pi.hProcess, pi.hThread, pi.dwProcessId, pi.dwThreadId);

for (k=0; k<10; k++)

{printf("I am Parent... (my K=%d)\n", k); Sleep(2000); }

CloseHandle(pi.hProcess); CloseHandle(pi.hThread);

return 0;

}

Листинг 7.2.2а. Программа Parent.c для Windows

#include <stdio.h>

#include <windows.h>

int main()

{int k;

printf("Demonstration processes, Child Proccess\n");

for (k=0; k<30; k++)

{printf("I am Child ... (k=%d)\n\r", k); Sleep(1000); }

return 0;

}

Листинг 7.2.2b. Программа Child1.c для Windows

 

В программе описаны переменные si, pi типов STARTUPINFO и PROCESS_INFORMATION, причем в поле cb структуры si записана длина переменной si (делать это надо обязательно перед использованием такого аргумента в вызове функции CreateProcess), а остальные поля этой структуры обнулены обращением к функции

memset(&si, 0, sizeof(STARTUPINFO))

Последняя записывает в память, начало которой задано первым аргументом, столько значений, заданных вторым аргументом, сколько указано числом в третьем аргументе. Значение параметра CreationFlags задает нормальный класс приоритета. Вызов функции создания процесса в качестве ненулевых содержит только параметры pCommandLine, CreateFlag, pstartinfo, pProcInfo, в качестве которых использованы "child1.exe", NORMAL_PRIORITY_CLASS, &si, &pi. После вызова функции ее код возврата в переменной rc используется для анализа ситуации. При rc, равной нулю, выдается сообщение об ошибке запуска, код которой извлекается системной функцией GetLastError(), и процесс завершается оператором return 0.

При отсутствии ошибки на экран выдается информация о запущенном процессе путем вывода значений полей hProcess, hThread, dwProcessId, dwThreadId возвращаемой переменной pi. Далее 10 раз с приостановкой на 2 с. выдается сообщение о шаге работы процесса-родителя. Приостановка выполняется функцией Sleep(), аргумент которой задается в миллисекундах. В конце программы полученные при создании процесса хэндлы закрываются вызовом функции CloseHandle(). Для понимания следует обратить внимание, что в данном примере закрываются не только хэндлы, но и связанные с ними управляющие блоки (структуры описания процесса и нити). (Это имеет место, когда на управляющие блоки указывает только один хэндл, в общем случае дублирования хэндлов ситуация может быть иной.)

Функция execl(), использованная в примере порождения дочернего процесса для Unix, является на самом деле лишь одним из представителей довольно широкого семейства функций с общим префиксом exec. В это семейство входят функции execl(), execlp(), execle(), execv(), execvp(), execve(). Несмотря на обилие этих функций, их действия идентичны, а отличаются они только наборами аргументов. Все перечисленные функции предназначены для замены текущей выполняемой программы на новую исполняемую программу, имя которой задается первым аргументом вызова всех перечисленных функций.

Наличие и предназначение следующих в списке аргументов условно обозначается в именах рассматриваемых функций с помощью одной из дополнительных букв. Символ l в составе имени функции задает использование переменного числа аргументов, символ v указывает, что список аргументов командной строки задается массивом указателей на текстовые строки. (Поэтому символы l и v в имени рассматриваемых функций являются альтернативными и исключают друг друга.) При наличии символа p в составе имени єункции, последняя0ищет указанный пѐи вћзове0исполнџемћй0файл во вёех каталогах,0заданнћх переменной ёѐеды PQTH, все дрѓгие функции использѓюђ только явное указание имени функции. (Функция с символом p в имени по списку аргументов не отличается от соответствующей ей функции без этого символа.) При наличии в имени функции символа e в качестве дополнительной информации в списке аргументов присутствует аргумент – массив переменных окружения.

В частности, прототипом функции execve() является

int execve(char *filename, char *argv[ ], char *env[ ]),

(которая и служит наиболее общей исходной системной функцией), а прототип функции execlp() описывается как

int execlp(char *filename, char *arg1, char *arg2, . . . , char *argn, NULL),

где последний аргумент нулевым указателем задает конец списка аргументов.

(Заметим, что использование перечисленных функций с параметром argv[ ] позволяет программисту "обмануть" вызываемую программу, задав в качестве нулевого элемента списка аргументов текстовое наименование, отличное от имени запускаемой программы.) При неудачном запуске все рассматриваемые функции возвращают значение -1. (При их успешном запуске некому воспользоваться возвращаемым значением, так как после этого запуска будет выполняться совсем другая программа.)

В операционных системах Windows аналогичный по назначению набор функций имеет названия, начинающиеся с префикса spawn. Набор этот состоит из функций spawnl(), spawnle(), spawnlp(), spawnlpe(), spawnv(), spawnve(), spawnvp(), spawnvpe(), где первая и последняя функция из набора задается прототипами

int spawnl(int mode, char *path, char *arg0, arg1, ..., argn, NULL),

int spawnvpe(int mode, char *path, char *argv[ ], char *envp[ ]).

Остальные функции имеют аналогичное строение и используют вспомогательные символы l, e, p и v по тому же соглашению, что и функции Unix. Первый аргумент mode всех рассматриваемых функций задает действия родительского процесса после создания дочернего. Если этот параметр задается символической константой P_WAIT, то родительский процесс приостанавливается до тех пор, пока не завершится запущенный дочерний процесс. При задании первого аргумента символической константой P_NOWAIT родительский процесс продолжается одновременно с дочерним, обеспечивая тем самым параллельное выполнение обоих процессов. Заметим, что функции набора spawn не входят в стандарт языка C, а специфичны для указанных операционных систем.