Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

СПО (Корнилов) / Лекции / вар2 / Операционные системы (Корнилов)

.pdf
Скачиваний:
53
Добавлен:
17.04.2013
Размер:
2.11 Mб
Скачать

СПО Лекция 1 [23.12.04]

21

На нулевом уровне приоритета работает процесс обнуления страниц виртуальной памяти.

Классы IDLE, NORMAL и HIGH относятся к классам динамического приоритета, т.е. процессам этих классов система может изменять приоритет. Например, Windows95 Увеличивает приоритет на 2 процессу, окно которого в фокусе. NT увеличивает квант в 3 раза. Система временно повышает приоритет в результате некоторых событий (нажатие на клавишу или кнопку мыши).

Для программного изменения приоритетов служат функции: SetPriorityClass

SetThreadPriority

SetPriorityBoost (для изменения (отмены) правил динамического управления приоритетами)

Лекция 5: Взаимодействующие процессы. Задача взаимного исключения.

9Взаимодействующие процессы

9Атомарные операции

9Программное решение задачи взаимного исключения

Независимые и взаимодействующие процессы

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

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

Преимущества:

пропускная способность (совмещение операций ввода-вывода с вычислениями)

возможность распараллеливать вычисления в однопроцессорных и многопроцессорных системах

модульность: декомпозиция сложной задачи

Проблемы:

синхронизация нитей при доступе к общим ресурсам

недетерминированность и неповторяемость многонитевых программ! Следствие: сложность отладки, необходимость в хорошей методике написания таких программ.

Пример 1:

 

Нить A

Нить B

x = 1

x = 2

Каков результат, если x – глобальная переменная?

СПО Лекция 1 [23.12.04]

 

22

Пример 2:

 

 

Нить A

 

Нить B

a) y = 12

 

 

b) x = 1

 

d) y = 2

c) x = x +y

 

e) y = y + 2

Каков результат, если x и y– глобальные переменные?

d, e, a, b, c:

x=13 y = 12

a, b, c, d, e:

x=13

y = 4

a, b, c1, d, e, c2:

x=5

y = 4

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

Атомарные операции

Операция, которая неделима, непрерываема и, начавшись, обязательно завешается

В большинстве машин атомарными являются операции записи в память и чтения из памяти.

Неатомарными, например, являются операции записи (чтения) переменных типа DOUBLE на машинах с 32 битной архитектурой.

Пример 3: Два процесса добавляют новый элемент в конец линейного списка с глобальными указателями head и tail. Действия:

(A) a = new Item();

(B) a->next = null;

(C) tail->next = a; - это не атомарная операция: читать tail; писать в tail->next

(D) tail = a

Последовательность действий: C1, D1, C2, D2 - все OK

C1, C2, D1, D2 - потеряли элемент, список - OK

C1, C2, D2, D1 - tail

указывает на оторванный элемент, последующие добавления в список

приведут к созданию списка, на голову которого нет указателя

Пример 4:

 

Нить A

Нить B

i=0;

i=0;

while(i<10000) i++;

while(i>-10000) i--;

printf(“A wins”);

printf(“B wins”);

Кто победит? Кто быстрее!

СПО Лекция 1 [23.12.04]

23

Есть гарантия, что кто-то выиграет?

Нет. Могут крутиться бесконечно!

Если выполняются на отдельных идентичных процессорах, то будут ли крутиться бесконечно? Совсем не факт!

Мораль: с общими переменными надо работать аккуратно.

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

«Критическая секция» – часть кода, которая в любой момент времени может выполняться только одним процессом

unlock

 

lock

 

lock

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Критическая секция

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

как атомарную операцию!

Если критическая секция заперта, то процесс ждет снятия блокировки. С синхронизацией всегда связано ОЖИДАНИЕ.!

Решение: «Поочередный вход в критическую секцию»

Нить A

Нить B

 

while (q==1);

while (q==2);

// Вход

КС

КС

// Критическая секция

q = 1;

q = 2;

// Выход

Что если одна нить завершится, а другая попытается войти в критическую секцию? Будет навечно заблокирован в цикле.

Решение задачи взаимного исключения ищется при следующих предположениях:

Процессы асинхронные

Только один может быть в критической секции в каждый момент времени

Остановка вне критической секции не мешает работать другим

СПО Лекция 1 [23.12.04]

24

Вход в критическую секцию гарантируется за конечное время

Программное решение задачи взаимного исключения

Решение 1:

 

 

Используем 2 флага:

 

 

q1

– нить А внутри критической секции

 

q2

- нить В внутри критической секции

 

Нить A

Нить B

 

while (q2);

while (q1);

// Вход

q1=true;

q2=true;

 

КС

КС

// Критическая секция

q1

= false;

q2 = false;

// Выход

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

Решение 2.

 

 

Используем 2 флага:

 

 

q1

– нить А внутри критической секции

 

q2

- нить В внутри критической секции

 

Нить A

Нить B

 

q1=true;

q2=true;

 

while (q2);

while (q1);

// Вход

КС

КС

// Критическая секция

q1

= false;

q2 = false;

// Выход

В этом решении изменение порядка установки флаг и проверки может привести к блокировке, если оба успели установить флаги до проверки.

Решение 3.

Используем 2 флага и постараемся избежать блокировки:

q1

– нить А внутри критической секции

 

q2

- нить В внутри критической секции

 

Нить A

Нить B

 

q1=true;

q2=true;

 

while (q2) {

while (q1) {

// Вход

q1 = false;

q2 = false;

// снять флаг

sleep(?);

sleep(?);

 

q1=true;

q2=true;

 

}

 

}

 

КС

КС

// Критическая секция

q1

= false;

q2 = false;

// Выход

СПО Лекция 1 [23.12.04]

25

В этом решении практически все правильно, но теоретически можно предположить одинаковый темп работы процессов, так что возникнет бесконечное откладывание

Решение Деккера (1970).

Вводим дополнительную переменную для того, чтобы две нити вели себя по-разному: q1 – нить А внутри критической секции

q2 - нить В внутри критической секции

 

Нить A

Нить B

 

q1=true;

q2=true;

 

while (q2) {

while (q1) {

// Вход

if(z==2) {

if(z==1) {

 

q1 = false;

q2 = false;

// снять флаг

while(z==2);

while(z==1);

 

q1=true;

q2=true;

 

}

}

 

}

}

 

КС

КС

// Критическая секция

z = 2;

z = 1;

// Выход

q1 = false;

q2 = false;

 

Решение Петерсена (1981)

 

 

Int lastID; // 0 , 1

 

 

Bool InSect[2];

 

 

Void Enter(int procID) {

Void Leave(int procID) {

Int other = 1 - procID;

InSect[procID] = false;

InSect[procID] = true;

}

 

LastID = procID;

 

 

While(lastID==procID && InSect[other]);

}

Резюме

Решение слишком сложное для такой простой задачи

Решение не симметричное. А что, если не две задачи?

Пока А ждет, он занимает процессор («занятое ожидание»)

Что нужно?

Более сложные атомарные инструкции на аппаратном уровне

Высокоуровневые примитивы в языках для взаимного исключения, опирающиеся на аппаратную поддержку (lock-unlock).

СПО Лекция 1 [23.12.04]

26

Исключить «занятое ожидание»

Хотелось бы видеть решение примерно в таком виде:

Lock();

КС; UnLock();

Лекция 6: Реализация взаимного исключения

9Аппаратные средства для поддержки синхронизации.

9Абстракции более высокого уровня для синхронизации на основе аппаратных средств.

9Определение семафора

9Пример использования

Способы реализации взаимного исключения

Абстракция нитей хороша, но синхронизация непосредственно на операциях чтения-записи в память слишком сложно и подвержено ошибкам.

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

В этой лекции, мы рассмотрим способы построения примитивов синхронизации на базе атомарных операций, обеспеченных аппаратными средствами.

Низкий уровень

Высокий уровень

Load/store

чтение/запись памяти

Lock (mutex)

Interrupts

запрет прерываний

Semaphore

Test & Set инструкция «проверить и установить»

Condition variables

 

 

Monitor

 

 

Send & Receive

Есть примеры прямой аппаратной реализации инструкций взаимного исключения и переключения контекста процессов (Intel 432). Решение получается меленным, а аппаратура слишком сложной.

На основе инструкций чтения-записи памяти

Уже видели во что выливается такое решение: сложно, не симметрично, имеет место занятое ожидание

На основе запрета / разрешения прерываний (однопроцессорные)

Процесс может быть прерван в результате одного из следующих событий:

внутреннего (системный вызов и связанное с ним переключение контекста

внешнее событие (прерывание или вызов планировщика по таймеру).

СПО Лекция 1 [23.12.04]

27

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

Обойти внутренние события легко (не использовать «нежелательные» системные вызовы). Блокировать внешние события можно путем запрета прерываний.

Почему бы не сделать так:

void Lock::Acquire() { asm{cli}; } // Запрет прерываний void Lock::Release() { asm{sti}; } // Разрешение прерываний

Нельзя позволять процессу пользователя управлять запретом прерываний (работа ОС блокируется и ОС может никогда не получить управление).

В системах реального времени необходимо гарантировать отклик на прерывание в течение ограниченного интервала времени, а продолжительность критической секции может быть сколь угодно велика.

Основная идея: управлять флажком разрешения входа в критическую секцию и использовать взаимное исключение для доступа к этому флагу.

Class Lock { bool busy;

public:

Lock() { busy = false; } void Acquire();

void Release(); };

void Lock::Acquire ()

{

// Запрет прерываний if(busy) {

....................// (1)? разрешить прерывания ? // поставить в очередь к этому Lock

....................// (2)? разрешить прерывания ? schedule(); // заснуть и передать управление другому

}

else

busy = true;

// Разрешить прерывания

}

void Lock::Release ()

{

//Запрет прерываний if(очередь не пуста) {

//исключить первого из очереди

//поставить в очередь готовых

}

else busy = false;

//Разрешить прерывания

}

СПО Лекция 1 [23.12.04]

28

Запрет прерываний гарантирует что между проверкой и установкой флага busy никто не сможет вмешаться. Гарантирует что два процесса не смогут получить разрешение на вход в критическую секцию одновременно.

Заметим, что Release – не приводит к переключению контекста! Когда разрешать прерывания?

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

(2)Если разрешить прерывания после включения процесса в очередь ожидающих, но до schedule, то другая нить, вызвав Release, может поставить процесс в очередь готовых, а сам процесс считает , что он должен заснуть и вызывает schedule.

Разрешение прерываний можно переложить на schedule! При вызове в стеке сохраняется текущее состояние обработки прерываний, соответственно, при восстановлении контекста активизируемой нити восстанавливается ее состояние обработки прерываний.

Нить А

Нить В

. . .

Прерывания разрешены

Запрет прерываний

 

Schedule()

 

 

Прерывания разрешены

 

. . .

 

Schedule()

Прерывания запрещены

 

. . .

 

На основе инструкции «Test & Set»

Запрет прерываний не работает на многопроцессорных машинах!

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

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

СПО Лекция 1 [23.12.04]

29

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

Будем называть такие инструкции «test&set» и считать, что в одно действие эта инструкция считывает содержимое ячейки памяти и записывает в нее 1.

Как это работает?

Пусть переменная value = 0.

void Lock::Acquire()

{

while(test&set(value) == 1); // Ждем

}

void Lock::Release() { value) = 0; } // Освобождаем

Недостаток: - занятое ожидание; в результате которого задерживается выполнение нити, захватившей Lock.

Можно ли устранить занятое ожидание? – Нет!

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

Применять можно, если минимизировать занятое ожидание, например, так:

Class Lock { bool busy int guard;

public:

Lock() { busy = false; guard = 0; } void Acquire();

void Release(); };

void Lock::Acquire ()

{

while(test&set(quard)); if(busy) {

// поставить в очередь к этому Lock schedule(&gaurd); // передать управление другому

// и сбросить guard

}

else {

busy = true; guard = 0;

}

}

СПО Лекция 1 [23.12.04]

30

void Lock::Release ()

{

while(test&set(quard)); if(очередь не пуста) {

//исключить первого из очереди

//поставить в очередь готовых

}

else

busy = false; guard = 0;

}

Резюме

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

Одна из первых реализаций взаимного исключения без занятого ожидания реализована в UNIX в виде системных вызовов (sleep и wakeup).

Особенности:

wakeup до sleep эквивалентен «пустому» действию и факт его выполнения не регистрируется

Если ждут несколько, то wakeup “ разбудит ” всех сразу.

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

Семафоры

Семафоры – примитивы для синхронизации параллельных процессов, предложенные (Edsger Dijkstra) в середине 60-х. Являлись основным примитивом синхронизации в раннем UNIXе. Сейчас используются в

OS/2, Windows NT, UNIX.

С семафором связана целочисленная переменная, очередь ожидающих на семафоре процессов и две функции:

P (proberen)- атомарная операция, которая останавливает работу процесса(нити) до тех пор, пока переменная семафора станет положительной, затем уменьшает переменную на 1. Является аналогом операции wait.

V (verhogen)- атомарная операция, которая увеличивает переменную семафора на 1 и будит один

процесс из очереди к семафору, если кто-то ждет. Является аналогом операции signal.

Class Semaphore {

int count;

ThreadDescriptor* queue;

Pubic:

Semaphore(int n) { count = n; queue = null; }

Void P();

Соседние файлы в папке вар2