Порты ввода/вывода и память ввода/вывода
Каждое периферийное устройство управляется записью и чтением его регистров. В большинстве случаев устройство имеет несколько регистров и они доступны по последовательным адресам либо в адресном пространстве памяти, либо в адресном пространстве ввода/вывода.
На аппаратном уровне нет концептуальной разницы между областями памяти и областями ввода/вывода: обе они доступны установкой электрических сигналов на адресной шине и шине управления (то есть сигналами чтения и записи) (* Не все компьютерные платформы используют сигнал чтения и записи; некоторые имеют другие средства для адресации внешних цепей. Однако, на программном уровне разница не имеет значения и для упрощения обсуждения мы будем предполагать, что все имеют чтение и запись.) и чтением или записью на шине данных.
Хотя некоторые производители процессоров реализовали в своих чипах единое адресное пространство, другие решили, что периферийные устройства отличаются от памяти и, следовательно, заслуживают отдельного адресного пространства. Некоторые процессоры (прежде всего семейство x86) имеют раздельные электрические линии чтения и записи для портов ввода/вывода и специальные инструкции процессора для доступа к портам.
Так как периферийные устройства изготовлены с учётом периферийных шин и на персональном компьютере смоделированы самые популярные шины ввода/вывода, даже процессоры, которые не имеют отдельного адресного пространства для портов ввода/вывода, должны подделывать чтение и запись портов ввода/вывода при доступе к некоторым периферийным устройствам, обычно, с помощью внешних чипсетов (наборов микросхем) или дополнительных схем в ядре процессора. Последнее решение распространено в крошечных процессорах, предназначенных для встроенного использования.
По той же причине Linux реализует концепцию портов ввода/вывода на всех компьютерных платформах, где работает, даже на платформах, где процессор реализует единое адресное пространство. Осуществление доступа к порту иногда зависит от особенностей реализации и модели данного компьютера (потому что разные модели используют разные чипсеты для связывания шинных транзакций с адресным пространством памяти).
Даже если периферийная шина имеет отдельное адресное пространство для портов ввода/вывода, не все устройства связывают свои регистры с портами ввода/вывода. В то время, как использование портов ввода/вывода распространено для периферийных плат ISA, большинство устройств PCI связывают регистры с областью адресов в памяти. Такой подход с вводом/выводом в память является наиболее предпочтительным, поскольку он не требует использования инструкций процессора специального назначения; процессорные ядра выполняют доступ к памяти намного более эффективно и при обращении к памяти компилятор имеет гораздо больше свободы в распределении регистров и выборе режима адресации.
Несмотря на сильное сходство между аппаратными регистрами и памятью, программист, адресующий регистры ввода/вывода, должен быть осторожен, чтобы избежать обмана оптимизацией в процессоре (или компиляторе), которые могут изменить ожидаемое поведение ввода/вывода.
Основным различием между регистрами ввода/вывода и ОЗУ является то, что операции ввода/вывода имеют побочные эффекты, а операции с памятью не имеют никакого: единственным эффектом записи в память является сохранение значения по адресу, а чтение из памяти возвращает последнее записанное туда значение. Так как скорость доступа к памяти является столь важной для производительности процессора, случай "без-побочных-эффектов" был оптимизирован несколькими способами: значения кэшируются и инструкции чтения/записи переупорядочиваются.
Компилятор может кэшировать значения данных в регистрах процессора без записи их в память и даже если он сохраняет их, обе операции записи и чтения могут выполняться в кэш-памяти не достигая физической памяти. Реорганизация может случиться как на уровне компилятора, так и на аппаратном уровне: часто последовательность инструкций может быть выполнена быстрее, если она выполняется в порядке, отличном от того, который появляется в тексте программы, например, для предотвращения блокировки в конвейере RISC. На процессорах CISC операции, которые занимают значительное количество времени, могут быть выполнены одновременно с другими более быстрыми.
Эти оптимизации являются прозрачными и благоприятными, когда применяются к обычной памяти (по крайней мере на однопроцессорных системах), однако они могут быть фатальными для правильных операций ввода/вывода, потому что они сталкиваются с этими "побочными эффектами", что является основной причиной, почему драйверы адресуют регистры ввода/вывода. Процессор не может прогнозировать ситуацию, в которой некоторые другие процессы (работающие на отдельном процессоре, или нечто происходящее внутри контроллера ввода/вывода), зависят от порядка доступа к памяти. Компилятор или процессор может просто пытаться перехитрить вас и переупорядочить операции, которые вы запросили; результатом могут быть странные ошибки, которые очень трудно выявить. Таким образом, драйвер должен обеспечить, чтобы при доступе к регистрам кэширование не производилось и не имело место переупорядочивание чтения или записи.
Проблема с аппаратным кэшированием является самой простой из стоящих: нижележащее оборудование уже настроено (автоматически или кодом инициализации Linux) для отключения любого аппаратного кэширования при доступе к областям ввода/вывода (независимо от того, являются ли они областями памяти или портов).
Решением для (избегания) оптимизации компилятором и аппаратного переупорядочивания является размещение барьера памяти между операциями, которые должны быть видимыми для аппаратуры (или другим процессором) в определённом порядке. Linux предоставляет четыре макроса для покрытия всех возможных потребностей упорядочивания:
#include
void barrier(void)
Эта функция говорит компилятору вставить барьер памяти, но не влияет на оборудование. Скомпилированный код сохраняет в память все значения, которые в настоящее время модифицированы и постоянно находятся в регистрах процессора, и перечитывает их позже, когда они необходимы. Вызов barrier препятствует оптимизациям компилятора пересечь этот барьер, но оставляет свободу оборудованию делать своё собственное переупорядочивание.
#include
void rmb(void);
void read_barrier_depends(void);
void wmb(void);
void mb(void);
Эти функции вставляют аппаратные барьеры памяти в поток скомпилированных инструкций; их фактическая реализация зависит от платформы. rmb (барьер чтения памяти) гарантирует, что любые чтения, находящиеся до барьера, завершатся до выполнения любого последующего чтения. wmb гарантирует порядок в операциях записи, а инструкция mb гарантирует оба порядка. Каждая из этих функций является надстройкой над barrier.
read_barrier_depends является особой более слабой формой барьера чтения. Если rmb препятствует переупорядочиванию всех чтений через барьер, read_barrier_depends блокирует только переупорядочивание операций чтения, которые зависят от данных других операций чтения. Различие тонкое и оно не существует на всех архитектурах. Если вы не понимаете точно, что происходит, и у вас нет оснований полагать, что полный барьер чтения чрезмерно уменьшает производительность, вам, вероятно, следует придерживаться использования rmb.
void smp_rmb(void);
void smp_read_barrier_depends(void);
void smp_wmb(void);
void smp_mb(void);
Эти версии барьерных макросов вставляют аппаратные барьеры только тогда, когда ядро скомпилировано для многопроцессорных систем; в противном случае, все они становятся простым вызовом barrier.
Типичное использование барьеров памяти в драйвере устройства может иметь такую форму:
writel(dev->registers.addr, io_destination_address);
writel(dev->registers.size, io_size);
writel(dev->registers.operation, DEV_READ);
wmb( );
writel(dev->registers.control, DEV_GO);
В этом случае важно убедиться, что все регистры устройства, контролирующие отдельные операции, были надлежащим образом установлены перед командами начала работы. Барьер памяти обеспечивает завершение команд записи в необходимом порядке.
Так как барьеры памяти влияют на производительность, они должны использоваться только там, где они действительно необходимы. Разные виды барьеров могут также иметь разные характеристики производительности, поэтому стоит использовать по возможности наиболее подходящий тип. Например, на архитектуре x86 wmb( ) в настоящее время ничего не делает, поскольку за пределами процессора записи не переупорядочиваются. Чтение является переупорядочиваемым, поэтому mb( ) работает медленнее, чем wmb( ).
Стоит отметить, что большинство других примитивов ядра, имеющих дело с синхронизацией, такие, как операции спи-блокировки и atomic_t, также функционируют как барьеры памяти. Также следует отметить, что некоторые периферийные шины (такие, как шины PCI) имеют собственные проблемы кэширования; мы обсудим их, когда мы доберёмся до них в последующих главах.
Некоторые архитектуры позволяют эффективно комбинировать присваивание и барьер памяти. Ядро предоставляет несколько макросов, которые выполняют эту комбинацию; в стандартном случае они определяются следующим образом:
#define set_mb(var, value) do {var = value; mb( );} while 0
#define set_wmb(var, value) do {var = value; wmb( );} while 0
#define set_rmb(var, value) do {var = value; rmb( );} while 0
Где уместно, определяет эти макросы для использования архитектурно-зависимых инструкций, которые выполняют задачу более быстро. Обратите внимание, что set_rmb определена лишь на небольшом числе архитектур. (Использование конструкции do...while является стандартной идиомой языка Си, которая заставляет раскрытый макрос во всех контекстах работать как обычный оператор Си.)
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.