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

  1. Использование функции read/write, которые определены в структуре struct proc_dir_entry

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

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

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

Специальные функции:

  1. copy_to_user
  2. 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)

  • обязательно надо указать лицензию

Чтобы управлять модулем, нужны:

  1. Подпрограмма, которая реализует нужную функциональность
  2. Структура данных, которая принимает адреса соответствующих функций

В простейшем модуле будет 2 управляющие функции: xxx_INIT, xxx_EXIT, которые являются точками входа в модуль.

В простейших модулях - 2 точки входа (init, exit). В более сложных модулях (драйверах) точек входа больше, точки входа описываются соответствующими структурами (например, struct usb_driver).

Точка входа - место, с которого начинает выполняться код.

  • INIT - когда выполняется команда insmod.
  • EXIT - когда выполняется команда rmmod.

В этом примере есть только read, а нам нужно read, write. Мы должны проработать и передачу в USER, и передачу в KERNEL. В write будет использоваться copy_from_user.