ВСЕВОЛОД СТАХОВ
Процессы и нити
В данной статье я расскажу о работе с процессами в многозадачных операционных системах, в частности о программировании процессов и нитей в Windows и POSIX-совместимых системах.
Как вы знаете, Windows NT – это многозадачная ОС, а это значит, что вы можете создавать программным путём другие процессы и нити внутри процесса. Для создания процесса могут использоваться две основные функции: WinExec и CreateProcess. Первая очень проста в применении, имеет только два параметра и может использоваться для создания оконных процессов (к ним можно отнести и консольные программы). Однако возможности WinExec сильно ограничены, и Microsoft громогласно объявила, что вместо этого следует использовать функцию CreateProcess. Но рассмотреть WinExec всё же надо, хотя бы для быстрого написания простых программ.
int WinExec(char *command_line, unsigned int show_mode);
Функция выполняет программу сommand_line в режиме отображения окна show_mode и ждёт, пока дочерний процесс вызовет функцию GetMessage или пока не прошло время ожидания (это может вызвать задержку выполнения). При успешном выполнении функция возвращает значение, большее 31; меньшее значение сигнализирует об ошибке:
- 0 – не хватает ресурсов;
- ERROR_BAD_FORMAT – указываемый файл неисполняемый;
- ERROR_FILE_NOT_FOUND – указываемый файл не найден;
- ERROR_PATH_NOT_FOUND – путь не существует.
Режимы отображения окна будут описаны далее.
Функция CreateProcess принимает большое количество параметров и может использоваться для указания множества атрибутов порождаемым процессам, например, можно сделать оболочку для DOS-программы с графическим интерфейсом ввода/вывода данных, переопределив STDIN и STDOUT и многое другое. Но использовать данную функцию достаточно непросто, так как приходится учитывать некоторые нюансы, а можно ещё и мучиться с атрибутами безопасности, но это уже на любителя... Итак, вот что написано в MSDN:
BOOL CreateProcess(
// Имя исполняемого файла
LPCTSTR lpApplicationName,
// Командная строка
LPTSTR lpCommandLine,
// Атрибуты безопасности процесса
LPSECURITY_ATTRIBUTES lpProcessAttributes,
// Атрибуты безопасности потока
LPSECURITY_ATTRIBUTES lpThreadAttributes,
// Наследует ли дочерний процесс дескрипторы родителя
BOOL bInheritHandles,
// Флаги создания процесса
DWORD dwCreationFlags,
// Указатель на environment для дочернего процесса
LPVOID lpEnvironment,
// Текущая директория для процесса
LPCTSTR lpCurrentDirectory,
// Структура для запуска процесса
LPSTARTUPINFO lpStartupInfo,
// Указатель, получающий данные о дочернем процессе
LPPROCESS_INFORMATION lpProcessInformation
);
Краткое объяснение параметров:
LPCTSTR lpApplicationName – путь (полный или относительный) к исполняемому файлу. Если путь длинный или содержит пробелы, то необходимо заключить его в кавычки: ""C:very long pathvery long filename.exe"". Учтите, что в WindowsNT для запуска 16-ти разрядных программ необходимо указывать путь к файлу в lpCommandLine, а данный параметр должен равняться NULL (в Win9x 16-ти разрядные файлы выполняются, как и все другие).
LPTSTR lpCommandLine – параметры, передаваемые порождаемому процессу в командной строке для 16-ти разрядных приложений в WinNT. При указании пути к 16-ти разрядному приложению надо путь к нему заключать в кавычки, чтобы отделить конец самого пути и начало передаваемых аргументов командной строки. Но такой метод срабатывает и для 32-х разрядных приложений. Поэтому параметр lpApplicationName оставляют в NULL, а в данном параметре прописывают полную командную строку, предварённую путём к файлу. При этом учтите, что если путь неполный и файл не найден относительно текущей директории, то происходит поиск в следующих местах:
- в директории системных файлов (winntsystem32 или windowssystem);
- в директории 16-ти разрядных системных файлов для WinNT winntsystem;
- в директории windows (winnt windows);
- в директориях, описанных в переменной окружения PATH.
LPSECURITY_ATTRIBUTES lpProcessAttributes – атрибуты безопасности процесса. Для большинства случаев следует писать NULL, т.е. атрибуты безопасности наследуются порождаемым процессом.
LPSECURITY_ATTRIBUTES lpThreadAttributes – атрибуты безопасности потока, тоже обычно NULL.
BOOL bInheritHandles – флаг, определяющий наследует ли дочерний процесс дескрипторы родителя. Данный флаг удобно применять для межпроцессовых коммуникаций через неименованные трубки (pipe) и для совместного использования файлов. Общее правило: если процесс собирается общаться с потомком, этот параметр должен быть TRUE.
DWORD dwCreationFlags – флаги создания процесса. Битовая маска, определяющая различные параметры создания процесса. Обычно используется для указания приоритета процесса:
- HIGH_PRIORITY_CLASS – высокий приоритет процесса (нельзя создать процесс данного класса, не получив соответствующих привилегий).
- IDLE_PRIORITY_CLASS – низкий приоритет процесса.
- NORMAL_PRIORITY_CLASS – нормальный приоритет процесса.
- REALTIME_PRIORITY_CLASS – режим реального времени для процесса (для запуска необходимы привилегии администратора, что обычно используется при создании сервисов реального времени, которые запускаются от пользователя SYSTEM) даёт порождаемому процессу 100% CPU, и если последний начнёт «пожирать» все ресурсы процессора, то ОС наглухо зависнет: мышь двигаться не сможет, буферы на диск не сбрасываются. В общем, хорошо, что всем нельзя такие процессы делать.
Есть ещё интересные параметры для отладки приложений, позволяющие применять функции непосредственного доступа к памяти процесса ReadProcessMemory и WriteProcessMemory, но описание этого выходит за рамки данной статьи.
LPVOID lpEnvironment – указатель на environment для дочернего процесса, представляет собой блок строк, заканчивающихся NULL, содержащих в себе описание переменных окружения для дочернего процесса в формате «имя=значение». Если данный параметр NULL, то по традиции он наследуется от родительского процесса.
LPCTSTR lpCurrentDirectory – текущая директория для процесса.
LPSTARTUPINFO lpStartupInfo – структура для запуска процесса – это самая интересная часть CreateProcess, будет описана далее.
LPPROCESS_INFORMATION lpProcessInformation – указатель, получающий данные о дочернем процессе в формате:
struct PROCESS_INFORMATION{
HANDLE hProcess; – дескриптор порождённого процесса;
HANDLE hThread; – дескриптор главного потока дочернего процесса;
DWORD dwProcessId; – идентификатор порождённого процесса;
DWORD dwThreadId; – идентификатор порождённого потока.
}
А теперь я приведу описание структуры STARTUPINFO, и многие поймут, почему системных программистов Windows возводят в ранг великомучеников.
struct STARTUPINFO {
DWORD cb;
LPTSTR lpReserved;
LPTSTR lpDesktop;
LPTSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
};
Теперь «краткое» описание полей:
- DWORD cb – размер структуры в байтах (sizeof(STARTUPINFO)).
- LPTSTR lpReserved – очередная шутка Microsoft, должно быть NULL.
- LPTSTR lpDesktop – указатель на рабочий стол для окна (полезно для сервисов, работающих с рабочим столом).
- LPTSTR lpTitle – используется при создании окна консоли с заголовком, в остальных случаях должен быть NULL.
- DWORD dwX – размеры окна по х.
- DWORD dwY – размеры окна по у.
- DWORD dwXSize – ширина.
- DWORD dwYSize – высота.
Параметры размера игнорируются, если только в dwFlags не установлен атрибут STARTF_USEPOSITION и/или STARTF_USESIZE.
- DWORD dwXCountChars – количество символов по ширине.
- DWORD dwYCountChars – количество символов по высоте.
- Данные параметры применяются для консольных приложений, если установлен флаг STARTF_USECOUNT-CHARS.
- DWORD dwFillAttribute – устанавливают цвет (FORE-GROUND_BLUE, FOREGROUND_GREEN, FOREGRO-UND_RED, FOREGROUND_INTENSITY, BACKGRO-UND_BLUE, BACKGROUND_GREEN, BACKGRO-UND_RED, и BACKGROUND_INTENSITY) для консольных приложений, если установлен флаг STARTF_USEFILLATTRIBUTE.
- DWORD dwFlags – а вот это те самые флаги; кроме тех, что были уже упомянуты выше, можно отметить флаг STARTF_USESTDHANDLES – переопределение стандартных потоков для потомка.
- WORD wShowWindow – режим открытия окна (для его изменения необходимо установить флаг STARTF_USESHOWWINDOW), приведу несколько самых используемых:
- SW_HIDE – спрятать окно и показать следующее;
- SW_MAXIMIZE – развернуть и активизировать окно;
- SW_MINIMIZE – свернуть окно и показать следующее;
- SW_RESTORE – восстановить окно;
- SW_SHOWNORMAL – восстановить и активизировать окно;
- SW_SHOW – просто активизировать и показать окно.
- WORD cbReserved2 – NULL.
- LPBYTE lpReserved2 – NULL.
- HANDLE hStdInput – дескриптор стандартного ввода.
- HANDLE hStdOutput – дескриптор стандартного вывода.
- HANDLE hStdError – дескриптор стандартных ошибок.
Тут могу посоветовать одно: обнуляйте структуру перед использованием, а затем заполняйте нужные поля, остальные установятся по умолчанию. Ну что же, пришло время для чего-нибудь интересного. Итак, примеры.
Для начала запустим какой-нибудь notepad и загрузим в него какой-нибудь файл, в данном примере test.txt:
// Информация о процессе
STARTUPINFO si;
// Обнуление структуры
memset(&si, 0, sizeof(STARTUPINFO));
// Заполнение полей
si.cb = sizeof(STARTUPINFO);
// Флаги
si.dwFlags = STARTF_USESHOWWINDOW;
// Показываем окошко
si.wShowWindow = SW_SHOWNORMAL;
// Информация о процессе
PROCESS_INFORMATION pi;
// Вот и всё! Теперь блокнот запущен
CreateProcess(NULL, "notepad.exe test.txt", NULL, NULL, FALSE, 0, 0, 0, &si, ?);
А теперь более интересный пример – создание оболочки для программы MS-DOS. Для общения с консольной программой можно использовать только неименованные каналы (pipe). Проблема в том, что в WinNT неименованные каналы не наследуются порождаемым процессом. Для того чтобы наследовать неименованные каналы, необходимо установить соответствующие параметры безопасности. Но я подробно на этом останавливаться не буду – просто примите это на веру, так как дескрипторы безопасности – это отдельная большая тема.
SECURITY_DESCRIPTOR sd; // Дескриптор безопасности
SECURITY_ATTRIBUTES sa; // и его атрибуты
LPSECURITY_ATTRIBUTES lpsa = NULL;
// Это Windows NT
if (GetVersion() < 0x80000000) {
// Инициализация дескриптора
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(&sd, true, NULL, false);
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
// Разрешаем наследование дескрипторов
sa.bInheritHandle = true;
sa.lpSecurityDescriptor = &sd;
// А вот это уже нормальный дескриптор безопасности
lpsa = &sa;
}
// Создаём неименованный канал и получаем дескрипторы чтения/записи
HANDLE hReadPipe;
HANDLE hWritePipe;
// Создание канала
CreatePipe(&hReadPipe, &hWritePipe, lpsa,25000);
// Ну а это инициализация STARTUPINFO
STARTUPINFO si;
// Обнуление
memset(&si, 0, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
// Флаг перенаправления дескрипторов
si.dwFlags = STARTF_USESHOWWINDOW |STARTF_USESTDHANDLES;
// Прячем окошко
si.wShowWindow = SW_HIDE;
// Указатели stdout и stderr перенаправляются в канал
si.hStdOutput = hWritePipe;
si.hStdError = hWritePipe;
PROCESS_INFORMATION pi;
if(CreateProcess(NULL, "bcc -otest test.c", NULL, NULL, TRUE, 0, 0, 0, &si, &pi)){
// Закрываем дескриптор потока
CloseHandle(pi.hThread);
// Ждём завершения дочернего процесса 90 сек.
WaitForSingleObject(pi.hProcess, 90000);
// Читаем из канала данные
DWORD BytesRead; // Количество считанных байт
char dest[4000]; // Вот в этот буфер писать и будем
int LoopDone = 0;
int FBreak = 0;
// Цикл чтения данных из канала, с защитой от тайм-аута, т.к. чтение неблокирующее
while (!LoopDone) {
memset(dest, 0, 4000);
ReadFile(hReadPipe, &dest, sizeof(dest), &BytesRead, NULL);
if (BytesRead < 4000 || FBreak > 150) LoopDone = -1;
else LoopDone = 0;
FBreak++;
}
}
А теперь приведу пример взаимодействия сервиса с рабочим столом, например, сервис, вызывающий explorer с привилегиями LocalSystem (фактически привилегии операционной системы):
PROCESS_INFORMATION pi;
// Ну а это инициализация STARTUPINFO
STARTUPINFO si;
memset(&si, 0, sizeof(STARTUPINFO)); // Обнуление
si.cb = sizeof(STARTUPINFO); // И установка полей
si.lpDesktop = "WinSta0\\Default"; // Это рабочий стол по умолчанию!
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOW;
if (CreateProcess(NULL, "explorer", NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
// Закрываем все дескрипторы в конце программы
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
После создания дочернего процесса собственного написания для взаимодействия с сервисом наиболее удобно использовать именованные каналы, но это уже другая тема...
Теперь я расскажу о нитях. Вообще термин thread очень сложно однозначно перевести, но я буду называть их нити или потоки. Поток – это функция, которая выполняется внутри процесса одновременно с другими функциями, т.е. процесс разделяется на нити, выполняющиеся одновременно и использующими одно и то же адресное пространство, что нужно для создания нескольких параллельных потоков, работающих над одной задачей, чтобы не тратить много времени на перегонку результатов через именованные и неименованные каналы... Когда процессы разные, важно, чтобы один другому не мешал и не мог бы его случайно повредить и прочее. Когда же процесс один, но многопотоковый, наоборот, нужно одно общее адресное пространство.
Для создания потока в WinNT необходимо применить функцию CreateThread. Учтите, что работа с потоками кардинальным образом отличается от работы с процессами, т.к. нет необходимости применять межпроцессные коммуникации ОС и с потоком можно работать также, как и с любой другой функцией, например, передавать в неё параметры. Кроме этого, так как все потоки используют общее адресное пространство, то глобальные и статические переменные одинаковы во всех нитях (иногда встают проблемы синхронизации потоков), хотя межпроцессные коммуникации также возможны (у нити есть собственный дескриптор), что делает нить неким «гибридом» между функцией и процессом по способу взаимодействия.
HANDLE CreateThread(
// Атрибуты безопасности (NULL)
LPSECURITY_ATTRIBUTES lpThreadAttributes,
// Размер стека для потока (если 0, то используется умолчание
DWORD dwStackSize,
// Указатель на функцию потока
LPTHREAD_START_ROUTINE lpStartAddress,
// Аргументы, передаваемые потоку
LPVOID lpParameter,
// Флаги создания
DWORD dwCreationFlags,
// Получает ID потока
LPDWORD lpThreadId
);
Синтаксис данной команды довольно простой, но я хотел бы рассказать следующее: если вы передаёте нити аргументы, то их нужно прямо приводить к типу void *, а в самом потоке делать обратное преобразование. Данный способ позволяет передать потоку абсолютно любые параметры. Если вы не хотите, чтобы поток сразу же запускался, то укажите флаг CREATE_SUSPENDED, тогда нить создастся, но запускаться не будет. Функция CreateThread возвращает дескриптор потока или NULL при ошибке. Функция потока имеет вид DWORD WINAPI ThreadFunc(LPVOID data), где data – передаваемые параметры.
Для управления состоянием потоков используются функции SuspendThread и ResumeThread для соответственно приостанавливания и восстановления потоков. Функции принимают один аргумент – дескриптор потока и возвращают 1 в случае ошибки. Для убиения потока используется функция TerminateThread, она принимает два параметра: дескриптор потока и код ошибки. Если поток хочет сообщить вызывающему потоку код завершения, то он должен использовать функцию ExitThread(DWORD dwExit-Code), а вызывающий поток должен вызвать GetExitThread-Code(HANDLE hThread, LPDWORD lpdwExitCode), и поле lpdwExitCode заполняется кодом завершения потока.
Пример создания простого сервиса:
// Формат функции потока жёстко задан
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
char szMsg[80];
wsprintf( szMsg, "Поток: параметр = %d ", *lpParam );
MessageBox( NULL, szMsg, "Поток создан.", MB_OK );
return 0;
}
int main(void)
{
//Параметр для потока
DWORD dwThreadId, dwThrdParam = 1;
HANDLE hThread;
hThread = CreateThread(
NULL, // безопасность по умолчанию
0, // размер стека по умолчанию
ThreadFunc, // функция потока
(void *) &dwThrdParam, // параметр для функции нити
0, // нет специальных флагов создания
&dwThreadId); // получает ID нити
// Проверяем правильность работы потока.
if (hThread == NULL)
return -1;
// Негласное правило – закрываем дескрипторы вручную
CloseHandle(hThread);
return 0;
}
Потоки – это мощное и хорошо реализованное в WinNT средство. Потоки очень часто применяются в серверных приложениях, сервисах и прочих программах, использующих «разделение труда». Потоки легко использовать и управлять ими. Единственное, о чём приходится заботиться, – это синхронизация данных между потоками, работающими одновременно, для этого существуют механизмы семафоров и мьютексов, но опять же это другая тема...
В других операционных системах также есть свои функции создания процессов и потоков. Наиболее переносимой является семейство функций exec*, которые производят запуск дочернего процесса в адресном пространстве родительского, т.е. после выполнения exec дочерний процесс заменяет родительский. Данная функция есть практически во всех ОС: POSIX, Windows, DOS, OS/2. Формат функции зависит от суффикса функции, например: execl, execlp, execve. Буквы l и v обозначают способ передачи аргументов дочернему процессу: в виде массива строк (суффикс v) или в виде последовательных строк, заканчивающихся нулевой строкой (суффикс l). Суффикс e сигнализирует, что дочернему процессу также передаётся массив строк окружения (имя_переменной=значение). Суффикс p означает, что нужно произвести поиск исполняемого файла в PATH. Итак, общий формат функции:
// указываем переменные окружения
int execve(char *path, char *args[], char *env[])
// идет поиск в PATH
int execlp(char *path, char *arg1, char *arg2 ... char *argn, NULL)
При удачном выполнении функция не возвращает ничего (возвращать-то некому! родительский процесс уже заменён на дочерний), иначе произошла ошибка. Замечу, что в DOS, Windows и OS/2 есть ещё семейство функций spawn*, подобных exec*, но не заменяющих родительский процесс, а создающих параллельный. Причём возвращают они идентификатор процесса. Кроме этого, функции семейства spawn принимают первым параметром режим запуска, который может принимать следующие значения:
- P_WAIT – родительский процесс дожидается завершения дочернего, а затем продолжает выполнение;
- P_NOWAIT – родительский и дочерний процессы выполняются одновременно, но родительский может применить функцию wait для ожидания завершения дочернего процесса (такой режим доступен только для Win32 и OS/2);
- P_NOWAITO – идентично P_NOWAIT, но невозможен вызов wait (работает везде);
- P_DETACH – то же, что и P_NOWAITO, но дочерний процесс выполняется в фоновом режиме без доступа к клавиатуре и дисплею.
Ещё раз повторяю: функции spawn* не поддерживаются POSIX-стандартом. Для ожидания окончания дочернего процесса используется функция pid_t wait(int *process_stat). Данная функция дожидается окончания дочернего процесса и заполняет параметр process_stat. При нормальном завершении дочернего процесса старшие биты данной структуры содержат код завершения программы, иначе устанавливаются биты 1, 2, 3. Если дочерний процесс уже завершился, то функция wait немедленно возвращается, а ресурсы, занятые потомком, освобождаются. В POSIX-системах возможно также применение функции wait к нитям, но об этом далее.
Пример использования функций exec и spawn:
int main()
{
//Формирование массива аргументов
char *ar[2] = {"test.c" "-otest"};
//А теперь нашего процесса уже нет, он заменился gcc
return execvp("gcc", ar);
}
---------------------------------------------------------
int main()
{
//Запускаем gcc параллельно
spawnlp(P_NOWAIT, "gcc", "test.c", "-otest", NULL);
// Для чего-то ждём завершения gcc и выходим
return wait(NULL);
}
В POSIX определён также замечательный системный вызов fork, который выполняет «разделение» существующего процесса. После вызова fork происходит полное копирование адресного пространства существующего процесса, и далее продолжают выполняться 2 процесса, отличающихся только идентификатором процесса. Причём родителю fork возвращает идентификатор дочернего процесса, а дочернему – 0. Так можно определить тип процесса: родитель или потомок. У двух процессов после fork идентично содержимое памяти, стека, файловых дескрипторов, идентификаторов пользователя процесса (UID), каналов и т. д. Не наследуются только те участки памяти, которые были закреплены с помощью mlock(void *mem, size_t size). Можно сказать, что после вызова fork происходит разделение исходного процесса на два идентичных, но при этом между собой не связанных процесса (т.е. адресное пространство у них разное). Далее обычно дочерний и родительский процессы начинают вести себя по-разному, в зависимости от значения, возвращённого fork. C помощью fork можно выполнить множество полезных действий, например, создание демонов, сокетных серверов и т. д. Например, с помощью функции fork можно организовать поведение, аналогичное spawn в win32:
void main()
{
pid_t pid; // Идентификатор процесса
pid = fork(); // А теперь уже 2 процесса
if(pid == -1) // Произошла ошибка
return;
if(pid == 0){ // Это дочерний процесс
execlp("gcc", "test.c", "-otest", NULL);
}
else{
printf("Compiling now\n");
// Так как ждём мы другой процесс, то надо указать его PID
waitpid(pid, NULL, 0);
printf("That`s all\n");
}
}
Кроме вышеперечисленных функций существуют также функции system и popen, выполняющие команды средствами операционной системы. Первая из них int system(char * command) выполняет файл command и возвращает вызывающей программе код завершения. Функция блокирует выполнение родительского процесса до завершения работы потомка. В системах POSIX command выполняется вызовом /bin/sh –c command. Функция system не должна употребляться с программами, устанавливающими биты suid sgid, во избежание «странного» поведения. Функция popen используется в POSIX-системах аналогично system (выполняется /bin/sh –c), но данная функция возвращает файловый поток, связанный с дочерней программой (аналог конвейера | в shell), что позволяет взаимодействовать с потомком. FILE *popen(char *path, char *mode), где mode – режим открытия канала, аналогичный режимам открытия файла в fopen (учтите, что нельзя открывать такой поток одновременно для чтения и записи), при этом реально создается неименнованный канал.
После этого возможно взаимодействие с потомком через этот поток стандартными функциями работы с файлами (fprintf, fscanf, fread, fwrite). Работа с потоком является буферизованной и блокирующей, поэтому всегда надо готовиться к худшему (к получению сигнала SIGCHILD). Если потомок завершился, то попытки чтения из трубы неудачны, а feof возвращает не ноль. Поток, открытый popen, должен завершаться функцией pclose, ждущей окончания процесса и закрывающей созданный канал.
int main()
{
char buf[1024];
FILE *f = NULL;
//Открываем канал в режиме чтения
f = popen("ls -l", "r");
if(f==NULL){ //Что-то не так
perror("Failed to execute ls!");
return -1;
}
while(!feof){ //Пока потомок работает
fgets(buf, 1024, f); //Читаем из канала
printf("%s", buf);
}
pclose(f); //Закрываем канал
return 0;
}
В форуме журнала был вопрос, связанный с поиском файлов в UNIX. Одним из решений может служить взаимодействие со стандартной утилитой find при помощи функции popen("find some_file", "r"). В качестве альтернативы можно также использовать команды locate и whereis.
Во многих UNIX-подобных системах (в частности в Linux) организована поддержка нитей. К сожалению, возможность эта ещё пока недостаточно отлажена и использовать её не так-то просто. Но я, тем не менее, расскажу об основных принципах создания нитей в UNIX. Нити в UNIX представляют собой особый вид процессов и создание нити очень похоже на вызов fork. Для создания нити необходимо применять функции библиотеки pthread. Для каждого потока схема создания следующая: инициализация атрибутов нити, создание нити, уничтожение нити и уничтожение её атрибутов.
int pthread_attr_init(pthread_attr_t *a)
– установка атрибутов по умолчанию в структуре a. Далее можно устанавливать приоритет потока и другие его атрибуты функциями, специальными для каждого атрибута (таких функций 5 пар, отвечающих за включение и выключение отдельных атрибутов).
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void * (*start_func)(void *), void *arg)
– создаёт нить с атрибутами attr на основе функции start_func и передающей этой функции параметры arg типа void * (здесь всё, как и в WinNT). Если создание нити прошло удачно, функция возвращает 0, а поле thread заполняется идентификатором потока.
int pthread_join(pthread_t thread, void **retval)
– данный поток приостанавливает своё исполнение и ждёт завершения потока thread, который заполняет поле retval.
void pthread_exit(void *retval)
– завершает текущий поток и записывает результат выхода retval.
int pthread_attr_destroy(pthread_attr_t *attr)
– очистка структуры атрибутов нити.
Итак, приведу простенький пример создания нити в Linux.
#include <pthread.h>
void *thread_func(void *arg)
{
printf("Thread is running\n");
//Выходим из потока
pthread_exit(NULL);
}
int main()
{
//Атрибуты
pthread_attr_t a;
//Два потока
pthread_t thread1;
pthread_t thread2;
//Создаём атрибуты по умолчанию
pthread_attr_init(&a);
//Создаём потоки
pthread_create(&thread1, &a, thread_func, NULL);
pthread_create(&thread2, &a, thread_func, NULL);
//Ждём завершения 2-го потока
pthread_join(thread2, NULL);
//Убиваем атрибуты
pthread_attr_destroy(&a);
return 0;
}
Компилируем так:
gcc test_thread.c -lpthread