Некоторые важные структуры данных

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

Как вы можете себе представить, регистрация номера устройства является лишь первой из многих задач, которые должен выполнять код драйвера. В скором времени мы рассмотрим другие важные компоненты драйвера, но одно отступление необходимо в первую очередь. Большинство основных операций драйвера включает три важных структуры данных ядра, называемые file_operations, file и inode. Требуется базовое знакомство с этими структурами, чтобы быть способным делать много всего интересного, так что сейчас мы бросим быстрый взгляд на каждую из них, прежде чем углубляться в подробности того, как осуществляются основные операции драйвера.

Файловые операции

До этого мы зарезервировали некоторые номера устройств для нашего использования, но мы ещё не подключили никаких своих драйверных операций на эти номера. Структура file_operations это то, как символьный драйвер устанавливает эту связь. Структура определена в <linux/fs.h>, это коллекция указателей на функции. Каждое открытие файла (представленное внутри структуры file, которую мы рассмотрим в ближайшее время) связано с собственным набором функций (включая поле, названное f_op, которое указывает на структуру file_operations). Операции в основном отвечают за осуществление системных вызовов и, таким образом, названы open (открыть), read (читать) и так далее. Мы можем рассматривать файл как "объект" и функции, работающие с ним, будут его "методами", используя терминологию объектно-ориентированного программирования для обозначения действий, декларируемых исполняющим их объектом. Это первый признак объектно-ориентированного программирования, видимого нами в ядре Linux, и мы увидим больше в последующих главах.

 

Обычно, структура file_operations или указатель на неё называется fops (или как-то похоже). Каждое поле структуры должно указывать на функцию в драйвере, которая реализует соответствующие операции, или оставить NULL для неподдерживаемых операций. Точное поведение ядра, когда задан указатель NULL, отличается для каждой функции, список показан позже в этом разделе.

 

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

 

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

 

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

 

struct module *owner

Первое поле file_operations вовсе не операция, это указатель на модуль, который "владеет" структурой. Это поле используется для предотвращения выгрузки модуля во время использования его операций. Почти всегда оно просто инициализируется THIS_MODULE, макрос определён в <linux/module.h>.

 

loff_t (*llseek) (struct file *, loff_t, int);

Метод llseek используется для изменения текущей позиции чтения/записи в файле, а новая позиция передаётся как (положительное) возвращаемое значение. Параметром loff_t является "длинное смещение" и он, по крайней мере, 64 бита даже на 32-х разрядных платформах. Об ошибках сигнализируют отрицательные возвращаемые значения. Если указатель на эту функцию NULL, вызовы поиска позиции будут изменять счётчик позиции в структуре file (описанной в разделе "Структура file") потенциально непредсказуемым образом.

 

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

Используется для получения данных от устройства. Указатель NULL в этой позиции заставляет системный вызов read вернуться с ошибкой -EINVAL (“Invalid argument”, "Недопустимый аргумент"). Неотрицательное возвращаемое значение представляет собой число успешно считанных байт (возвращаемое значение является типом "размер со знаком", обычно родным (нативным) целочисленным типом для целевой платформы).

 

ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);

Начинает асинхронное чтение - операцию чтения, которая может быть не завершена перед возвратом из функции. Если этот метод NULL, все операции будут обрабатываться (синхронно) вместо неё функцией read.

 

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

Отправляет данные на устройство. Если NULL, -EINVAL возвращается в программу, сделавшую системный вызов write. Возвращаемое значение, если оно неотрицательное, представляет собой число успешно записанных байт.

 

ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *);

Начинает асинхронную операции записи на устройство.

 

int (*readdir) (struct file *, void *, filldir_t);

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

 

unsigned int (*poll) (struct file *, struct poll_table_struct *);

Метод poll (опрос) является внутренним для трёх системных вызовов: poll, epoll и select, все они используются для запросов чтения или записи, чтобы заблокировать один или несколько файловых дескрипторов. Метод poll должен вернуть битовую маску, показывающую, возможны ли неблокирующие чтение или запись, и, возможно, предоставить ядру информацию, которая может быть использована вызывающим процессом для ожидания, когда ввод/вывод станет возможным. Если драйвер оставляет метод poll NULL, предполагается, что устройство читаемо и записываемо без блокировки.

 

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

Системный вызов ioctl (управление вводом-выводом) открывает путь к выполнению зависящих от устройства команд (например, форматирование дорожки гибкого диска, которая и не чтение, и не запись). Кроме того, несколько команд ioctl распознаются ядром без ссылки на таблицу fops. Если устройство не обеспечивает метод ioctl, системный вызов возвращает ошибку на любой запрос, который не является предопределенным (-ENOTTY, “No such ioctl for device”, "Нет такого управления вводом-выводом для устройства").

 

int (*mmap) (struct file *, struct vm_area_struct *);

mmap (отобразить в память) используется, чтобы запросить отображение памяти устройства на адресное пространство процесса. Если этот метод NULL, системный вызов mmap возвращает -ENODEV.

 

int (*open) (struct inode *, struct file *);

Хотя это всегда первая операция, выполняющаяся с файлом устройства, драйверу не требуется декларировать соответствующий метод. Если этот параметр NULL, открытие устройства всегда успешно, но ваш драйвер не уведомляется.

 

int (*flush) (struct file *);

Операция flush (сбросить на диск) вызывается, когда процесс закрывает свою копию файла дескриптора устройства; она должна выполнить (и ожидает это) любую ожидающую выполнения операцию на устройстве. Не следует путать это с операцией fsync, запрашиваемой программами пользователя. В настоящее время flush используется в очень редких драйверах; например, драйвер ленточного накопителя SCSI использует её, чтобы гарантировать, что все данные записаны на ленту, прежде чем устройство закроется. Если flush NULL, ядро просто игнорирует запрос пользовательского приложения.

 

int (*release) (struct inode *, struct file *);

Эта операция (отключение) вызывается, когда файловая структура освобождается. Как и open, release может быть NULL. (* Обратите внимание, что release не вызывается каждый раз, когда процесс вызывает close. Когда файловая структура используется несколькими процессами (например, после fork или dup), release не будет вызван, пока не будут закрыты все копии. Если вам необходимо при завершении копирования сбросить на диск ожидающие данные, вы должны реализовать метод flush.)

 

int (*fsync) (struct file *, struct dentry *, int);

Этот метод является внутренним системным вызовом fsync, который пользователь вызывает для сброса на диск любых ожидающих данных. Если этот указатель NULL, системный вызов возвращает -EINVAL.

 

int (*aio_fsync)(struct kiocb *, int);

Это асинхронная версия метода fsync.

 

int (*fasync) (int, struct file *, int);

Эта операция используется, чтобы уведомить устройство, изменив его флаг FASYNC. Асинхронные уведомления - сложная тема и она описана в Главе 6. Поле может быть NULL, если драйвер не поддерживает асинхронные уведомления.

 

int (*lock) (struct file *, int, struct file_lock *);

Метод lock (блокировка) используется для реализации блокировки файла; блокировка является неотъемлемой функцией для обычных файлов, но почти никогда не реализуется драйверами устройств.

 

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

Эти методы осуществляют разбросанные (scatter/gather, разборка/сборка) операции чтения и записи. Приложениям иногда необходимо сделать одну операцию чтения или записи с участием нескольких областей памяти; эти системные вызовы позволяют им сделать это без необходимости дополнительных операций копирования данных. Если эти указатели на функции остаются NULL, вместо них вызываются методы read и write (возможно, несколько раз).

 

ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);

Этот метод реализует читающую сторону системного вызова sendfile (послать файл), который перемещает данные из одного файлового дескриптора на другой с минимумом копирования. Используется, например, веб-сервером, которому необходимо отправить содержимое файла из сети наружу. Драйверы устройств обычно оставляют sendfile NULL.

 

ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

sendpage (послать страницу) - это другая половина sendfile, она вызывается ядром для передачи данных, одну страницу за один раз, в соответствующий файл. Драйверы устройств, как правило, не реализуют sendpage.

 

unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

Цель этого метода - найти подходящее место в адресном пространстве процесса, чтобы отобразить в сегменте памяти нижележащее устройство. Эта задача обычно выполняется кодом управления памятью; этот метод существует, чтобы разрешить драйверу реализовать любое согласование требований, которое может иметь специфичное устройство. Большинство драйверов может оставить этот метод NULL.

 

int (*check_flags)(int)

Этот метод позволяет модулю проверить флаги, передаваемые вызову fcntl(F_SETFL...).

 

int (*dir_notify)(struct file *, unsigned long);

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

 

Драйвер устройства scull реализует только самые важные методы устройства. Его структура file_operations инициализируется следующим образом:

 

struct file_operations scull_fops = {

    .owner   = THIS_MODULE,

    .llseek  = scull_llseek,

    .read    = scull_read,

    .write   = scull_write,

    .ioctl   = scull_ioctl,

    .open    = scull_open,

    .release = scull_release,

};

 

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

 

Маркированная инициализация разрешает переназначение членов структуры; в некоторых случаях были реализованы существенные улучшения производительности путём размещения указателей к наиболее часто используемым членам в одной строке кэша оборудования.

Структура file

Структура file, определённая в <linux/fs.h>, является второй наиболее важной структурой данных, используемой драйверами устройств. Обратите внимание, что file не имеет ничего общего с указателями FILE программ пространства пользователя. FILE определён в библиотеке Си и никогда не появляется в коде ядра. Структура file, с другой стороны, это структура ядра, которая никогда не появляется в пользовательских программах.

 

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

 

В исходных текстах ядра указатель на структуру file обычно назван или file или filp ("указатель на файл"). Мы будем использовать название указателя filp для предотвращения путаницы с самой структурой. Таким образом, file относится к структуре, а filp - указатель на структуру.

 

Здесь показаны наиболее важные поля структуры file. Как и в предыдущем разделе, список может быть пропущен при первом чтении. Тем не менее, позже в этой главе, когда мы столкнёмся с некоторым реальным кодом Си, мы обсудим эти поля более подробно.

 

mode_t f_mode;

Определяет режим файла как или читаемый или записываемый (или и то и другое) с помощью битов FMODE_READ и FMODE_WRITE. Вы можете захотеть проверить это поле для разрешения чтения/записи в своей функции open или ioctl, но вам не нужно проверять разрешения для read и write, потому что ядро проверяет это перед вызовом вашего метода. Попытка чтения или записи, если файл не был открыт для этого типа доступа, отклоняется, так что драйвер даже не узнаёт об этом.

 

loff_t f_pos;

Текущая позиция чтения или записи. loff_t является 64-х разрядным значением на всех платформах (long long в терминологии gcc). Драйвер может читать это значение, чтобы узнать текущую позицию в файле, но обычно не должен изменять его; read и write должны обновить позицию с помощью указателя, который они получают в качестве последнего аргумента, вместо воздействия на filp->f_pos напрямую. Единственным исключением из этого правила является метод llseek, целью которого является изменение позиции файла.

 

unsigned int f_flags;

Существуют флаги файлов, такие как O_RDONLY, O_NONBLOCK и O_SYNC. Драйвер должен проверить флаг O_NONBLOCK, чтобы увидеть, была ли запрошена неблокирующая операция (мы обсудим неблокирующий ввод/вывод в разделе "Блокирующие и неблокирующие операции" в Главе 6); другие флаги используются редко. В частности, разрешение на чтение/запись должно быть проверено с помощью f_mode, а не f_flags. Все флаги определены в заголовке <linux/fcntl.h>.

 

struct file_operations *f_op;

Операции, связанные с файлом. Ядро присваивает указатель как часть своей реализации open, а затем считывает его, когда необходимо выполнить любые операции. Значение в filp->f_op никогда не сохраняется ядром для дальнейшего использования; это означает, что вы можете изменять файловые операции, связанные с вашим файлом, и новые методы будут действовать после вашего возвращения к вызвавшей программе. Например, код для open, связанный со старшим номером 1 (/dev/null, /dev/zero, и так далее), заменяет операции в filp->f_op в зависимости от открытого младшего номера. Эта практика позволяет реализовать несколько поведений под тем же основным номером без дополнительных накладных расходов при каждом системном вызове. Возможность заменить файловые операции является в ядре эквивалентом "переопределения метода” в объектно-ориентированном программировании.

 

void *private_data;

Системный вызов open устанавливает этот указатель в NULL для драйвера перед вызовом метода open. Вы можете сделать своё собственное использование поля или игнорировать его; вы можете использовать поле как указатель на выделенные данные, но тогда вы должны помнить об освобождении памяти в методе release до уничтожения структуры file ядром. private_data является полезным ресурсом для сохранения информации о состоянии через системные вызовы и используется в большинстве наших примеров модулей.

 

struct dentry *f_dentry;

Структура элемента каталога (directory entry, dentry), связанного с файлом. Авторам драйверов устройств обычно не  требуется заботиться о структуре dentry, помимо доступа к структуре inode через filp->f_dentry->d_inode.

 

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

Структура inode

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

 

Структура inode содержит большое количество информации о файле. По общему правилу только два поля этой структуры представляют интерес для написания кода драйвера:

 

dev_t i_rdev;

Это поле содержит фактический номер устройства для индексных дескрипторов, которые представляют файлы устройств.

 

struct cdev *i_cdev;

Структура cdev является внутренней структурой ядра, которая представляет символьные устройства; это поле содержит указатель на ту структуру, где inode ссылается на файл символьного устройства.

 

Тип i_rdev изменился в течение серии разработки ядра версии 2.5, поломав множество драйверов. В качестве средства поощрения более переносимого программирования разработчики ядра добавили два макроса, которые могут быть использованы для получения старшего и младшего номера inode:

 

unsigned int iminor(struct inode *inode);

unsigned int imajor(struct inode *inode);

 

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

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