В операционной системе существует некоторое количество процессов, готовых к выполнению в определенный момент времени. Для своего выполнения эти процессы требуют различные ресурсы. Таким образом, имеется некий набор общих системных ресурсов системы (память, периферийные устройства), за доступ к которым конкурируют разные процессы. Однако следует иметь в виду, что совместное использование ресурсов возможно только при условии, что в любой момент времени к каждому определенному ресурсу имеет доступ только один процесс. Например, если в системе есть принтер, и этот принтер может использоваться несколькими процессами но одновременно только один процесс может использовать принтер. Нельзя разрешить двум процессам использовать принтер в один и тот же момент времени. Это называется синхронизацией процессов.
В многозадачном программировании часто встречается такой термин, как синхронизация. По сути это не что иное, как организация упорядоченного функционирования каких-нибудь отдельных процессов, когда они работают над одной общей задачей, или когда результаты работы одних процессов могут повлиять на поведение других процессов.
С подобной синхронизацией мы часто сталкиваемся и в обычной жизни, например когда ждем автобус на остановке. Пока автобус (один процесс) не приедет, вы (второй процесс) не сможете попасть в него, чтобы приехать в офис. И автобус, и вы существуете и работаете независимо, но чтобы выполнить определенную задачу доставки вас до нужного места, необходима синхронизация между вами и автобусом.
В мире микроконтроллеров тоже существует множество ситуаций, когда необходима синхронизация при доступе к общему ресурсу. Например, есть SPI-память flash, которая используется для записи в неё лога диагностики, и эта же память используется для хранения WAV-файлов голосового интерфейса пользователя. Если пользователь нажал на кнопку, то нужно проиграть WAV-файл голосового приветствия, и в это время запись информации в лог должна быть приостановлена.
В этой статье (перевод [1]) мы обсудим синхронизацию процессов и критическую секцию. Также рассмотрим решения проблемы критической секции, например мьютексы и семафоры.
[Синхронизация процессов и критическая секция]
Рассмотрим ситуацию, когда у нас есть 2 процесса, и они используют одну и ту же переменную "a". Они считывают эту переменную, обновляют её значение, и затем записывают данные в память.
SomeProcess()
{
...
read(a) // инструкция 1
a = a +5// инструкция 2
write(a) // инструкция 3
...
}
В этом примере куска псевдокода мы видим, что он делает операции последовательно - считывает читает переменную "a", инкрементирует значение "a" на 5, и в завершение записывает значение "a" в память. Все хорошо, пока у нас один процесс, работа кода хорошо прогнозируема. Но теперь у нас есть два аналогичных процесса P1 и P2, которые должны выполнится. Рассмотрим 2 возможных случая, и предположим, что начальное значение переменной "a" равно 10.
Случай 1. В этом случае процесс P1 выполнится непрерывно (т. е. выполнит все свои 3 инструкции), и после этого запустится P2. Тогда P1 прочитает из "a" значение 10, увеличит его на 5, получится 15. В завершение это значение будет записано в память. А памяти окажется значение 15, которое прочитает P2, увеличит на 5, получится 20, и запишет это значение в память. В этом случае конечное значение переменной "a" будет 20.
Случай 2. Предположим, что начал свое выполнение P1. Он прочитает "a" из памяти значение 10 (мы предположили, что начальное значение переменной "a" равно 10). И в этот момент произошло переключение контекста между процессом P1 и P2 (переключение контекста иногда называют вытеснением, говорят что P2 вытесняет P1, т. е. P1 временно приостанавливает свою работу с сохранением текущего состояния, и управление выполнением кода переходит к процессу P2). Таким образом, P2 перейдет в рабочее состояние, и P1 окажется в ожидании, сохранив текущее состояние. Поскольку процесс P1 не успел пока поменять значение "a", то P2 также прочитает из "a" значение 10. P2 увеличит "a" на 5, и запишет полученное значение 15 в память. После того, как P2 завершит свою работу, снова запустится процесс P1. Он восстановит свое состояние, в котором он уже прочитал из переменной "a" значение 10 (потому что P1 уже выполнил инструкцию 1). Затем P1 увеличит это значение на 5, и полученное значение 15 запишет в память. В этом случае конечное значение переменной "a" будет 15.
В этих двух описанных выше случаях конечные результаты операций различаются - в одном случае получилось 20, а в другом 15. В чем причина такого неоднозначного поведения?
Очевидно, что проблема в не разграниченном доступе к общему ресурсу, т. е. к переменной "a". В первом случае сначала выполнился P1, и за ним запустился P2. Но во втором случае процесс P1 был приостановлен после выполнения первой инструкции, и начал выполнение процесс P2. И здесь оба процесса обратились одновременно к одному и тому же ресурсу переменной "a". Это и есть критическая секция обработки. Таким образом, в этом месте должна быть реализована какая-то синхронизация между процессами, когда они пытаются обратиться к одному и тому же общему ресурсу.
Общие ресурсы могут использоваться всеми процессами, но процессы должны быть уверены, что в определенный момент времени только один процесс должен использовать этот общий ресурс. Это называется синхронизацией процессов.
Для синхронизации процессов используют 2 метода:
1. Мьютекс. 2. Семафор.
[Мьютекс]
Мьютекс называется так от английской аббревиатуры Mutex (т. е. Mutual Exclusion Object, или объект взаимного исключения). Он используется, чтобы дать в любой момент времени доступ к ресурсу только одному процессу. Объект мьютекса позволяет всем процессам использовать один и тот же ресурс, но при этом всегда есть гарантия, что в любой момент времени ресурсом пользуется только тот процесс, который владеет мьютексом. Мьютекс использует технику, основанную на блокировке потоков, которые не владеют мьютексом, и тем самым решается проблема критической секции.
Каждый раз, когда процесс запрашивает ресурс у системы, система создает объект-мьютекс с уникальным именем или идентификатором. Таким образом, каждый раз, когда процесс хочет использовать этот ресурс, процесс с помощью мьютекса блокирует доступ к объекту, и благодаря этому на некоторое время получает монопольный доступ к ресурсу (для примера выше он может атомарно выполнить инструкции 1, 2 и 3). После захвата мьютекса процесс использует ресурс и по завершению работы с ресурсом освобождает объект-мьютекс. После этого другие процессы могут создать объект-мьютекс таким же образом и использовать его.
С помощью блокировки объекта этот конкретный ресурс назначается для доступа со стороны конкретного процесса, и никакие другие процессы не могут обратиться к этому ресурсу. Так что в критической секции никакие другие процессы не могут вмешаться и повлиять на общий ресурс. Таким способом на объекте мьютекса может быть достигнута синхронизация процессов.
[Семафор]
Семафор это целочисленная переменная S, которая инициализируется количеством ресурсов, имеющихся в системе, и семафор также используется для синхронизации процессов. При этом используются две функции для изменения значения семафора S, например wait() и signal(). Обе эти функции используются для модификации значения семафора, однако при этом в любой момент времени разрешается менять значение семафора только одной функции, т. е. никакие два процесса не могут поменять значение семафора одновременно. Существуют 2 вида семафоров - семафор со счетчиком (counting semaphore) и двоичный семафор (binary semaphore).
Counting semaphore. При создании семафора со счетчиком его переменная инициализируется количеством доступных ресурсов определенного вида. После этого любой процесс, который нуждается в некотором ресурсе, вызовет функцию wait(), и значение переменной семафора уменьшается при этом на 1. Далее процесс использует ресурс, и когда завершит работу с ним, вызовет функцию signal(), которая увеличит переменную семафора на 1. Таким образом, когда значение переменной семафора дойдет до 0 (например когда было множество вызовов wait() и недостаточное количество вызовов signal()), то все ресурсы оказываются занятыми. В этой ситуации если какой-нибудь процесс попытается получить доступ к ресурсу, защищенному семафором, то он заблокирует свое выполнение на вызове функции wait() и будет ждать, пока запрашиваемый ресурс не освободится. Таким способом мы достигаем синхронизации процессов.
Binary semaphore. С двоичным семафором значение переменной семафора может быть 0 или 1. Изначально значение переменной семафора устанавливается в 1, и если какой-то процесс хочет завладеть ресурсом, то он вызовет функцию wait(), и значение переменной семафора станет равным 0. Затем процесс работает с ресурсом, и когда закончит работу с ним, он вызывает функцию signal(), после чего значение переменной семафора возвращается к значению 1. В течение интервала времени, пока переменная семафора равна 0, если какой-либо другой процесс захочет получить доступ к этому ресурсу и вызовет для этого функцию wait(), то он заблокирует свое выполнение и будет ждать, пока переменная семафора снова не установится в значение 1. Таким способом мы можем реализовать синхронизацию между процессами.
[Чем отличается мьютекс от семафора]
● Мьютекс использует механизм блокировки, т. е. процесс который хочет использовать ресурс, блокирует его, и после использования освобождает. С другой стороны, семафор использует механизм сигнализации, где методы wait() и signal() используются, чтобы показать, освободил ли процесс ресурс или занял его. ● Мьютекс это объект, а семафор это целочисленная переменная. ● Для работы с семафорами у нас есть функции wait() и signal(). Но при работе с мьютексом таких функций нет. ● Объект мьютекса позволяет многим потокам обращаться к одному и тому же общему ресурсу, но в любой момент времени доступ имеет только один поток. С другой стороны, семафор позволяет нескольким потокам использовать одновременно ограниченный объем ресурса определенного вида, пока этот ресурс не исчерпается. ● С мьютексом блокировку и освобождение ресурса должен сделать один и тот же процесс. Но значение переменной семафора может быть изменено любым процессом, которому нужен какой-то ресурс, но это изменение всегда атомарное, т. е. в любой момент времени изменить переменную семафора может только один процесс.
Примечание: мьютекс по сути является разновидностью двоичного семафора, просто может различаться API-интерфейс для использования мьютексов и двоичных семафоров. Во многих RTOS-системах на микроконтроллерах физически это одно и то же, и используется один и тот же механизм блокировки выполнения потоков, пока не будет освобожден нужный ресурс.