Linux内核模块编程--替任务调度
任务调度常常的,我们有‘家务管理’的任务需要在某个时间做或者偶尔经常如此。如果任务由进程完成,我们可以将它放在 crontab 文件中。如果任务由内核模块完成,我们有两种可能。第一个是在 crontab 文件中放置一个在必要的时候通过系统调用唤醒模块的进程,例如通过打开文件。这是非常低效的,然而--我们运行一个不在 crontab 中的新进程, 读一个新的可执行的进程到内存,而所有这些只是唤醒在内存中的内核模块。
替代的,我们可以创建一个对每个定时器中断被调用一次的函数。我们的办法是创建一个包含在 tq_struct结构中的任务,而该结构包含该函数的指针。然后我们使用 queue_task 将那个任务放置在被称为tq_timer 的任务列表中,该列表是在下一个定时器中断将被执行的任务的列表。因为我们我们想该函数在下一次定时器中断时继续被执行,我们需要在它被调用后将它放回 tq_timer。
这还有一点我们需要记住的。当一个模块被 rmmod 移除时,它的引用计数器首先被检查,如果它为0,module_cleanup 将被调用。然后模块连同它的所有函数被从内存中清除。没有人去检查看在定时器任务列表中是否碰巧包含一个这样的不再可见的函数的指针。一段时间后(从计算机的观点看,而从人的观点看它什么也不是,它少于百分之一秒),内核有了一个定时器中断并试图去调用任务列表中的函数。不幸的,那个函数不在那儿。在大多情况下它刚才所在内存页没有被使用,而你会得到一个难看的错误消息。但是如果别的某些代码现在位于同一个内存位置,事情会变得 非常 难看。不幸的,我们没有一个简单的办法将一个任务从任务列表中注销。
既然 cleanup_module 不能返回错误代码(它是一个void函数),解决的办法是根本不让它返回。替代的,它调用sleep_on 或 module_sleep_on(他们实际上是相同的。 )使 rmmod 进程睡眠。在此之前,它通过设置一个全局变量通知在定时器中断将被调用的函数停止连接自己。然后,在下一次定时器中断, rmmod进程被唤醒,当我们的函数不再在那个队列中时移除那个模块就是安全的了。
范例 sched.c
/* sched.c - 安排一个函数在每次定时器中断时被调用 *//* Copyright (C) 1998 by Ori Pomerantz *//* 必要头文件 *//* 标准头文件 */#include /* 内核工作 */#include /* 明确指定是模块 *//* 处理 CONFIG_MODVERSIONS */#if CONFIG_MODVERSIONS==1#define MODVERSIONS#include#endif/* 我们使用 proc 文件系统所必要的 */#include/* 我们在这儿安排任务 */#include/* 我们也需要睡眠和唤醒的能力 */#include/* 2.2.3 版/usr/include/linux/version.h 包含该宏* 但2.0.35 版不包含 - 加入以备需要 */#ifndef KERNEL_VERSION#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))#endif/* 定时器中断已经被调用的次数 */static int TimerIntrpt = 0;/* 这被清除模块使用,以防止当 intrpt_routine 仍在任务队列里时模块被清除 */static struct wait_queue *WaitQ = NULL;static void intrpt_routine(void *);/* 这个任务的任务队列结构,来自 tqueue.h */static struct tq_struct Task = {NULL, /* 列表的下一项 - queue_task 将为我们做这个 */0, /* 一个标志,意思是我们还没有易做图入任务队列 */intrpt_routine, /* 运行的函数 */NULL /* 函数的void* 参数 */};/* 这个函数将在每次定时器中断时被调用。注意 void* 指针 -* 任务函数可以用于多个目的,每次得到不同的参数。 */static void intrpt_routine(void *irrelevant){/* 增加计数器 */TimerIntrpt++;/* 如果清除模块想我们死亡 */if (WaitQ != NULL)wake_up(&WaitQ); /* 现在 cleanup_module 可以返回 */else/* 将我们放回任务队列 */queue_task(&Task, &tq_timer);}/* 将数据放入proc 文件 */int procfile_read(char *buffer,char **buffer_location, off_t offset,int buffer_length, int zero){int len; /* 实际使用的字节数 *//* 这是静态的因此当我们离开这个函数时它仍然在内存中 */static char my_buffer[80];static int count = 1;/* 我们将所有的信息放在一个里面,因此当有人问我们是否有更多 信息时答案是否。 */if (offset > 0)return 0;/* 填充缓冲区并得到它的长度 */len = sprintf(my_buffer,"Timer was called %d times so far
",TimerIntrpt);count++;/* 告诉调用我们的函数缓冲区在哪儿 */*buffer_location = my_buffer;/* 返回长度 */return len;}struct proc_dir_entry Our_Proc_File ={0, /* 节点数 - 忽略,它将被 proc_register_dynamic 填充*/5, /* 文件名长度 */"sched", /* 文件名 */S_IFREG | S_IRUGO,/* 文件模式 - 这是一个可以被其拥有者,用户组和其他任何人读取的普通文件 */1, /* 连接数 (文件被引用的目录)*/0, 0, /* 文件的UID和GID - 我们将它赋予root */80, /* 由ls报告的文件长度 */NULL, /* 节点函数(连接,删除,等等) - 不支持 */procfile_read,/* 文件的读函数,当某人试图从中读什么时被调用 */NULL/* 可以在这儿设置一个填充文件节点的函数,以使我们可以修改权限,拥有关系等。 */};/* 初始化模块--登记 proc 文件 */int init_module(){/* 将任务放置在 tq_timer 任务队列,因此在下次定时器中断时它将被执行 */queue_task(&Task, &tq_timer);/* proc_register_dynamic 成功则成功,否则失败 */#if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)return proc_register(&proc_root, &Our_Proc_File);#elsereturn proc_register_dynamic(&proc_root, &Our_Proc_File);#endif}/* 清除 */void cleanup_module(){/* 注销 /proc 文件 */proc_unregister(&proc_root, Our_Proc_File.low_ino);/* 睡眠,直到 intrpt_routine 上次被调用。这是必要的,因为否则我们将释放 intrpt_routine* 和tq_timer仍然引用的任务占有的内存。注意不允许信号中断。** 既然 WaitQ 现在不为 NULL,这自动的告诉中断程序它死亡的时间到了。 */sleep_on(&WaitQ);}