Виртуальная файловая система /proc предлагает новый подход к взаимодействию ядра Linux® и пользовательского пространства. В этой файловой системе содержатся виртуальные файлы, путем чтения и записи которых можно манипулировать структурами ядра. В отличие от обыкновенных файлов, их содержимое динамически генерируется ядром. Данная статья расскажет вам о виртуальной файловой системе /proc и покажет ее в действии
Изначально файловая система /proc разрабатывалась как средство предоставления информации о выполняющихся в системе процессах. Но из-за ее удобства многие подсистемы ядра стали использовать эту файловую систему как средство предоставления информации и динамического конфигурирования.
Файловая система /proc содержит каталоги (для структурирования информации) и виртуальные файлы. Виртуальный файл, как уже было сказано, может предоставлять пользователю информацию, полученную из ядра и, кроме того, служить средством передачи в ядро пользовательской информации. На самом деле, виртуальный файл не обязательно выполняет обе функции, но в этой статье я расскажу о том, как настроить файловую систему как для ввода, так и для вывода.
В короткой статье нельзя описать файловую систему /proc во всех деталях, но вполне возможно продемонстрировать несколько вариантов ее использования, дающих представление о ее возможностях. В листинге 1 показан интерактивный обзор некоторых элементов /proc. Мы видим корневой каталог файловой системы /proc. Обратите внимание на файлы с номерными именами в левой части листинга. Это – каталоги, содержащие информацию о выполняющихся в системе процессах. Process-id равный 1 присвоен процессу init, который в системе GNU/Linux запускается первым. Если выполнить команду ls для такого каталога, будет отображен список находящихся в нем файлов. В каждом файле содержатся те или иные сведения о процессе. Например, для того, чтобы посмотреть сведения о параметрах командной строки, с которыми был запущен процесс init, достаточно просмотреть содержимое файла cmdline с помощью команды cat.
В /proc есть и другие интересные файлы. Например, cpuinfo, содержащий сведения о типе и производительности центрального процессора, pci, из которого можно получить информацию об устройствах на шине PCI и modules, в котором находится список загруженных в ядро модулей.
Листинг 1. Интерактивный обзор файловой системы /proc
|
В листинге 2 показаны чтение и запись параметров ядра в виртуальный файл, находящийся в /proc. Приведенный пример кода отображает значение параметра, управляющего режимом "IP forwarding" стека TCP/IP ядра и затем включает его.
Листинг 2. Чтение и запись /proc (конфигурирование ядра)
|
Другим способом изменения параметров конфигурации ядра является использование команды sysctl.
На самом деле, /proc – не единственная виртуальная файловая система в ОС GNU/Linux. Аналогичная файловая система sysfs имеет сходные функциональные возможности и немного более удачную структуру (при ее разработке был учтен опыт /proc). Тем не менее /proc является де-факто стандартом и, несмотря на то, что sysfs имеет некоторые преимущества, будет и впредь оставаться таковым. Можно упомянуть еще одну виртуальную файловую систему – debugfs, которая (как следует из ее названия), представляет собой скорее отладочный интерфейс. Ее преимуществом является простота, с которой происходит экспорт значения из ядра в пользовательское пространство (фактически, это требует единственного системного вызова).
Хорошим примером для демонстрации возможностей файловой системы /proc являются загружаемые модули ядра (LKM), позволяющие динамически добавлять и, при необходимости, удалять код из ядра Linux. LKM завоевали популярность как удобный механизм реализации в ядре Linux драйверов устройств и файловых систем.
Если вам приходилось вручную собирать ядро Linux, вы, вероятно, обращали внимание на то, что многие драйверы устройств и другие компоненты ядра компилируются в виде модулей. Если драйвер скомпилирован как часть ядра, его код и статические данные занимают память даже тогда, когда он не используется. Но если скомпилировать драйвер как модуль, он будет занимать память только если он действительно необходим и загружен в ядро. Удивительно, но заметной потери производительности при использовании LKM не происходит. Это делает загружаемые модули незаменимым средством при сборке ядра с низкими требованиями к объему памяти и возможностью использования не только штатного набора оборудования, но и подключаемых устройств.
Сравним код простого загружаемого модуля, приведенный в листинге 3 и обычный код ядра (не загружаемый динамически). В листинге 3 приведен код простейшего загружаемого модуля. (. Затем, с помощью макроса MODULE_LICENSE, указывается тип лицензии, под которой распространяется модуль. В данном примере мы используем лицензию GPL, чтобы не получать предупреждений о "заражении" ядра проприетарным кодом.
Далее в листинге 3 следует определение функций модуля init и cleanup. Функция my_module_init вызывается при загрузке модуля и поэтому может использоваться для инициализации. Другая функция, my_module_cleanup, вызывается в момент выгрузки модуля. В ней происходит освобождение памяти и ликвидация следов пребывания модуля в ядре. Обратите внимание на то, что мы используем функцию printk: это аналог printf для ядра. С помощью макроса KERN_INFO можно записать в кольцевой буфер ядра произвольную строку (аналогично функции syslog).
Функции, вызываемые при загрузке и выгрузке модуля, задаются в заключительных строках листинга с помощью макросов module_init and module_exit. Такой способ определения вспомогательных функций init and cleanup позволяет называть их как угодно. Достаточно лишь сообщить их имена ядру.
Листинг 3. Простой, но работоспособный загружаемый модуль (simple-lkm.c)
|
В листинге 3 приведен код простого, но работоспособного загружаемого модуля. Теперь мы соберем его и протестируем на ядре версии 2.6. Начиная с этой версии, в ядре появилась поддержка нового метода сборки модулей ядра, который, на мой взгляд, проще, чем методы, использовавшиеся ранее. Чтобы им воспользоваться, необходимо, помимо файла simple-lkm.c, создать make-файл, содержащий единственную строку, приведенную ниже:
obj-m += simple-lkm.o |
Для сборки модуля, выполните команду make, как показано в листинге 4.
|
После сборки должен появиться файл simple-lkm.ko. Новый способ именования позволяет легко отличать модули ядра от обыкновенных объектов. Теперь, когда модуль готов, можно его загрузить, затем выгрузить и посмотреть на выведенные при этом сообщения. Чтобы загрузить модуль, выполните команду insmod; для выгрузки выполните команду rmmod. Команда lsmod выводит список модулей, загруженных в данный момент (см. листинг 5).
Листинг 5. Загрузка, проверка загрузки и выгрузка модуля
|
Следует учесть, что сообщения, генерируемые кодом ядра, выводятся в кольцевой буфер ядра, а не в stdout, поскольку последний привязан к конкретному процессу. Для просмотра сообщений в кольцевом буфере ядра, вы можете воспользоваться командой dmesg (или просмотреть сообщения в самой файловой системе /proc с помощью команды cat /proc/kmsg). В листинге 6 приведены несколько последних сообщений, выведенных по команде dmesg.
Листинг 6. Сообщения, сгенерированные тестовым модулем
|
Сообщения, выведенные тестовым модулем, видны в общем потоке сообщений ядра. А теперь предлагаю перейти от нашего простого примера к интерфейсам ядра, позволяющим разрабатывать загружаемые модули.
![]() |
|
Интеграция с файловой системой /proc
Разработчики загружаемых модулей могут использовать все интерфейсы, доступные их коллегам, разрабатывющим само ядро. Допускается даже использование ядром переменных и функций, экспортируемых загружаемым модулем. Детальный анализ этих интерфейсов выходит за рамки статьи, так что я просто расскажу о некоторых элементах, которыми я собираюсь воспользоваться для разработки более сложного модуля.
Создание и удаление виртуального файла в /proc
Для того, чтобы создать виртуальный файл в файловой системе /proc, используется функция create_proc_entry. Эта функция принимает в качестве параметров имя создаваемого файла, режим доступа к нему и один из подкаталогов /proc для его размещения. Функция create_proc_entry возвращает указатель на структуру proc_dir_entry (или NULL в случае возникновения ошибки). Полученный указатель можно использовать для настройки остальных параметров виртуального файла, таких, как функция, вызываемая при чтении из файла. Прототип функции create_proc_entry и фрагмент структуры proc_dir_entry показаны в листинге 7.
Листинг 7. Элементы интерфейсов управления виртуальными файлами /proc
|
Чуть позже вы узнаете, как использовать команды read_proc and write_proc для задания функций чтения и записи виртуального файла.
Для удаления файла из /proc, используйте функцию remove_proc_entry. При вызове в эту функцию передается строка, содержащая имя удаляемого файла и его местонахождение в файловой системе /proc (родительский каталог). Прототип этой функции приведен в листинге 7.
Параметр parent принимает значение NULL если файл находится непосредственно в каталоге /proc или другое значение, соответствующее каталогу, в который вы хотите поместить файл. В таблице 1 приведена часть предопределенных переменных proc_dir_entry, передаваемых как значение параметра parent, и соответствующих им каталогов файловой системы /proc.
Таблица 1. Список предопределенных переменных proc_dir_entry
Переменная proc_dir_entry | Каталог |
---|---|
proc_root_fs | /proc |
proc_net | /proc/net |
proc_bus | /proc/bus |
proc_root_driver | /proc/driver |
Вы можете записывать данные в виртуальный файл (из пользовательского пространства в ядро) с помощью функции write_proc. Эта функция имеет прототип следующего вида:
int mod_write( struct file *filp, const char __user *buff, |
Параметр filp представляет собой структуру, соответствующую открытому файлу устройства (нам он не понадобится). Параметр buff соответствует строке, передаваемой в модуль. Поскольку буфер, в котором находится строка находится в пользовательском пространстве, к нему нельзя будет получить непосредственный доступ из модуля. Параметр len содержит количество подлежащих записи байт, находящихся в buff. Параметр data содержит указатель на локальные данные (см. листинг 7). В нашем тестовом модуле сallback-функция записи служит для обработки входящих данных.
В Linux предусмотрен набор API для перемещения данных между пользовательским пространством и пространством ядра. Для операций с данными, находящимися в пользовательском пространстве, в функции write_proc из нашего примера, используется семейство функций copy_from_user.
Вы можете считать данные из виртуального файла (из ядра в пользовательское пространство) с помощью функции read_proc. Ее прототип выглядит так:
int mod_read( char *page, char **start, off_t off, |
Параметр page содержит указатель на буфер, в который будут записаны данные, полученные из ядра, при этом параметр count определяет максимальное число символов, которое может быть записано в данный буфер. Если планируется получить более одной страницы данных (обычно, 4KB), следует использовать параметры start и off. После того, как все данные получены, установите признак eof (конец файла). По аналогии с кодом функции write, параметр data соответствует локальным данным. Буфер page, используемый в данной функции располагается в пространстве ядра. Следовательно, для записи в него не требуется вызов функции copy_to_user.
Кроме обыкновенных файлов, в файловой системе /proc можно создавать каталоги, используя функцию proc_mkdir и символьные ссылки (symlinks), используя proc_symlink. Файлы /proc, для которых определена только операция чтения (функция read), можно создать единственным вызовом функции create_proc_read_entry, создающей файл и задающей для него функцию read_proc. Прототипы вышеупомянутых функций показаны в листинге 8.
Листинг 8. Прочие полезные функции для работы с /proc
|
![]() |
|
Выдача 'фортунок' с помощью файловой системы /proc
В этом примере мы создадим загружаемый модуль ядра с поддержкой операций чтения и записи. Это простое приложение будет по запросу выдавать изречения-'фортунки'. После загрузки модуля, пользователь сможет записать в него текст 'фортунок' с помощью команды echo и затем считывать их по одной в случайном порядке, используя команду cat.
Исходный код данного модуля приведен в листинге 9. Init-функция (init_fortune_module) выделяет блок памяти для хранения 'фортунок' вызовом vmalloc и затем заполняет его нулями с помощью memset. После того, как cookie_pot создан и обнулен, в каталоге /proc создается виртуальный файл (типproc_dir_entry) с именем fortune. После того, как файл (proc_entry) успешно создан, происходит инициализация локальных переменных и структуры proc_entry. В соответствующие поля этой структуры записываются указатели на функции модуля read и write (см. листинги 9 и 10, а также информация о владельце модуля. Функция cleanup удаляет файл из файловой системы /proc и освобождает память, занимаемую cookie_pot.
Хранилище 'фортунок' cookie_pot занимает страницу памяти (4KB) и обслуживается двумя индексами. Первый из них, cookie_index, определяет адрес, по которому будет записана следующая 'фортунка'. торой индекс – переменная next_fortune, содержит адрес 'фортунки', которая будет выдана по следующему запросу. После того как выдана последняя 'фортунка', переменной next_fortune присваивается адрес первого элемента и выдача начинается сначала.
Листинг 9. Функции init/cleanup и переменные модуля.
|
Записать 'фортунку' в хранилище очень просто (см. листинг 10). Зная длину записываемой 'фортунки', можно определить, достаточно ли места для ее размещения. Если места недостаточно, модуль возвратит пользовательскому процессу код -ENOSPC. В противном случае строка копируется в cookie_pot с помощью функции copy_from_user. После этого происходит увеличение значения переменной cookie_index (на величину, зависящую от длины полученной строки), в конец строки дописывается NULL. Алгоритм завершает свою работу тем, что возвращает пользовательскому процессу количество символов фактически записанных в cookie_pot.
Листинг 10. Функция записи 'фортунки'
|
Чтение 'фортунки' нисколько не сложнее ее записи. Поскольку буфер, в который нужно произвести запись 'фортунки' (page), уже находится в пользовательском пространстве, для вывода фортунки можно использовать непосредственно функцию sprintf. Если значение индекса next_fortune превышает значение cookie_index (индекс следующего свободного для записи элемента), переменной next_fortune присваивается 0, то есть, индекс первого элемента. После того, как фортунка записана в буфер пользовательского процесса, я увеличиваю индекс next_fortune на ее длину. Теперь этот индекс содержит адрес 'фортунки', которая будет выдана следующей . Длина 'фортунки' также передается пользовательскому процессу в качестве возвращаемого значения.
Листинг 11. Функция чтения 'фортунки'
|
Это простой пример показывает что обмен данными между ядром и пользовательским процессом является тривиальной задачей. А сейчас предлагаю посмотреть на модуль выдачи 'фортунок' в действии (листинг 12).
Листинг 12. Загружаемый модуль выдачи 'фортунок' в действии
|
Виртуальная файловая система /proc широко используется как средство сбора информации о состоянии ядра и для его динамического конфигурирования. Она незаменима для разработки драйверов и модулей ядра, в чем несложно убедиться.