Почему пользовательское приложение работает некорректно? Существует не так много способов, которые бы помогали выявить проблему. В большинстве случаев для поддержания высокой доступности требуется стороннее программное обеспечение. В статье рассказываем, как настроить мониторинг пользовательского приложения через модуль ядра 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()
.
Коротко о главном
В статье рассмотрели способы мониторинга пользовательских приложений и их перезапуска в случае сбоя или зависания. А также разобрали способы ротации логов ядра и установления связи сокета второго уровня с пользовательскими приложениями.
ссылка на оригинал статьи https://habr.com/ru/company/southbridge/blog/712414/
Добавить комментарий