Материал: 396_OS_uchebnoe_posobie_part1_2012

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

Листинг 5. Запуск двух нитей

12.{

13.pthread_t thread_id1, thread_id2;

14.char ch1 = ‘A’, ch2 = ‘B’, ch3 = ‘C’;

15.

16.pthread_create (&pthread_id1, NULL, my_thread,

17.

(void *)&ch1);

18.pthread_create (&pthread_id2, NULL, my_thread,

19.

(void *)&ch2);

20.my_thread ((void *)&ch3);

21.return (0);

22.}

Встроках 1-2 подключаются необходимые заголовочные файлы. В дан-

ном случае нам требуются функции ввода-вывода (stdio.h) и функции по работе с нитями (pthread.h)51. Далее в строках 4-9 описывается функция, которая будет нами дальше использоваться как тело отдельной нити. Создание нитей и запуск их на выполнение происходит в строках 16-19. Основная нить также выполняет функцию my_thread (строка 20). В итоге на экран терминала будет выведена последовательность из символов A, B и C.

Следует обратить внимание, что каждая из нитей использует собственную локальную переменную ch. Забегая вперёд скажем, что приведённый пример не совсем корректен, так как функция printf из стандартной библиотеки glibc не является «безопасной для нитей». Подробнее об этом будет сказано в следующем разделе.

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

Существует несколько способов завершения программы: возврат из функции main (оператор return) и вызов функций exit. Системный вызов exit выглядит следующим образом:

#include <unistd.h>

void exit (int status);

Аргумент status, передаваемый функции exit, возвращается родительскому процессу и представляет собой код возврата программы. При этом считается, что программа возвращает 0 в случае «успеха» и другую величину в случае неудачи. Значение кода неудачи может иметь дополнительную трактовку, определяемую самой программой. Наличие кода возврата позволяет организовать условный запуск программ.

51 Следует помнить, что для компиляции приведённого примера следует указать ключ –lpthread, который определяет, что используется дополнительная библиотека pthread по работе с нитями.

71

При завершении программы важную роль играет значение поля PPID

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

Помимо передачи кода возврата, функция exit производит ряд действий, в частности выводит буферизированные данные и закрывает потоки вводавывода. Альтернативой ей является функция _exit, которая не производит вызовов библиотеки ввода-вывода, а сразу вызывает завершение процесса. Пример использования функции _exit приведён в листинге 6. В приведённом примере функция printf ничего не выдаст на экран. Это связано с тем, что вывод на терминал в каноническом режиме по умолчанию буферизируется52. Фактический вывод символов на экран осуществляется только после помещения в буфер символа конца строки или выполнения вызова fflush.

Листинг 6. Завершение процесса с помощью _exit

1.#include <stdio.h>

2.#include <unistd.h>

3.

4.int main (void){

5.printf (“Строка, которая не будет выведена.”);

6._exit(0);

7.}

Функция exit может быть вызвана в любом месте программы. В некоторых ситуациях неожиданное завершение процесса может привести к необратимым последствиям. Например, если процесс занимается обработкой некоторой информации, начинает процедуру записи и завершается, не закончив её, то обрабатываемые данные могу быть испорчены. Чтобы избежать подобной ситуации процесс может зарегистрировать обработчики выхода (англ. exit handlers) – функции, которые вызываются функцией exit после выполнения стандартных действий, но до окончательного завершения процесса (вызова функции _exit). Обработчики выхода регистрируются с помощью функции atexit. Функцией atexit может быть зарегистрировано до 32 обработчиков. На рисунке 7 проиллюстрированы возможные варианты завершения программы.

#include <unistd.h>

int atexit(void (*func)(void));

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

52 Подробнее о терминалах, режимах их работы рассказывается в курсе «Организация ЭВМ и систем».

72

использовании вызова exit. Если процесс завершается с использованием вызова _exit, то обработчики выхода не вызываются.

Рисунок 7. Механизмы завершения программ

Пример программы, регистрирующей обработчики выхода, представлен в листинге 7.

Листинг 7. Определение обработчиков завершения процесса

1.#include <stdio.h>

2.#include <stdlib.h>

3.

4.void myexit1(void){

5.printf (“Это завершение1.\n”);

6.}

7.

8.void myexit2(void){

9.printf (“Это завершение2.\n”);

10.}

11.

12.int main (void){

13.printf (“Программа запущена.\n”);

14.atexit (myexit1);

15.atexit (myexit2);

16.printf (“Программа отработала.\n”);

17.exit (10);

18.}

73

5.8. Завершение потока (нити)

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

#include <pthread.h>

int pthread_exit(void *value_ptr)

Этот вызов завершает выполняемую нить, возвращая в качестве результата её выполнения value_ptr.

Следует помнить, что завершение процесса всегда сопровождается завершением всех его нитей. Т.е. если нить выполнит вызов exit (а не pthread_exit), то будут завершены все нити и сам процесс.

Нить, так же как и процесс, может быть завершена принудительно. Для отправки требования о завершении нити используется вызов pthread_cancel или pthread_kill:

#include <pthread.h>

int pthread_cancel(pthread_t thread);

int pthread_kill(pthread_t thread, int sig);

5.9. Обработка ошибок

Обычно в случае возникновения ошибки системные вызовы возвращают –1 и устанавливают значение переменной errno, указывающее причину возникновения ошибки. Так, например, существует более десятка причин завершения вызова ореn с ошибкой, и все они могут быть определены с помощью переменной errno. Заголовочный файл errno.h содержит коды ошибок, значения которых может принимать переменная errno, с краткими комментариями.

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

Поскольку базовым способом получения функций ядра являются системные вызовы, рассмотрим более подробно обработку ошибок в этом случае. Переменная errno определена следующим образом:

external int errno;

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

74

Стандарт ANSI С определяет две функции, помогающие сообщить причину ошибочной ситуации: strerror и реrrоr.

Функция strerror имеет вид:

char *strerror(int errnum);

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

Функция реrrоr объявлена следующим образом:

void perror(const char *s);

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

Листинг 8. Использование переменной errno и функций

strerror и perror

1.#include <errno.h>

2.#include <stdio.h>

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

4.fprintf(stderr, “ENOMEM: %s\n”, strerror(ENOMEM));

5.errno = ENOEXEC;

6.perror(argv[0]);

7.}

ВПриложении 1 приведены наиболее общие ошибки системных вызовов, включая сообщения, которые обычно выводят функции strerror и perrоr, а также их краткое описание.

Контрольные вопросы

1.Что означает «запустить программу на выполнение»?

2.Что такое процесс? Дескриптор процесса?

3.Перечислите основные типы процессов. Объясните, чем они отличаются.

4.В каких состояних может находиться процесс? Какой процесс называется «зомби»?

5.Перечислите основные атрибуты процесса. Расскажите об их значении.

6.Что такое группа процессов? Что такое сеанс пользователя?

7.Какие команды используют для работы с процессами? Расскажите о системных вызовах для получения значения идентификаторов.

8.Что такое поток (нить)? В чём отличия процесса от нити?

75

Источник: https://studfile.net/preview/16708918/