Главная страница
Навигация по странице:

  • Таблица 7.3.

  • Механизм обработки нижних половин Контекст выполнения Сериализация

  • Блокировки между обработчиками нижних половин

  • Таблица 7.4.

  • Внизу обработки нижних половин

  • Второе издание


    Скачать 3.09 Mb.
    НазваниеВторое издание
    Дата08.09.2019
    Размер3.09 Mb.
    Формат файлаpdf
    Имя файлаLav_Robert_Razrabotka_yadra_Linux_Litmir.net_264560_original_254.pdf
    ТипДокументы
    #86226
    страница21 из 53
    1   ...   17   18   19   20   21   22   23   24   ...   53
    Какие обработчики нижних половин
    необходимо использовать
    Решение о том, какой из механизмов обработки нижних половин следует исполь- зовать, является важным. В современных ядрах серии 2.6 есть три варианта выбора:
    отложенные прерывания (softirq), тасклеты (tasklet) и очереди отложенных действий
    (work queue). Тасклеты построены на основе отложенных прерываний, и поэтому
    8
    Названия для механизмов обработки нижних половин, очевидно, выбираются из соображений конспирации, чтобы сбивать с толку молодых и неопытных разработчиков ядра.
    Обработка нижних половин и отложенные действия 157
    эти два механизма похожи. Механизм очередей действий полностью от них отлича- ется, он построен на базе потоков пространства ядра.
    Благодаря своей реализации, отложенные прерывания обеспечивают наибольший параллелизм. Это требует от обработчиков отложенных прерываний применения до- полнительных мер для того, чтобы гарантировать безопасный доступ к совместно используемым данным, так как два или более экземпляров одного и того же отло- женного прерывания могут выполняться параллельно на разных процессорах. Если код уже очень хорошо распараллелен для многопоточного выполнения, как, напри- мер, сетевая подсистема, которая использует данные, связанные с процессорами, то использование отложенных прерываний— это хороший выбор. Они, конечно, пред- ставляют собой наиболее быстрый механизм для критичных ко времени или частоте выполнения задач. Тасклеты имеет больший смысл использовать для кода, который не очень хорошо распараллелен для многопоточности. Они имеют более простой интерфейс, и поскольку тасклеты одного типа не могут выполняться параллельно,
    то их легко программировать. Тасклеты — это фактически отложенные прерывания,
    которые не могут выполняться параллельно. Разработчики драйверов всегда должны использовать тасклеты, а не отложенные прерывания, кроме, конечно, случаев, ког- да они готовы связываться с такими вещами, как переменные, связанные с процес- сорами (рег-CPU data), или другими хитростями, чтобы гарантировать безопасное параллельное выполнение отложенных прерываний на разных процессорах.
    Если отложенные операции требуют выполнения в контексте процесса, то из трех возможных вариантов остается единственный выбор — это очереди действий.
    Если выполнение в контексте процесса не является обязательным, в частности, если нет необходимости переходить в состояние ожидания (sleep), то использование от- ложенных прерываний или тасклетов, скорее всего, подойдет больше. Очереди дей- ствий вносят наибольшие накладные расходы, так как они используют потоки ядра и, соответственно, переключение контекста. Нельзя сказать, что они не эффектив- ны, но в свете тех тысяч прерываний в секунду, что сетевая подсистема может обе- спечить, использование других механизмов может иметь больший смысл. Хотя для большинства ситуаций очередей действий также бывает достаточно.
    В плане простоты использования пальму первенства получают очереди дей- ствий. Использование очереди events, которая существует по умолчанию, — это про- сто детская игра. Далее идут тасклеты, которые тоже имеют простой интерфейс.
    Последними стоят отложенные прерывания, которые должны быть определены ста- тически.
    В табл. 7.3 приведено сравнение различных механизмов обработки нижних по- ловин.
    Таблица 7.3. Сравнение механизмов обработки нижних половин
    Отложенные прерывания (softirq) Прерывание
    Тасклеты (tasklet) Прерывание
    Очереди отложенных действий Процесс
    (work queue)
    Отсутствует
    По отношению к тасклету такого же типа
    Отсутствует (планируется на вы- полнение как контекст процесса)
    158
    Глава 7
    Механизм обработки нижних половин Контекст выполнения Сериализация

    Если коротко, то разработчики обычных драйверов имеют всего два варианта выбора. Необходимо ли использовать возможности планировщика, чтобы выпол- нять отложенные действия, т.е. необходимо ли переходить в состояние ожидания по какой-либо причине? Если да, то единственный вариант — очереди отложенных действий. В противном случае предпочтительно использовать тасклеты. Только если важна масштабируемость, то стоит обратиться к отложенным прерываниям.
    Блокировки между обработчиками
    нижних половин
    Мы еще не касались вопросов, связанных с блокировками. Этой теме посвящены следующие две главы. Тем не менее очень важно понимать, что решающим момен- том при обработке нижних половин является защита данных общего доступа от кон- курентных изменений, даже на однопроцессорной машине. Следует помнить, что об- работчик нижней половины прерывания потенциально может выполняться в любой момент времени. Может потребоваться вернуться к текущему разделу, после прочте- ния следующих двух глав, если вы далеки от вопросов, связанных с блокировками.
    Одно из преимуществ использования тасклетов состоит в том, что они всегда вы- полняются последовательно по отношению к себе: один и тот же такслет никогда не будет выполняться параллельно себе даже на двух разных процессорах. Это означа- ет, что нет необходимости заботиться о проблемах, связанных с конкурентным вы- полнением тасклетов одного типа. Конкурентное выполнение тасклетов нескольких разных типов (в случае, если они совместно используют одни данные) требует при- менения блокировок.
    Так как отложенные прерывания не обеспечивают строгой последовательности выполнения (даже два обработчика одного и того же отложенного прерывания мо- гут выполняться параллельно), то все совместно используемые данные требуют соот- ветствующих блокировок.
    Если из контекста процесса необходимо обращаться к данным, которые исполь- зуются как контекстом процесса, так и обработчиком нижней половины, то необхо- димо запретить обработку нижних половин и захватить блокировку перед тем, как начинать работу с данными. Это позволяет гарантировать защиту совместно исполь- зуемых данных как на локальном процессоре, так и на разных процессорах SMP си- стемы, а также предотвратить взаимоблокировки.
    Если имеются данные, которые могут совместно использоваться в контексте пре- рывания и в обработчике нижней половины, то необходимо запретить прерывания и захватить блокировку перед тем, как обращаться к этим данным. Именно эти две операции позволяют предотвратить взаимоблокировку и обеспечить защиту для
    SMP-систем.
    Все совместно используемые данные, к которым необходимо обращаться из оче- редей действий, также требуют применения блокировок. Проблема блокировок в этом случае ничем не отличается от блокировок обычного кода ядра, так как очере- ди действий всегда выполняются в контексте процесса.
    В главе 8 будут рассмотрены хитрости, связанные с блокировками. В главе 9 будут описаны базовые элементы ядра, которые позволяют осуществлять блокировки.
    Обработка нижних половин и отложенные действия 159

    Далее в этом разделе рассказывается о том, как защитить данные, которые ис- пользуются обработчиками нижних половин.
    Запрещение обработки нижних половин
    Обычно только одного запрещения обработки нижних половин недостаточно.
    Наиболее часто, чтобы полностью защитить совместно используемые данные, необ- ходимо захватить блокировку и запретить обработку нижних половин. Методы, ко- торые позволяют это сделать и которые обычно используются при разработке драй- веров, будут рассмотрены в главе 9. Однако при разработке самого кода ядра иногда необходимо запретить только обработку нижних половин.
    Для того чтобы запретить обработку всех типов нижних половин (всех отло- женных прерываний и, соответственно, тасклетов), необходимо вызвать функцию local_bh_disable (). Для разрешения обработки нижних половин необходимо вы- звать функцию local_bh_enable (). Да, у этих функций "неправильные" названия.
    Никто не потрудился переименовать эти функции, когда интерфейс ВН уступил ме- сто интерфейсу отложенных прерываний. В табл. 7.4 приведены сведения об этих функциях.
    Таблица 7.4. Список функций управления обработкой нижних половин
    Функция Описание
    void local_bh_disable () Запретить обработку всех отложенных прерываний (softirq)
    и тасклетов (tasklet) на локальном процессоре void local_bh_enable () Разрешить обработку всех отложенных прерываний (softirq)
    и тасклетов (tasklet) на локальном процессоре
    Вызовы этих функций могут быть вложенными — при этом только последний вы- зов функции local_bh_enable () разрешает обработку нижних половин. Например,
    при первом вызове функции local_bh_disable () запрещается выполнение отло- женных прерываний на текущем процессоре. Если функция local_bh_disable ()
    вызывается еще три раза, то выполнение отложенных прерываний будет запрещено.
    Их выполнение не будет разрешено до тех пор, пока функция local_bh_enable ()
    не будет вызвана четыре раза.
    Такая функциональность реализована с помощью счетчика preerapt_count, кото- рый поддерживается для каждого задания (интересно, что этот же счетчик исполь- зуется и для вытеснения процессов в режиме ядра)
    11
    . Когда значение этого счетчика достигает нуля, то можно начать обработку нижних половин. Так как при вызове функции local_bh_enable () обработка нижних половин запрещена, то эта функ- ция также проверяет наличие ожидающих на обработку нижних половин и выпол- няет их.
    1 1
    На самом деле этот счетчик используется как системой обработки прерываний, так и системой обработки нижних половин. Наличие одного счетчика для задания позволяет в операционной сис- теме Linux реализоиать атомарность заданий. Как показала практика, такой подход очень полезен для нахождения ошибок, например, связанных с тем, что задание переходит в состояние ожида- ния в то время, когда выполняет атомарные операции (sleeping-while-atomic bug).
    160 Глава 7

    Обработка нижних половин и отложенные действия 161
    Для каждой поддерживаемой аппаратной платформы имеются спои функ- ции, которые обычно реализуются через сложные макросы, описанные в файле
    . Для любопытных ниже приведены соответствующие реализации на языке программирования С.
    /*
    * запрещение обработки нижних половин путем увеличения значения счетчика preempt_count
    */
    void local_bh_disable(void)
    {
    struct thread_info *t = current_thread_info();
    t->preempt_count += SOFTIRQ_OFFSET;
    }
    /*
    * уменьшение значения счетчика preempt_count "автоматически" разрешает
    * обработку нижних половин, если значение счетчика равно нулю
    *
    * опционально запускает все обработчики нижних половин,
    которые ожидают на обработку
    */
    void local_bh_enable(void)
    {
    struct thread_info *t = current_thread_info();
    t->preempt_count -= SOFTIRQ_OFFSET;
    /*
    * равно ли значение переменной preempt_count нулю и ожидают ли на обработку какие-либо обработчики нижних половин?
    * если да, то запустить их
    /
    if (unlikely(!t->preempt_count &&
    softirq_pending (smp_processor_id())))
    do_softirq();
    }
    Эти функции не запрещают выполнения очередей действий. Так как очереди дей- ствий выполняются в контексте процесса, нет никаких проблем с асинхронным вы- полнением и нет необходимости запрещать их. Поскольку отложенные прерывания и тасклеты могут "возникать" асинхронно (например, при возвращении из обработ- чика аппаратного прерывания), то ядру может потребоваться запрещать их. В слу- чае использования очередей отложенных действий защита совместно используемых данных осуществляется так же, как и при работе в контексте процесса. Детали рас- смотрены в главах 8 и 9.

    Внизу обработки нижних половин
    В этой главе были рассмотрены три механизма, которые используются для ре- ализации отложенных действий в ядре Linux, — отложенные прерывания (softirq),
    тасклеты (tasklet) и очереди отложенных действий (work queue). Было показано, как эти механизмы работают и как они реализованы. Также обсуждались основные мо- менты, связанные с использованием этих механизмов в собственном программном коде, и было показано, какие у них неподходящие названия. Для того чтобы вос- становить историческую справедливость, мы также рассмотрели те механизмы об- работки нижних половин, которые существовали в предыдущих версиях ядра Linux:
    механизмы ВН и task queue.
    Очень часто в главе поднимались вопросы, связанные с синхронизацией и парал- лельным выполнением, потому что эти моменты имеют прямое отношение к обра- ботке нижних половин. В эту главу специально был включен раздел, который каса- ется запрещения обработки нижних половин для защиты от конкурентного доступа,
    Теперь настало время углубиться в эти моменты с головой. В следующей главе будут рассмотрены особенности синхронизации и параллельного выполнения кода в ядре:
    основные понятия и соответствующие проблемы. Далее будут рассмотрены интер- фейсы, которые позволяют осуществлять синхронизацию в ядре и решать указанные проблемы. Вооруженные следующими двумя главами, вы сможете покорить мир.
    162 Глава 7

    8
    Введение
    в синхронизацию
    выполнения кода ядра
    В
    приложениях, рассчитанных на работу с совместно используемой памятью
    (shared memory), необходимо позаботиться о том, чтобы совместно исполь- зуемые ресурсы были защищены от конкурентного доступа. Ядро — не исключение.
    Совместно используемые ресурсы требуют защиты от конкурентного доступа в свя- зи с тем, что несколько потоков выполнения
    1
    могут одновременно манипулировать одними и теми же данными: эти потоки могут переписывать изменения, сделанные другими потоками, а также обращаться к данным, которые находятся в несогласо- ванном (противоречивом, неконсистентном) состоянии. Конкурентный доступ к совместно используемым данным — это хороший способ получить нестабильность системы, причины которой, как показывает опыт, впоследствии очень сложно об- наружить и исправить. В связи с этим важно при разработке сразу сделать все пра- вильно.
    Осуществить необходимую защиу совместно используемых ресурсов может ока- заться трудной задачей. Много лет назад, когда операционная система Linux не под- держивала симметричную многопроцессорную обработку, предотвратить конкурент- ный доступ к данным было просто. Так как поддерживался только один процессор,
    то единственная возможность конкурентного доступа к данным возникала при полу- чении прерывания или когда выполнение кода ядра явно перепланировалось, давая возможность выполняться другому заданию. Да, раньше жить было проще.
    Эти дни закончились. Поддержка симметричной многопроцессорности была введена в ядрах серии 2.0, и с тех пор эта поддержка постоянно совершенствуется.
    Поддержка мультипроцессорности предполагает, что код ядра может одновременно выполняться на двух или более процессорах. Следовательно, без специальной защи- ты части кода ядра, которые выполняются на двух разных процессорах, принципи- ально могут обратиться к совместно используемым данным в один и тот же момент
    1
    Термин поток выполнения подразумевает любой выполняющийся код. Это включает, например,
    задание в ядре, обработчик прерывания или поток пространства ядра. В этой главе поток выпол- нения сокращенно называется просто поток. Следует помнить, что этот термин подразумевает лю- бой выполняющийся код.
    времени. Начиная с серии ядер 2.6 ядро операционной системы Linux является пре- емптивным (вытесняемым). Это подразумевает, что (при отсутствии необходимой защиты) планировщик может вытеснить код ядра в любой момент времени и запу- стить на выполнение другое задание. Сегодня есть много сценариев, благодаря кото- рым может возникнуть конкурентный доступ к данным в ядре, и все эти варианты требуют защиты данных.
    В этой главе рассматриваются проблемы, связанные с параллельным выпол- нением кода и синхронизацией выполнения кода в ядре операционной системы.
    В следующей главе детально рассмотрены механизмы и интерфейсы, которые предо- ставляет ядро операционной системы Linux для решения проблем синхронизации и предотвращения состояния конкуренции за ресурс (race condition, состояние "гонок").
    Критические участки и состояние конкуренции за ресурсы
    Ветки кода, которые получают доступ к совместно используемыми данным и ма- нипулируют ими, называются критическими участками (critical region). Обычно не- безопасно нескольким потокам выполнения одновременно обращаться к одному и тому же ресурсу. Для предотвращения конкурентного доступа во время выполнения критических участков программист, т.е. Вы, должен гарантировать, что код выпол- няется атомарно - без перерывов, так если бы весь критический участок был одной неделимой машинной инструкцией. Если два потока выполнения одновременно находятся в критическом участке, то это— ошибка в программе. Если такое вдруг случается, то такая ситуация называется состоянием, конкуренции за ресурс (состояние "гонок", race condition). Название связано с тем, что потоки как бы соревнуются друг с другом за доступ к ресурсу. Следует обратить внимание на то, насколько редко такая ситуация может возникать, — поэтому обнаружение состояний конкуренции за ресурсы при отладке программ часто очень сложная задача, потому что подобную ситуацию очень трудно воспроизвести. Обеспечение гарантии того, что конкурен- ции не будет и, следовательно, что состояний конкуренции за ресурсы возникнуть не может, называется синхронизацией.
    Зачем нужна защита
    Для лучшего понимания того, к чему может привести состояние конкуренции, да- вайте рассмотрим примеры повсеместно встречающихся критических участков.
    В качестве первого примера рассмотрим ситуацию из реальной жизни; банкомат
    (который еще называют ATM, Automated Teller Machine, или кэш-машиной).
    Одно из наиболее часто встречающихся действий, которые приходится выпол- нять с помощью банкомата— это снятие денег с персонального банковского счета физического лица. Человек подходит к банкомату, вставляет карточку, вводит PIN- код, проходит аутентификацию, выбирает пункт меню Снятие наличных, вводит необходимую сумму, нажимает ОК, забирает деньги и отправляет их автору этой книги.
    После того как пользователь ввел необходимую сумму, банкомат должен прове- рить, что такая сумма действительно есть на счету. Если такие деньги есть, то необ-
    164 Глава 8
    ходимо вычесть снимаемую сумму из общего количества доступных денег. Код, кото- рый выполняет эту операцию, может выглядеть следующим образом.
    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.
    Получаем деньги в подарок!
    Введение в синхронизацию выполнения кода ядра 165

    Это выражение можно перевести в инструкции процессора следующим образом.
    Загрузить текущее значение переменной i из памяти в регистр.
    Добавить единицу к значению, которое находится в регистре.
    Записать новое значение переменной i обратно в память.
    Теперь предположим, что есть два потока, которые одновременно выполняют этот критический участок, и начальное значение переменной i равно 7. Результат выполнения будет примерно следующим (каждая строка соответствует одному интер- валу времени ).
    1   ...   17   18   19   20   21   22   23   24   ...   53


    написать администратору сайта