Доступ к адресному пространству пользователя из ядра Linux

Знакомство с архитектурой памяти и API-функциями для работы с пространством пользователя в Linux

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

M. Тим Джонс, инженер-консультант, Emulex Corp.

М. Тим ДжонсМ. Тим Джонс - архитектор встроенного ПО и, кроме того, автор книг Artificial Intelligence: A Systems Approach, GNU/Linux Application Programming (выдержавшей на данный момент второе издание), AI Application Programming (второе издание) и BSD Sockets Programming from a Multilanguage Perspective. Он имеет обширный опыт разработки ПО в самых разных предметных областях - от ядер специальных ОС для геосинхронных космических аппаратов до архитектур встраиваемых систем и сетевых протоколов. Тим - инженер-консультант Emulex Corp., Лонгмонт, Колорадо.



23.06.2011

Как связаться с Тимом

Тим – один из наших наиболее популярных и плодовитых авторов. Познакомьтесь со всеми статьями Тима (EN), опубликованными на сайте developerWorks. Вы можете найти контактные данные в профиле Тима и связаться с ним, а также с другими авторами и участниками ресурса My developerWorks.

Хотя наименьшей адресуемой ячейкой памяти в операционной системе Linux® и является байт, в качестве абстрактного управляемого элемента памяти выступает страница. В этой статье мы обсудим вопросы, связанные с управлением памятью в ОС Linux, а затем рассмотрим методы управления адресным пространством пользователя из ядра этой операционной системы.

Память в Linux

В операционной системе Linux память пользователя и память ядра являются независимыми друг от друга и расположены в различных адресных пространствах. Проще говоря, адресные пространства виртуализированы, т. е. абстрагированы от физической памяти. Поскольку адресные пространства являются виртуальными, в системе их может быть много. Фактически само ядро и каждый из процессов располагаются в своих собственных изолированных адресных пространствах. Эти адресные пространства состоят из адресов виртуальной памяти, что позволяет нескольким процессам с независимыми адресными пространствами обращаться к физическому адресному пространству (т. е. к памяти, физически установленной на устройстве), имеющему значительно меньший объем. Такой подход обеспечивает не только удобство, но и безопасность, поскольку все адресные пространства являются независимыми и изолированными друг от друга.

Однако такая безопасность не дается даром. Поскольку ядро и каждый из процессов могут иметь одинаковые адреса, ссылающиеся на различные области физической памяти, при использовании общей памяти могут возникать задержки. К счастью, существует несколько решений этой проблемы. Совместное использование памяти пользовательскими процессами может обеспечиваться с помощью POSIX-механизма совместно используемой памяти shmem (shared memory mechanism), причем процессы могут иметь различные виртуальные адреса, ссылающиеся на одну и ту же область физической памяти.

Отображение виртуальной памяти на физическую осуществляется через аппаратно реализованные страничные таблицы (рисунок 1). Аппаратная часть непосредственно выполняет отображение, а ядро управляет таблицами и их конфигурацией. Обратите внимание на то, что процесс может иметь большое, но разбросанное адресное пространство (как показано на рисунке); это означает, что небольшие области (страницы) адресного пространства отображаются на физическую память через страничные таблицы. Это позволяет процессу иметь большое адресное пространство, занятое только теми страницами, с которыми необходимо работать в текущий момент.

Рисунок 1. Страничные таблицы обеспечивают отображение виртуальных адресов на физические
Рисунок 1. Страничные таблицы обеспечивают отображение виртуальных адресов на физические

Поскольку память может выделяться процессам только тогда, когда это необходимо, объем используемой виртуальной памяти может быть больше, чем объем установленной физической памяти. В процессе замещения страниц (paging), который в Linux называется подкачкой (swap), наиболее редко используемые страницы перемещаются на более медленное устройство хранения (например, на жесткий диск), что позволяет обеспечить доступ к страницам, с которыми необходимо работать в данный момент (рисунок 2). Данный подход позволяет размещать в ОЗУ наиболее часто используемые страницы, а наиболее редко используемые выгружать на жесткий диск. Таким образом, обеспечивается более эффективное использование физической памяти. Заметьте, что некоторые страницы могут ссылаться на файлы – в этом случае грязные страницы могут быть сброшены на диск, а чистые страницы быть просто помеченными как свободные.

Рисунок 2. Подкачка позволяет эффективнее использовать физическую память, перемещая редко используемые страницы на более медленные и дешевые устройства
Рисунок 2. Подкачка позволяет эффективнее использовать физическую память, перемещая редко используемые страницы на более медленные и дешевые устройства

Процессоры без блока управления памятью (MMU)

Не все процессоры имеют блок управления памятью, поэтому дистрибутив uClinux (версия ОС Linux для микроконтроллеров) поддерживает одноадресное пространство операций. Архитектура без MMU не имеет защиты, обеспечиваемой MMU-блоком, но позволяет ОС Linux работать на процессорах с различными типами архитектур. Для получения информации об операционной системе uClinux обратитесь к разделу Ресурсы.

Процесс выбора страниц, которые должны быть выгружены на диск, называется алгоритмом замещения страниц и может быть реализован по-разному (например, существует алгоритм замещения блока данных с наиболее длительным отсутствием обращений). Этот процесс может начаться в момент обращения к адресу памяти, страница которого не находится в ОЗУ (в блоке управления памятью отсутствует ее отображение). Это событие называется ошибкой отсутствия страницы (page fault) и обнаруживается на аппаратном уровне блоком MMU, после чего выдается соответствующее аппаратное прерывание, обрабатываемое встроенным ПО. Этот стек изображен на рисунке 3.

В операционной системе Linux подкачка реализована интересным способом, обладающим рядом полезных свойств. Linux позволяет создавать и использовать несколько разделов подкачки и задавать приоритеты для устройств с различным быстродействием, на которые производится выгрузка данных (например, первоочередным устройством для выгрузки является твердотельный накопитель – SSD-диск, а второстепенным – более медленное устройство). Если SSD-накопителю назначен более высокий приоритет, выгрузка данных будет производиться на него до тех пор, пока на нем не закончится свободное место; лишь после этого страницы памяти будут выгружаться на более медленное устройство с меньшим приоритетом.

Рисунок 3. Адресные пространства и элементы отображения виртуальных адресов на физические
Рисунок 3. Адресные пространства и элементы отображения виртуальных адресов на физические

Не все страницы памяти могут быть выгружены (например, это может быть код ядра, отвечающий за прерывания, или код управления страничными таблицами и логикой подкачки). Очевидно, что такие страницы никогда не должны выгружаться и поэтому являются закрепленными (pinned), т. е. постоянно находятся в оперативной памяти. В отличие от страниц ядра, которые никогда не выгружаются, страницы адресного пространства пользователя могут быть выгружены, однако их можно закрепить с помощью функции mlock (или mlockall). Рассмотрим следующую ситуацию: ядро предположило, что переданный пользователем адрес являлся допустимым и доступным, но по какой-либо причине возникло состояние отсутствия страницы в ядре (например, потому, что пользовательская страница была выгружена). В этом случае может наступить состояние паники ядра. API-функция mlock предназначена именно для того, чтобы обеспечивать корректную обработку таких ситуаций.


API-функции ядра

Давайте рассмотрим API-функции ядра для управления пользовательским адресным пространством. В этом разделе мы рассмотрим функции для работы с ядром и адресным пространством пользователя (эти функции перечислены в таблице 1), а в следующем разделе – ряд API-функций для работы с памятью.

Таблица 1. API-интерфейсы для доступа к адресному пространству пользователя
ФункцияОписание
access_okПроверяет, является ли указатель пространства пользователя действительным.
get_userПолучает простую переменную из пространства пользователя.
put_userПомещает простую переменную в пространство пользователя.
clear_userОчищает или обнуляет блок памяти в пространстве пользователя.
copy_to_userКопирует блок данных из пространства ядра в пространство пользователя.
copy_from_userКопирует блок данных из пространства пользователя в пространство ядра.
strnlen_userОпределяет размер строкового буфера в пространстве пользователя.
strncpy_from_userКопирует строку из пространства пользователя в пространство ядра.

Как и следует ожидать, реализация этих функций зависит от архитектуры. Для x86-архитектур эти функции определены в файле ./linux/arch/x86/include/asm/uaccess.h, а их исходный код – в файлах ./linux/arch/x86/lib/usercopy_32.c и usercopy_64.c.

Функции для перемещения различных типов данных (простых и сложных) изображены на рисунке 4.

Рисунок 4. Перемещение данных с помощью API-функций доступа к адресному пространству пользователя
Рисунок 4. Перемещение данных с помощью API-функций доступа к адресному пространству пользователя

Функция access_ok

Функция access_ok используется для проверки действительности указателя пространства пользователя, по которому необходимо получить доступ. Входными аргументами этой функции являются указатель на начало блока памяти, размер блока и тип доступа (чтение или запись). Прототип функции определен следующим образом.

access_ok( type, addr, size );

Аргумент type может принимать значения VERIFY_READ и VERIFY_WRITE. Значение VERIFY_WRITE означает, что область памяти доступна не только для записи, но и для чтения. Если доступ к указанной области памяти разрешен, функция возвращает ненулевое значение (однако при попытке доступа может возникнуть ошибка -EFAULT). Эта функция просто проверяет, что адрес находится в пространстве пользователя, а не в пространстве ядра.

Функция get_user

Функция get_user используется для считывания простой переменной из пространства пользователя. Эта функция работает с простыми типами данных, такими как char и int; для работы со сложными типами, такими как структуры, необходимо использовать функцию copy_from_user. Входными аргументами этой функции являются имя переменной (в которую будут сохранены полученные данные) и адрес пользовательского пространства, значение которого будет считываться.

get_user( x, ptr );

Функция get_user сопоставляется одной из двух внутренних функций. С помощью внутренних механизмов эта функция определяет размер переменной (на основании входной переменной для сохранения данных), к которой происходит обращение, и осуществляет внутренний вызов через макрос __get_user_x. В случае успеха возвращается нулевое значение. Функции get_user и put_user работают быстрее, чем их аналоги для копирования блоков данных, поэтому при работе с простыми типами следует использовать именно их.

Функция put_user

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

put_user( x, ptr );

Так же как и функция get_user, функция put_user осуществляет внутренний вызов put_user_x и возвращает 0 в случае успеха и -EFAULT – в случае ошибки.

Функция clear_user

Функция clear_user используется для обнуления блока памяти в пользовательском пространстве. Входными аргументами этой функции являются указатель на блок памяти в адресном пространстве пользователя и размер блока (в байтах) для обнуления.

clear_user( ptr, n );

Функция clear_user проверяет (с помощью access_ok), доступен ли указатель пространства пользователя для записи, после чего вызывает внутреннюю функцию (написанную на встроенном ассемблере) для очистки данных. Эта оптимизированная функция реализована в виде сжатого цикла и использует строковые инструкции с префиксом повторения. В случае успеха функция возвращает нулевое значение, а в случае неудачного выполнения – числовое значение, равное количеству байтов, которые не удалось очистить.

Функция copy_to_user

Функция copy_to_user копирует блок данных из пространства ядра в пространство пользователя. Входными аргументами этой функции являются указатель на буфер пространства пользователя, указатель на буфер ядра и размер блока (в байтах). В случае успеха функция возвращает нулевое значение, а в случае неудачного выполнения – числовое значение, равное количеству байтов, которые не удалось передать.

copy_to_user( to, from, n );

Функция проверяет (с помощью access_ok), доступен ли указатель пространства пользователя для записи, после чего выполняет вызов внутренней функции __copy_to_user, которая, в свою очередь, вызывает функцию __copy_from_user_inatomic, определенную в заголовке ./linux/arch/x86/include/asm/uaccess_XX.h, где XX – разрядность процессора (32 или 64). Наконец, после определения размера блоков для копирования (1, 2 или 4 байта), происходит вызов функции __copy_to_user_ll, которая и выполняет основную работу. На старых архитектурах процессоров, предшествующих i486 (в которых WP-бит не принимал прерывания на обработку в привилегированном режиме), страничные таблицы могли меняться в произвольные моменты времени, поэтому требовалось закреплять в памяти требуемые страницы, чтобы они не могли быть выгружены в момент обращения к ним. В архитектурах от i486 и выше этот процесс представляет собой простое оптимизированное копирование.

Функция copy_from_user

Функция copy_from_user копирует блок данных из пространства пользователя в буфер ядра. Входными аргументами этой функции являются указатель на буфер назначения в пространстве ядра, указатель на буфер источника в пространстве пользователя и размер копируемого блока (в байтах). Так же как и функция copy_to_user, в случае успеха эта функция возвращает нулевое значение, а в случае неудачного выполнения – числовое значение, равное количеству байтов, которые не удалось скопировать.

copy_from_user( to, from, n );

Функция проверяет (с помощью access_ok), доступен ли указатель на буфер в пространстве пользователя для чтения, после чего выполняется вызов внутренней функции __copy_from_user и последующий вызов функции __copy_from_user_ll. Далее в зависимости от архитектуры процессора выполняется копирование из буфера пользователя в буфер ядра (с обнулением отсутствующих байтов). Оптимизированные функции, написанные на ассемблере, имеют возможности управления.

Функция strnlen_user

Функция strnlen_user используется так же, как и функция strnlen, но в данном случае предполагается, что буфер в пространстве пользователя доступен. Входными аргументами функции strnlen_user являются адрес буфера пространства пользователя и максимальный размер проверяемого блока.

strnlen_user( src, n );

Функция strnlen_user проверяет (с помощью access_ok), что буфер в пространстве пользователя доступен для чтения. При выполнении этого условия происходит вызов функции strlen, а аргумент max length игнорируется.

Функция strncpy_from_user

Функция strncpy_from_user копирует строку из пространства пользователя в буфер ядра. Входными аргументами этой функции являются адрес назначения в пространстве ядра, адрес источника в пространстве пользователя и максимальная длина строки.

strncpy_from_user( dest, src, n );

Так же как и функция copy_from_user, эта функция проверяет (с помощью access_ok), что буфер в пространстве пользователя доступен для чтения, и является оптимизированной функцией, написанной на ассемблере (модуль ./linux/arch/x86/lib/usercopy_XX.c).


Другие схемы отображения памяти

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

Обратите внимание на то, что пользовательские процессы выполняются в изолированных областях адресного пространства, поэтому для того, чтобы они могли обмениваться данными, необходимо использовать определенные механизмы взаимодействия. В ОС Linux существуют различные схемы такого взаимодействия (например, очереди сообщений), но наиболее примечательным является POSIX-механизм совместно используемой памяти shmem, позволяющий процессам создавать область разделяемой памяти и использовать ее совместно с другими процессами. Заметьте, что различные процессы могут отображать область совместно используемой памяти на различные адреса своих адресных пространств, поэтому необходимо использовать относительную адресацию с указанием величины смещения.

Функция mmap позволяет приложению, работающему в адресном пространстве пользователя, отображать память на виртуальное адресное пространство. В драйверах устройств одного класса эта функция имеет общую функциональность (для обеспечения производительности) и позволяет отображать физическую память устройства на виртуальное адресное пространство процесса. Внутри драйвера функция mmap реализуется через функцию ядра remap_pfn_range, позволяющую выполнять линейное отображение памяти устройства на адресное пространство пользователя.


Заключение

В этой статье были рассмотрены вопросы управления памятью в операционной системе Linux, а также связанные с ними функции доступа к адресному пространству пользователя. Перемещать данные между ядром и пространством пользователя не так просто, как кажется, однако в операционной системе Linux имеется ряд API-функций, существенно упрощающих этот процесс.

Ресурсы

Научиться

  • Оригинал статьи: User space memory access from the Linux kernel (EN).
  • В 4 главе руководства по администрированию Red Hat Enterprise Linux 4 рассматриваются основные принципы виртуальной памяти.
  • В статье All about Linux swap space (EN) рассматривается, для чего нужна область подкачки и где она располагается, а также приводятся различные команды для работы с ней.
  • Для улучшения производительности кэширования была разработана схема сжатого кэша (EN), в которой в роли кэш-диска выступает быстрый RAM-диск, расположенный в ОЗУ. Страницы этого RAM-диска для повышения эффективности хранения хранятся в сжатом виде.
  • Книга Драйверы устройств Linux (третье издание) (EN) является одним из лучших источников информации, касающейся управления памятью в Linux, а также всего, что связано с драйверами устройств.
  • Одно из отличий страниц пространства пользователя и пространства ядра заключается в том, что страницы ядра постоянно находятся в оперативной памяти, тогда как страницы пользователя могут быть выгружены из памяти на устройство хранения. Закрепить часть страниц виртуального адресного пространства в памяти можно с помощью системных вызовов mlock() и mlockall() (EN).
  • Поскольку не все процессоры имеют блок управления памятью, был создан специальный дистрибутив операционной системы Linux – uClinux (EN), поддерживающий архитектуры без блока MMU (например, микроконтроллеры).
  • В Википедии можно найти много полезной информации на различные темы, связанные с управлением памятью, такие как виртуальная память, замещение страниц, страничные таблицы и алгоритмы замещения страниц (EN).
  • Следите на последними новостями на портале Web-трансляций и технических мероприятий developerWorks (EN).

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=682222
ArticleTitle=Доступ к адресному пространству пользователя из ядра Linux
publish-date=06232011