第 11 章。调度任务

调度任务

通常,我们有 "内务处理" 任务需要在特定时间或每隔一段时间执行。如果任务要由进程完成,我们会将其放在crontab文件中。如果任务要由内核模块完成,我们有两种可能性。第一种是将一个进程放在crontab文件中,该文件将在必要时通过系统调用唤醒模块,例如通过打开一个文件。然而,这是非常低效的 -- 我们从crontab读取一个新的可执行文件到内存,而这一切仅仅是为了唤醒一个已经在内存中的内核模块。

除了这样做,我们可以创建一个函数,该函数将在每次定时器中断时被调用一次。我们的方法是创建一个任务,保存在一个 tq_struct 结构中,该结构将保存一个指向函数的指针。然后,我们使用queue_task将该任务放在名为 tq_timer 的任务列表中,该列表是在下一次定时器中断时要执行的任务列表。因为我们希望该函数持续执行,所以我们需要在每次调用后将其放回 tq_timer,以便在下一次定时器中断时再次执行。

这里还有一点我们需要记住。当模块被 rmmod 移除时,首先会检查其引用计数。如果为零,则module_cleanup被调用。然后,模块及其所有函数将从内存中移除。没有人检查定时器的任务列表是否恰好包含指向其中一个函数的指针,这些函数将不再可用。过了很久(从计算机的角度来看,从人类的角度来看什么都不是,不到百分之一秒),内核发生定时器中断并尝试调用任务列表中的函数。不幸的是,该函数已不再存在。在大多数情况下,它所在的内存页未被使用,你会收到一个难看的错误消息。但是,如果现在有一些其他代码位于相同的内存位置,情况可能会变得 非常 糟糕。不幸的是,我们没有简单的方法从任务列表中取消注册任务。

由于cleanup_module无法返回错误代码(它是一个 void 函数),解决方案是不让它返回。相反,它调用sleep_onmodule_sleep_on[1] 以使 rmmod 进程进入休眠状态。在此之前,它会通知在定时器中断时调用的函数停止附加自身,方法是设置一个全局变量。然后,在下一次定时器中断时,rmmod 进程将被唤醒,此时我们的函数不再在队列中,可以安全地移除模块。

示例 11-1. sched.c

/*  sched.c - scheduale a function to be called on every timer interrupt.
 *
 *  Copyright (C) 2001 by Peter Jay Salzman
 *
 *  06/20/2006 - Updated by Rodrigo Rubira Branco <rodrigo@kernelhacking.com>
 */

/* Kernel Programming */
#define MODULE
#define LINUX
#define __KERNEL__

/* The necessary header files */

/* Standard in kernel modules */
#include <linux/kernel.h>                   /* We're doing kernel work */
#include <linux/module.h>                   /* Specifically, a module */

/* Deal with CONFIG_MODVERSIONS */
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include <linux/modversions.h>
#endif        

/* Necessary because we use the proc fs */
#include <linux/proc_fs.h>

/* We scheduale tasks here */
#include <linux/tqueue.h>

/* We also need the ability to put ourselves to sleep and wake up later */
#include <linux/sched.h>

/* In 2.2.3 /usr/include/linux/version.h includes a macro for this, but
 * 2.0.35 doesn't - so I add it here if necessary.
 */
#ifndef KERNEL_VERSION
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif

/* The number of times the timer interrupt has been called so far */
static int TimerIntrpt = 0;

/* This is used by cleanup, to prevent the module from being unloaded while
 * intrpt_routine is still in the task queue
 */
static DECLARE_WAIT_QUEUE_HEAD(WaitQ);
int waitq=0;

static void intrpt_routine(void *);

/* The task queue structure for this task, from tqueue.h */
static struct tq_struct Task = {
   routine: (void (*)(void *)) intrpt_routine, /* The function to run */
   data: NULL            /* The void* parameter for that function */
};

/* This function will be called on every timer interrupt. Notice the void*
 * pointer - task functions can be used for more than one purpose, each time 
 * getting a different parameter.
 */
static void intrpt_routine(void *irrelevant)
{
   /* Increment the counter */
   TimerIntrpt++;

   /* If cleanup wants us to die */
   if (waitq) 
      wake_up(&WaitQ);               /* Now cleanup_module can return */
   else
      /* Put ourselves back in the task queue */
      queue_task(&Task, &tq_timer);  
}

/* Put data into the proc fs file. */
int procfile_read(char *buffer, 
                  char **buffer_location, off_t offset, 
                  int buffer_length, int *eof, void *data)
{
   int len;  /* The number of bytes actually used */

   /* It's static so it will still be in memory when we leave this function
    */
   static char my_buffer[80];  

   static int count = 1;

   /* We give all of our information in one go, so if the anybody asks us
    * if we have more information the answer should always be no. 
    */
   if (offset > 0)
      return 0;

   /* Fill the buffer and get its length */
   len = sprintf(my_buffer, "Timer called %d times so far\n", TimerIntrpt);
   count++;

   /* Tell the function which called us where the buffer is */
   *buffer_location = my_buffer;

   /* Return the length */
   return len;
}

/* Proc structure pointer */
struct proc_dir_entry *Our_Proc_File;

/* Initialize the module - register the proc file */
int init_module()
{
   /* Put the task in the tq_timer task queue, so it will be executed at
    * next timer interrupt
    */
   queue_task(&Task, &tq_timer);

   /* Success if proc_register_dynamic is a success, failure otherwise */
   Our_Proc_File=create_proc_read_entry("sched", 0444, NULL, procfile_read, NULL);

   if ( Our_Proc_File == NULL )
	return -ENOMEM;
   else
	return 0;

}

/* Cleanup */
void cleanup_module()
{
   /* Unregister our /proc file */
   remove_proc_entry("sched", NULL);
  
   /* Sleep until intrpt_routine is called one last time. This is necessary,
    * because otherwise we'll deallocate the memory holding intrpt_routine
    * and Task while tq_timer still references them.  Notice that here we
    * don't allow signals to interrupt us. 
    *
    * Since WaitQ is now not NULL, this automatically tells the interrupt
    * routine it's time to die.
    */
   waitq=1;
   sleep_on(&WaitQ);
}  

MODULE_LICENSE("GPL");

注释

[1]

它们实际上是相同的。