Средства синхронизации в ядре

Средства синхронизации в ядре, интерфейсы к ним, особенности работы. Порядок выполнения атомарных операций. Функции работы со спин-блокировками. Создание и инициализация семафоров. Условные переменные и секвентные блокировки. Барьеры компилятора и памяти.

Рубрика Программирование, компьютеры и кибернетика
Вид лекция
Язык русский
Дата добавления 22.12.2011
Размер файла 45,9 K

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

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

Размещено на http://www.allbest.ru/

Взаимоисключение параллельных процессов и потоков

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

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

Атомарные операции (atomic operations) предоставляют инструкции, которые выполняются атомарно, -- т.е. не прерываясь. Так же как и атом вначале считался неделимой частицей, атомарные операции являются неделимыми инструкциями. Например, как было показано в предыдущей главе, операция атомарного инкремента позволяет считывать из памяти и увеличивать на единицу значение переменной за один неделимый и непрерывный шаг. В отличие от состояния конкуренции за ресурс, которая обсуждалась в предыдущей главе, результат выполнения такой операции всегда один и тот же, например, как показано в следующем примере (допустим, что значение переменной i вначале равно 7).

Поток 1

инкремент i (7->8)

--

Поток 2

--

инкремент i (8->9)

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

Ядро предоставляет два набора интерфейсов для выполнения атомарных операций: один -- для работы с целыми числами, а другой -- для работы с отдельными битами. Эти интерфейсы реализованы для всех аппаратных платформ, которые поддерживаются операционной системой Linux. Большинство аппаратных платформ поддерживают атомарные операции или непосредственно, или путем блокировки шины доступа к памяти при выполнении одной операции (что в свою очередь гарантирует, что другая операция не может выполниться параллельно). Это как-то позволяет справиться с проблемой в случае аппаратных платформ, таких как SPARC, которые не поддерживают базовых машинных инструкций для выполнения атомарных операций.

Целочисленные атомарные операции

Средства выполнения атомарных операций с целыми числами работают с типом данных atomic_t. Вместо того, чтобы использовать функции, которые работают непосредственно с типом данных int языка С, по ряду причин используется специальный тип данных. Во-первых, функции, которые выполняют атомарные операции, принимают только аргументы типа atomic_t, это гарантирует, что атомарные операции выполняются только с данными этого специального типа. В то же время это также гарантирует, что данные этого типа не смогут передаваться в другие функции, которые не выполняют атомарных операций. Действительно, ничего хорошего не будет от таких атомарных операций, которые иногда атомарные, а иногда -- нет. Следующий момент-- использование типа atomic_t позволяет гарантировать, что компилятор (по ошибке, но для повышения эффективности) не будет оптимизировать операции обращения к атомарным переменным. Важно, чтобы атомарные операции получали правильное значение адреса переменной в памяти, а не адреса временных копий. И наконец, за типом atomic_t скрываются различия между реализациями для различных аппаратных платформ.

Кроме того, что тип atomic_t -- это 32-разрядное целое число на всех машинах, которые поддерживаются операционной системой Linux, при разработке кода необходимо учитывать, что максимальный диапазон значений переменной этого типа не может быть больше 24 бит. Это связано с аппаратной платформой SPARC, для которой используется несколько странная реализация атомарных операций: в младшие 8 бит 32-разрядного целого числа типа int встроена блокировка, как показано на рис. 9.1.

32-разрядный тип atomic_t

24-битовое знаковое целое

Блокировка

(Бит) 31

7

0

Рис. 9.1. Структура 32-битового типа atomic_t для, аппаратной платформы SPARC в старой реализации

Блокировка используется для предотвращения параллельного доступа к переменной атомарного типа, так как для аппаратной платформы SPARC отсутствует соответствующая поддержка на уровне машинных инструкций. Следовательно, на машинах SPARC могут быть использованы только 24 бит. Хотя код, который рассчитан на использование полного 32-битового диапазона значений, будет работать и на машинах других типов, он может приводить к странным и коварным ошибкам на машинах типа SPARC, и так делать не нужно. В последнее время умные хакеры додумались, как для аппаратной платформы SPARC обеспечить тип atomic_t, который позволяет хранить полноценное 32-разрядное целое число, и указанного ограничения больше не существует. Тем не менее старая 24-битовая реализация все еще используется в старом коде для аппаратной платформы SPARC, и этот код все еще имеется в файле <asm/atomic.h> для этой аппаратной платформы.

Объявления всего, что необходимо для использования целочисленных атомарных операций, находятся в заголовочном файле <asm/atomic.h>. Для некоторых аппаратных платформ существуют дополнительные средства, которые уникальны только для этой платформы, но для всех аппаратных платформ существует минимальный набор операций, которые используются в ядре повсюду. При написании кода ядра необходимо гарантировать, что соответствующие операции доступны и правильно реализованы для всех аппаратных платформ.

Объявление переменных типа atomic_t производится обычным образом. При необходимости можно установить заданное значение этой переменной.

atomic_t u; /* определение переменной и */

atomic_t v = ATOMIC_INIT (0); /* определение переменной v и

инициализация ее в значение нуль */

Выполнять операции так же просто.

atomic_set(&v, 4); /* v = 4 (атомарно) */

atoraic_add(2, &v); /* v = v + 2 = 6 (атомарно) */

atomic_inc(&v); /*v = v + l = 7 (атомарно) */

Если необходимо конвертировать тип atomic_t в тип int. то нужно использовать функцию atomic_read().

printk("%d\n", atomic_read (sv) ); /* будет напечатано "7" */

Наиболее частое использование атомарных целочисленных операций -- это инкремент счетчиков. Защищать один счетчик с помощью сложной системы блокировок -- это глупо, поэтому разработчики используют вызовы atcmic_int () и atomic_dec (), которые значительно быстрее. Еще одно использование атомарных целочисленных операций -- это атомарное выполнение операции с проверкой результата. Наиболее распространенный пример -- это атомарные декремент и проверка результата, с помощью функции

int atomic_dec_and_test (atoraic_t *v).

Эта функция уменьшает на единицу значение заданной переменной атомарного типа. Если результат выполнения операции равен нулю, то возвращается значение true, иначе возвращается false. Полный список всех атомарных операций с целыми числами (т.е. тех, которые доступны для всех аппаратных платформ) приведен в табл. 9.1. Все операции, которые реализованы для определенной аппаратной платформы, приведены в файле <asm/atomic. h>.

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

Таблица 9.1, Полный список всех атомарных операций с целыми числами

Атомарная целочисленная операция

Описание

ATOMIC_INIT(int i)

int atomic read(atomic_t *v)

void atomic set (atomic t *v, int i)

void atomic add(int i, atomic_t *v)

void atomic_sub(int i, atomic_t *v)

void atomic_inc(atomic_t *v)

void atomic_dec(atomic_t *v)

int atomic_sub_and_test (int i, atomic_t *v)

int atomic_add_negative(int i, atoraic_t *v)

int atomic dec and test(atomic t *v)

int atomic inc and test(atomic t *v)

Объявление и инициализация в значение i переменной типа atomic_t

Атомарное считывание значения целочисленной переменной v

Атомарно установить переменную v в значение i

Атомарно прибавить значение i к переменной v

Атомарно вычесть значение i из переменной v

Атомарно прибавить единицу к переменной v

Атомарно вычесть единицу из переменной v

Атомарно вычесть значение i из переменной v и возвратить true, если результат равен нулю, и false в противном случае

Атомарно прибавить значение i к переменной v и возвратить true, если результат операции меньше нуля, иначе возвратить false

Атомарно вычесть единицу из переменной v и возвратить true, если результат операции равен нулю, иначе возвратить false

Атомарно прибавить единицу к переменной v и возвратить true, если результат операции равен нулю, иначе возвратить false

Атомарность и порядок выполнения

От атомарных операций чтения перейдем к различиям между атомарностью и порядком выполнения. Как уже рассказывалось, операции чтения одного машинного слова всегда выполняются атомарно. Эти операции никогда не перекрываются операциями записи того же машинного слова. Иными словами, операция чтения данных всегда возвращает машинное слово в консистентном состоянии: иногда возвращается значение, которое было до записи, а иногда -- то, которое стало после записи, но никогда не возвращается значение, которое было во время записи. Например, если целочисленное значение вначале было равно 42, а потом стало 365, то операция чтения всегда вернет значение 42 или 365, но никогда не смешанное значение. Это называется атомарностью.

Иногда бывает так, что вашему коду необходимо нечто большее, например операция чтения всегда выполняется перед ожидающей операцией записи. Это называется не атомарностью, а порядком выполнения (ordering). Атомарность гарантирует, что инструкции выполняются не прерываясь и что они либо выполняются полностью, либо не выполняются совсем. Порядок выполнения же гарантирует, что две или более инструкций, даже если они выполняются разными потоками или разными процессами, всегда выполняются в нужном порядке. Атомарные операции, которые обсуждаются в этом разделе, гарантируют только атомарность. Порядок выполнения гарантируется с помощью операций барьеров (barrier), которые будут рассмотрены дальше в текущей главе.

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

Битовые атомарные операции

В дополнение к атомарным операциям с целыми числами, ядро также предоставляет семейство функций, которые позволяют работать на уровне отдельных битов. Не удивительно, что эти операции зависят от аппаратной платформы и определены в файле <asm/bitops. h>.

Тем не менее может вызвать удивление то, что функции, которые реализуют битовые операции, работают с обычными адресами памяти. Аргументами функций являются указатель и номер бита. Бит 0-- это наименее значащий бит числа, которое находится по указанному адресу. На 32-разрядных машинах бит 31 -- это наиболее значащий бит, а бит 0-- наименее значащий бит машинного слова. Нет ограничений на значение номера бита, которое передается в функцию, хотя большинство пользователей работают с машинными словами и номерами битов от 0 до 31 (или до 63 для 64-битовых машин).

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

unsigned long word = 0;

set_bit(0, &word);

/* атомарно устанавливается бит 0 */

set_bit(l, &word);

/* атомарно устанавливается бит 1 */

printk("%ul\n", word);

/* будет напечатано "3" */

clear_bit(l, &word);

/* атомарно очищается бит 1 */

change_bit (0, &word);

/* атомарно изменяется значение бита 1, теперь он очищен */

/* атомарно устанавливается бит нуль и возвращается предыдущее

значение этого бита (нуль) */

if (test_and_set_bit(0, &word)) {

/* условие никогда не выполнится... */

}

Список стандартных атомарных битовых операций приведен в табл. 9.2.

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

Таблица 9.2, Список стандартных атомарных битовых операций

Атомарная битовая операция

Описание

void set_bit(int nr, void *addr)

void clear_bit (int nr, void *addr)

void change_bit (int nr, void *addr)

int test_and_set_bit (int nr, void *addr)

int test_and_clear_bit(int nr, void *addr)

int test_and_change_bit(int nr, void *addr)

int test_bit(int nr, void *addr)

Атомарно установить nr -й бит в области памяти, которая начинается с адреса addr

Атомарно очистить nr -й бит в области памяти, которая начинается с адреса addr

Атомарно изменить значение nr -го бита в области памяти, которая начинается с адреса addr, на инвертированное

Атомарно установить значение nr -го бита в области памяти, которая начинается с адреса addr, и возвратить предыдущее значение этого бита

Атомарно очистить значение nr -го бита в области памяти, которая начинается с адреса addr, и возвратить предыдущее значение этого бита

Атомарно изменить значение nr -го бита в области памяти, которая начинается с адреса addr, на инвертированное и возвратить предыдущее значение этого бита

Атомарно возвратить значение nr-го бита в области памяти, которая начинается с адреса addr

Откуда берутся неатомарные битовые операции

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

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

Ядро также предоставляет функции, которые позволяют найти номер первого установленного (или не установленного) бита, в области памяти, которая начинается с адреса addr:

int find_first_bit(unsigned long *addr, unsigned int size)

int find_first_zero_bit(unsigned long *addr, unsigned int size)

Обе функции в качестве первого аргумента принимают указатель на область памяти и в качестве второго аргумента - количество битов, по которым будет производиться поиск. Эти функции возвращают номер первого установленного или не установленного бита соответственно. Если код производит поиск в одном машинном слове, то оптимальным решением будет использовать функцииf f s () и f f z (), которые в качестве единственного параметра принимают машинное слово, где будет производиться поиск.

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

Спин-блокировки

Было бы очень хорошо, если бы все критические участки были такие же простые, как инкремент или декремент переменной, однако в жизни все более серьезно. В реальной жизни критические участки могут включать в себя несколько вызовов функций. Например, очень часто данные необходимо извлечь из одной структуры, затем отформатировать, произвести анализ этих данных и добавить результат в другую структуру. Весь этот набор операций должен выполняться атомарно. Никакой другой код не должен иметь возможности читать ни одну из структур данных до того, как данные этих структур будут полностью обновлены. Так как ясно, что простые атомарные операции не могут обеспечить необходимую защиту, то используется более сложный метод защиты -- блокировки (lock).

Наиболее часто используемый тип блокировки в ядре Linux -- это спин-блокировки (spin lock). Спин-блокировка-- это блокировка, которую может удерживать не более чем один поток выполнения. Если поток выполнения пытается захватить блокировку, которая находится в состоянии конфликта (contended), т.е. уже захвачена, поток начинает выполнять постоянную циклическую проверку (busy loop) -- "вращаться" (spin), ожидая на освобождение блокировки. Если блокировка не находится в состоянии конфликта при захвате, то поток может сразу же захватить блокировку и продолжить выполнение. Циклическая проверка предотвращает ситуацию, в которой более одного потока одновременно может находиться в критическом участке. Следует заметить, что одна и та же блокировка может использоваться в нескольких разных местах кода, и при этом всегда будет гарантирована защита и синхронизация при доступе, например, к какой-нибудь структуре данных.

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

Спин-блокировки являются зависимыми от аппаратной платформы и реализованы на языке ассемблера. Зависимый от аппаратной платформы код определен в заголовочном файле <asm/spinlock.h>. Интерфейс пользователя определен в файле <linux/spinlock.h>. Рассмотрим пример использования спин-блокировок.

spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;

spin_lock(&mr_lock);

/* критический участок... */

spin_unlock(&mr_lock);

В любой момент времени блокировка может удерживаться не более чем одним потоком выполнения. Следовательно, только одному потоку позволено войти в критический участок в данный момент времени. Это позволяет организовать защиту от состояний конкуренции на многопроцессорной машине. Заметим, что на однопроцессорной машине блокировки не компилируются в исполняемый код, и, соответственно, их просто не существует. Блокировки играют роль маркеров, чтобы запрещать и разрешать вытеснение кода (преемптивность) в режиме ядра. Если преемптивность ядра отключена, то блокировки совсем не компилируются.

Внимание: спин-блокировки не рекурсивны!

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

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

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

spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;

unsigned long flags;

spin_lock_irqsave(&mr_lock, flags);

/* критический участок... */

spln_unlock_irqrestore(&mr_lock, flags);

Подпрограмма spin_lock_irqsave О сохраняет текущее состояние системы прерываний, запрещает прерывания и захватывает указанную блокировку. Функция spin_unlock_irqrestore (), наоборот, освобождает указанную блокировку и восстанавливает предыдущее состояние системы прерываний. Таким образом, если прерывания были запрещены, показанный код не разрешит их по ошибке. Заметим, что переменная flags передается по значению. Это потому, что указанные функции частично выполнены в виде макросов.

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

Что необходимо блокировать

Важно, чтобы каждая блокировка была четко связана с тем, что она блокирует. Еще более важно -- это защищать данные, а не код. Несмотря на то что во всех примерах этой главы рассматриваются критические участки, в основе этих критических участков лежат данные, которые требуют защиты, а никак не код. Если блокировки просто блокируют участки кода, то такой код труднопонимаем и подвержен состояниям гонок. Необходимо ассоциировать данные с соответствующими блокировками. Например, структура struct foo блокируется с помощью блокировки f oo_lock. С данной блокировкой также необходимо ассоциировать некоторые данные. Если к некоторым данным осуществляется доступ, то необходимо гарантировать, что этот доступ будет безопасным. Наиболее часто это означает, что перед тем, как осуществить манипуляции с данными, необходимо захватить соответствующую блокировку, и освободить эту блокировку нужно после завершения манипуляций.

Если точно известно, что прерывания разрешены, то нет необходимости восстанавливать предыдущее состояние системы прерываний. Можно просто разрешить прерывания при освобождении блокировки. В этом случае оптимальным будет использование функций spin_lock_irq() и spin_unlock_irq().

spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;

spin_lock_irq(&mr_lock);

/* критический участок... */

spin_unlock_irq(&mr_lock);

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

Отладка спин-блокировок

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

Другие средства работы со спин-блокировками

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

Функция spin try lock () производит попытку захватить указанную спин-блокировку. Если блокировка находится в состоянии конфликта, то, вместо циклической проверки и ожидания на освобождение блокировки, эта функция возвращает ненулевое значение. Если блокировка была захвачена успешно, то функция возвращает нуль. Аналогично функция spinislocked () возвращает ненулевое значение, если блокировка в данный момент захвачена. В противном случае возвращается нуль. Эта функция никогда не захватывает блокировку.

В табл. 9.3 приведен полный список функций работы со спин-блокировками.

Таблица 9.3. Список функций работы со спин-блокировками

Функция

Описание

spin lock ()

spin_lock_irq ()

spin_lcck_irqsave ()

spin unlock ()

spin_unlock_irq (

Spin_unlock_irqrestore ()

spin_lock_init()

spin_trylock()

spin_is_locked()

Захватить указанную блокировку

Запретить прерывания на локальном процессоре и захватить указанную блокировку

Сохранить текущее состояние системы прерываний, запретить прерывания на локальном процессоре и захватить указанную блокировку

Освободить указанную блокировку

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

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

Инициализировать объект типа spinlock_t в заданной области памяти

Выполнить попытку захвата указанной блокировки и в случае неудачи возвратить ненулевое значение

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

Спин-блокировки и обработчики нижних половин

Как было указано в главе 7, "Обработка нижних половин и отложенные действия", при использовании блокировок в работе с обработчиками нижних половин необходимо принимать некоторые меры предосторожности. Функция spin_lock_bh () позволяет захватить указанную блокировку и запретить все обработчики нижних половин. Функция spin_unlock_bh() выполняет обратные действия.

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

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

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

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

Спин-блокировки чтения-записи

Иногда в соответствии с целью использования блокировок их можно разделить два типа -- блокировки чтения (reader lock) и блокировки записи (writer lock). Рассмотрим некоторый список, который может обновляться и в котором может выполняться поиск. Когда список обновляется (в него осуществляется запись), никакой другой код не может параллельно осуществлять запись или чтение этого списка. Запись означает исключительный доступ. С другой стороны, если в списке выполняется поиск (чтение информации), важно только, чтобы никто другой не выполнял записи в список. Работа со списком заданий в системе (как обсуждалось в главе 3, "Управление процессами") аналогична только что описанной ситуации. Не удивительно, что список заданий в системе защищен с помощью спин-блокировки чтения1-записи (reader-writer spin lock).

Если работа со структурой данных может быть четко разделена на этапы чтения/записи, как в только что рассмотренном случае, то имеет смысл использовать механизмы блокировок с аналогичной семантикой. Для таких ситуаций операционная система Linux предоставляет спин-блокировки чтения-записи. Спин-блокировки чтения-записи обеспечивают два варианта блокировки. Один или больше потоков выполнения, которые одновременно выполняют операции считывания, могут удерживать такую блокировку. Блокировка на запись, наоборот, может удерживаться в любой момент времени только одним потоком, осуществляющим запись, и никаких ' параллельных считываний не разрешается. Блокировки чтения-записи иногда также называются соответственно shared/exclusive (общая/исключающая) или concurrent/ exclusive (параллельная/исключающая).

Инициализировать блокировку для чтения-записи можно с помощью следующего программного кода.

rwlock_t mr_rwlock = RW_LOCK_UNLOCKED;

Следующий код осуществляет считывание.

read_lock(&mr_rwlock);

/* критический участок (только для считывания)... */

read unlock(&mr rwlock);

И наконец, показанный ниже код осуществляет запись.

write_lock(Smr_rwlock);

/* критический участок (чтение и запись)... */

write_unlock(&mr_rwlock);

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

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

read_lock(&mr_rwlock);

write_lock(&mr_rwlock);

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

Несколько потоков чтения безопасно могут удерживать одну и ту же блокировку чтения-записи. На самом деле один поток также может безопасно рекурсивно захватывать одну и ту же блокировку для чтения. Это позволяет выполнить полезную и часто используемую оптимизацию. Если в обработчиках прерываний осуществляется только чтение и не выполняется запись, то можно "смешивать" использование блокировок с запрещением прерываний и без запрещения. Для защиты данных при чтении можно использовать функцию read_lock () вместо read_lock_irqsave (). При обращении к данным для записи все равно необходимо запрещать прерывания, например использовать функцию write_lock_irqsave (), так как в обработчике прерывания может возникнуть взаимоблокировка в связи с ожиданием захвата блокировки на чтение при. захваченной блокировке на запись. В табл. 9.4 показан полный список средств работы с блокировками чтения-записи.

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

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

Таблица 9.4. Список функций работы со спин-блокировками чтения-записи

Функция

Описание

read_lock()

read_lock_irq()

read_lock_irqsave()

read_unlock()

read_unlock_irq()

read_unlock_irqrestore()

write_lock()

write_lock_irq()

write_lock_irqsave()

write_unlock()

write_unlock_irq()

write_unlock_irqrestore ()

write_trylock()

rw_lock_init()

rw_is_locked ()

Захватить указанную блокировку на чтение

Запретить прерывания на локальном процессоре и захватить указанную блокировку на чтение

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

Освободить указанную блокировку, захваченную для чтения

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

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

Захватить заданную блокировку на запись

Запретить прерывания на локальном процессоре и захватить указанную блокировку на запись

Сохранить состояние системы прерываний на текущем процессоре, запретить прерывания на локальном процессоре и захватить указанную блокировку на запись

Освободить указанную блокировку, захваченную, для записи

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

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

Выполнить попытку захватить заданную блокировку на запись и в случае неудачи возвратить ненулевое значение

Инициализировать объект типа rwlockt в заданной области памяти

Возвратить ненулевое значение, если указанная блокировка захвачена, иначе возвратить нуль

Семафоры

В операционной системе Linux семафоры (semaphore) -- это блокировки, кото-.рые переводят процессы в состояние ожидания. Когда задание пытается захватить семафор, который уже удерживается, семафор помещает это задание в очередь ожидания (wait queue) и переводит это задание в состояние ожидания (sleep). Когда процессы3, которые удерживают семафор, освобождают блокировку, одно из заданий очереди ожидания возвращается к выполнению и может захватить семафор.

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

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

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

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

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

· Так как поток выполнения во время конфликта при захвате блокировки находится в состоянии ожидания, то семафоры можно захватывать только в контексте процесса. Контекст прерывания планировщиком не управляется.

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

· При захвате семафора нельзя удерживать спин-блокировку, поскольку процесс может переходить в состояние ожидания, ожидая на освобождение семафора, а при удержании спин-блокировки в состояние ожидания переходить нельзя.

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

Последняя полезная функция семафоров-- это то, что они позволяют иметь любое количество потоков, которые одновременно удерживают семафор. В то время как спин-блокировки позволяют удерживать блокировку только одному заданию в любой момент времени, количество заданий, которым разрешено одновременно удерживать семафор, может быть задано при декларации семафора. Это значение называется счетчиком использования (usage count) или просто счетчиком (count). Наиболее часто встречается ситуация, когда разрешенное количество потоков, которые одновременно могут удерживать семафор, равно одному, как и для спин-блокировок. В таком случае счетчик использования равен единице и семафоры называются бинарными семафорами (binary semaphore) (потому что он может удерживаться только одним заданием или совсем никем не удерживаться) или взаимоисключающими блокировками (mutex, мъютекс) (потому что он гарантирует взаимоисключающий доступ--mutual exclusion). Кроме того, счетчику при инициализации может быть присвоено значение, большее единицы. В этом случае семафор называется счетным семафором (counting semaphore, семафор-счетчик), и он допускает количество потоков, которые одновременно удерживают блокировку, не большее чем значение счетчика использования. Семафоры-счетчики не используются для обеспечения взаимоисключающего доступа, так как они позволяют нескольким потокам выполнения одновременно находиться в критическом участке. Вместо этого они используются для установки лимитов в определенном коде. В ядре они используются мало. Если вы используете семафор, то, скорее всего, вы используете взаимоисключающую блокировку (семафор со счетчиком, равным единице).

Семафоры были формализованы Эдсгером Вайбом Дейкстрой4 (Edsger Wybe Dijkstra) в 1968 году как обобщенный механизм блокировок. Семафор поддерживает две атомарные операции Р () и V (), название которых происходит от голландских слов Proben (тестировать) и Verhogen (выполнить инкремент). Позже эти операции начали называть down () и up () соответственно.

В операционной системе Linux они имеют такое же название. Операция down () используется для того, чтобы захватить семафор путем уменьшения его счетчика на единицу. Если значение этого счетчика больше или равно нулю, то блокировка захватывается успешно и задание может входить в критический участок. Если значение счетчика меньше нуля, то задание помещается в очередь ожидания и процес-" сор переходит к выполнению каких-либо других операций. Об использовании этой функции говорят в форме глагола-- семафор опускается (down) для того, чтобы его захватить. Метод up () используется для того, чтобы освободить семафор после завершения выполнения критического участка. Эту операцию называют поднятием (upping) семафора.

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

Создание и инициализация семафоров

Реализация семафоров зависит от аппаратной платформы и определена в файле <asm/semaphore.h>. Структура struct semaphore представляет объекты типа семафор. Статическое определение семафоров выполняется следующим образом.

static DECLARE_SEMAPHORE_GENERIC(name, count);

где name -- имя переменной семефора, a count -- счетчик семафора. Более короткая запись для создания взаимоисключающей блокировки (mutex), которая используются наиболее часто, имеет следующий вид.

static DECLARE_MUTEX(name);

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

sema_init(sem, count);

где sem-- это указатель, a count -- счетчик использования семафора. Аналогично для инициализации динамически создаваемой взаимоисключающей блокировки можно использовать функцию

init_MUTEX(sem);

Неизвестно, почему слово "mutex" в имени функции init_MUTEX () выделено большими буквами и почему слово "ink" идет перед ним, в то время как имя функции sema_init () таких особенностей не имеет. Тем не менее ясно, что это выглядит не логично, и я приношу свои извинения за это несоответствие. Надеюсь, что после прочтения главы 7 ни у кого уже не будет вызывать удивление то, какие имена придумывают символам ядра.

Использование семафоров

Функция down_interruptible () выполняет попытку захватить данный семафор. Если эта попытка неудачна, то задание переводится в состояние ожидания с флагом TASK_INTERRUPTIBLE. Из материала главы 3 следует вспомнить, что такое состояние процесса означает, что задание может быть возвращено к выполнению с помощью сигнала и что такая возможность обычно очень ценная. Если сигнал приходит в тот момент, когда задание ожидает на освобождение семафора, то задание возвращается к выполнению, а функция down_interruptible О возвращает значение -EINTR. Альтернативой рассмотренной функции выступает функция down (), которая переводит задание в состояние ожидания с флагом TASK_UNINTERRUPTIBLE. В большинстве случаев это нежелательно, так как процесс, который ожидает на освобождение семафора, не будет отвечать на сигналы. Поэтому функция down_interruptible () используется значительно более широко, чем функция down (). Да, имена этих функций, конечно, далеки от идеала.

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

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

/* объявление и описание семафора с именем mr_sera и первоначальным значением счетчика, равным 1 */

static DECLARE_MUTEX(mr_sem);

...

if (down_interruptible(&mr_sem))*

/* получен сигнал и семафор не захвачен */

/* критический участок... */

/* освободить семафор */

up(Srar_sem);

Полный список функций работы с семафорами приведен в табл. 9.5.

Таблица 9.5. Список функций работы с семафорами

Функция

Описание

sema_init (struct semaphore *, int)

init_MUTEX (struct semaphore *)

init_MUTEX_LOCKED (struct semaphore *)

down_interruptible (struct semaphore *)

down (struct semaphore *)

down_trylock (struct semaphore *)

up(struct semaphore *)

Инициализация динамически созданного семафора и установка для него указанного значения счетчика использования

Инициализация динамически созданного семафора и установка его счетчика использования в значение 1

Инициализация динамически созданного семафора и установка его счетчика использования в значение 0 (т.е. семафор изначально заблокирован)

Выполнить попытку захватить семафор и перейти в прерываемое состояние ожидания, если семафор находится в состоянии конфликта при захвате (contended)

Выполнить попытку захватить семафор и перейти в непрерываемое состояние ожидания, если семафор находится в состоянии конфликта при захвате (contended)

Выполнить попытку захватить семафор и немедленно возвратить ненулевое значение, если семафор находится в состоянии конфликта при захвате (contended)

Освободить указанный семафор и возвратить к выполнению ожидающее задание, если такое есть

Семафоры чтения-записи

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

Семафоры чтения-записи представляются с помощью структуры struct rw_ semaphore, которая определена в файле <asm/rwsem.h>. Статически определенный семафор чтения-записи может быть создан с помощью функции

static DECLARE_RWSEM(name);

где name -- это имя нового семафора.

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

init_rwsem(struct rw_semaphore *sem)

Все семафоры чтения-записи являются взаимоисключающими (mutex), т.е. их счетчик использования равен единице. Любое количество потоков чтения может одновременно удерживать блокировку чтения, если при этом нет ни одного потока записи. И наоборот, только один поток записи может удерживать блокировку, захваченную на запись, если нет ни одного потока чтения. Все семафоры чтения-записи используют непрерываемое состояние ожидания, поэтому существует только одна версия функции down (). Рассмотрим следующий пример.

static DECLARE_RWSEM(mr_rwsem);

/* попытка захватить семафор для чтения */

down_read(&mr_rwsem);

/* критический участок (только чтение)... */

/* освобождаем семафор */

up_read(Smr_rwsem);

/*...*/

/* попытка захватить семафор на запись */

down_write(&mr_rwsem);

/* освобождаем семафор */

/* критический участок (чтение и запись) */


Подобные документы

  • Функции программного интерфейса операционной системы Windows, предназначенные для работы с семафорами. Средства синхронизации Win32 АРI, основанные на использовании объектов исполнительной системы с дескрипторами. Проблемы при использовании семафоров.

    реферат [67,4 K], добавлен 06.10.2010

  • Классификация компьютерной памяти. Использование оперативной, статической и динамической оперативной памяти. Принцип работы DDR SDRAM. Форматирование магнитных дисков. Основная проблема синхронизации. Теория вычислительных процессов. Адресация памяти.

    курсовая работа [1,5 M], добавлен 28.05.2016

  • История разработки многозадачной операционной системы POSIX-стандарта - FreeBSD; описание ее виртуальной памяти, файловой системы, уровня защиты. Описание основных средств синхронизации процессов - сигналов и семафоров. Способы блокировки файлов.

    презентация [584,2 K], добавлен 02.06.2011

  • Разработка приложения, автоматизирующего процесс синхронизации файлов между сменным носителем и каталогом на другом диске. Классы для работы с файловой системой. Интерфейс программы и способы взаимодействия пользователя с ним. Создание новой синхропары.

    курсовая работа [632,0 K], добавлен 21.10.2015

  • Архитектура многопроцессорных систем с общей шиной и с неоднородным доступом к памяти. Структура кэш памяти. Взаимодействие user space с kernel space. Средства синхронизации ядра Linux. Обход каталогов страниц. Инструментация кода средствами Clang.

    дипломная работа [513,7 K], добавлен 14.11.2017

  • Основные ограничения синхронизации, необходимые для корректного функционирования системы. Добавление в код производителя и потребителя операторов синхронизации для обеспечения ее корректной работы. Сигнал конечного буфера производителя-потребителя.

    курсовая работа [167,0 K], добавлен 05.12.2012

  • Диаграмма последовательности работы в интерфейсе программы. Интерфейсы необходимых классов и их взаимодействие. Средства обработки исключений. Начальный экран работы программы. Инструменты работы с персоналом. Основные функции работника регистратуры.

    курсовая работа [3,7 M], добавлен 09.10.2013

  • Модель памяти как набор опций компилятора, ее виды в BC++2.0, размеры и взаимное расположение. Назначение сегментных регистров в различных моделях памяти, порядок просмотра переменных. Основные и дополнительные функции динамических переменных в памяти.

    лабораторная работа [28,4 K], добавлен 06.07.2009

  • Программно-аппаратный комплекс производства компании Nvidia. Код для сложения векторов, представленный в CUDA. Вычислительная схема СPU с несколькими ядрами SMP. Выделение памяти на видеокарте. Проведение синхронизации работы основной и GPU программ.

    презентация [392,5 K], добавлен 14.12.2013

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

    дипломная работа [1,7 M], добавлен 03.06.2012

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.