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

  • Таблица 9.9.

  • Барьеры и порядок выполнения

  • Поток 1 Поток 2

  • Таблица 9.10.

  • Информация о времени в ядре

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


    Скачать 3.09 Mb.
    НазваниеВторое издание
    Дата08.09.2019
    Размер3.09 Mb.
    Формат файлаpdf
    Имя файлаLav_Robert_Razrabotka_yadra_Linux_Litmir.net_264560_original_254.pdf
    ТипДокументы
    #86226
    страница27 из 53
    1   ...   23   24   25   26   27   28   29   30   ...   53
    Средства запрещения преемптивности
    Так как ядро является вытесняемым, процесс, работающий в режиме ядра, мо- жет прекратить выполнение в любой момент, чтобы позволить выполняться более высокоприоритетному процессу. Это означает, что новое задание может начать вы- полняться в том же критическом участке, в котором выполнялось вытесненное за- дание. Для того чтобы предотвратить такую возможность, код, который отвечает за преемптивность ядра, использует спин-блокировки в качестве маркеров, чтобы от- мечать участки "непреемптивности". Если спин-блокировка захвачена, то ядро явля- ется невытесняемым. Так как проблемы, связанные с параллелизмом, в случае SMP
    и преемптивного ядра одинаковы, то, если ядро уже является безопасным для SMP- обработки, такое простое дополнение позволяет также сделать ядро безопасным и при вытеснении.
    Будем надеяться, что это действительно так. На самом деле возникают некоторые ситуации, в которых нет необходимости использовать спин-блокировки, но нужно запрещать преемптивность ядра. Наиболее часто ситуация такого рода возникает из-за данных, привязанных к определенным процессорам (рег-processor data). Если используются данные, уникальные для каждого процессора, то может быть необя- зательным защищать их с помощью спин-блокировок, потому что только один про- цессор может получать доступ к этим данным. Если никакая спин-блокировка не за- хвачена и ядро является преемптивным, то появляется возможность доступа к тем же переменным для вновь запланированного задания, как показано в следующем примере.
    задание А манипулирует переменной foo задание А вытесняется задание В планируется на выполнение задание В манипулирует переменной foo задание В завершается задание А планируется на выполнение задание А манипулирует переменной foo
    Следовательно, даже для однопроцессорного компьютера к некоторой перемен- ной может псевдопараллельно обращаться несколько процессов. В обычной ситуа- ции для такой переменной требуется спин-блокировка (для защиты при истинном параллелизме на многопроцессорной машине). Если эта переменная связана с одним процессором, то для нее не требуется блокировка.
    200 Глава 9

    Для решения указанной проблемы преемптивность ядра можно запретить с по- мощью функции p r e e m p t _ d i s a b l e () . Этот вызов может быть вложенным, т.е.
    функцию можно вызывать много раз подряд. Для каждого такого вызова требуется соответствующий вызов функции preerapt_enable (). Последний вызов функции preemptenable() разрешает преемптивность, как показано в следующем примере.
    preempt_disable();
    /* преемптивность запрещена ... */
    preempt_enable();
    Счетчик преемптивности текущего процесса содержит значение, равное количе- ству захваченных этим процессом блокировок плюс количество вызовов функции preempt_disable(). Если значение этого счетчика равно нулю, то ядро является вытесняемым. Если значение этого счетчика больше или равно единице, то ядро не вытесняемое. Данный счетчик невероятно полезен для отладки атомарных опе- раций совместно с переходами в состояние ожидания. Функция preempt_count ()
    возвращает значение данного счетчика. В табл. 9.9 показан полный список функций управления преемптивностыо.
    Таблица 9.9. Функции управления преемптивностыо ядра
    Функция Описание
    preempt_disable() Запретить вытеснение кода ядра preempt_enable() Разрешить вытеснение кода ядра preempt_enable_no_resched() Разрешить вытеснение кода ядра,
    но не перепланировать выполнение процесса preempt_count() Возвратить значение счетчика преемптивности
    Более полное решение задачи работы с данными, связанными с определенным процессором, — это получение номера процессора (который используется в качестве индекса для доступа к данным, связанным с определенным процессором) с помощью функции get_cpu (). Эта функция запрещает преемптивность ядра перед тем, как возвратить номер текущего процессора.
    int cpu = get_cpu();
    /* работаем с данными, связанными с текущим процессором ... */
    /* работа закончена, снова разрешаем вытеснение кода ядра */
    put_cpu();
    Барьеры и порядок выполнения
    В случае, когда необходимо иметь дело с синхронизацией между разными про- цессорами или разными аппаратными устройствами, иногда возникает требование,
    чтобы чтение памяти (load) или запись в память (save) выполнялись в том же по- рядке, как это указано в исходном программном коде. При работе с аппаратными устройствами часто необходимо, чтобы некоторая указанная операция чтения была выполнена перед другими операциями чтения или записи. В дополнение к этому, на
    Средства синхронизации в ядре 201
    симметричной многопроцессорной системе может оказаться необходимым, чтобы операции записи выполнялись строго в том порядке, как это указано в исходном программном коде (обычно для того, чтобы гарантировать, что последовательные операции чтения получают данные в том же порядке). Эти проблемы усложняются тем, что как компилятор, так и процессор могут менять порядок операций чтения и записи
    6
    для повышения производительности. К счастью, все процессоры, которые переопределяют порядок операций чтения или записи предоставляют машинные инструкции, которые требуют выполнения операций чтения-записи памяти в ука- занном порядке. Также существует возможность дать инструкцию компилятору, что нельзя изменять порядок выполнения операций при переходе через определенную точку программы. Эти инструкции называются барьерами (barrier).
    Рассмотрим следующий код.
    а = 1;
    Ь = 2;
    На некоторых процессорах запись нового значения в область памяти, занимае- мую переменной b, может выполниться до того, как будет записано новое значение в область памяти переменной а. Компилятор может выполнить такую перестановку статически и внести в файл объектного кода, что значение переменной b должно быть установлено перед переменной a. Процессор может изменить порядок выпол- нения динамически путем предварительной выборки и планирования выполнения внешне вроде бы независимых инструкций для повышения производительности.
    В большинстве случаев такая перестановка операций будет оптимальной, так как между переменными a и b нет никакой зависимости. Тем не менее иногда програм- мисту все-таки виднее.
    Хотя в предыдущем примере и может быть изменен порядок выполнения, ни процессор, ни компилятор никогда не будут менять порядок выполнения следующе- го кода, где переменные а и b являются глобальными.
    а = 1;
    b = а;
    Это происходит потому, что в последнем случае четко видно зависимость между переменными a и b. Однако ни компилятор, ни процессор не имеют никакой ин- формации о коде, который выполняется в других контекстах. Часто важно, чтобы результаты записи в память "виделись" в нужном порядке другим кодом, который выполняется за пределами нашей досягаемости. Такая ситуация часто имеет место при работе с аппаратными устройствами, а также возникает на многопроцессорных машинах.
    Функция rmb () позволяет установить барьер чтения памяти (read memory barrier).
    Она гарантирует, что никакие операции чтения памяти, которые выполняются перед вызовом функции rmb (), не будут переставлены местами с операциями, которые вы- полняются после этого вызова. Иными словами, все операции чтения, которые ука- заны до этого вызова, будут выполнены перед этим вызовом, а все операции чтения,
    которые указаны после этого вызова никогда не будут выполняться перед ним.
    6
    Процессоры Intel х86 никогда не переопределяют порядок операций записи, Т.е. выполняют за- пись всегда в указанном порядке. Тем не менее другие процессоры могут нести себя и по-другому,
    202 Глава 9

    Функция wmb () позволяет установить барьер записи памяти (write barrier). Она работает так же, как и функция rmb (), но не с операциями чтения, а с операциями записи — гарантируется, что операции записи, которые находятся по разные сторо- ны барьера, никогда не будут переставлены местами друг с другом.
    Функция rnb () позволяет создать барьер на чтение и запись. Никакие операции чтения и записи, которые указаны по разные стороны вызова функции rab {) , не бу- дут переставлены местами друг с другом. Эта функция предоставляется пользовате- лю, так как существует машинная инструкция (часто та же инструкция, что использу- ется вызовом rmb ()), которая позволяет установить барьер на чтение и запись.
    Вариант функции rmb() - read_barrier_depends() - обеспечивает создание барьера чтения, но только для тех операций чтения, от которых зависят следующие, за
    ними операции чтения. Гарантируется, что все операции чтения, которые указаны перед барьером выполнятся перед теми операциями чтения, которые находятся по- сле барьера и зависят от операций чтения, идущих перед барьером. Все понятно?
    В общем, эта функция позволяет создать барьер чтения, так же как и функция rmb (),
    но этот барьер будет установлен только для некоторых операций чтения — тех, кото- рые зависят друг от друга.
    Для некоторых аппаратных платформ функция read_barrier_depends () вы- полняется значительно быстрее, чем функция rmb (), так как для этих платформ функция read_barrier_depends () просто не нужна и вместо нее выполняется ин- струкция nоор (нет операции).
    Рассмотрим пример использования функций mb () и rmb (). Первоначальное зна- чение переменной а равно 1, а переменной b равно 2-
    Поток 1 Поток 2
    а=3;
    mb();
    b=4; c=b;
    rmb();
    d=a;
    Без использования барьеров памяти для некоторых процессоров возможна ситу- ация, в которой после выполнения этих фрагментов кода переменной с присвоится
    новое, значение переменной b, в то время как переменной d присвоится старое зна- чение переменной а. Например, переменная с может стать равной 4 (что мы и хо- тим), а переменная d может остаться равной 1 (чего мы не хотим). Использование функции mb () позволяет гарантировать, что переменные a и b записываются в ука- занном порядке, а функция rmb () гарантирует, что чтение переменных b и а будет выполнено в указанном порядке.
    Такое изменение порядка выполнения операций может возникнуть из-за того,
    что современные процессоры обрабатывают и передают на выполнение инструкции в измененном порядке для того, чтобы оптимизировать использование конвейеров.
    Это может привести к тому, что инструкции чтения переменных b и а выполнятся не в том порядке. Функции rmb () и wmb () соответствуют инструкциям, которые за- ставляют процессор выполнить все незаконченные операции чтения и записи перед тем, как продолжить работу далее.
    Рассмотрим простой пример случая, когда можно использовать функцию read_
    barrier_depends () вместо функции rmb(). В этом примере изначально перемен- ная а равна 1, b - 2, а p - &b.
    Средства синхронизации в ядре 203

    Поток 1 Поток 2
    а=3;
    mb();
    p=&а; pp=р;
    read_barrier_depends();
    b=*pp;
    Снова без использования барьеров памяти появляется возможность того, что переменной b будет присвоено значение *рр до того, как переменной рр будет при- своено значение переменной р. Функция r e a d _ b a r r i e r _ d e p e n d s () обеспечивает достаточный барьер, так как считывание значения *рр зависит от считывания пере- менной р. Здесь также будет достаточно использовать функцию rmb (), но посколь- ку операции чтения зависимы между собой, то можно использовать потенциально более быструю функцию r e a d _ b a r r i e r _ d e p e n d s (). Заметим, что в обоих случаях требуется использовать функцию mb () для того, чтобы гарантировать необходимый порядок выполнения операций чтения-записи в потоке 1.
    Макросы smp_rmb () , smp_wmb (), smp_mb() и smpread_barrier_depends ()
    позволяют выполнить полезную оптимизацию. Для SMP-ядра они определены как обычные барьеры памяти, а для ядра, рассчитанного на однопроцессорную маши- ну, — только как барьер компилятора. Эти SMP-варианты барьеров можно использо- вать, когда ограничения на порядок выполнения операций являются специфичными для SMP-систем.
    Функция b a r r i e r () предотвращает возможность оптимизации компилятором операций считывания и записи данных, если эти операции находятся по разные стороны от вызова данной функции (т.е. запрещает изменение порядка операций).
    Компилятор не изменяет порядок операций записи и считывания в случаях, когда это может повлиять на правильность выполнения кода, написанного на языке С, или на существующие зависимости между данными. Однако у компилятора нет инфор- мации о событиях, которые могут произойти вне текущего контекста. Например,
    компилятор не может иметь информацию о прерываниях, в контексте которых может выполняться считывание данных, которые в данный момент записываются.
    Например, по этой причине может оказаться необходимым гарантировать, что опе- рация записи выполнится перед операцией считывания. Указанные ранее барьеры памяти работают и как барьеры компилятора, но барьер компилятора значитель- но быстрее, чем барьер памяти (практически не влияет на производительность).
    Использование барьера компилятора на практике является опциональным, так как он просто предотвращает возможность того, что компилятор что-либо изменит.
    В табл. 9.10 приведен полный список функций установки барьеров памяти и ком- пилятора, которые доступны для разных аппаратных платформ, поддерживаемых ядром Linux.
    Следует заметить, что эффекты установки барьеров могут быть разными для раз- ных аппаратных платформ. Например, если машина не изменяет порядок операций записи (как в случае набора микросхем Intel x86), то функция wmb () не выполняет никаких действий. Можно использовать соответствующий барьер памяти для самой плохой ситуации (т.е. для процессора с самым плохим порядком выполнения), и ваш код будет скомпилирован оптимально для вашей аппаратной платформы.
    204 Глава 9

    Таблица 9.10. Средства установки барьеров компилятора и памяти
    Барьер
    Описание
    Предотвращает изменение порядка выполнения операций чте- ния данных из памяти при переходе через барьер
    Предотвращает изменение порядка выполнения операций чте- ния данных из памяти при переходе через барьер, но только для операций чтения, которые зависимы друг от друга
    Предотвращает изменение порядка выполнения операций за- писи данных в память при переходе через барьер
    Предотвращает изменение порядка выполнения операций чте- ния и записи данных при переходе через барьер
    Для SMP-ядер эквивалентно функции rmb() , а для ядер, рас- считанных на однопроцессорные машины, эквивалентно функ- ции b a r r i e r ( )
    Для SMP-ядер эквивалентно функции read_barrier_depends().
    а для ядер, рассчитанных на однопроцессорные машины, экви- валентно функции b a r r i e r ( )
    Для SMP-ядер эквивалентно функции wmb(), а для ядер, рас- считанных на однопроцессорные машины, эквивалентно функ- ции b a r r i e r ( )
    Для SMP-ядер эквивалентно функции mb(), а для ядер, рас- считанных на однопроцессорные машины, эквивалентно функ- ции b a r r i e r ( )
    Предотвращает оптимизации компилятора по чтению и записи данных при переходе через барьер
    Средства синхронизации в ядре
    205
    rmb()
    read_barrier_depends()
    wtnb()
    mb()
    smp_rmb()
    smp_read_barrier_depends()
    srap_wmb()
    smp_mb()
    barrier()

    10
    Таймеры и управление
    временем
    О
    тслеживание хода времени очень важно для ядра. Большое количество функ- ций, которые выполняет ядро, управляются временем (time driven), в отличие от тех функций, которые выполянются по событиям
    1
    (event driven). Некоторые из этих функций выполняются периодически, как, например, балансировка очередей выполнения планировщика или обновление содержимого экрана. Такие функции вы- зываются в соответствии с постоянным планом, например 100 раз в секунду. Другие функции, такие как отложенные дисковые операции ввода-выпода, ядро планирует на выполнение в некоторый относительный момент времени в будущем. Например,
    ядро может запланировать работу на выполнение в момент времени, который на- ступит позже текущего на 500 миллисекунд. Наконец, ядро должно вычислять время работы системы (uptime), а также текущую дату и время.
    Следует обратить внимание на разницу между относительным и абсолютным вре- менем. Планирование выполнения некоторой работы через 5 секунд в будущем не требует учета абсолютного времени, а только относительного (например, через пять секунд от текущего момента времени). В рассмотренной ситуации расчет текущей даты и времени требует от ядра не только учета хода времени, но и абсолютного из- мерения времени. Обе концепции являются важными для управления временем.
    Также следует обратить внимание па отличия между событиями, которые воз- никают периодически, и событиями, которые ядро планирует на выполнение в не- который фиксированный момент времени в будущем. События, которые возникают периодически, скажем каждые 10 миллисекунд, управляются системным, таймером.
    Системный таймер — это программируемое аппаратное устройство, которое генери- рует аппаратное прерывание с фиксированной частотой. Обработчик этого преры- вания, который называется прерыванием таймера (timer interrupt), обновляет значение системного времени и выполняет периодические действия. Системный таймер и его прерывание являются важными для работы оператщонной системы Linux, и в теку- щей главе им уделяется главное внимание.
    1
    Если быть точными, то функции, которые управляются временем, также управляются и события- ми. В этом случае событие соответствует ходу времени. В этой главе будут рассмотрены, в основ- ном, события, управляемыми временем, так как они встречаются очень часто и являются важными для ядра.

    Кроме того, в этой главе будут рассмотрены динамические таймеры (dynamic timers)
    средства, позволяющие планировать события, которые выполняются один раз, по- сле того как истек некоторый интервал времени. Например, драйвер накопителя на гибких магнитных дисках использует таймер, чтобы остановить двигатель дисково- да, если дисковод неактивен в течение некоторого периода времени. В ядре можно динамически создавать и ликвидировать таймеры. В данной главе рассказывается о реализации динамических таймеров, а также об интерфейсе, который доступен для использования в программном коде.
    Информация о времени в ядре
    Концепция времени для компьютера является несколько неопределенной. В дей- ствительности, для того чтобы получать информацию о времени и управлять си- стемным временем, ядро должно взаимодействовать с системным аппаратным обе- спечением. Аппаратное обеспечение предоставляет системный таймер, который используется ядром для измерения времени. Системный таймер работает от элек- тронного эталона времени, такого как цифровые электронные часы или тактовый генератор процессора. Интервал времени системного таймера периодически исте- кает (еще говорят таймер срабатывает— hitting, popping) с определенной запрограм- мированной частотой. Эта частота называется частотой импульсов таймлра, (tick rate).
    Когда срабатывает системный таймер, он генерирует прерывание, которое ядро об- рабатывает с помощью специального обработчика прерывания.
    Так как в ядре есть информация о запрограммированной частоте следования им- пульсов таймера, ядро может вычислить интервал времени между двумя успешными прерываниями таймера. Этот интервал называется временной отметкой или импуль-
    сом таймера (tick) и в секундах равен единице, деленной на частоту импульсов. Как будет показано дальше, именно таким способом ядро отслеживает абсолютное время (wall time) и время работы системы (uptime). Абсолютное время— это фактическое время дня, которое наиболее важно для пользовательских приложений. Ядро отслеживает это время просто потому, что оно контролирует прерывание таймера. В ядре есть семейство системных вызовов, которое позволяет пользовательским приложениям получать информацию о дате и времени дня. Это необходимо, так как многие про- граммы должны иметь информацию о ходе времени. Разница между двумя значени- ями времени работы системы — "сейчас" и "позже" — это простой способ измерения относительности событий.
    Прерывание таймера очень важно для управления работой всей операционной системы. Большое количество функций ядра действуют и завершаются в соответ- ствии с ходом времени. Следующие действия периодически выполняются систем- ным таймером.
    • Обновление значения времени работы системы (uptime).
    • Обновление значения абсолютного времени (time of day).
    • Для SMP-систем выполняется проверка балансировки очередей выполнения планировщика, и если они не сбалансированы, то их необходимо сбалансиро- вать (как было рассказано в главе 4, "Планирование выполнения процессов").
    208 Глава 10

    • Проверка, не израсходовал ли текущий процесс свой квант времени, и если израсходовал, то выполнятся планирование выполнения нового процесса (как это было рассказано в главе 4).
    • Выполнение обработчиков всех динамических таймеров, для которых истек период времени.
    • Обновление статистики по использованию процессорного времени и других ресурсов.
    Некоторые из этих действий выполняются при каждом прерывании таймера, т.е.
    эта работа выполняется с частотой системного таймера. Другие действия также вы- полняются периодически, но только через каждые п прерываний системного тайме- ра. Иными словами, эти функции выполняются с частотой, которая равна некото- рой доле частоты системного таймера. В разделе "Обработчик прерываний таймера"
    будет рассмотрена сама функция обработки прерываний системного таймера.
    Частота импульсов таймера: HZ
    Частота системного таймера (частота импульсов, tick rate) программируется при загрузке системы на основании параметра ядра НZ, который определен с помощью директивы препроцессора. Значение параметра HZ отличается для различных под- держиваемых аппаратных платформ. На самом деле, для некоторых аппаратных платформ значение параметра HZ отличается даже для разных типов машин.
    Данный параметр ядра определен в файле . Частота системного таймера равна значению параметра HZ, период таймера равен 1/HZ. Например, в файле include/asm-i386/param.h для аппаратной платформы i386 этот параметр определен следующим образом.
    #define HZ 1000 /* internal kernel time frequency */
    Поэтому для аппаратной платформы i368 прерывание таймера генерируется с частотой 1000 Гц, т.е. 1000 раз в секунду (каждую тысячную долю секунды или одну миллисекунду). Для большинства других аппаратных платформ значение частоты системного таймера равно 100 Гц. В табл. 10.1 приведен полный список всех под- держиваемых аппаратных платформ и определенных для них значений частоты си- стемного таймера.
    При написании кода ядра нельзя считать, что параметр HZ имеет определенное заданное значение. В наши дни это уже не такая часто встречающаяся ошибка, так как поддерживается много различных аппаратных платформ с разными частотами системного таймера. Раньше аппаратная платформа Alpha была единственной, для которой частота системного таймера отличалась от 100 Гц, и часто можно было встретить код, в котором жестко было прописано значение 100 там, где нужно ис- пользовать параметр HZ. Примеры использования параметра HZ в коде ядра будут приведены ниже.
    Частота системного таймера достаточно важна. Как будет видно, обработчик пре- рыпания таймера выполняет много работы. Вся информация о времени в ядре по- лучается из периодичности системного таймера. Весь компромисс состоит только в том, чтобы выбрать правильное значение данного параметра исходя из взаимоотно- шения между разными факторами.
    Таймеры и управление временем 209

    1   ...   23   24   25   26   27   28   29   30   ...   53


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