11.1. 任务调度

经常我们要定期的抽空处理一些“家务活”。如果这样的任务通过一个用户进程完成的,那么我们可以将它放到一个 crontab 文件中。如果是通过一个内核模块来完成,那么我们有两种选择。

第一种选择是使用 crontab 文件,启动一个进程,通过一个系统调用唤醒内核模块,例如打开一个文件。这很没效率。我们通过 crontab 生成了一个新进程,读取了一段新的可执行代码进入内存,只是为了唤醒一个已经在内存中的内核模块。

第二种选择是我们构造一个函数,然后该函数在每次时间中断发生时被调用。实现方法是我们构造一个任务,使用结构体tq_struct,而该结构体又保存着指向该函数的指针。然后,我们用 queue_task 把该任务放在叫做tq_timer任务队列中。该队列是将在下个时间中断发生时执行的任务。因为我们想要使它不停的执行,所以当该函数执行完后我们还要将它放回tq_timer任务队列中等待下一次时间中断。

但我们似乎忘了一点。当一个模块用rmmod卸载时,它会检查使用计数。如果该计数为零,则调用 module_cleanup。然后,模块就同它的所有函数调用从内存中消失了。此时没人去检查任务队列中是否正好还有一个等待执行的这些函数的指针。在可能是一段漫长的时间后(当然是相对计算机而言,对于我们这点时间什么都不是,也就差不多百分之一秒吧),内核接收到一个时间中断,然后准备调用那个在任务队列中的函数。不幸的是,该函数已经不存在了。大多数情况下,由于访问的内存页是空白的,你只会收到一个不愉快的消息。但是如果其它的一些代码恰好就在那里,结果可能将会非常糟糕。同样不幸的是,我们也没有一种轻易的向任务队列注销任务的机制。

既然 cleanup_module 不能返回一个错误代码(它是一个void函数),解决之道是让它不要返回。相反,调用 sleep_onmodule_sleep_on[1] rmmod 的进程休眠。在此之前,它通知被时间中断调度出任务队列的那个函数不要在返回队列。这样,在下一个时间中断发生时, rmmod 就会被唤醒,此时我们的函数已经不在队列中,可以很安全的卸载我们的模块了。

Example 11-1. sched.c

/*  sched.c - scheduale a function to be called on every timer interrupt.
 *
 *  Copyright (C) 2001 by Peter Jay Salzman
 */

/* 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 struct wait_queue *WaitQ = NULL;

static void intrpt_routine(void *);

/* The task queue structure for this task, from tqueue.h */
static struct tq_struct Task = {
   NULL,          /* Next item in list - queue_task will do this for us */
   0,             /* A flag meaning we haven't been inserted into a task
                   * queue yet 
                   */
   intrpt_routine, /* The function to run */
   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 != NULL) 
      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 zero)
{
   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;
}

struct proc_dir_entry Our_Proc_File = {
   0,  /* Inode number - ignore, it'll be filled by proc_register_dynamic */
   5,                 /* Length of the file name */
   "sched",           /* The file name */
   S_IFREG | S_IRUGO, /* File mode - this is a regular file which can be
                       * read by its owner, its group, and everybody else
                       */
   1,      /* Number of links (directories where the file is referenced) */
   0, 0,             /* The uid and gid for the file - we give it to root */
   80,               /* The size of the file reported by ls. */
   NULL, /* functions which can be done on the inode (linking, removing,
          * etc). - we don't * support any.
          */
   procfile_read,  /* The read function for this file, the function called
									  * when somebody tries to read something from it.
                    */
   NULL /* We could have here a function to fill the file's inode, to
         * enable us to play with permissions, ownership, etc.
         */
  }; 

/* 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 */
#if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)
   return proc_register(&proc_root, &Our_Proc_File);
#else
   return proc_register_dynamic(&proc_root, &Our_Proc_File);
#endif
}

/* Cleanup */
void cleanup_module()
{
   /* Unregister our /proc file */
   proc_unregister(&proc_root, Our_Proc_File.low_ino);
  
   /* 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.
    */
   sleep_on(&WaitQ);
}  

注意

[1]

它们实际上是一回事。