任務調度
常常的,我們有‘家務管理’的任務需要在某個時間做或者偶爾經常如此。如果任務由進程完成,我們可以將它放在 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\n", 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); #else return 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); }