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

Завершение процесса

Завершить процесс можно, направив ему сигнал с помощью утилиты slay или kill в командной строке, или из программы с помощью функций: kill(), sigqueue() и др. Завершение процесса выполняется в две стадии.

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

На второй – уничтожается структура данных о процессе (метаданные), хранящаяся в оперционной системе. Эта стадия выполняется Администратором процессов внутри самого себя.

3.4. Потоки в qnx6

Поток можно понимать, как любой автономный последовательный (линейный) набор команд процессора. Идентификатором потока (значимым только внутри одного процесса!) является TID (Thread ID), присваиваемый потоку при его создании вызовом pthread_create(). TID позволяет процессу (а также системе в рамках процесса) однозначно идентифицировать каждый поток. Нумерация TID в QNX начинается с 1 (это всегда главный поток процесса, порожденный main()) и последовательно возрастает по мере создания потоков (до 32767) [15]. Другим важнейшим атрибутом потока является приоритет его выполения. Для каждого из уровней приоритетов, обслуживаемях системой (в QNX 6.3 таких уровней 256), поддерживается циклическая очередь потоков, готовых к исполению (фактически большая часть из таких очередей оказывается пустой).

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

Завершение потока (pthread_exit(), pthread_cancel()) включает в себя останов потока и освобождение ресурсов потока. Когда поток запущен, он может находиться в одном из двух состояний: «готов» (ready) или «блокирован» (blocked) [18]. Поток может иметь одно из следующих состояний (рис.49):

  • CONDVAR- поток блокирован на условной переменной (например, при вызове функции pthread_condvar_wait());

  • DEAD- поток завершен и ожидает завершение другого потока;

  • INTERRUPT-поток блокирован и ожидает прерывания (т.е. поток вызвал функцию InterruptWait);

  • JOIN-поток- блокирован и ожидает завершение другого потока ( например, при вызове функции pthread_join());

  • MUTEX- поток блокирован блокировкой взаимного исключения;

  • NANOSLEEP-поток находиться в режиме ожидания в течение короткого периода времени (например, при вызове функции nanosleep());

  • NET_REPLY-поток ожидает ответа на сообщение от другого узла сети (т.е. поток вызвал функцию MsgReply*());

  • NET_SEND- поток ожидает получения импульса или сигнала от другого узла сети (т.е. поток вызвал функцию MsgSendPulse(), MsgDeliverEvent() или SignalKill());

  • READY- поток ожидает выполнения, пока процессор занят выполнением другого потока равного или более высокого приоритета; RECEIVE- поток блокирован на операции получения сообщения (например, при вызове функции MsgReceive());

  • RUNNING- поток выполняется процессором;

Рис. 49 Возможные состояния потока в QNX Neutrino

  • REPLY- поток блокирован при ответе на сообщение (т.е. при вызове функции MsgSend() и получении сообщения сервером);

  • SEM- поток ожидает освобождения семафора (т.е. поток вызвал функцию SyncSemWait());

  • SEND- поток блокируется при отправке сообщения (т.е. после вызова функции MsgSend(), но получение сообщения сервером еще не произошло );

  • SIGSUSPEND- поток блокирован и ожидает сигнала (т.е поток вызвал функцию sigsuspend());

  • SIGWAITINFO- блокирован и ожидает сигнала (т.е. поток вызвал функцию sigwaitinfo());

  • STACK-поток ожидает выделения виртуального адресного пространства для своего стека (родительский поток вызывает функцию ThreadCreate());

  • STOPPED- поток блокирован и ожидает сигнала SIGCONT;

  • WAITCTX-поток ожидает доступности нецелочисленного контекста (например, с плавающей запятой);

  • WAITPAGE- поток ожидает выделения физической памяти для виртуального адреса;

  • WAITTHREAD- поток ожидает завершения создания дочернего потока (т.е. поток вызвал функцию ThreadCreate()).

Управление потоком заключается в его создании, изменении его параметров в процессе выполнения и завершения потока. Процесс состоит минимум из одного потока. Этот первый (и, возможно, единственный) поток начинается функцией main() и включает все те функции, которые будут вызваны. Остальные потоки создаются с помощью функции:

pthread_create(&tid, &attr, &func, &arg).

Для вызова функции pthread_create() необходимо использовать библиотеку pthead.h. Функция имеет 4 параметра [19]. Обязательным параметром является только func – точка входа в поток, т.е. имя функции, с которой начнет выполнение поток, своего рода «потоковая функция» main().

Первым параметром является возвращаемое значение идетификатора TID созданного потока. Его бывает полезно знать, например, для уничтожения созданного потока функцией: pthread_cancel( tid ).

Второй параметр содержит адрес структура атрибутов attr, на основе которой задаются атрибуты создаваемого потока. Оно содержит ряд весьма полезных полей. Если вместо этой структуры при создании потока указать NULL, то поток будет создан с атрибутами по умолчанию (при этом параметры диспетчеризации будут наследоваться от родительского потока), но изменить их после создания потока нельзя, поэтому лучше сразу создать структуру атрибутов pthread_attr_init(&attr).

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

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

Список функций управления атрибутами выглядит довольно большим (18 функций), но они сгруппированы по парам «get» — « set», т e. в каждой паре есть функция как получения параметров (get), так и их установки (set) .

Флаги (булевы характеристики)

pthread_attr_getdetachstate(); pthread_attr_setdetachstate();

pthread_attr_getinheritsched(); pthread_attr_setinheritsched();

pthread_attr_getscope(); pthread_attr_setscope();

Параметры стека

pthread_attr_getguardsize(); pthread_attr_setguardsize()

pthread_attr_getstackaddr(); pthread_attr_setstackaddr()

pthread_attr_getstacksize(); pthread_attr_setstacksize()

Параметры диспетчеризации

pthread_attr_getschedparar(); pthread_attr_setschedparain()

pthread_attr_getschedpolicy(); pthread_attr_setschedpolicy()

Третим параметером является точка входа в поток, т.е. имя функции, с которой начнет выполнение поток (обязательный параметр).

Четвертый параметр содержит параметры для создаваемого потока.

В коде реальных приложений очень часто можно видеть простейшую форму вызова, порождающего новый поток, в следующем виде:

pthread_create( NULL, NULL, &thread_func, NULL );

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

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

Можно ожидать завершение присоединенного потока в некотором другом потоке процесса (чаще всего именно в родительском, но это не обязательно) с помощью следующего вызова:

int pthread_join( pthread_t thread, void** value_ptr ),

где thread - идентификатор TID ожидаемого потока, который возвращался как первый параметр вызова pthread_create( pthread_t* thread, …) при создании потока или же пможет быть получен после своего создания вызовом pthread_self (); value_ptr - NULL или указатель на область данных (результата выполнения), которую завершающийся поток, возможно, захочет сделать доступной для внешнего мира после своего завершения. Этот указатель функция потока возвращает оператором return или вызовом pthread_exit().

Для дочернего потока может потребоваться установить (через структуру attr) иную по отношению к родителю дисциплину (политику) диспетчеризации (SCHED_FIFO, SCHED_RR, SCHED_SPORADIC) [17]:

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

Round-robin – карусельная, вытесняющая многозадачность, режим квантования времени. Поток работает непрерывно только в течение определенного кванта времени. После истечения кванта его вытесняет поток равного приоритета.

Спорадическая (адаптивная) – предназначена для установления лимита использования потоком процессора в течение определенного времени. Потоку задаются параметры: нормальный N, низкий L приоритеты, начальный бюджет C, период восстановления T, максимальное число пропусков восстановлений P. Когда поток находится в состоянии «готов», его приоритет имеет значение N в течении С, после чего приоритет снижается до L, ограниченный временем Т или истечением количества вызовов P.

Все политики диспетчеризации работают с потоками из одной очереди (для QNX 6.3 их 256), очереди потоков наивысшего из присутствующих в системе приоритетов (потоков в состоянии «готов»).

Если в системе выполняется поток высокого приоритета, то ни один поток более низкого приоритета не получит управление до тех пор, пока поток высокого приоритета не будет переведен в блокированное состояние при ожидании некоторого события (рис. 50).

На рис. 50 представлены два процесса (процесс A с PID=671179 и процесс В с PID=620004), каждый из которых создает внутри себя несколько потоков с диспечеризацией Round-robin, но с различными приоритетами (10 и 12). Жирной сплошной линией на рис.50 показан порядок, в котором потоки высокого приоритета (12) объединены в циклическую очередь диспетчеризации. Эта активная очередь диспетчеризации (наивысшего приоритета). Пукнктирной линией показан порядок потоков в другой очереди (приоритета 10). До тех пор пока все потоки активной очереди (приоритет 12) не окажутся в силу каких-либо обстоятельств в блокированном состоянии, ни один из потоков очереди приоритета 10 не получит ни единого кванта времени.

P rio

Пр/П

Пр/П

Пр/П

10

A/1

11

12

А)

Prio

Пр/П

Пр/П

Пр/П

10

A/1

11

12

B/1

Б)

Prio

Пр/П

Пр/П

Пр/П

10

A/1

B/2

11

12

B/1

В)

P rio

Пр/П

Пр/П

Пр/П

10

A/1

B/2

11

12

B/1

Г)

Prio

Пр/П

Пр/П

Пр/П

10

A/1

B/2

11

12

B/1

A/2

Д)

Prio

Пр/П

Пр/П

Пр/П

10

A/1

B/2

11

12

B/3

A/2

B/1

Е)

Рис. 50 Диспетчеризация потоков

Рассмотрим временную диаграмму выполнения процессов А и B, состоящую из 8 этапов:

    1. Вместе с модулем микроядра procnto в системе работает интерпретатор команд пользователя sh. Пользователь запускает процесс А

    2. В системе создается процесс А с главным потоком (tid=1) и приоритетом 10. Это показано в таблице очередей приоритетов на рис 50а. Очередь приоритета 10 становится активной. После некоторого времени работы главный поток процесса А вызывает запуск процесса В.

    3. В системе создается процесс В с главным потоком (tid=1) и приоритетом 12. Это показано в таблице очередей приоритетов на рис 50б. Очередь приоритета 12 становится активной.

    4. Затем главный поток процесса В запускает второй поток (tid=2) процесса В с приоритетом 10. Созданный поток (В/2) не получает управления так как его приоритет ниже 12 и его очередь приоритетов не активна рис 50в.

    5. После некоторого времени работы главный поток (В/1) процесса В блокируется в ожидании доступа к некоторому ресурсу. Очередь приоритетов 12 становится не активной. Активной становится очередь приоритетов 10, где находятся потоки А/1 и В/2 рис 50г. Они некоторое время попеременно работают.

    6. Затем главный поток процесса А запускает второй поток (А/2) с tid=2 и приоритетом 12. Очередь приоритетов 12 становится активной и поток А/2 работает в системе один, при этом потоки А/1 и В/2 с приоритетами 10 перестают получать управление. Поток В/1 получает недостающий ресурс и начинает делить процессорное время с потокам А/2, так как они в активной очереди приоритетов рис 50д.

    7. Главный поток процесса В запускает свой третий поток (В/3) с tid=3 и приоритетом 12. Созданный поток (В/3) получает управления и участвует в конкуренции за процессорное время наравне с потоками В/1 и А/2 очереди приоритетов 12 рис 50е.

    8. Пока все потоки (В/1, А/2, В/3) активной очереди (приоритет 12) не окажутся в силу каких-либо обстоятельств в блокированном состоянии, ни один из потоков (А/1 и В/2) очереди приоритета 10 не получит ни единого кванта времени.

Рассмотрим временные затраты на создание потока на примере программы p2-2.cc [17]:

//P2-2.cc

#include <stdlib.h>

#include <stdio.h>

#include <inttypes.h>

#include <iostream.h>

#include <sys/neutrino.h>

#include <pthread.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;

};

void* threadfunc ( void* data ) {

pthread_exit( NULL );

};

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

uint64_t t = ClockCycles();

pthread_t tid;

pthread_create( &tid, NULL, threadfunc, NULL );

pthread_join( tid, NULL );

t = ClockCycles() - t;

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

exit( EXIT_SUCCESS );

};

На рис.51 показано выполнение программы p2-2.cc в перспективе System Profiler.

Рис.51. Выполнение программы p2-2.cc в перспективе System Profiler

Программа начинается с точки входа – функции main(). Главный поток, реализуемый функцией main() имеет tid = 1. Переменная имеет тип данных uint64_t. С помощью функции ClockCycles() в ней определяется текущее количество процессорных циклов, которое необходимо для вычисления времни затрачиваемого на создание потока.

Функция pthread_create() создает дочерний поток c tid = 2, реализуемый функцией threadfunc(). Дочерний поток сразу после запуска завершает функцией pthread_exit( NULL ). Во время работы дочернего потока, родительский ожидает его завершения на функции pthread_join(). Как только родительский поток разблокируется в переменной t = ClockCycles() - t определяется количество процессорных затраченных на порожденние потока. Затем результаты выводятся на экран через поток и родительский поток завершается функцией exit( EXIT_SUCCESS ).

На результаты этого теста значительно влияет приоритет, с которым выполняется программа. Для запуска программы p2-2.cc из командной строки может использоваться команда nice и сторока запуска будет выглядеть так #nicen-19 p2-2. При одиинаковых приоритетах программ p2-1.cc и p2-2.cc создание потока выполняется в 20 раз быстрее чем процесса.

Теперь рассмотрим скрость переключения потоков на примере программы p5t.cc [17]. Она организована аналогично программе p5.cc. На рис.52 показано выполнение программы p5t.cc в перспективе System Profiler.

// P5t.cc

#include <stdlib.h>

#include <stdio.h>

#include <inttypes.h>

#include <iostream.h>

#include <unistd.h>

#include <pthread.h>

#include <errno.h>

#include <sys/neutrino.h>

unsigned long N = 1000; // количество циклов

static pthread_barrier_t bstart;

void* threadfunc ( void* data ) {

pthread_barrier_wait( &bstart );

uint64_t t = ClockCycles();

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

t = ClockCycles() - t; // задает количество потоков, которое будет создано

delay( 100 );

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

return NULL;

};

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

int opt, val;

while ( ( opt = getopt( argc, argv, "n:" ) ) != -1 ) {

switch( opt ) {

case 'n' :

if( sscanf( optarg, "%i", &val ) != 1 )

cout << "parse command line error" << endl, exit( EXIT_FAILURE );

if( val > 0 ) N = val;

break;

default :

cout << "parse command line failed" << endl, exit( EXIT_FAILURE );

break;

}

};

const int T = 2;

pthread_t tid[ T ];

if( pthread_barrier_init( &bstart, NULL, T ) != EOK )

cout << "barrier init error", exit( EXIT_FAILURE );

for( int i = 0; i < T; i++ )

if( pthread_create( tid + i, NULL, threadfunc, NULL ) != EOK )

cout << "thread create error", exit( EXIT_FAILURE );

for( int i = 0; i < T; i++ ) pthread_join( tid[ i ], NULL );

exit( EXIT_SUCCESS );

};

Программа начинается с точки входа – функции main(). Если при запуске программы в командной строке задано количество повторений цикла с ключом n, то с помощью функций getopt() и sscanf() оно преобразуется в новое значение переменной n. Затем функцией pthread_barrier_init( &bstart, NULL, T ) создается барьер для синхронизации запуска двух дочерних потоков. Главный поток создает два дочерних потока функцией pthread_create() с tid=2 и tid=3, а затем ожидает их завершения на функции pthread_join().

Рис.52. Выполнение программы p5t.cc в перспективе System Profiler

Каждый дочерний поток при запуске блокируется на функции pthread_barrier_wait( &bstart ). Когда последний дочерний поток создан, то все блокированные на барьере &bstart потоки одновременно разблокируются и становятся готовыми к выполнению. В переменных t (своей для каждого дочернего потока) имеющей тип данных uint64_t функцией ClockCycles() определяется текущее колисество процессорных циклов.

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

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

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