Управляем модулем ядра Linux

от автора

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

Введение

Двустороннее взаимодействие между пользовательским приложением и модулем ядра:

Application: любое приложение, запущенное на уровне пользователя, которое может взаимодействовать с модулем ядра.

Kernel Module: содержит определение системных вызовов, которые могут использоваться API-интерфейсами приложений и ядра для мониторинга работоспособности.

«Администрирование Linux. Мега»

Проверка состояния модуля ядра

Разберём команды, которые полезны при написании и использовании расширений ядра.

Загрузка модуля ядра

insmod: используется для вставки модуля в ядро.

пример: insmod ‘kernel_ext_binary’

# insmod helloWorld.ko Welcome to Hello world Module.

Выгрузка модуля ядра

пример: rmmod ‘kernel_ext_binary’

# rmmod helloWorld.ko Goodbye, from Hello world.

Список всех запущенных модулей ядра

lsmod: выводит список всех загруженных модулей ядра. 

пример: lsmod | grep ‘kernel_ext_binary’

# lsmod | grep hello helloWorld 12189  1

Подробная информация о модуле ядра

modinfo: отображает дополнительную информацию о модуле. 

пример: modinfo hello*.ko

# modinfo helloWorld.ko filename:       /root/helloWorld.ko description:    Basic Hello World KE author:         helloWorld license:        GPL rhelversion:    7.3 srcversion:     5F60F86F84D8477986C3A50 depends: vermagic:       3.10.0-514.el7.ppc64le SMP mod_unload modversions

Перечисленные команды можно запускать на консоли и через бинарное приложение с помощью вызова system().

Связь с пользовательским пространством

Пользовательское пространство должно открыть файл по указанному пути с помощью open() API. Этот файл используется пользовательским приложением и модулем ядра для взаимодействия друг с другом. Все команды и данные из пользовательского приложения записываются в этот файл, из которого модуль ядра считывает и выполняет действия. Возможно и обратное.

#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>  int open(const char *pathname, int flags);

Пример:

int fd; #define DEVICE_FILE_NAME "/dev/char_dev" fd = open(DEVICE_FILE_NAME, 0);

Возвращаемое значение open() — дескриптор файла, небольшое неотрицательное целое число, которое используется в последующих системных вызовах (в данном случае ioctl).

Использование вызовов ioctl

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

#include <sys/ioctl.h>  int ioctl(int fd, int cmd, ...);

fd — это файловый дескриптор, возвращаемый из open(), а cmd — то же самое, что реализовано в ioctl() модуля ядра.

Пример:

#define IOCTL_SEND_MSG _IOR(MAJOR_NUM, 0, char *) int ret_val; char message[100]; ret_val = ioctl(file_desc, IOCTL_SEND_MSG, message); if (ret_val < 0) { printf("ioctl_send_msg failed:%d\n", ret_val); exit(−1); }

В приведенном примере IOCTL_SEND_MSG — команда, которая отправляется модулю.

_IOR означает, что приложение создаёт номер команды ioctl для передачи информации из пользовательского приложения в модуль ядра. 

Первый аргумент, MAJOR_NUM, — основной номер используемого нами устройства.

Второй аргумент — номер команды (их может быть несколько с разным значением).

Третий аргумент — тип, который мы хотим передать от процесса к ядру.

Точно так же пользовательское приложение может получить сообщение от ядра с небольшим изменением аргументов ioctl.

Обработка потоков в модуле ядра

В следующих разделах рассмотрим способы обработки многопоточности в контексте ядра. 

Создание потока

Мы можем создать несколько потоков в модуле, используя следующие вызовы:

#include <linux/kthread.h> static struct task_struct * sampleThread = NULL; sampleThread = kthread_run(threadfn, data, namefmt, …)

kthread_run() создаёт новый поток и сообщает ему о запуске.

threadfn — имя функции для запуска. 

data * — указатель на аргументы функции.

namefmt — имя потока (в выводе команды ps)

Остановка потока

Мы можем остановить запущенные потоки, используя вызов:

kthread_stop(sampleThread)

Установка связи с сокетом

Можно создать необработанный сокет с помощью функции sock_create(). Через этот сокет модуль ядра будет взаимодействовать с другими приложениями пользовательского уровня внутри или вне хоста.

struct socket *sock; struct sockaddr_ll *s1 = kmalloc(sizeof(struct sockaddr_ll),GFP_KERNEL); result = sock_create(PF_PACKET, SOCK_RAW, htons(ETH_P_IP), &sock); if(result < 0) { printk(KERN_INFO "[vmmKE] unable to create socket");     return -1; }  //copy the interface name to ifr.name  and other required information. strcpy((char *)ifr.ifr_name, InfName); s1->sll_family = AF_PACKET; s1->sll_ifindex = ifindex; s1->sll_halen = ETH_ALEN; s1->sll_protocol = htons(ETH_P_IP);  result = sock->ops->bind(sock, (struct sockaddr *)s1, sizeof(struct sockaddr_ll)); if(result < 0) { printk(KERN_INFO "[vmmKE] unable to bind socket");     return -1; }

С помощью sock_sendmsg() модуль ядра может отправлять данные, используя структуру сообщения.

struct msghdr message; int ret= sock_sendmsg(sock, (struct msghdr *)&message);

Генерация сигналов процессу пользовательского пространства

Сигналы тоже можно сгенерировать из модуля ядра в пользовательское приложение. Если идентификатор процесса (PID) известен ядру, используя этот pid, модуль может заполнить требуемую структуру pid и передать ее в send_sig_info() для запуска сигнала.

struct pid *pid_struct = find_get_pid(pid); struct task_struct *task = pid_task(pid_struct,PIDTYPE_PID); int signum = SIGKILL, sig_ret; struct siginfo info; memset(&info, '\0', sizeof(struct siginfo)); info.si_signo = signum; //send a SIGKILL to the daemon sig_ret = send_sig_info(signum, &info, task); if (sig_ret < 0) { printk(KERN_INFO "error sending signal\n"); return -1; }

Ротация логов 

Если пользователь хочет перенаправить все логи, связанные с модулем ядра, в определённый файл, необходимо добавить запись в rsyslog (/etc/rsyslog.conf) следующим образом:

:msg,startswith,"[HelloModule]" /var/log/helloModule.log 

Это позволяет rsyslog перенаправлять все логи ядра, начинающиеся с [Hello Module], в модуль /var/log/helloModule.log file. 

Пример: пользователи могут написать собственный сценарий ротации и поместить его в /etc/logrotate.d.

"/var/log/helloModule.log" { daily rotate 4 maxsize 2M create 0600 root postrotate     service rsyslog restart > /dev/null endscript }

Сценарий ежедневно проверяет, не превышает ли размер файла логов 2 МБ, и поддерживает 4 ротации этого файла. Если размер логов превышает 2 МБ, будет создан новый файл с тем же именем и правами доступа к файлу 0600, а к старому файлу будет добавлена отметка даты и времени.

После ротации он перезапустит службу rsyslog.

Создание файла

Обратитесь к содержимому makefile, чтобы сгенерировать двоичные файлы для сэмпла программы:

obj−m += helloWorld.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

Примечание: пример основан на варианте RHEL. Другие варианты реализации makefile могут отличаться.

Интеграция модуля ядра с пользовательским приложением 

Пользовательское приложение использует вызовы ioctl для отправки данных в модуль ядра. В приведённом ниже примере эти вызовы ioctl можно использовать для отправки сведений о приложении или отправки обновлений в более поздний момент времени.

Пример пользовательского приложения

Пример включает в себя все концепции, описанные ранее.

# cat helloWorld.h  #ifndef HELLOWORLD_H #define HELLOWORLD_H #include <linux/ioctl.h>  // cmd ‘KE_DATA_VAR’ to send the integer type data #define KE_DATA_VAR _IOR('q', 1, int *)  #endif  # cat helloWorld.c  #include <stdio.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <sys/ioctl.h> #include <stdlib.h> #include "helloWorld.h"  /* @brief: function to load the kernel module */ void load_KE() {     printf ("loading KE\n");     if (system ("insmod /root/helloWorld.ko") == 0)     {         printf ("KE loaded successfully");     } }  /* @brief: function to unload the kernel module */ void unload_KE() {     printf ("unloading KE\n");     if (system ("rmmod /root/helloWorld.ko") == 0)     {         printf ("KE unloaded successfully");     } }  /* @brief: method to send data to kernel module */ void send_data(int fd) {     int v;      printf("Enter value: ");     scanf("%d", &v);     getchar();     if (ioctl(fd, KE_DATA_VAR, &v) == -1)     {         perror("send data error at ioctl");     } }  int main(int argc, char *argv[]) {     const char *file_name = "/dev/char_device"; //used by ioctl     int fd;     enum     {         e_load, //load the kernel module         e_unload, //unload the kernel module         e_send, //send a HB from test binary to kernel module     } option;      if (argc == 2)     {         if (strcmp(argv[1], "-l") == 0)         {             option = e_load;         }         else if (strcmp(argv[1], "-u") == 0)         {             option = e_unload;         }                 }         else if (strcmp(argv[1], "-s") == 0)         {             option = e_send;         }         else         {             fprintf(stderr, "Usage: %s [-l | -u | -s ]\n", argv[0]);             return 1;         }     }     else     {         fprintf(stderr, "Usage: %s [-l | -u | -s ]\n", argv[0]);         return 1;     }      if ((option != e_load) && (option != e_unload))     {         fd = open(file_name, O_RDWR);         if (fd == -1)         {             perror("KE ioctl file open");             return 2;         }     }     switch (option)     {         case e_load:             load_KE();             break;         case e_unload:             unload_KE();             break;         case e_send:             send_data(fd);             break;         default:             break;     }      if ((option != e_load) && (option != e_unload))     {         close (fd);     } return 0; }  Sample kernel module # cat helloWorld.c #include <linux/slab.h> #include <linux/kthread.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/version.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/errno.h> #include <asm/uaccess.h> #include <linux/time.h> #include <linux/mutex.h> #include <linux/socket.h> #include <linux/ioctl.h> #include <linux/notifier.h> #include <linux/reboot.h> #include <linux/sched.h> #include <linux/pid.h> #include <linux/kmod.h> #include <linux/if.h> #include <linux/net.h> #include <linux/if_ether.h> #include <linux/if_packet.h> #include <linux/unistd.h> #include <linux/types.h> #include <linux/time.h> #include <linux/delay.h>  typedef struct {     char ethInfName[8];     char srcMacAdr[15];     char destMacAdr[15];     int ifindex; }KEConfig_t;  MODULE_LICENSE("GPL"); MODULE_AUTHOR("Owner Name"); MODULE_DESCRIPTION("Sample Hello world"); MODULE_VERSION("0.1");  static char *name = "world"; static struct task_struct *ke_thread; static struct KEConfig_t KECfg; module_param(name, charp, S_IRUGO); MODULE_PARM_DESC(name, "The name to display in /var/log/kern.log");   /* @brief: create socket and send required data to HM  * creates the socket and binds on to it.  * This method will also send event notification  * to HM.  * */ static int createSocketandSendData(char *data) {     int ret_l =0;     mm_segment_t oldfs;     struct msghdr message;     struct iovec ioVector;      int result;     struct ifreq ifr;     struct socket *sock;     struct sockaddr_ll *s1 = kmalloc(sizeof(struct sockaddr_ll),GFP_KERNEL);     if (!s1)     {        printk(KERN_INFO "failed to allocate memory");        return -1;     }     printk(KERN_INFO "inside configureSocket");     memset(s1, '\0', sizeof(struct sockaddr_ll));     memset(픦, '\0', sizeof(ifr));      result = sock_create(PF_PACKET, SOCK_RAW, htons(ETH_P_IP), &sock);     if(result < 0)     {         printk(KERN_INFO "unable to create socket");         return -1;     }     printk(KERN_INFO "interface: %s", KECfg.ethInfName);     printk(KERN_INFO "ifr index: %d", KECfg.ifindex);     strcpy((char *)ifr.ifr_name, KECfg.ethInfName);      s1->sll_family = AF_PACKET;     s1->sll_ifindex = KECfg.ifindex;     s1->sll_halen = ETH_ALEN;     s1->sll_protocol = htons(ETH_P_IP); result = sock->ops->bind(sock, (struct sockaddr *)s1, sizeof(struct sockaddr_ll));     if(result < 0)     {         printk(KERN_INFO "Unable to bind socket");         return -1;     }      //create the message header     memset(&message, 0, sizeof(message));     message.msg_name = sockData->sock_ll;     message.msg_namelen = sizeof(*(sock_ll));      ioVector.iov_base = data;     ioVector.iov_len  = sizeof(data);     message.msg_iov = &ioVector;     message.msg_iovlen = 1;     message.msg_control = NULL;     message.msg_controllen = 0;     oldfs = get_fs();     set_fs(KERNEL_DS);     ret_l = sock_sendmsg(sockData->sock, &message, sizeof(data));       return 0; }  static long ke_ioctl(struct file *f, unsigned int cmd, unsigned long arg) {     int b;      switch (cmd)     {         case KE_DATA_VAR:         if (get_user(b, (int *)arg))         {         return -EACCES;         }         //set the time of HB here         mutex_lock(&dataLock);         do_gettimeofday(&hbTv);         printk(KERN_INFO "time of day is %ld:%lu \n", hbTv.tv_sec, hbTv.tv_usec);         printk(KERN_INFO "data %d\n", b);         //send data out         createSocketandSendData(&b);         mutex_unlock(&dataLock);         break;         default:             return -EINVAL;     }      return 0; }  /* @brief: method to register the ioctl call */ static struct file_operations ke_fops = {     .owner = THIS_MODULE, #if (LINUX_VERSION_CODE < KERNEL_VERSION(2,6,35))     .ioctl = ke_ioctl #else     .unlocked_ioctl = ke_ioctl #endif };  /* @brief The thread function */ int ke_init() {     printk(KERN_INFO "Inside function");     return 0; }  /* @brief The LKM initialization function */ static int __init module_init(void) {    printk(KERN_INFO "module_init initialized\n");    if ((ret = alloc_chrdev_region(&dev, FIRST_MINOR, MINOR_CNT, "KE_ioctl")) < 0)    {        return ret;    }     cdev_init(&c_dev, &ke_fops);     if ((ret = cdev_add(&c_dev, dev, MINOR_CNT)) < 0)    {        return ret;    }     if (IS_ERR(cl = class_create(THIS_MODULE, "char")))    {        cdev_del(&c_dev);        unregister_chrdev_region(dev, MINOR_CNT);        return PTR_ERR(cl);    }    if (IS_ERR(dev_ret = device_create(cl, NULL, dev, NULL, "KEDevice")))    {        class_destroy(cl);        cdev_del(&c_dev);        unregister_chrdev_region(dev, MINOR_CNT);        return PTR_ERR(dev_ret);    }     //create related threads    mutex_init(&dataLock); //initialize the lock    KEThread = kthread_run(ke_init,"KE thread","KEThread");    return 0; }  void thread_cleanup(void) {     int ret = 0;      if (ke_thread)     ret = kthread_stop(ke_thread);     if (!ret)         printk(KERN_INFO "Kernel thread stopped"); }  /* @brief The LKM cleanup function */ static void __exit module_exit(void) {    device_destroy(cl, dev);    class_destroy(cl);    cdev_del(&c_dev);    unregister_chrdev_region(dev, MINOR_CNT);     thread_cleanup();    printk(KERN_INFO "Exit %s from the Hello world!\n", name); }  module_init(module_init); module_exit(module_exit);

Модуль всегда начинается либо с init_module, либо с функции, которую вы указываете с помощью вызова module_init(). Эта функция сообщает ядру, какие функциональные возможности предоставляет модуль, и настраивает ядро для запуска функций модуля, когда они необходимы.

Все модули заканчиваются вызовом либо cleanup_module(), либо функции, которую вы указываете с помощью вызова module_exit(). Это функция выхода для модулей — она отменяет всё, что сделала функция ввода.

Примечание: предположим, файл открыт в пользовательском пространстве с помощью функции open(), которая используется для связи с модулем ядра. 

Если какой-либо вызов execve() выполняется пользовательским процессом, нужно установить параметр сокета FD_CLOEXEC в fd (файловый дескриптор).

fd = open(“/dev/char_device”, O_RDWR); fcntl(fd, F_SETFD, FD_CLOEXEC);

Если параметр FD_CLOEXEC не установлен для этого fd, дескриптор файла должен оставаться открытым при вызове execve(). 

Коротко о главном

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

«Администрирование Linux. Мега»


ссылка на оригинал статьи https://habr.com/ru/company/southbridge/blog/712414/


Комментарии

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

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