Отладка через запросы

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

В предыдущем разделе мы описали, как работает printk, и как можно её использовать. Но пока ничего не говорили о её недостатках.

 

Массовое использование printk может заметно замедлить работу системы, даже если вы понизили console_loglevel во избежание загрузки консольного устройства, потому что syslogd поддерживает синхронизацию своих выходных файлов; таким образом, каждая печатаемая строка является причиной дисковой операции. Это правильно с точки зрения syslogd. Он пытается записать всё на диск на случай падения системы сразу после печати сообщения; однако, вы не хотите замедлить работу системы только ради отладочных сообщений. Эта проблема может быть решена с помощью префикса с дефисом в имени вашего файла протокола, как это показано в /etc/syslogd.conf. (* Дефис, или знак минус, это "магический" маркер syslogd для предотвращения сброса на диск каждого нового сообщения, документированный в syslog.conf (5), описание стоит прочитать.) Проблемой с изменением конфигурационного файла является то, что изменение, вероятно, останется там и после окончания отладки, хотя при нормальной работе системы вы хотите, чтобы сообщения сбрасывались на диск как можно скорее. Альтернативой такому постоянному изменению является работа другой программы вместо klogd (такой, как cat /proc/kmsg, как предлагалось ранее), но это не может обеспечить благоприятные условия для нормального функционирования системы.

 

Чаще всего лучшим способом для получения соответствующей информации является запрос к системе когда вам нужна информация, а не постоянный вывод данных. По сути, каждая система Unix предлагает множество инструментов для получения системной информации: ps, netstat, vmstat и так далее.

 

Для разработчиков драйверов для запросов к системе доступны несколько методов: создание файла в файловой системе /proc, используя метод драйвера ioctl, и экспорт атрибутов через sysfs. Использование sysfs требует некоторой информации о драйверной модели. Это обсуждается в Главе 14.

Использование файловой системы /proc

Файловая система /proc является специальной программно созданной файловой системой, которая используется ядром для экспорта информации в мир. Каждый файл в /proc связан с функцией ядра, которая порождает "содержимое" файла на лету, когда файл читается. Мы уже видели некоторые из этих файлов в действии; /proc/modules, например, всегда возвращает список загруженных модулей.

 

/proc широко используется в системе Linux. Многие утилиты в современных дистрибутивах Linux, такие как ps, top и uptime получают свою информацию из /proc. Некоторые драйверы также экспортируют информацию через /proc и ваш может делать то же самое. Файловая система /proc динамическая, так что ваш модуль может добавлять или удалять записи в любое время. Полнофункциональные записи в /proc могут быть сложными созданиями; среди прочего, они могут быть записываемыми, также как и читаемыми. Однако, в большинстве случаев записи в /proc являются только читаемыми файлами. Этот раздел имеет дело с простым случаем "только для чтения". Те, кто заинтересован в осуществлении чего-то более сложного, могут посмотреть здесь основу; затем для полной картины можно посмотреть исходный код ядра.

 

Однако, прежде чем мы продолжим, следует отметить, что добавление файлов в каталоге /proc не приветствуется. Файловая система /proc рассматривается разработчиками ядра как немного неконтролируемый беспорядок, который вышел далеко за пределы своей первоначальной цели (которая была в предоставлении информации о процессах, запущенных в системе). Рекомендуемый способ предоставления информации в новом коде - через sysfs. Однако, работа с sysfs требует понимания драйверной модели в Linux и мы не сможем сделать это, пока не дойдём до Главы 14. Между тем, файлы в /proc создать намного легче и они полностью подходят для отладки, поэтому мы и рассмотрим их здесь.

Работа с файлами в /proc

Все модули, которые работают с /proc, должны для определения соответствующих функций подключать <linux/proc_fs.h>.

 

Чтобы создать файл только для чтения в /proc, ваш драйвер должен содержать функцию для предоставления данных, когда файл читается. Когда какой-то процесс читает файл (используя системный вызов call), запрос достигает модуля с помощью этой функции. Мы рассмотрим эту функцию первой и далее в этом разделе доберёмся до регистрации интерфейса. Когда процесс читает из вашего файла в /proc, ядро выделяет страницу памяти (то есть PAGE_SIZE байт), куда драйвер может записать данные для возврата в пространство пользователя. Этот буфер передаётся в вашу функцию, которая представляет собой метод, названный read_proc:

 

int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);

 

Указатель page является буфером, куда вы будете записывать ваши данные; start используется функцией, чтобы сказать, где на странице были записаны интересующие данные (подробнее об этом позже); offset и count имеют такое же значение, как и для метода read. Аргумент eof указывает на целое число, которое должно быть установлено драйвером, чтобы сигнализировать, что у него нет больше данных для возвращения, в то время как data является указателем на специфические данные драйвера, которые можно использовать для внутренней бухгалтерии.

 

Эта функция должна возвращать количество байт данных, фактически размещённых в буфере page, так же как делает метод read для других файлов. Другие выходные значения - это *eof и *start. eof - это простой флаг, а вот использование значения start является несколько более сложным; его целью является помощь в реализации больших (более одной страницы) /proc файлов.

 

Параметр start имеет несколько нетрадиционное использование. Его целью является указать, где (в пределах страницы) находятся данные, которые будут возвращены пользователю. Когда вызывается метод proc_read, *start будет NULL. Если вы оставите его NULL, ядро предполагает, что данные были помещены на страницу, как если бы offset был 0; другими словами, оно предполагает простую версию proc_read, которая размещает всё содержимое виртуального файла на страницу, не обращая внимания на параметр смещения. Вместо этого, если вы установите *start в значение не-NULL, ядро предполагает, что данные, на которые указывает *start, принимают во внимание offset и готовы быть возвращены непосредственно к пользователю. В общем, простые методы proc_read, которые возвращают крошечные объёмы данных, просто игнорируют start. Более сложные методы устанавливают *start к page и размещают данные только начиная с запрошенного здесь смещения.

 

Давно существует другой важный вопрос с файлами /proc, которые также призван решить start. Иногда между последовательными вызовами read изменяется ASCII представление структур данных ядра, так что читающий процесс может обнаружить противоречивые данные от одного вызова к другому. Если *start установлен как небольшое целое значение, вызывающий использует его для увеличения filp->f_pos независимо от объёма возвращаемых данных, тем самым делая f_pos внутренним номером записи вашей процедуры read_proc. Если, например, ваша функция read_proc возвращает информацию из большого массива структур и пять из этих структур были возвращены в первом вызове, *start мог бы быть установлен в 5. Следующий вызов предоставляет то же значение, как offset; драйвер теперь знает, что надо начинать возвращать данные с шестой структуры в массиве. Это признается как "взлом" его авторами и это можно увидеть в fs/proc/generic.c.

 

Отметим, что существует лучший путь для реализации больших /proc файлов; он называется seq_file и мы обсудим его в ближайшее время. Сейчас же настало время для примера. Вот простая (хотя и несколько некрасивая) реализация read_proc для устройства scull:

 

int scull_read_procmem(char *buf, char **start, off_t offset, int count, int *eof, void *data)

{

    int i, j, len = 0;

    int limit = count - 80; /* Не печатать больше, чем это */

 

    for (i = 0; i < scull_nr_devs && len <= limit; i++) {

        struct scull_dev *d = &scull_devices[i];

        struct scull_qset *qs = d->data;

        if (down_interruptible(&d->sem))

            return -ERESTARTSYS;

        len += sprintf(buf+len,"\nDevice %i: qset %i, q %i, sz %li\n",

                        i, d->qset, d->quantum, d->size);

        for (; qs && len <= limit; qs = qs->next) { /* сканируем список */

            len += sprintf(buf + len, " item at %p, qset at %p\n",

                                        qs, qs->data);

            if (qs->data && !qs->next) /* выводим только последний элемент */

                for (j = 0; j < d->qset; j++) {

                    if (qs->data[j])

                        len += sprintf(buf + len,

                                        " % 4i: %8p\n",

                                        j, qs->data[j]);

                }

        }

        up(&scull_devices[i].sem);

    }

    *eof = 1;

    return len;

}

 

Это довольно типичная реализация read_proc. Она предполагает, что никогда не будет необходимо создать более одной страницы данных и, таким образом, игнорирует значения start и offset. Однако, на всякий случай, делает это осторожно, чтобы не переполнить буфер.

Устаревший интерфейс

Если вы почитаете исходный код ядра, то можете столкнуться с кодом реализующим /proc файлы со старым интерфейсом:

 

int (*get_info)(char *page, char **start, off_t offset, int count);

 

Все эти аргументы имеют тот же смысл, что и для read_proc, но аргументы eof и data отсутствуют. Этот интерфейс всё ещё поддерживается, но он может уйти в будущем; новый код должен использовать взамен ему интерфейс read_proc.

Создание вашего файла в /proc

После того, как функция read_proc определена, необходимо подключить её к записи в иерархии /proc. Это делается с помощью вызова create_proc_read_entry:

 

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);

 

Здесь, name это имя файла для создания, mode является маской защиты файла (может быть передана как 0 для общесистемного значения по умолчанию), base указывает каталог, в котором должен быть создан файл (если base равен NULL, файл создаётся в каталоге корне /proc), read_proc является функцией read_proc, которая реализует файлов и data игнорируется ядром (но передаётся в read_proc). Вызов, использующийся scull, чтобы создать функцию /proc, доступную как /proc/scullmem:

 

create_proc_read_entry("scullmem", 0 /* режим по умолчанию */,

                       NULL /* родительская директория */, scull_read_procmem,

                       NULL /* клиентские данные */);

 

Здесь мы создаём файл с именем scullmem прямо в /proc, с защитой со значением по умолчанию, для чтения всеми.

 

Указатель директории может быть использован для создания всей иерархии каталогов в /proc. Однако, следует отметить, что запись может быть более легко помещена в подкаталог /proc просто указанием имени каталога в качестве части названия записи, если сам каталог уже существует. Например, (часто игнорирующееся) соглашение говорит, что записи в /proc, связанные с драйверами устройств, должны вести в подкаталог driver/; scull мог бы поместить свою запись там просто задав своё имя как driver/scullmem.

 

Записи в /proc, конечно же, должны быть удалены при выгрузке модуля. remove_proc_entry является функцией, которая отменяет уже сделанную create_proc_read_entry:

 

remove_proc_entry("scullmem", NULL /* родительская директория */);

 

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

 

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

 

Наиболее важная проблема связана с удалением записей в /proc. Такое удаление может случиться во время использования файла, так как нет владельца, связанного с записями /proc, поэтому их использование не воздействует на счётчик ссылок на модуль. Эта проблема, к примеру, может быть легко получена запуском sleep 100 < /proc/myfile перед удалением модуля.

 

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

Интерфейс seq_file

Как уже отмечалось выше, реализация больших файлов в /proc немного затруднительна. С течением времени методы /proc стали пользоваться дурной славой из-за ошибочных реализаций, когда объём вывода сильно увеличивается. Для очистки кода /proc и облегчения жизни программистам ядра был добавлен интерфейс seq_file. Этот интерфейс предоставляет простой набор функций для реализации больших виртуальных файлов ядра.

 

Интерфейс seq_file предполагает, что вы создаёте виртуальный файл, который шагает через последовательность элементов, которые должны быть возвращены в пространство пользователя. Для использования seq_file вы должны создать простой объект "итератор", который может содержать позицию в последовательности, шагнуть вперёд и вывести один элемент последовательности. Это может показаться сложным, но по сути этот процесс довольно прост. Чтобы показать, как это делается, мы пройдём через создание файла /proc в драйвере scull.

 

Первый неизбежным шагом является включение <linux/seq_file.h>. Затем вы должны создать четыре итерационных метода, названных start, next, stop и show.

 

Метод start всегда вызывается первым. Прототип этой функции:

 

void *start(struct seq_file *sfile, loff_t *pos);

 

Аргументом sfile почти всегда можно пренебречь. pos является целой позицией, указывающей, где должно начаться чтение. Интерпретация позиции полностью зависит от реализации; она не должна быть позицией байта в результирующем файле. В реализациях seq_file обычно шагающих через последовательность интересующих объектов, позиция часто интерпретируется как курсор, указывающий на следующий элемент последовательности. Драйвер scull интерпретирует каждое устройство как один элемент последовательности, так что входящее значение pos - просто индекс в массиве scull_devices. Таким образом, метод start, используемый в scull:

 

static void *scull_seq_start(struct seq_file *s, loff_t *pos)

{

    if (*pos >= scull_nr_devs)

        return NULL; /* Больше читать нечего */

    return scull_devices + *pos;

}

 

Возвращаемое значения, если не NULL, является внутренней величиной, которая может быть использована реализацией итератора.

 

Следующая функция должна сдвигать итератор на следующую позицию, возвращая NULL, если в последовательности ничего не осталось. Прототип этого метода:

 

void *next(struct seq_file *sfile, void *v, loff_t *pos);

 

Здесь, v является итератором, возвращённым предыдущим вызовом start или next, а pos - текущая позиция в файле. next должен увеличить значение, указываемое pos; в зависимости от того, как работает ваш итератор, можно (хотя, вероятно, и нет) захотеть увеличить pos больше, чем на одно. Вот что делает scull:

 

static void *scull_seq_next(struct seq_file *s, void *v, loff_t *pos)

{

    (*pos)++;

    if (*pos >= scull_nr_devs)

        return NULL;

    return scull_devices + *pos;

}

 

Когда итерации закончены, ядро вызывает для очистки stop:

 

void stop(struct seq_file *sfile, void *v);

 

Реализация scull не выполняет работу по очистке, так что его метод stop пуст.

 

Стоит отметить, что код seq_file по своему дизайну не спит или не выполняет другие неатомарные задачи между вызовами start и stop. Вам гарантировано также получить один вызов stop вскоре после вызова start. Таким образом, она является безопасной для вашего метода start при использовании семафоров или спин-блокировок. Пока ваши другие методы seq_file атомарны, вся последовательность вызовов является атомарной. (Если этот параграф не имеет смысла для вас, вернитесь к нему после прочтения следующей главы).

 

Для фактического вывода чего-то интересного для пространства пользователя между этими вызовами ядро вызывает метод show. Прототип этого метода:

 

int show(struct seq_file *sfile, void *v);

 

Этот метод должен создать вывод для элемента в последовательности, указанной итератором v. Однако, он не должен использовать printk, вместо этого для вывода существует специальный набор функций seq_file:

 

int seq_printf(struct seq_file *sfile, const char *fmt, ...);

Это эквивалент printf для реализаций seq_file; он принимает обычную строку формата и дополнительные аргументы значений. Однако, вы также должны передать ей структуру seq_file, которая передаётся в функцию show. Если seq_printf возвращает ненулевое значение, это означает, что буфер заполнен и вывод будет отброшен. Большинство реализаций, однако, игнорирует возвращаемое значение.

 

int seq_putc(struct seq_file *sfile, char c);

int seq_puts(struct seq_file *sfile, const char *s);

Эти эквиваленты функциям putc и puts пользовательского пространства.

 

int seq_escape(struct seq_file *m, const char *s, const char *esc);

Эта функция эквивалентна seq_puts с тем исключением, что любой символ в s, который также находится в esc, напечатается в восьмеричной форме. Общим значением для esc является " \t\n\\", которое предохраняет встроенное незаполненное пространство от беспорядка при выводе и, возможно, путаницы скриптов оболочки.

 

int seq_path(struct seq_file *sfile, struct vfsmount *m, struct dentry *dentry, char *esc);

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

 

Вернёмся назад к нашему примеру; метод show, используемый в scull:

 

static int scull_seq_show(struct seq_file *s, void *v)

{

    struct scull_dev *dev = (struct scull_dev *) v;

    struct scull_qset *d;

    int i;

 

    if (down_interruptible(&dev->sem))

        return -ERESTARTSYS;

    seq_printf(s, "\nDevice %i: qset %i, q %i, sz %li\n",

                (int) (dev - scull_devices), dev->qset,

                dev->quantum, dev->size);

    for (d = dev->data; d; d = d->next) { /* scan the list */

        seq_printf(s, " item at %p, qset at %p\n", d, d->data);

        if (d->data && !d->next) /* вывести только последний элемент */

            for (i = 0; i < dev->qset; i++) {

                if (d->data[i])

                    seq_printf(s, " % 4i: %8p\n",

                                i, d->data[i]);

            }

    }

    up(&dev->sem);

    return 0;

}

 

Здесь мы, наконец, интерпретировали наше значение "итератор", которое является просто указателем на структуру scull_dev.

 

Теперь, когда имеется полный набор итерационных операций, scull должен упаковать их и подключить их к файлу в /proc. Первым шагом является заполнение структуры seq_operations:

 

static struct seq_operations scull_seq_ops = {

    .start = scull_seq_start,

    .next  = scull_seq_next,

    .stop  = scull_seq_stop,

    .show  = scull_seq_show

};

 

Вместе с этой структурой мы должны создать реализацию файла, которую поймёт ядро. Мы не используем метод read_proc, описанный ранее; когда используется seq_file, лучше подключиться к /proc на немного более низком уровне. Это означает создание структуры file_operations (да, той же структуры, используемой для символьных драйверов), реализующей выполнение всех необходимых операций ядром для обработки чтения и позиционирования в этом файле. К счастью, эта задача проста. Первым шагом является создание метода open, который подключает файл к операциям seq_file:

 

static int scull_proc_open(struct inode *inode, struct file *file)

{

    return seq_open(file, &scull_seq_ops);

}

 

Вызов к seq_open подключает структуру file с нашей последовательностью операций, определённых выше. Как оказалось, единственной файловой операцией, которую мы должны реализовать сами, является open, поэтому сейчас мы можем создать нашу структуру file_operations:

 

static struct file_operations scull_proc_ops = {

    .owner   = THIS_MODULE,

    .open    = scull_proc_open,

    .read    = seq_read,

    .llseek  = seq_lseek,

    .release = seq_release

};

 

Здесь мы определяем наш собственный метод open, но используем заранее подготовленные методы seq_read, seq_lseek и seq_release для всего остального. Последний шаг заключается в фактическом создании файла в /proc:

 

entry = create_proc_entry("scullseq", 0, NULL);

if (entry)

    entry->proc_fops = &scull_proc_ops;

 

Вместо того, чтобы использовать create_proc_read_entry, мы вызываем низкоуровневую create_proc_entry, которая имеет следующий прототип:

 

struct proc_dir_entry *create_proc_entry(const char *name,

                        mode_t mode,

                        struct proc_dir_entry *parent);

 

Аргументы такие же, как их аналоги в create_proc_read_entry: имя файла, режим защиты, и родительская директория.

 

Используя приведённый выше код, scull имеет новую запись в /proc, которая выглядит так же, как предыдущая. Она, однако, лучше, поскольку работает независимо от того, насколько большим становится вывод, она обрабатывает запросы должным образом, и, как правило, легче для чтения и поддержки. Мы рекомендуем использовать seq_file для реализации файлов, которые содержат больше, чем очень небольшое число строк вывода.

Метод ioctl

ioctl, использование которого мы покажем вам в Главе 6, это системный вызов, который действует на дескриптор файла; он получает номер, идентифицирующий команду, которая будет выполняться и (опционально) ещё один аргумент, как правило, указатель. В качестве альтернативы использованию файловой системы /proc вы можете реализовать несколько ioctl команд специально для отладки. Эти команды могут копировать соответствующие структуры данных из драйвера в пространство пользователя, где вы сможете их проверить.

 

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

 

Бывают моменты, когда ioctl является лучшим способом получения информации, потому что работает быстрее, чем чтение /proc. Если перед записью на экран с данными должна быть выполнена какая-то работа, получение данных в двоичной форме более эффективно, чем чтение текстового файла. Кроме того, ioctl не требует разделения данных на фрагменты меньшие, чем страница. Еще одним интересным преимуществом подхода ioctl является то, что информационно-поисковые команды могут быть оставлены в драйвере даже когда вся другая отладка будет отключена. В отличие от /proc файла, который будет виден всем, кто посмотрит в директории (и слишком многие люди, вероятно, удивятся "что это за странный файл"), недокументированные команды ioctl, вероятно, останутся незамеченными. Кроме того, они всё ещё будут там, если с драйвером случится что-то странное. Единственный недостаток заключается в том, что модуль будет немного больше.

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