Chapter 11. Scheduling Tasks

任务调度

经常我们要定期的抽空处理一些“家务活”。如果这样的任务通过一个用户进程完成的,那么我们可以将它放到一个 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 */
#include <linux/proc_fs.h>	/* Necessary because we use the proc fs */
#include <linux/workqueue.h>	/* We scheduale tasks here */
#include <linux/sched.h>	/* We need to put ourselves to sleep 
				   and wake up later */
#include <linux/init.h>		/* For __init and __exit */
#include <linux/interrupt.h>	/* For irqreturn_t */

struct proc_dir_entry *Our_Proc_File;
#define PROC_ENTRY_FILENAME "sched"
#define MY_WORK_QUEUE_NAME "WQsched.c"

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

static void intrpt_routine(void *);

static int die = 0;		/* set this to 1 for shutdown */

/* 
 * The work queue structure for this task, from workqueue.h 
 */
static struct workqueue_struct *my_workqueue;

static struct work_struct Task;
static DECLARE_WORK(Task, intrpt_routine, NULL);

/* 
 * 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 (die == 0)
		queue_delayed_work(my_workqueue, &Task, 100);
}

/* 
 * Put data into the proc fs file. 
 */
ssize_t
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;
}

/* 
 * Initialize the module - register the proc file 
 */
int __init init_module()
{
	int rv = 0;
	/* 
	 * Put the task in the work_timer task queue, so it will be executed at
	 * next timer interrupt
	 */
	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;
}

/* 
 * Cleanup
 */
void __exit cleanup_module()
{
	/* 
	 * Unregister our /proc file 
	 */
	remove_proc_entry(PROC_ENTRY_FILENAME, &proc_root);
	printk(KERN_INFO "/proc/%s removed\n", PROC_ENTRY_FILENAME);

	die = 1;		/* keep intrp_routine from queueing itself */
	cancel_delayed_work(&Task);	/* no "new ones" */
	flush_workqueue(my_workqueue);	/* wait till all "old ones" finished */
	destroy_workqueue(my_workqueue);

	/* 
	 * 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 work_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.
	 */

}

/* 
 * some work_queue related functions
 * are just available to GPL licensed Modules 
 */
MODULE_LICENSE("GPL");

Notes

[1]

它们实际上是一回事。