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

Введение

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

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

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

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

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

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

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. Мега

Курс «Администрирование Linux. Мега» системного инженера Платона Платонова поможет разобраться не только во всех «фишках» контроля прав, но повысить владение Linux до уровня «бог» за 5 недель. Это самая хардовая и самая «прикладная» программа в духе Слерм + Southbridge: 12 часов теории, 48 часов практики на стендах, 9 масштабных тем и несчитанное количество реальных кейсов. 

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

Принять вызов ⬅️