Управляем модулем ядра Linux
Почему пользовательское приложение работает некорректно? Существует не так много способов, которые бы помогали выявить проблему. В большинстве случаев для поддержания высокой доступности требуется стороннее программное обеспечение. В статье рассказываем, как настроить мониторинг пользовательского приложения через модуль ядра 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 масштабных тем и несчитанное количество реальных кейсов.
Документ о прохождении курса получит каждый участник. А те, кто выполнит финальный итоговый проект на стенде, добавят к своему портфолио специальный номерной сертификат. Цель нашего хардового финального тестирования — проверить полученные знания выпускников совокупно, поэтому мы включим каждую изученную тему.