Wiki

Глава 10. Планирование задач

Очень часто возникает необходимость запуска вспомогательных задач по расписанию. Если запускаемая задача -- обычный процесс, то помещаем ее в файл crontab. Если же задача является модулем ядра, то у нас есть две возможности. Первая состоит в том, чтобы поместить некую задачу в файл crontab, которая будет "будить" модуль системным вызовом в заданный момент времени, например, открывая файл. Это очень неэффективно, т.к. при запуске нового процесса из crontab приходится загружать программу в память и всем это только для того, чтобы "разбудить" модуль ядра, который уже находится в памяти.

Вместо этого мы попробуем создать функцию, которая будет вызываться по прерываниям от таймера. Для этого, создадим задачу struct work_struct. Эта структура будет хранить указатель на функцию, срабатывающую по таймеру. Затем, с помощью queue_delayed_work, поместим задачу в очередь tq_timer, где должны располагаться задачи, срабатывающие по таймеру. А так как предполагается срабатывание функции каждый раз, по истечении заданного интервала времени, мы должны всякий раз опять вставлять ее в очередь tq_timer.

И еще один немаловажный момент. Когда модуль выгружается командой rmmod, сначала проверяется счетчик обращений к модулю. Если он равен нулю, то вызывается module_cleanup. После чего модуль удаляется из памяти со всеми его функциями. И никто не проверяет -- содержит ли очередь задач таймера указатель на одну из удаляемых функций. По прошествии некоторого времени (с точки зрения человека -- практически мгновенно), ядро получит прерывание от таймера и попробует вызывать удаленную из очереди задачу. Но функции-то больше нет! В большинстве случаев страница памяти, где она была, будет рассматриваться как неиспользуемая и вы получите сообщение об ошибке. Но может случиться так, что на этом месте окажется некоторый другой код и тогда ваше дело -- табак. К сожалению, у нас нет достаточно простого способа удаления задачи из очереди таймера.

Так как cleanup_module не может вернуть код ошибки (она не имеет возвращаемого значения), то напрашивается решение -- приостановить процедуру завершения работы модуля. Вместо того, чтобы немедленно завершить работу функции cleanup_module, мы можем приостановить работу команды rmmod. Затем, установив глобальную переменную, сообщить функции, вызываемой по прерыванию таймера, чтобы она убрала себя из очереди (точнее -- чтобы она опять не вставляла себя в очередь). На ближайшем прерывании таймера, процесс rmmod будет "разбужен", когда функция удалит себя из очереди таймера и удаление модуля станет безопасным.

Пример 10-1. sched.c

/*
 *  sched.c - реализация срабатывания по таймеру.
 *
 *  Copyright (C) 2001 by Peter Jay Salzman
 */
 
/* 
 * Необходимые заголовочные файлы
 */
 
/* 
 * Обычные, для модулей ядра
 */
#include <linux/kernel.h>    /* Все-таки мы работаем с ядром! */
#include <linux/module.h>    /* Необходимо для любого модуля */
#include <linux/proc_fs.h>   /* Необходимо для работы с /proc */
#include <linux/workqueue.h> /* очереди задач */
#include <linux/sched.h>     /* Взаимодействие с планировщиком */
#include <linux/init.h>      /* макросы  __init и  __exit */
#include <linux/interrupt.h> /* определение irqreturn_t */
 
struct proc_dir_entry *Our_Proc_File;
#define PROC_ENTRY_FILENAME "sched"
#define MY_WORK_QUEUE_NAME "WQsched.c"
 
/* 
 * Счетчик срабатываний по таймеру
 */
static int TimerIntrpt = 0;
 
static void intrpt_routine(void *);
 
static int die = 0;  /* 1 -- завершить работу */
 
/* 
 * очередь задач, создается для того, чтобы поместить в очередь таймера (workqueue.h)
 */
static struct workqueue_struct *my_workqueue;
 
static struct work_struct Task;
static DECLARE_WORK(Task, intrpt_routine, NULL);
 
/* 
 * Функция-обработчик прерывания от таймера. Обратите внимание на аргумент типа void*
 * функция может получать дополнительные аргументы посредством этого указателя.
 */
static void intrpt_routine(void *irrelevant)
{
  /* 
   * Нарастить счетчик
   */
  TimerIntrpt++;
 
  /* 
   * Если признак завершения сброшен, то опять вставить себя в очередь таймера
   */
  if (die == 0)
    queue_delayed_work(my_workqueue, &Task, 100);
}
 
/* 
 * Запись данных в файл /proc.
 */
ssize_t
procfile_read(char *buffer,
        char **buffer_location,
        off_t offset, int buffer_length, int *eof, void *data)
{
  int len;              /* Фактическое число записанных байт */
 
  /* 
   * Переменные объявлены как static, поэтому они располагаются не на стеке
   * функции, а в памяти модуля
   */
  static char my_buffer[80];
 
  static int count = 1;
 
  /* 
   * Все сведения выдаются за один присест, поэтому, если смещение != 0, то значит
   * нам нечего больше сказать, поэтому возвращается 0, в качестве признака конца файла.
   */
  if (offset > 0)
    return 0;
 
  /* 
   * Заполнить буфер и получить его длину
   */
  len = sprintf(my_buffer, "Timer called %d times so far\n", TimerIntrpt);
  count++;
 
  /* 
   * Указать адрес буфера
   */
  *buffer_location = my_buffer;
 
  /* 
   * Вернуть длину буфера
   */
  return len;
}
 
/* 
 * Функция инициализации - зарегистрировать файл в /proc
 */
int __init init_module()
{
  int rv = 0;
  /* 
   * Создать очередь задач с нашей задачей и поместить ее в очередь таймера
   */
  my_workqueue = create_workqueue(MY_WORK_QUEUE_NAME);
  queue_delayed_work(my_workqueue, &Task, 100);
 
  Our_Proc_File = create_proc_entry(PROC_ENTRY_FILENAME, 0644, NULL);
  Our_Proc_File->read_proc = procfile_read;
  Our_Proc_File->owner = THIS_MODULE;
  Our_Proc_File->mode = S_IFREG | S_IRUGO;
  Our_Proc_File->uid = 0;
  Our_Proc_File->gid = 0;
  Our_Proc_File->size = 80;
 
  if (Our_Proc_File == NULL) {
    rv = -ENOMEM;
    remove_proc_entry(PROC_ENTRY_FILENAME, &proc_root);
    printk(KERN_INFO "Error: Could not initialize /proc/%s\n",
           PROC_ENTRY_FILENAME);
  }
 
  return rv;
}
 
/* 
 * Завершение работы
 */
void __exit cleanup_module()
{
  /* 
   * Удалить файл из /proc
   */
  remove_proc_entry(PROC_ENTRY_FILENAME, &proc_root);
  printk(KERN_INFO "/proc/%s removed\n", PROC_ENTRY_FILENAME);
 
  die = 1;                    /* Известить функцию обработки прерываний о завершении работы */
  cancel_delayed_work(&Task); 
  flush_workqueue(my_workqueue); /* ждать пока отработает таймер */
  destroy_workqueue(my_workqueue);
 
  /* 
   * Приостановить работу, пока intrpt_routine не отработает в последний раз. 
   * Это необходимо, поскольку мы освобождаем память, занимаемую этой функцией.
   */
 
}
 
/* 
 * некоторые функции, относящиеся к work_queue 
 * доступны только если модуль лицензирован под GPL
 */
MODULE_LICENSE("GPL");