Доступ к ядру Linux через файловую систему /proc

Виртуальная файловая система /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

                

[root@plato]# ls /proc

1 2040 2347 2874 474 fb mdstat sys

104 2061 2356 2930 9 filesystems meminfo sysrq-trigger

113 2073 2375 2933 acpi fs misc sysvipc

1375 21 2409 2934 buddyinfo ide modules tty

1395 2189 2445 2935 bus interrupts mounts uptime

1706 2201 2514 2938 cmdline iomem mtrr version

179 2211 2515 2947 cpuinfo ioports net vmstat

180 2223 2607 3 crypto irq partitions

181 2278 2608 3004 devices kallsyms pci

182 2291 2609 3008 diskstats kcore self

2 2301 263 3056 dma kmsg slabinfo

2015 2311 2805 394 driver loadavg stat

2019 2337 2821 4 execdomains locks swaps

[root@plato 1]# ls /proc/1

auxv cwd exe loginuid mem oom_adj root statm task

cmdline environ fd maps mounts oom_score stat status wchan

[root@plato]# cat /proc/1/cmdline

init [5]

[root@plato]#

В листинге 2 показаны чтение и запись параметров ядра в виртуальный файл, находящийся в /proc. Приведенный пример кода отображает значение параметра, управляющего режимом "IP forwarding" стека TCP/IP ядра и затем включает его.

Листинг 2. Чтение и запись /proc (конфигурирование ядра)

                

[root@plato]# cat /proc/sys/net/ipv4/ip_forward

0

[root@plato]# echo "1" > /proc/sys/net/ipv4/ip_forward

[root@plato]# cat /proc/sys/net/ipv4/ip_forward

1

[root@plato]#

Другим способом изменения параметров конфигурации ядра является использование команды 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)

                

#include <linux/module.h>



/* Задается тип лицензии загружаемого модуля */

MODULE_LICENSE("GPL");



/* Init-функция, вызываемая при загрузке модуля */

int my_module_init( void )

{

printk(KERN_INFO "my_module_init called. Module is now loaded.n");



return 0;

}



/* Cleanup-функция, вызываемая при выгрузке модуля */

void my_module_cleanup( void )

{

printk(KERN_INFO "my_module_cleanup called. Module is now unloaded.n");



return;

}



/* Ядру сообщаются названия функций, вызываемых при загрузке и выгрузке модуля */

module_init( my_module_init );

module_exit( my_module_cleanup );

В листинге 3 приведен код простого, но работоспособного загружаемого модуля. Теперь мы соберем его и протестируем на ядре версии 2.6. Начиная с этой версии, в ядре появилась поддержка нового метода сборки модулей ядра, который, на мой взгляд, проще, чем методы, использовавшиеся ранее. Чтобы им воспользоваться, необходимо, помимо файла simple-lkm.c, создать make-файл, содержащий единственную строку, приведенную ниже:

obj-m += simple-lkm.o

Для сборки модуля, выполните команду make, как показано в листинге 4.

Листинг 4. Сборка модуля

                

[root@plato]# make -C /usr/src/linux-`uname -r` SUBDIRS=$PWD modules

make: Entering directory `/usr/src/linux-2.6.11'

CC [M] /root/projects/misc/module2.6/simple/simple-lkm.o

Building modules, stage 2.

MODPOST

CC /root/projects/misc/module2.6/simple/simple-lkm.mod.o

LD [M] /root/projects/misc/module2.6/simple/simple-lkm.ko

make: Leaving directory `/usr/src/linux-2.6.11'

[root@plato]#

После сборки должен появиться файл simple-lkm.ko. Новый способ именования позволяет легко отличать модули ядра от обыкновенных объектов. Теперь, когда модуль готов, можно его загрузить, затем выгрузить и посмотреть на выведенные при этом сообщения. Чтобы загрузить модуль, выполните команду insmod; для выгрузки выполните команду rmmod. Команда lsmod выводит список модулей, загруженных в данный момент (см. листинг 5).

Листинг 5. Загрузка, проверка загрузки и выгрузка модуля

                

[root@plato]# insmod simple-lkm.ko

[root@plato]# lsmod

Module Size Used by

simple_lkm 1536 0

autofs4 26244 0

video 13956 0

button 5264 0

battery 7684 0

ac 3716 0

yenta_socket 18952 3

rsrc_nonstatic 9472 1 yenta_socket

uhci_hcd 32144 0

i2c_piix4 7824 0

dm_mod 56468 3

[root@plato]# rmmod simple-lkm

[root@plato]#

Следует учесть, что сообщения, генерируемые кодом ядра, выводятся в кольцевой буфер ядра, а не в stdout, поскольку последний привязан к конкретному процессу. Для просмотра сообщений в кольцевом буфере ядра, вы можете воспользоваться командой dmesg (или просмотреть сообщения в самой файловой системе /proc с помощью команды cat /proc/kmsg). В листинге 6 приведены несколько последних сообщений, выведенных по команде dmesg.

Листинг 6. Сообщения, сгенерированные тестовым модулем

                

[root@plato]# dmesg | tail -5

cs: IO port probe 0xa00-0xaff: clean.

eth0: Link is down

eth0: Link is up, running at 100Mbit half-duplex

my_module_init called. Module is now loaded.

my_module_cleanup called. Module is now unloaded.

[root@plato]#

Сообщения, выведенные тестовым модулем, видны в общем потоке сообщений ядра. А теперь предлагаю перейти от нашего простого примера к интерфейсам ядра, позволяющим разрабатывать загружаемые модули.

   

Интеграция с файловой системой /proc

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

Создание и удаление виртуального файла в /proc

Для того, чтобы создать виртуальный файл в файловой системе /proc, используется функция create_proc_entry. Эта функция принимает в качестве параметров имя создаваемого файла, режим доступа к нему и один из подкаталогов /proc для его размещения. Функция create_proc_entry возвращает указатель на структуру proc_dir_entry (или NULL в случае возникновения ошибки). Полученный указатель можно использовать для настройки остальных параметров виртуального файла, таких, как функция, вызываемая при чтении из файла. Прототип функции create_proc_entry и фрагмент структуры proc_dir_entry показаны в листинге 7.

Листинг 7. Элементы интерфейсов управления виртуальными файлами /proc

                

struct proc_dir_entry *create_proc_entry( const char *name, mode_t mode,

struct proc_dir_entry *parent );



struct proc_dir_entry {

const char *name; // имя виртуального файла

mode_t mode; // режим доступа

uid_t uid; // уникальный номер пользователя -
// владельца файла

uid_t uid; // уникальный номер группы, которой
// принадлежит файл

struct inode_operations *proc_iops; // функции-обработчики операций с inode

struct inode_operations *proc_iops; // функции-обработчики операций с файлом

struct proc_dir_entry *parent; // Родительский каталог

...

read_proc_t *read_proc; // функция чтения из /proc

write_proc_t *write_proc; // функция записи в /proc

void *data; // Указатель на локальные данные

atomic_t count; // счетчик ссылок на файл

...

};



void remove_proc_entry( const char *name, struct proc_dir_entry *parent );

Чуть позже вы узнаете, как использовать команды 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

Callback-функция записи

Вы можете записывать данные в виртуальный файл (из пользовательского пространства в ядро) с помощью функции write_proc. Эта функция имеет прототип следующего вида:

int mod_write( struct file *filp, const char __user *buff,

unsigned long len, void *data );

Параметр filp представляет собой структуру, соответствующую открытому файлу устройства (нам он не понадобится). Параметр buff соответствует строке, передаваемой в модуль. Поскольку буфер, в котором находится строка находится в пользовательском пространстве, к нему нельзя будет получить непосредственный доступ из модуля. Параметр len содержит количество подлежащих записи байт, находящихся в buff. Параметр data содержит указатель на локальные данные (см. листинг 7). В нашем тестовом модуле сallback-функция записи служит для обработки входящих данных.

В Linux предусмотрен набор API для перемещения данных между пользовательским пространством и пространством ядра. Для операций с данными, находящимися в пользовательском пространстве, в функции write_proc из нашего примера, используется семейство функций copy_from_user.

Callback-функция чтения

Вы можете считать данные из виртуального файла (из ядра в пользовательское пространство) с помощью функции read_proc. Ее прототип выглядит так:

int mod_read( char *page, char **start, off_t off,

int count, int *eof, void *data );

Параметр 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 */

struct proc_dir_entry *proc_mkdir( const char *name,

struct proc_dir_entry *parent );



/* Создание символической ссылки в файловой системе proc */

struct proc_dir_entry *proc_symlink( const char *name,

struct proc_dir_entry *parent,

const char *dest );



/* Создание proc_dir_entry с read_proc_t за один вызов */

struct proc_dir_entry *create_proc_read_entry( const char *name,

mode_t mode,

struct proc_dir_entry *base,

read_proc_t *read_proc,

void *data );



/* Копирование буфера из пространства ядра в пользовательское пространство */

unsigned long copy_to_user( void __user *to,

const void *from,

unsigned long n );



/* Копирование буфера из пользовательского пространства в пространство ядра */

unsigned long copy_from_user( void *to,

const void __user *from,

unsigned long n );



/* Выделение 'виртуально' непрерывного блока памяти */

void *vmalloc( unsigned long size );



/* Освобождение блока памяти, выделенного функцией vmalloc */

void vfree( void *addr );



/* Экспорт символа в ядро (после этого ядро сможет его видеть) */

EXPORT_SYMBOL( symbol );



/* Экспорт в ядро всех символов, объявленных в файле (должен предшествовать подключению
файла module.h) */

EXPORT_SYMTAB
   

Выдача 'фортунок' с помощью файловой системы /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 и переменные модуля.

                

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/proc_fs.h>

#include <linux/string.h>

#include <linux/vmalloc.h>

#include <asm/uaccess.h>



MODULE_LICENSE("GPL");

MODULE_DESCRIPTION("Fortune Cookie Kernel Module");

MODULE_AUTHOR("M. Tim Jones");



#define MAX_COOKIE_LENGTH PAGE_SIZE

static struct proc_dir_entry *proc_entry;



static char *cookie_pot; // Хранилище 'фортунок'

static int cookie_index; // Индекс первого свободного для записи элемента хранилища

static int cookie_index; // Индекс элемента хранилища, содержащего
// следующую 'фортунку' для вывода по запросу





int init_fortune_module( void )

{

int ret = 0;



cookie_pot = (char *)vmalloc( MAX_COOKIE_LENGTH );



if (!cookie_pot) {

ret = -ENOMEM;

} else {



memset( cookie_pot, 0, MAX_COOKIE_LENGTH );



proc_entry = create_proc_entry( "fortune", 0644, NULL );



if (proc_entry == NULL) {



ret = -ENOMEM;

vfree(cookie_pot);

printk(KERN_INFO "fortune: Couldn't create proc entryn");



} else {



cookie_index = 0;

next_fortune = 0;



proc_entry->read_proc = fortune_read;

proc_entry->write_proc = fortune_write;

proc_entry->owner = THIS_MODULE;

printk(KERN_INFO "fortune: Module loaded.n");



}



}



return ret;

}





void cleanup_fortune_module( void )

{

remove_proc_entry("fortune", &proc_root);

vfree(cookie_pot);

printk(KERN_INFO "fortune: Module unloaded.n");

}





module_init( init_fortune_module );

module_exit( cleanup_fortune_module );

Записать 'фортунку' в хранилище очень просто (см. листинг 10). Зная длину записываемой 'фортунки', можно определить, достаточно ли места для ее размещения. Если места недостаточно, модуль возвратит пользовательскому процессу код -ENOSPC. В противном случае строка копируется в cookie_pot с помощью функции copy_from_user. После этого происходит увеличение значения переменной cookie_index (на величину, зависящую от длины полученной строки), в конец строки дописывается NULL. Алгоритм завершает свою работу тем, что возвращает пользовательскому процессу количество символов фактически записанных в cookie_pot.

Листинг 10. Функция записи 'фортунки'

                

ssize_t fortune_write( struct file *filp, const char __user *buff,

unsigned long len, void *data )

{

int space_available = (MAX_COOKIE_LENGTH-cookie_index)+1;



if (len > space_available) {



printk(KERN_INFO "fortune: cookie pot is full!n");

return -ENOSPC;



}



if (copy_from_user( &cookie_pot[cookie_index], buff, len )) {

return -EFAULT;

}



cookie_index += len;

cookie_pot[cookie_index-1] = 0;



return len;

}

Чтение 'фортунки' нисколько не сложнее ее записи. Поскольку буфер, в который нужно произвести запись 'фортунки' (page), уже находится в пользовательском пространстве, для вывода фортунки можно использовать непосредственно функцию sprintf. Если значение индекса next_fortune превышает значение cookie_index (индекс следующего свободного для записи элемента), переменной next_fortune присваивается 0, то есть, индекс первого элемента. После того, как фортунка записана в буфер пользовательского процесса, я увеличиваю индекс next_fortune на ее длину. Теперь этот индекс содержит адрес 'фортунки', которая будет выдана следующей . Длина 'фортунки' также передается пользовательскому процессу в качестве возвращаемого значения.

Листинг 11. Функция чтения 'фортунки'

                

int fortune_read( char *page, char **start, off_t off,

int count, int *eof, void *data )

{

int len;



if (off > 0) {

*eof = 1;

return 0;

}



/* Перевод индекса на первый элемент */

if (next_fortune >= cookie_index) next_fortune = 0;



len = sprintf(page, "%sn", &cookie_pot[next_fortune]);



next_fortune += len;



return len;

}

Это простой пример показывает что обмен данными между ядром и пользовательским процессом является тривиальной задачей. А сейчас предлагаю посмотреть на модуль выдачи 'фортунок' в действии (листинг 12).

Листинг 12. Загружаемый модуль выдачи 'фортунок' в действии

                

[root@plato]# insmod fortune.ko

[root@plato]# echo "Success is an individual proposition. Thomas Watson" > /proc/fortune

[root@plato]# echo "If a man does his best, what else is there?
Gen. Patton" > /proc/fortune


[root@plato]# echo "Cats: All your base are belong to us. Zero Wing" > /proc/fortune

[root@plato]# cat /proc/fortune

Success is an individual proposition. Thomas Watson

[root@plato]# cat /proc/fortune

If a man does his best, what else is there? Gen. Patton

[root@plato]#

Виртуальная файловая система /proc широко используется как средство сбора информации о состоянии ядра и для его динамического конфигурирования. Она незаменима для разработки драйверов и модулей ядра, в чем несложно убедиться.

Оставьте комментарий