Хабрахабр

Использование файлов-последовательностей ядра Linux

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

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

Самый лучший – это исходники самого ядра с достаточно подробными комментариями. В сети доступно множество материалов по этой теме. Если не знать в точности, что искать, то лучше, имея лишь ограниченное время, и не пробовать. Проблема этого источника информации в его объёме. Источники не новые, но весьма солидные.
Рассмотрим сначала по-подробнее, когда естественно использовать файлы-последовательности. У меня, когда возник интерес к теме, Google нашёл несколько на первый взгляд отличных источников информации: известную книгу The Linux Kernel Module Programming Guide и серию статей Роба Дея (Rob Day) по нужной теме. Читая файлы этой системы, можно получать самую разнообразную информацию об используемой аппаратуре, её драйверах, об оперативной памяти, о процессах и т. Самая типичная ситуация – это создание своего файла в файловой системе /proc. п.

Но работа в режиме ядра ОС накладывает множество ограничений, которые могут показаться совершенно невообразимыми разработчику прикладного ПО. Казалось бы распечатка чего бы то ни было – это самое простое из всего, что есть в программировании. Для архитектуры х86 – это четыре килобайта. В режиме ядра размер буфера печати ограничен размером страницы виртуальной памяти. Можно, конечно, печатать посимвольно, что сильно бы все упростило, но у нас речь о хороших программах. Поэтому хорошая программа при распечатке данных большего объёма должна сначала добиться максимального заполнения этого буфера, потом его распечатать, а потом опять повторять эту итерацию до полного исчерпания данных для печати.

В книге, например, некоторая информация оказалась вообще неверной и именно это подвинуло меня написать эту заметку. Названные выше источники оказались несколько хуже, чем ожидалось. Использование такой схемы может привести к серьёзным ошибкам. Неверной оказалась самая лёгкая для восприятия и использования информация, приведённая в виде схемы. Так получается из-за того, что в этом примере при обращении к /proc/iter печатается за один раз лишь несколько байт. Хотя приведённый в книге пример работает правильно и следует именно этой схеме. Упомянутая выше серия статей явных ошибок не содержит, но не сообщает о некоторых важные для понимания темы деталях. Если же использовать его как шаблон для печати текстов, размером более страницы памяти, то возникнут сюрпризы.

Итак, рассмотрим сначала правильную схему того, как работают с файлом-последовательностью.

Для работы с таким файлом нужно создать функции start, stop, next и show. Названия этих функций могут быть любыми, выбрал наиболее короткие и соответствующие по смыслу. При наличие таких функций и их правильном подключении к системам ядра они начинают работать автоматически при обращении к связанному с ними файлу в каталоге /proc. Наиболее запутывающим выглядит использование функции stop, которая может вызываться в трёх разных контекстах. Её вызов после start означает окончание работы по распечатке данных. Её вызов после show означает, что при выполнении последней операции печати в буфер (обычно для этого используется функция seq_printf) случилось переполнение страничного буфера и эта операция печати была отменена. А её вызов после next – это самый интересный случай, который происходит, когда заканчивается печать в буфер одних данных и нужно либо завершать работу, либо использовать новые данные. Например, предположим, что наш файл в каталоге /proc при обращении к нему выдаёт сначала некоторую информацию по блочным устройствам, а затем по символьным. Сначала функция start инициализирует печать по блочным устройствам, а функция next и, возможно, show используют данные этой инициализации для последующей печати шаг за шагом нужной информации по блочным устройствам. Когда всё будет готово, после последнего вызова next и будет сделан рассматриваемый случай вызова stop, после которого вызывается start, который на этот раз должен уже проиницилизировать дальнейшую распечатку по символьным устройствам.

В ней пришлось заменить вызов функции, которая отсутствует в современных ядрах, на её актуальный эквивалент. Приведу немного измененный пример (содержимое файла evens.c) из статьи Роба Дея. Комментарии на английском сменил на на русском.

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>
#include <linux/fs.h>
#include <linux/seq_file.h>
#include <linux/slab.h> static int limit = 10; //значение по умолчанию, его можно менять здесь
module_param(limit, int, S_IRUGO); //или, передавая параметр, при загрузке модуля static int* even_ptr; //усложняющий пример указатель, демонстрирующий, как работать //с динамической памятью /** * start */
static void *ct_seq_start(struct seq_file *s, loff_t *pos) //Выделяем память для числа, которое будет хранить наше растущее четное значение even_ptr = kmalloc(sizeof(int), GFP_KERNEL); if (!even_ptr) //фатальная ошибка return NULL; printk(KERN_INFO "In start(), even_ptr = %pX.\n", even_ptr); *even_ptr = (*pos)*2; return even_ptr;
} /** * show */
static int ct_seq_show(struct seq_file *s, void *v) { printk(KERN_INFO "In show(), even = %d.\n", *(int*)v); seq_printf(s, "The current value of the even number is %d\n", *(int*)v); return 0;
} /** * next */
static void *ct_seq_next(struct seq_file *s, void *v, loff_t *pos) { printk(KERN_INFO "In next(), v = %pX, pos = %Ld, seq-file pos = %lu.\n", v, *pos, s->count); (*pos)++; //увеличиваем счётчик if (*pos >= limit) //заканчиваем? return NULL; *(int*)v += 2; //переходим к следующему чётному return v; } /** * stop */
static void ct_seq_stop(struct seq_file *s, void *v) { printk(KERN_INFO "Entering stop().\n"); if (v) printk(KERN_INFO "v is %pX.\n", v); else printk(KERN_INFO "v is null.\n"); printk(KERN_INFO "In stop(), even_ptr = %pX.\n", even_ptr); if (even_ptr) { printk(KERN_INFO "Freeing and clearing even_ptr.\n"); kfree(even_ptr); even_ptr = NULL; } else printk(KERN_INFO "even_ptr is already null.\n");
} /** * Эта структура собирает функции для управления последовательным чтением */
static struct seq_operations ct_seq_ops = { .start = ct_seq_start, .next = ct_seq_next, .stop = ct_seq_stop, .show = ct_seq_show
}; /** * Эта функция вызывается при открытии файла из /proc */
static int ct_open(struct inode *inode, struct file *file) { return seq_open(file, &ct_seq_ops);
}; /** * Эта структура собирает функции для управления файлом из /proc */
static struct file_operations ct_file_ops = { .owner = THIS_MODULE, .open = ct_open, .read = seq_read, .llseek = seq_lseek, .release = seq_release
}; /** * Эта функция вызывается, когда этот модуль загружается в ядро */
static int __init ct_init(void) { proc_create("evens", 0, NULL, &ct_file_ops); return 0;
} /** * Эта функция вызывается, когда этот модуль удаляют из ядра */
static void __exit ct_exit(void) { remove_proc_entry("evens", NULL);
} module_init(ct_init);
module_exit(ct_exit); MODULE_LICENSE("GPL");

Функции для работы с файлом-последовательностей используют два указателя с пересекающейся функциональностью (это также несколько запутывающий момент). Один из них должен указывать на текущий объект для распечатки в буфер функцией show – v в программе, а другой (pos) обычно используется для указания на счётчик.

Конечно, для успешной сборки необходимо наличие в системе исходников ядра Linux. Для тех, кто возможно впервые захочет запустить свою программу в режиме ядра, привожу пример Makefile для успешной сборки.

obj-m += evens.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Подключение к ядру производится командой sudo insmod evens.ko, проверка функциональности появившегося после этого файла /proc/evens командой cat /proc/evens, чтение журнала событий, поясняющих работу системы, командой sudo cat /var/log/messages.

Это значение можно как вписать в текст программы, так и использовать при загрузке модуля – sudo insmod evens.ko limit=500. Чтобы случилось переполнение страничного буфера нужно задать параметру limit значение побольше, например, 500.

Например, можно заметить, что перед вызовом stop после next или start система зануляет v. Анализ лога может пояснить остающиеся неясными моменты. Также можно заметить, что перед вызовом start после stop система распечатывает содержимое буфера.

Буду признателен, если кто-нибудь сообщит об обнаруженных в моей заметке неточностях или о том, что ещё следовало бы упомянуть.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть