06 сем Семинар 4 - chrislvt/OS GitHub Wiki
ЛР_4 часть_2
Чтобы можно было работать с proc
в ядре, должны быть объявлены соответствующие структуры.
Чтобы посмотреть информация, которая сейчас будет рассказываться, нужно ее смотреть в manual. Чтобы посмотреть, какую информацию proc предоставляет о конкретном процессе, надо набрать man /proc/pid
.
Чтобы посмотреть другую информацию, надо набрать man имя функции
или man имя структуры
Все, что будет написано далее, взято из manual, версия ядра: 4.10
Чтобы работать с виртуальной файловой системой proc в ядре, в ядре определена структура struct proc_dir_entry
.
Эта структура ядра, как многие другие структуры, так же переписывается. Поскольку содержание функций и структур ядра не регламентировано стандартом POSIX, они переписываются разработчиками, так как Linux имеет открытое ядро.
В структуре proc_dir_entry
имеется указатель на две функции read
, write
, опишем эти функции:
typedef int (read_proc_t)(char *page, char **start, off_t off, int count, int *eof, void *data);
typedef int (write_proc_t) (struct file *file, const char _user *buffer, unsigned long count, void *data);
struct proc_dir_entry
{
unsigned int low_ino;
unsigned short name_len;
const char *name;
mode_t mode;
nlink_t nlink;
uid_t uid;
gid_t gid;
loff_t size;
const struct inode_operations *proc_iops;
const struct file_operations *proc_fops;
read_proc_t *read_proc;
write_proc_t *write_proc;
atomic_t count;
}
low_ino
- номер inode
Мы работаем с виртуальной файловой системой, она создана, чтобы предоставлять информацию пользователю, используя стандартные средства работы с файлами => поэтому используется та же идентификация файлов, то есть номер inode.
name_len
- размер имени
*name
- имя виртуального файла
mode
- права доступа
nlink
- количество ссылок на файл
loff_t
- unsinged long
gid_t
, uid_t
, nlink_t
- integer
Для каждого поля используют свой тип, тем самым, уникальные имена (loff_t
, gid_t
и др.) позволяет проводить более жесткий контроль типов => увеличивается надежность ПО.
Для работы с proc_dir_entry
определены функции ядра (начиная с версии ядра 3.10), основные это:
proc_create_data
proc_create
Причем proc_create
является оберткой proc_create_data
.
До версии ядра 3.10 использовали структуру create_proc_entry
.
extern struct proc_dir_entry *proc_create_data(const char*, umode_t, struct proc_dir_entry*,
const struct file_operations*, void*);
static inline struct proc_dir_entry *proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent,
const struct file_operations *proc_fops);
{
// передается имя файла, права доступа, родительский каталог, определенные нами функции (на открытых файлах)
return proc_create_data(name, mode, parent, proc_fops, NULL);
}
В struct proc_dir_entry
кроме уже перечисленных полей, имеются указатели на struct file_operations и struct inode_operations.
Proc
- файловая система, значит в ней файлы идентифицируются номерами inode, но в этой файловой системе мы работаем с открытыми файлами, а открытые файлы описываются struct file
, и на struct file
определена структура struct file_operations
, в которой перечислены все функции, определенные в ОС для работы с файлами: открыть, прочитать, записать, flush(переписать), release и т.д.
Кроме указанных
В ЛР_4 нужно будет создать символическую ссылку и директорию + реализовать передачу из режима ядра в режим пользователя и наоборот.
extern struct proc_dir_entry *proc_symblink (const char*, struct proc_dir_entry*, const char*);
extern struct proc_dir_entry *proc_mkdir (const char*, struct proc_dir_entry*);
Чтобы работать с файлами файловой системы proc, нужно знать несколько приемов:
- Использование функции read/write, которые определены в структуре
struct proc_dir_entry
Нельзя использовать стандартные библиотечные функции в работе с данными ядра и нельзя использовать обычные функции для передачи данных из режима пользователя в режим ядра и обратно - это связано с тем, что у наших процессов адресное пространство виртуальное и описывается таблицами страниц, каждая страница адресного пространства процесса может быть загружена в физическую память, а могут быть и не загружена. Может возникнуть ситуация, когда страница была загружена в память, а в какой-то момент она была выгружена из памяти.
При этом адрес вычисляется так: в программе находится смещение; если страница загружена в физическую память, то смещение + физический адрес страницы памяти
- так получаем адрес байта памяти. Но виртуальные страницы могут загружаться в разные физические страницы, то есть у нас нет конкретного адреса, к которому можно обращаться постоянно.
Ядро не может само инициировать вычисление адреса. Преобразование выполняется, когда выполняется код приложения, и там высчитывается адрес, по которому происходит обращение к физической памяти. А нам надо из ядра обратиться в приложение и здесь может возникнуть ситуация, когда страница была загружена, а потом оказалась выгружена, то есть возникают проблемы, и чтобы они не возникали, нужно использовать специальные функции.
Специальные функции:
copy_to_user
copy_from_user
Чтение из page в user_mode Запись из user в ядро, (например, в файл, который мы создали в proc: /proc/имя_файла)
Чтобы это выполнить, надо написать содержимое этих функций.
Пример использования функций read_proc
, write_proc
:
copy_to_user
, copy_from_user
- в библиотеке <asm/uaccess.h>
#include <asm/uaccess.h>
...
static struct proc_dir_entry *proc_entry;
// string container
static cahr *line_cont;
int i, next;
ssize_t write_line (struct file *filp, const char _user *buff, unsigned long len, void *data)
{
if (copy_from_user (&line_count[i], buff, len))
return _EFAULT;
i += len;
line_cont[i - 1] = 0;
return len;
}
int read_line (char *page, char **start, off_t off, int count, int *eof, void *data)
{
...
len = sprintf(page, "%s \n", &line_cont next);
return len;
}
write
- пишем из user в ядро
read
- читаем из ядра в user
Написанные нами функции надо логировать (зарегистрировать в ядре). (Так же мы поступали с собственным обработчиком сигналом: чтобы система понимала, что она должна вызвать не стандартный обработчик, а наш обработчик, мы использовали системный вызов signal и логировали наш обработчик сигнала на соответствующем сигнале и когда возникал сигнал, то вызывался наш обработчик)
Так и здесь: мы должны зарегистрировать наши функции в ядре, для этого пишем функцию init
:
int init_mod (void)
{
line_cont = (char *) malloc (MAX_SIZE)
if (!line_cont)
return -ENOMEM;
...
proc_entry = proc_create_data (...)
if (proc_entry == NULL)
return -ENOMEM;
i = 0;
next = 0;
proc_entry -> write_proc = write_line;
printk(KERN_INFO "Success \n");
return 0;
}
Видим логирование функции read_line
, write_line
через proc_entry
(определили указатель на proc_dir_entry
как proc_entry
), к полю read_proc
, write_proc
обращаемся, используя стрелочку, тк это указатели и вместо read_proc
, write_proc
будет вызываться наша функция read_line, write_line
.
Обычно разработчики драйверов используют другой способ логирования, для регистрации своих функций используют структуру struct file_operations
.
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h> // sequence = последовательность
#define PROC_FILE_NAME "Hello"
static struct proc_dir_entry *proc_file
static char *out_str
static int proc_hello_show(struct seq_file *m, void *v)
{
int error = 0;
error = seq_printf(m, "%s \n", out_str); // выполняет действия, аналогичные `copy_to_user` или `sprintf`
return error;
}
static int proc_hello_open(struct inode *inode, struct file *file)
{
return single_open(file, proc_hello_show, NULL); // фактически логируем, внутри proc_hello_show - seq_printf
}
seq_printf
стандартная функция, выполняет действия, аналогичные copy_to_user
или sprintf
.
single_open
- стандартная функция, может быть одновременно вызвана только одним процессом (чтобы открыть определенный файл)
есть еще simple_open
- simple-функции специально написаны для разработчиков, содержат минимально необходимый набор действий (например, открыть, записать в файл);
Инициализируем свою структуру proc_hello_fops
следующим образом:
static const struct file_operations proc_hello_fops =
{
.owner = THIS_MODULE, // владелец
.open = proc_hello_open,
.release = single_release,
.read = seq_read
};
Надо зарегистрировать только функцию proc_hello_open
.
static int _init proc_hello_init(void)
{
ont_str = "Hello";
proc_file = proc_create_data(PROC_FILE_NAME, S_IRUGO, NULL,
&proc_hello_fops, NULL); // S_IRUGO-права доступа (у нас только чтение), NULL-файл будет создан в корневом каталоге proc
// proc_hello_fops-работаем с нашей структурой, передаем ядру адреса функций, NULL-нет данных
if (!proc_file)
return -ENOMEM;
return 0;
}
static void _exit proc_hello_exit(void)
{
if (proc_file)
remove_proc_entry(PROC_FILE_NAME, NULL); // будет удалена структура, описывающая созданный нами файл
}
Модуль init (proc_hello_init)
, модуль exit (proc_hello_exit)
Мы создали файл proc_file
с именем hello.
Чтобы посмотреть все это, надо использовать функцию cat из командной строки = чтение (читаем из ядра => надо использовать cat)
- обязательно надо указать лицензию
Чтобы управлять модулем, нужны:
- Подпрограмма, которая реализует нужную функциональность
- Структура данных, которая принимает адреса соответствующих функций
В простейшем модуле будет 2 управляющие функции: xxx_INIT
, xxx_EXIT
, которые являются точками входа в модуль.
В простейших модулях - 2 точки входа (init, exit). В более сложных модулях (драйверах) точек входа больше, точки входа описываются соответствующими структурами (например, struct usb_driver).
Точка входа - место, с которого начинает выполняться код.
INIT
- когда выполняется командаinsmod
.EXIT
- когда выполняется командаrmmod
.
В этом примере есть только read, а нам нужно read, write. Мы должны проработать и передачу в USER, и передачу в KERNEL. В write будет использоваться copy_from_user.