Выбрать главу

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

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

В этой главе рассматриваются проблемы, связанные с параллельным выполнением кода и синхронизацией выполнения кода в ядре операционной системы. В следующей главе детально рассмотрены механизмы и интерфейсы, которые предоставляет ядро операционной системы Linux для решения проблем синхронизации и предотвращения состояния конкуренции за ресурс (race condition, состояние "гонок").

Критические участки и состояние конкуренции за ресурсы

Ветки кода, которые получают доступ к совместно используемыми данным и манипулируют ими, называются критическими участками (critical region). Обычно небезопасно нескольким потокам выполнения одновременно обращаться к одному и тому же ресурсу. Для предотвращения конкурентного доступа во время выполнения критических участков программист, т.е. Вы, должен гарантировать, что код выполняется атомарно — без перерывов, так если бы весь критический участок был одной неделимой машинной инструкцией. Если два потока выполнения одновременно находятся в критическом участке, то это — ошибка в программе. Если такое вдруг случается, то такая ситуация называется состоянием, конкуренции за ресурс (состояние "гонок", race condition). Название связано с тем, что потоки как бы соревнуются друг с другом за доступ к ресурсу. Следует обратить внимание на то, насколько редко такая ситуация может возникать, — поэтому обнаружение состояний конкуренции за ресурсы при отладке программ часто очень сложная задача, потому что подобную ситуацию очень трудно воспроизвести. Обеспечение гарантии того, что конкуренции не будет и, следовательно, что состояний конкуренции за ресурсы возникнуть не может, называется синхронизацией.

Зачем нужна защита

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

В качестве первого примера рассмотрим ситуацию из реальной жизни; банкомат (который еще называют ATM, Automated Teller Machine, или кэш-машиной).

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

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

int total = get_total_from_account(); /* общее количество денег на счету */

int withdrawal = get_withdrawal_amount(); /* количество денег,

                                             которые хотят снять */

/* проверить, есть ли у пользователя деньги на счету */

if (total < withdrawal)

 error("У Вас нет таких денег!");

/* Да, у пользователя достаточно денег: вычесть снимаемую сумму из

   общего количества денег на счету */

total -= withdrawal;

update_total_funds(total);

/* Выдать пользователю деньги */

spit_out_money(withdrawal);

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

Обе системы, которые снимают деньги со счета, выполняют код, аналогичный только что рассмотренному: проверяется, что снятие денег возможно, после этого вычисляется новая сумма денег на счету и, наконец, деньги снимаются физически. Теперь рассмотрим некоторые численные значения. Допустим, что первая снимаемая сумма равна $100, а вторая — $10, например, за то, что пользователь зашел в банк (что не приветствуется: необходимо использовать банкомат, так как в банках людей не хотят видеть). Допустим также, что у пользователя на счету есть сумма, равная $105. Очевидно, что одна из этих двух транзакций не может завершиться успешно без получения минусов на счету.

Можно ожидать, что получится что-нибудь вроде следующего: первой завершится транзакция по снятию платы за вход в банк. Десять долларов — это меньше чем $105, поэтому, если от $105 отнять $10, на счету останется $95, а $10 заработает банк. Далее начнет выполняться снятие денег через банкомат, но оно завершится неудачно, так как $95 — это меньше чем $100.

Тем не менее жизнь может оказаться значительно интереснее, чем ожидалось. Допустим, что две указанные выше транзакции начинаются почти в один и тот же момент времени. Обе транзакции убеждаются, что на счету достаточно денег: $105 — это больше $100 и больше $10. После этого процесс снятия денег с банкомата вычтет $100 из $105 и получится $5. В это же время процесс снятия платы за вход сделает то же самое и вычтет $10 из $105, и получится $95. Далее процесс снятия денег обновит состояние счета пользователя: на счету окажется сумма $5. В конце транзакция снятия платы за вход также обновит состояние счета, и на счету окажется $95. Получаем деньги в подарок!

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

Общая переменная