Операция устройства mmap

Предыдущая  Содержание  Следующая V*D*V

Отображение памяти является одним из наиболее интересных особенностей современных систем UNIX. Что касается драйверов, отображение памяти может быть реализовано для предоставления пользовательским программам прямого доступа к памяти устройства.

 

Характерный пример использования mmap можно увидеть, посмотрев на подмножество виртуальных областей памяти для сервера X Window System:

 

cat /proc/731/maps

000a0000-000c0000 rwxs 000a0000 03:01 282652      /dev/mem

000f0000-00100000 r-xs 000f0000 03:01 282652      /dev/mem

00400000-005c0000 r-xp 00000000 03:01 1366927     /usr/X11R6/bin/Xorg

006bf000-006f7000 rw-p 001bf000 03:01 1366927     /usr/X11R6/bin/Xorg

2a95828000-2a958a8000 rw-s fcc00000 03:01 282652  /dev/mem

2a958a8000-2a9d8a8000 rw-s e8000000 03:01 282652  /dev/mem

...

 

Полный список областей VMA X сервера является длинным, но большинство из записей не представляют здесь интереса. Мы видим, однако, четыре отдельных отображения /dev/mem, которые дают некоторое понимание, как X сервер работает с видеокартой. Первым отображением является a0000, который является стандартным местом для видеопамяти в 640-Кб дыре ISA. Далее вниз мы видим большое отображение в e8000000, адрес которого выше самой высокого адреса ОЗУ в системе. Это является прямым отображением видео памяти на адаптер.

 

Эти регионы можно также увидеть в /proc/iomem:

 

000a0000-000bffff : Video RAM area

000c0000-000ccfff : Video ROM

000d1000-000d1fff : Adapter ROM

000f0000-000fffff : System ROM

d7f00000-f7efffff : PCI Bus #01

  e8000000-efffffff : 0000:01:00.0

fc700000-fccfffff : PCI Bus #01

  fcc00000-fcc0ffff : 0000:01:00.0

 

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

 

Как вы могли бы подозревать, не каждое устройство поддаётся абстракции mmap; это не имеет смысла, например, для последовательных портов и других поточно-ориентированных устройств. Другим ограничением mmap является то, что отображение разделено на PAGE_SIZE. Ядро может управлять виртуальными адресами только на уровне таблиц страниц; таким образом, отображённая область должна быть кратной PAGE_SIZE и должна находиться в физической памяти начиная с адреса, который кратен PAGE_SIZE. Ядро управляет размером разбиения делая регион немного больше, если его размер не является кратным размеру страницы.

 

Эти ограничения не являются большим препятствием для драйверов, потому что программа в любом случае обращается к устройству зависящим от устройства способом. Поскольку программа должна знать о том, как работает устройство, программист не слишком обеспокоен необходимостью следить за деталями выравнивания страниц. Существует большое ограничение, когда на некоторых не-x86 платформах используются ISA устройства, потому что их аппаратное представление ISA может не быть непрерывным. Например, некоторые компьютеры Alpha видят ISA память как разбросанный набор 8-ми, 16-ти, или 32-х разрядных объектов без прямого отображения. В таких случаях вы не можете использовать mmap совсем. Неспособность выполнять прямое отображение адресов ISA в адреса Alpha связано с несовместимыми спецификациями передачи данных этих двух систем. В то время, как ранние процессоры Alpha могли выдавать только 32-х разрядные и 64-х разрядные обращения к памяти, ISA может делать только 8-ми разрядные и 16-ти разрядные передачи, и нет никакого способа для прозрачной связи одного протокола с другим.

 

Существуют веские преимущества использования mmap, когда это возможно сделать. Например, мы уже видели X сервер, который передаёт большой объём данных в и из видеопамяти; отображение графического дисплея в пространство пользователя значительно улучшает пропускную способность, в противоположность реализации lseek/write. Ещё одним типичным примером является программа управления PCI устройством. Большинство PCI периферии отображает их управляющие регистры на адреса памяти и высокопроизводительные приложения, возможно, предпочтут иметь прямой доступ к регистрам, вместо того, чтобы постоянно вызывать ioctl для выполнения этой работы.

 

Метод mmap является частью структуры file_operations и вызывается, когда происходит системный вызов mmap. В случае с mmap, ядро выполняет много работы перед фактическим вызовом метода и, следовательно, прототип метода сильно отличается от системного вызова. Это является отличием от таких вызовов, как ioctl и poll, где до вызова метода ядро не делает ничего.

 

Системный вызов объявлен следующим образом (как описано на странице руководства mmap(2)):

 

mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset)

 

С другой стороны, файловая операция объявлена следующим образом:

 

int (*mmap) (struct file *filp, struct vm_area_struct *vma);

 

Аргумент filp в методе такой же, как представленный в Главе 3, а vma содержит информацию о диапазоне виртуальных адресов, которые используются для доступа к устройству. Таким образом, большая часть работы выполнена ядром; для реализации mmap драйверу необходимо только построить подходящие таблицы страниц для диапазона адресов и, если необходимо, заменить vma->vm_ops новым набором операций.

 

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

Использование remap_pfn_range

Работа по созданию новых таблиц страниц для отображения диапазона физических адресов выполняется remap_pfn_range и io_remap_page_range, которые имеют следующие прототипы:

 

int remap_pfn_range(struct vm_area_struct *vma,

                    unsigned long virt_addr, unsigned long pfn,

                    unsigned long size, pgprot_t prot);

int io_remap_page_range(struct vm_area_struct *vma,

                        unsigned long virt_addr, unsigned long phys_addr,

                        unsigned long size, pgprot_t prot);

 

Значение, возвращаемое функцией, является обычным 0 или отрицательным кодом ошибки. Давайте посмотрим на точное значение аргументов функций:

 

vma

Область виртуальной памяти, в которую сейчас отображается диапазон страниц.

 

virt_addr

Пользовательский виртуальный адрес, по которому должно начаться переназначение. Функция строит таблицы страниц для виртуального диапазона адресов между virt_addr и virt_addr+size.

 

pfn

Номер страничного блока, соответствующий физическому адресу, с которым должен быть связан виртуальный адрес. Номер страничного блока - это просто физический адрес, сдвинутый вправо на PAGE_SHIFT бит. Для большинства применений поле vm_pgoff структуры VMA содержит в точности необходимое вам значение. Функция оказывает влияние на физические адреса от (pfn<<PAGE_SHIFT) до (pfn<<PAGE_SHIFT)+size.

 

size

Размер в байтах области для переназначения.

 

prot

"Защита", требуемая для новой VMA. Драйвер может (и должен) использовать значение, находящееся в vma->vm_page_prot.

 

Аргументы для remap_pfn_range довольно просты и большинство из них уже вам предоставлены в VMA при вызове вашего метода mmap. Однако, вам может быть интересно, почему есть две функции. Первая (remap_pfn_range) предназначена для ситуаций, когда pfn ссылается на фактическое ОЗУ системы, а io_remap_page_range следует использовать, когда phys_addr указывает на память ввода/вывода. На практике эти две функции идентичны на всех архитектурах, кроме SPARC, и в большинстве ситуаций вы увидите использование remap_pfn_range. Однако, в интересах написания переносимых драйверов, вы должны использовать тот вариант remap_pfn_range, который подходит для вашей индивидуальной ситуации.

 

Другая сложность связана с кэшированием: как правило, ссылки на память устройства не следует кэшировать процессором. Часто системная BIOS настраивает всё должным образом, но также возможно запретить кэширование определённых VMA через поле защиты. К сожалению, отключение кэширования на этом уровне весьма зависимо от процессора. Возможно, любопытный читатель пожелает взглянуть на функцию pgprot_noncached из drivers/char/mem.c, чтобы увидеть как это происходит. Мы больше не будем здесь обсуждать эту тему.

Простая реализация

Если вашему драйверу необходимо сделать простое, линейное отображение памяти устройства в адресное пространство пользователя, remap_pfn_range является почти всем, что вам действительно необходимо, чтобы сделать эту работу. Следующий код взят из drivers/char/mem.c и показывает, как выполняется эта задача в типичном модуле, названном simple (Simple Implementation Mapping Pages with Little Enthusiasm, простая реализация отображения страниц без особого энтузиазма):

 

static int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma)

{

    if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff,

                vma->vm_end - vma->vm_start,

                vma->vm_page_prot))

        return -EAGAIN;

    vma->vm_ops = &simple_remap_vm_ops;

    simple_vma_open(vma);

    return 0;

}

 

Как вы можете видеть, переназначение памяти - это просто вопрос вызова remap_pfn_range для создания необходимых таблиц страниц.

Добавление операций VMA

Как мы уже видели, структура vm_area_struct содержит набор операций, которые могут быть применены к VMA. Теперь мы рассмотрим простой способ обеспечения этих операций. В частности, мы предоставим для нашей VMA операции open и close. Эти операции вызываются, когда процесс открывает или закрывает VMA; в частности, метод open вызывается при ветвлении процесса и создаёт новую ссылку на VMA. Методы VMA open и close вызываются в дополнение к обработке, выполняемой ядром, поэтому нет необходимости заново реализовывать любую работу, проделанную им. Они существуют как способ для драйверов сделать любую дополнительную обработку, которая может им потребоваться.

 

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

 

С этой целью мы переопределяем vma->vm_ops по умолчанию на операции, которые вызывают printk:

 

void simple_vma_open(struct vm_area_struct *vma)

{

    printk(KERN_NOTICE "Simple VMA open, virt %lx, phys %lx\n",

    vma->vm_start, vma->vm_pgoff << PAGE_SHIFT);

}

 

void simple_vma_close(struct vm_area_struct *vma)

{

    printk(KERN_NOTICE "Simple VMA close.\n");

}

 

static struct vm_operations_struct simple_remap_vm_ops = {

    .open = simple_vma_open,

    .close = simple_vma_close,

};

 

Чтобы сделать эти операции активными для заданного отображения, необходимо сохранить указатель на simple_remap_vm_ops в поле vm_ops соответствующей VMA. Это обычно выполняется в методе mmap. Если вернуться назад к примеру simple_remap_mmap, вы увидите такие строки кода:

 

vma->vm_ops = &simple_remap_vm_ops;

simple_vma_open(vma);

 

Обратите внимание на явный вызов simple_vma_open. Поскольку метод open не вызывается при начале mmap, мы должны вызвать его явно, если мы хотим, чтобы это заработало.

Отображение памяти с помощью nopage

Хотя remap_pfn_range работает хорошо во многих, если не в большинстве, реализаций в драйвере mmap, иногда бывает необходимо  быть немного более гибким. В таких ситуациях может быть востребована реализация с использованием метода VMA nopage.

 

Ситуация, в которой подход nopage является полезным, может быть создана системным вызовом mremap, который используется приложениями для изменения границ адресов отображаемого региона. Случается, что ядро не уведомляет драйверы непосредственно, когда отображённая VMA изменяется с помощью mremap. Если VMA уменьшается в размерах, ядро может тихо избавиться от ненужных страниц не уведомляя драйвер. Если, наоборот, VMA расширяется, драйвер в конце концов узнает об этом через вызовы nopage, когда для новых страниц потребуется создать отображение, поэтому нет необходимости выполнять отдельное уведомление. Поэтому, если вы хотите поддерживать системный вызов mremap, должен быть реализован метод nopage. Здесь мы покажем простую реализацию nopage для устройства simple.

 

Напомним, что метод nopage имеет следующий прототип:

 

struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);

 

Когда пользовательский процесс пытается получить доступ к странице в VMA, которая не присутствует в памяти, вызывается связанная с ней функция nopage. Параметр address содержит виртуальный адрес, который вызвал ошибку, округлённый вниз к началу страницы. Функция nopage должна найти и вернуть указатель struct page, который относится к той странице, которую хочет пользователь. Эта функция также должна заботиться об увеличении счётчика использования для страницы, которую она возвращает, вызывая макрос get_page:

 

get_page(struct page *pageptr);

 

Этот шаг необходим, чтобы на отображённых страницах сохранить счётчики ссылок верными. Ядро поддерживает этот счётчик для каждой страницы; когда счётчик уменьшается до 0, ядро знает, что страница может быть помещена в список свободных страниц. Когда VMA становится неотображённой, ядро уменьшает счётчик использования для каждой страницы в этой области. Если драйвер не увеличил счётчик при добавлении страницы в эту область, счётчик использования станет 0 преждевременно и целостность системы нарушится.

 

Методу nopage следует также хранить тип ошибки в месте, указываемом аргументом type, но только если этот аргумент не NULL. В драйверах устройств подходящим значением для type неизменно будет VM_FAULT_MINOR.

 

Обычно, если вы используете nopage, предстоит сделать очень мало работы, когда вызывается mmap; наша версия выглядит следующим образом:

 

static int simple_nopage_mmap(struct file *filp, struct vm_area_struct *vma)

{

    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;

 

    if (offset >= __pa(high_memory) || (filp->f_flags & O_SYNC))

        vma->vm_flags |= VM_IO;

    vma->vm_flags |= VM_RESERVED;

 

    vma->vm_ops = &simple_nopage_vm_ops;

    simple_vma_open(vma);

    return 0;

}

 

Основной вещью, которую mmap должна сделать, это заменить значение по умолчанию (NULL) указателя vm_ops своими собственными операциями. Метод nopage затем заботится о "переназначении" одной страницы за раз и возвращает адрес его структуры struct page. Поскольку мы просто реализуем здесь окно на физическую память, шаг переназначения прост: необходимо только найти и вернуть указатель на struct page для требуемого адреса. Наша метод nopage выглядит следующим образом:

 

struct page *simple_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type)

{

    struct page *pageptr;

    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;

    unsigned long physaddr = address - vma->vm_start + offset;

    unsigned long pageframe = physaddr >> PAGE_SHIFT;

 

    if (!pfn_valid(pageframe))

        return NOPAGE_SIGBUS;

    pageptr = pfn_to_page(pageframe);

    get_page(pageptr);

    if (type)

        *type = VM_FAULT_MINOR;

    return pageptr;

}

 

Хотя, опять же, мы просто отображаем здесь основную память, функции nopage необходимо только найти корректную struct page для ошибочного адреса и увеличить её счётчик ссылок. Таким образом, необходимой последовательностью событий является расчёт требуемого физического адреса и преобразование его в номер блока страницы, сдвигая его вправо на PAGE_SHIFT бит. Поскольку пространство пользователя может дать нам любой понравившийся ему адрес, мы должны гарантировать, что мы имеем верный блок страницы; для нас это делает функция pfn_valid. Если адрес вне диапазона, мы возвращаем NOPAGE_SIGBUS, который заставляет сигнал шины быть доставленным вызывающему процессу. В противном случае, pfn_to_page получает необходимый  указатель struct page, мы можем увеличить его счётчик ссылок (вызовом get_page) и возвратить его.

 

Метод nopage обычно возвращает указатель на struct page. Если по какой причине нормальная страница не может быть возвращена (например, запрошенный адрес находится за пределами области памяти устройства), для сигнализации об ошибке может быть возвращён NOPAGE_SIGBUS; это и есть то, что делает выше код simple. nopage также может вернуть NOPAGE_OOM для указания ошибок, вызываемых ограниченными ресурсами.

 

Обратите внимание, что эта реализация работает для областей памяти ISA, но не для них на PCI шине. Память PCI отображается выше самой высокой системной памяти и для таких адресов не существует записей в карте системной памяти. Поскольку не существует struct page, чтобы вернуть на неё указатель, nopage не может быть использована в таких ситуациях; вы должны использовать вместо неё remap_pfn_range.

 

Если метод nopage оставлен NULL, код ядра, который обрабатывает ошибки страницы, с ошибочным виртуальным адресом связывает нулевую страницу. Нулевая страница является страницей с механизмом копирования при записи (copy-on-write page), которая читается как 0 и которая используется, например, для отображения сегмента BSS. Любой процесс, ссылающийся на нулевую страницу, видит именно это: страница заполнена нулями. Если процесс пишет на страницу, это заканчивается изменением частной копии. Поэтому, если процесс расширяет отображаемую область, вызывая mremap, и драйвер не имеет реализованной nopage, процесс заканчивается с заполненной нулями памятью вместо ошибки сегментации.

Переназначение заданных областей ввода/вывода

Все примеры, которые мы видели до сих пор, являются заново выполненными реализациями /dev/mem; они переназначают физические адреса в пространстве пользователя. Однако, типичный драйвер хочет отображать только небольшой диапазон адресов, который относится к его периферийному устройству, а не всю память. Для отображения в пространство пользователя только части всего диапазона памяти, драйверу необходимо лишь поиграть со смещениями. Следующий пример добивается цели для драйвера, отображая область в simple_region_size байт, начиная с физического адреса simple_region_start (который должен быть выровнен по странице):

 

unsigned long off = vma->vm_pgoff << PAGE_SHIFT;

unsigned long pfn = page_to_pfn(simple_region_start + off);

unsigned long vsize = vma->vm_end - vma->vm_start;

unsigned long psize = simple_region_size - off;

 

if (vsize > psize)

    return -EINVAL; /* диапазон слишком велик */

remap_pfn_range(vma, vma_>vm_start, pfn, vsize, vma->vm_page_prot);

 

В дополнение к расчёту смещения, этот код представляет проверку, которая сообщает об ошибке, когда программа пытается отобразить больше памяти, чем имеется в области ввода/вывода целевого устройства. В этом коде psize является физическим размером ввода/вывода, который остаётся после указания смещения, и vsize является запрошенным размером виртуальной памяти; функция отказывается отображать адреса, которые выходят за рамки разрешённого диапазона памяти.

 

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

 

Простейшим способом предотвращения расширения отображения является реализация простого метода nopage, который всегда вызывает сигнал шины, который будет отправлен к ошибающемуся процессу. Такой метод будет выглядеть следующим образом:

 

struct page *simple_nopage(struct vm_area_struct *vma, unsigned long address, int *type)

{ return NOPAGE_SIGBUS; /* послать SIGBUS */}

 

Как мы уже видели, метод nopage вызывается только тогда, когда процесс разыменовывает адрес, который находится в пределах известной VMA, но для которых в настоящее время нет действительной записи в таблице страниц. Если мы использовали remap_pfn_range для отображения всего региона устройства, метод nopage, показанный здесь, вызывается только для ссылок за пределами этого региона. Таким образом, он может безопасно вернуть NOPAGE_SIGBUS, чтобы просигнализировать об ошибке. Конечно, более тщательная  реализация nopage может проверить, чтобы увидеть, находится ли ошибочный адрес в области устройства, и выполнить переназначение, если дело обстоит именно так. Однако, опять же, nopage не работает с областями памяти PCI, так что расширение PCI отображений невозможно.

Перераспределение ОЗУ

Интересным ограничением remap_pfn_range является то, что она даёт доступ только к зарезервированным страницам и физическим адресам выше вершины физической памяти. В Linux страницы физических адресов помечены как "зарезервированные" в карте памяти, чтобы указать, что они недоступны для управления памятью. На ПК, например, диапазон между 640 Кб и 1 Мб помечен как зарезервированный, как и страницы, которые содержат в себе код ядра. Зарезервированные страницы заблокированы в памяти и являются единственными, которые могут быть безопасно отображены в пользовательское пространство; это ограничение является основным требованием для системной стабильности.

 

Таким образом, remap_pfn_range не позволяет переназначить обычные адреса, в том числе те, которые вы получите, вызвав get_free_page. Вместо этого, она свяжет их с нулевой страницей. Всё представляется работающим, с тем исключением, что этот процесс  видит свои собственные, заполненные нулями страницы, а не переназначенную оперативную память, как он надеялся. Тем не менее, функция выполняет всё, что необходимо делать большинству драйверов оборудования, потому что она может переназначить верхние буферы PCI и ISA память.

 

Ограничения remap_pfn_range можно увидеть, запустив mapper, один из примеров программ в misc-progs в файлах, находящихся на FTP сайте O'Reilly. mapper является простым инструментом, который можно использовать для быстрой проверки системного вызова mmap; он отображает части файла, указанного параметрами командной строки, в режиме "только для чтения" и выводит отображённый регион на стандартный вывод. Следующая сессия, например, показывает, что /dev/mem не делает отображения физической страницы, расположенной по адресу 64  Кб, вместо этого мы видим страницу, заполненную нулями (в данном примере компьютером является ПК, но результат будет таким же и на других платформах):

 

morgana.root# ./mapper /dev/mem 0x10000 0x1000 | od -Ax -t x1

mapped "/dev/mem" from 65536 to 69632

000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

*

001000

 

Неспособность remap_pfn_range иметь дело с ОЗУ предполагает, что основанные на памяти устройства, такие как scull, не могут легко реализовать mmap, потому что его память устройства является обычным ОЗУ, а не памятью ввода/вывода. К счастью, доступно довольно простое решения для обхода этой проблемы для любого драйвера, которому необходимо отобразить ОЗУ в пользовательское пространство; используется метод nopage, с которым мы познакомились ранее.

Перераспределение ОЗУ с помощью метода nopage

Способом отображения реальной оперативной памяти в пользовательское пространство является использование vm_ops->nopage, чтобы иметь дело с ошибками страниц по одной. Пример реализации является частью модуля scullp, представленного в Главе 8.

 

scullp - это странично-ориентированное символьное устройство. Поскольку он странично-ориентированный, он может реализовать на своей памяти mmap. Код, реализующий отображение памяти, использует некоторые понятия, введённые в разделе "Управление памятью в Linux".

 

Перед изучением кода давайте посмотрим на дизайнерские решения, который влияют на реализацию mmap в scullp:

 

scullp не освобождает память устройства, пока устройство отображается. Это вопрос политики, а не требование, и это отличие от поведения scull и аналогичных устройств, которые обрезаются до длины 0, когда открываются для записи. Отказ от освобождения отображаемого устройства scullp позволяет процессу перезаписывать области активно используемые другим процессом, так что вы можете протестировать и посмотреть, как взаимодействуют процессы и память устройства. Чтобы избежать отключения подключенного устройства, драйвер должен хранить количество активных отображений; для этой цели в структуре устройства используется поле vmas.

Отображение памяти выполняется только тогда, когда параметр scullp order (установленный во время загрузки модуля) равен 0. Параметр определяет, как вызывается __get_free_pages (смотрите раздел "get_free_page и друзья" в Главе 8). Ограничение нулевого  порядка (которое заставляет страницы выделяться по одной за раз, а не большими группами) диктуется внутренностями __get_free_pages, функцией выделения, используемой в scullp. Чтобы увеличить эффективность выделения, ядро Linux поддерживает список свободных страниц для каждого порядка выделения и только счётчик ссылок на первой странице в кластере увеличивается с помощью get_free_pages и уменьшается с помощью free_pages. Метод mmap в устройстве scullp запрещается, если порядок больше нуля, потому что nopage имеет дело с одной страницей, а не кластерами страниц. scullp просто не знает, как правильно управлять счётчиками ссылок для страниц, которые являются частью выделений более высокого порядка. (Вернитесь к разделу "scull, использующий целые страницы: scullp" в Главе 8, если вам необходимо освежить в памяти scullp и понятие порядка выделения памяти.)

 

Ограничение нулевого порядка в основном предназначено, чтобы сохранить код простым. Можно правильно реализовать mmap для многостраничных выделений, играя со счётчиком использования на страницах, но это бы только добавило сложности примеру, не давая какой-либо интересной информации.

 

Коду, который предназначен для отображения ОЗУ в соответствии с только что изложенными правилами, необходимо реализовать VMA методы open, close и nopage; он также нуждается в доступе к карте памяти, чтобы настроить счётчики использования страниц.

 

Эта реализация scullp_mmap очень короткая, потому что опирается на функцию nopage, чтобы сделать всю интересную работу:

 

int scullp_mmap(struct file *filp, struct vm_area_struct *vma)

{

    struct inode *inode = filp->f_dentry->d_inode;

 

    /* отказать в отображении, если порядок не 0 */

    if (scullp_devices[iminor(inode)].order)

        return -ENODEV;

 

    /* не делаем здесь ничего: "nopage" будет заполнять дыры */

    vma->vm_ops = &scullp_vm_ops;

    vma->vm_flags |= VM_RESERVED;

    vma->vm_private_data = filp->private_data;

    scullp_vma_open(vma);

    return 0;

}

 

Целью оператора if является избежать отображения устройств, для которых порядок выделения не равен 0. Операции scullp хранятся в поле vm_ops и указатель на структуру устройства спрятан в поле vm_private_data. В конце концов, вызывается vm_ops->open для обновления числа активных отображений для данного устройства.

 

open и close просто отслеживают счётчик отображений и определены следующим образом:

 

void scullp_vma_open(struct vm_area_struct *vma)

{

    struct scullp_dev *dev = vma->vm_private_data;

 

    dev->vmas++;

}

 

void scullp_vma_close(struct vm_area_struct *vma)

{

    struct scullp_dev *dev = vma->vm_private_data;

 

    dev->vmas--;

}

 

Большинство работы затем выполняется в nopage. В реализации scullp, параметр address для nopage используется, чтобы вычислить смещение в устройстве; смещение затем используется для поиска нужной страницы в дереве памяти scullp:

 

struct page *scullp_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type)

{

    unsigned long offset;

    struct scullp_dev *ptr, *dev = vma->vm_private_data;

    struct page *page = NOPAGE_SIGBUS;

    void *pageptr = NULL; /* по умолчанию "не указан" */

 

    down(&dev->sem);

    offset = (address - vma->vm_start) + (vma->vm_pgoff << PAGE_SHIFT);

    if (offset >= dev->size) goto out; /* вне диапазона */

 

    /*

     * Теперь получим устройство scullp из списка, затем страницу.

     * Если устройство имеет дыру, процесс получает SIGBUS при

     * доступе к дыре.

     */

    offset >>= PAGE_SHIFT; /* смещение является числом страниц */

    for (ptr = dev; ptr && offset >= dev->qset;) {

        ptr = ptr->next;

        offset -= dev->qset;

    }

    if (ptr && ptr->data) pageptr = ptr->data[offset];

    if (!pageptr) goto out; /* дыра или конец файла */

    page = virt_to_page(pageptr);

 

    /* получили её, теперь увеличиваем счётчик */

    get_page(page);

    if (type)

        *type = VM_FAULT_MINOR;

out:

    up(&dev->sem);

    return page;

}

 

scullp использует память, полученную с помощью get_free_pages. Такая память адресуется используя логические адреса, так что всё, что требуется сделать scullp_nopage, чтобы получить указатель struct page, это вызвать virt_to_page.

 

Устройство scullp теперь работает как ожидается, как вы можете видеть в этом примере выходных данных из утилиты mapper. Здесь мы выполняем распечатку каталога /dev (который является большим) в устройство scullp и затем используем утилиту mapper, чтобы посмотреть на кусочки этого листинга с помощью mmap:

 

morgana% ls -l /dev > /dev/scullp

morgana% ./mapper /dev/scullp 0 140

mapped "/dev/scullp" from 0 (0x00000000) to 140 (0x0000008c)

total 232

crw-------    1 root     root      10,  10 Sep 15 07:40 adbmouse

crw-r--r--    1 root     root      10, 175 Sep 15 07:40 agpgart

morgana% ./mapper /dev/scullp 8192 200

mapped "/dev/scullp" from 8192 (0x00002000) to 8392 (0x000020c8)

d0h1494

brw-rw----    1 root     floppy     2,  92 Sep 15 07:40 fd0h1660

brw-rw----    1 root     floppy     2,  20 Sep 15 07:40 fd0h360

brw-rw----    1 root     floppy     2,  12 Sep 15 07:40 fd0H360

Перераспределение  виртуальных адресов ядра

Хотя это требуется редко, интересно увидеть, как драйвер может используя mmap отобразить виртуальный адрес ядра в пространство пользователя. Напомним, что верный виртуальный адрес ядра является адресом, возвращаемым такой функцией, как vmalloc, то есть виртуальный адрес отображается в таблицах страниц ядра. Код в этом разделе взят из scullv, который является модулем, работающим подобно scullp, но выделяющим своё хранилище через vmalloc.

 

Большая часть реализации scullv подобна той, что мы только что видели для scullp, за исключением того, что нет необходимости проверять параметр order, который управляет выделением памяти. Причиной этого является то, что vmalloc выделяет свои страницы по одной за раз, потому что одностраничные выделения имеют гораздо больше шансов на успех, чем многостраничные. Поэтому проблема порядка выделения не относится к пространству vmalloc.

 

Помимо этого, есть только одна разница между реализациями nopage, используемыми scullp и scullv. Напомним, что scullp, как только он нашёл интересующую страницу, получает соответствующий указатель struct page с помощью virt_to_page. Эта функция, однако, не работает с виртуальными адресами ядра. Взамен вы должны использовать vmalloc_to_page. Таким образом, заключительная часть версии nopage в scullv выглядит следующим образом:

 

    /*

     * После поиска scullv, "page" является теперь адресом страницы,

     * необходимой текущему процессу. Так как это адрес vmalloc,

     * преобразуем его в struct page.

     */

    page = vmalloc_to_page(pageptr);

 

    /* получили её, теперь увеличиваем счётчик */

    get_page(page);

    if (type)

        *type = VM_FAULT_MINOR;

out:

    up(&dev->sem);

    return page;

 

На основе этого обсуждения вы также могли бы захотеть отобразить в пространство пользователя адреса, возвращаемые ioremap. Однако, это было бы ошибкой; адреса от ioremap являются специальными и не могут рассматриваться как обычные виртуальные адреса ядра. Вместо этого для переназначения областей памяти ввода/вывода в пространство пользователя вы должны использовать remap_pfn_range.

Предыдущая  Содержание  Следующая