免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: lovesaka
打印 上一主题 下一主题

LINUX内核模块编程[转] [复制链接]

论坛徽章:
0
11 [报告]
发表于 2006-11-07 04:25 |只看该作者
Chapter 11. Scheduling Tasks
任务调度
经常我们要定期的抽空处理一些“家务活”。如果这样的任务通过一个用户进程完成的,那么我们可以将它放到一个 crontab文件中。如果是通过一个内核模块来完成,那么我们有两种选择。 第一种选择是使用crontab文件,启动一个进程,通过一个系统调用唤醒内核模块,例如打开一个文件。 这很没效率。我们通过crontab生成了一个新进程,读取了一段新的可执行代码进入内存, 只是为了唤醒一个已经在内存中的内核模块。

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

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

既然cleanup_module不能返回一个错误代码(它是一个void函数), 解决之道是让它不要返回。相反,调用sleep_on或module_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] 它们实际上是一回事。

论坛徽章:
0
12 [报告]
发表于 2006-11-07 04:26 |只看该作者
Chapter 12. Interrupt Handlers
Interrupt Handlers
Interrupt Handlers
除了刚结束的那章,我们目前在内核中所做的每件事都只不过是对某个请求的进程的响应, 要么是对某个特殊的文件的处理,要么是发送一个ioctl(),要么是调用一个系统调用。 但是内核的工作不仅仅是响应某个进程的请求。还有另外一项非常重要的工作就是负责对硬件的管理。

在CPU和硬件之间的活动大致可分为两种。第一种是CPU发送指令给硬件,第二种就是硬件要返回某些信息给CPU。 后面的那种又叫做中断,因为要知道何时同硬件对话才适宜而较难实现。硬件设备通常只有很少的缓存, 如果你不及时的读取里面的信息,这些信息就会丢失。

在Linux中,硬件中断被叫作IRQ(Interrupt Requests,中断请求)[1]。有两种硬件中断,短中断和长中断。短中断占用的时间非常短,在这段时间内, 整个系统被阻塞,任何其它中断都不会处理。长中断占用的时间相对较长,在此期间,可能会有别的中断发生请求处理 (不是相同设备发出的中断)。可能的话,尽量将中断声明为长中断。

当CPU接收到一个中断时,它停止正在处理的一切事务(除非它在处理另一个更重要的中断, 在这种情况下它只会处理完这个重要的中断才会回来处理新产生的中断), 将运行中的那些参数压入栈中然后调用中断处理程序。这同时意味着中断处理程序本身也有一些限制, 因为此时系统的状态并不确定。解决的办法是让中断处理程序尽快的完成它的事务,通常是从硬件读取信息和向硬件发送指令, 然后安排下一次接收信息的相关处理(这被称为"bottom half" [2] ),然后返回。内核确保被安排的事务被尽快的执行。当被执行时,在内核模块中允许的操作就是被允许的。

实现的方法是调用request_irq()函数,当接受到相应的IRQ时 (共有15种中断,在Intel架构平台上再加上1种用于串连中断控制器的中断)去调用你的中断 处理程序。该函数接收IRQ号,要调用的处理IRQ函数的名称,中断请求的类别标志位,文件 /proc/interrupts中声明的设备的名字,和传递给中断处理程序的参数。中断请求的类别标志位可以为 SA_SHIRQ来告诉系统你希望与其它中断处理程序共享该中断号 (这通常是由于一些设备共用相同的IRQ号),也可以为SA_INTERRUPT 来告诉系统这是一个快速中断,这种情况下该函数只有在该IRQ空闲时才会成功返回,或者同时你又决定共享该IQR。

然后,在中断处理程序内部,我们与硬件对话,接着使用带tq_immediate()和 mark_bh(BH_IMMEDIATE)的 queue_task_irq()去对bottom half队列进行调度。我们不能使用2.0版本种标准的 queue_task 的原因是中断可能就发生在别人的 queue_task[3] 中。我们需要mark_bh是因为早期版本的Linux只有一个可以存储32个bottom half的数组, 并且现在它们中的一个(BH_IMMEDIATE)已经被用来连接没有分配到队列中的入口的硬件 驱动的bottom half。

Intel架构中的键盘
剩余的这部分是只适用Intel架构的。如果你不使用Intel架构的平台,它们将不会工作,不要去尝试编译以下的代码。

在写这章的事例代码时,我遇到了一些困难。一方面,我需要一个可以得到实际有意义结果的, 能在各种平台上工作的例子。另一方面,内核中已经包括了各种设备驱动,并且这些驱动将无法和我的例子共存。 我找到的解决办法是为键盘中断写点东西,当然首先禁用普通的键盘中断。因为该中断在内核中定义为一个静态连接的符号 (见drivers/char/keyboard.c)),我们没有办法恢复。所以在 insmod前,如果你爱惜你的机器,新打开一个终端运行sleep 120 ; reboot。

该代码将自己绑定在IRQ 1, 也就是Intel架构中键盘的IRQ。然后,当接收到一个键盘中断请求时,它读取键盘的状态(那就是 inb(0x64)的目的)和扫描码,也就是键盘返回的键值。然后,一旦内核认为这是符合条件的,它运行 got_char去给出操作的键(扫描码的头7个位)和是按下键(扫描码的第8位为0) 还是弹起键(扫描码的第8位为1)。

Example 12-1. intrpt.c

/*
*  intrpt.c - An interrupt handler.
*
*  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/sched.h>
#include <linux/workqueue.h>
#include <linux/interrupt.h>        /* We want an interrupt */
#include <asm/io.h>

#define MY_WORK_QUEUE_NAME "WQsched.c"

static struct workqueue_struct *my_workqueue;

/*
* This will get called by the kernel as soon as it's safe
* to do everything normally allowed by kernel modules.
*/
static void got_char(void *scancode)
{
        printk("Scan Code %x %s.\n",
               (int)*((char *)scancode) & 0x7F,
               *((char *)scancode) & 0x80 ? "Released" : " Pressed" );
}

/*
* This function services keyboard interrupts. It reads the relevant
* information from the keyboard and then puts the non time critical
* part into the work queue. This will be run when the kernel considers it safe.
*/
irqreturn_t irq_handler(int irq, void *dev_id, struct pt_regs *regs)
{
        /*
         * This variables are static because they need to be
         * accessible (through pointers) to the bottom half routine.
         */
        static int initialised = 0;
        static unsigned char scancode;
        static struct work_struct task;
        unsigned char status;

        /*
         * Read keyboard status
         */
        status = inb(0x64);
        scancode = inb(0x60);

        if (initialised == 0) {
                INIT_WORK(&task, got_char, &scancode);
                initialised = 1;
        } else {
                PREPARE_WORK(&task, got_char, &scancode);
        }

        queue_work(my_workqueue, &task);

        return IRQ_HANDLED;
}

/*
* Initialize the module - register the IRQ handler
*/
int init_module()
{
        my_workqueue = create_workqueue(MY_WORK_QUEUE_NAME);

        /*
         * Since the keyboard handler won't co-exist with another handler,
         * such as us, we have to disable it (free its IRQ) before we do
         * anything.  Since we don't know where it is, there's no way to
         * reinstate it later - so the computer will have to be rebooted
         * when we're done.
         */
        free_irq(1, NULL);

        /*
         * Request IRQ 1, the keyboard IRQ, to go to our irq_handler.
         * SA_SHIRQ means we're willing to have othe handlers on this IRQ.
         * SA_INTERRUPT can be used to make the handler into a fast interrupt.
         */
        return request_irq(1,        /* The number of the keyboard IRQ on PCs */
                           irq_handler,        /* our handler */
                           SA_SHIRQ, "test_keyboard_irq_handler",
                           (void *)(irq_handler));
}

/*
* Cleanup
*/
void cleanup_module()
{
        /*
         * This is only here for completeness. It's totally irrelevant, since
         * we don't have a way to restore the normal keyboard interrupt so the
         * computer is completely useless and has to be rebooted.
         */
        free_irq(1, NULL);
}

/*
* some work_queue related functions are just available to GPL licensed Modules
*/
MODULE_LICENSE("GPL" );
Notes
[1] 这是Linux起源的Intel架构中的标准的起名方法。


[2] 这里是译者给出的关于“bottom half”的一点解释,来源是google上搜索到的英文资料:

“底部”,“bottom half”常在涉及中断的设备驱动中提到。


当内核接收到一个中断请求,对应的设备驱动被调用。因为在这段时间内无法处理别的任何事务, 让中断处理尽快的完成并重新让内核返回正常的工作状态是非常重要的。就是因为这个设计思想, 驱动的“顶部”和“底部”的概念被提出:“顶部”是被内核调用时最先被执行的部分, 快速的完成一些尽量少的却是必需的工作(像对硬件或其它资源的独享访问这种必须立刻执行的操作), 然后做一些设置让“底部”去完成那些要求时间相对比较宽裕的,剩下的工作。

“底部”什么时候如何运作是内核的设计问题。你也许会听到“底部”的设计已经在最近的内核中被废除了。 这种说法不是很确切,在新内核中其实你可以去选择怎样去执行:像软中断或任务,就像它们以前那样, 还是加入任务队列,更像启动一个用户进程。

[3] queue_task_irq被一个全局的锁(有锁定作用的变量) 保护着,在版本2.2中,并没有queue_task_irq而且queue_task 也是被一个锁保护的。

论坛徽章:
0
13 [报告]
发表于 2006-11-07 04:27 |只看该作者
Chapter 13. Symmetric Multi Processing
对称多线程处理
提高性能的最简单也是最便宜的方法是给你的主板加第二个CPU(如果你的主板支持的话)。 这可以通过让不同的CPU完成不同的工作(非对称多线程处理)或是相同的工作(对称多线程处理)。 实现高效率的非对称的多线程处理需要特殊硬件相关的知识,而对于Linux这样通用操作系统这是不可能的。 相对而言,对称多线程处理是较容易实现的。

我这里所说的相对容易,老实说,还是不容易。在一个对称多线程处理的环境中, 多个CPU共享内存,导致的结果是其中一个CPU运行的代码会对别的CPU也产生影响。 你不能再确定你代码中第一行中设置的变量在接下来的那行代码中还是那个设置值; 其它的CPU可能会趁你不注意已经把它修改了。显然,如果是这样的话,是无法进行任何编程的。

对于进程层面上的编程这通常不是个问题,因为一个进程通常同一时间只在一个CPU上运行 [1]。 但是,对于内核,就可以被在不同的CPU上的同时运行的不同的进程使用。

在内核版本2.0.x中,这还不算作什么问题,因为整个内核是一个spinlock [2],这就意味着一旦某个CPU进入内核态,别的CPU将不允许进入内核态。这使Linux的SMP实现很安全 [3],但缺乏效率。

在内核版本2.2.x以后,多CPU已经允许同时进入内核态。内核模块的作者应该意识到这一点。

Notes
[1] 存在例外,就是线程化的进程,这样的进程可以在多个CPU上同时运行。

[2] 抱歉,我没有找到合适的词语来表达这个单词。这是内核中的一种机制,可以对内核中的关键数据结构进行锁定保护, 防止其被破坏。

[3] 意味着这样的SMP机制使用起来很安全。

论坛徽章:
0
14 [报告]
发表于 2006-11-07 04:28 |只看该作者
Chapter 14. Common Pitfalls
注意
在我让你们进入内核模块的世界之前,我需要提醒你们下面的一些注意。如果我没警告到你们但是的确发生了, 那么你将问题报告我,我将全额退还你的书款。


使用标准库文件:
你无法这样做。在内核模块中,你只能使用内核提供的函数,也就是你在 /proc/kallsyms能查到的那些。

禁用中断:
你如果这样做了但只是一瞬间,没问题,当我没提这事。但是事后你没有恢复它们, 你就只能摁电源键来重启你僵死的系统了。

尝试一些非常危险的东西:
这也许不应该由我来说,但是以防万一,我还是提出来吧!


Appendix A. Changes: 2.0 To 2.2
从2.0到2.2的变化
从2.0到2.2的变化
我对内核的了解并不很完全所以我也无法写出所有的变化。在修改代码 (更确切的说,是采用Emmanuel Papirakis的修改)时,我遇到了以下的这些修改。 我将它们都列出来以方便模块编写者们,特别是学习该档案先前版本并熟悉我提到的这些技巧 (但已经更换到新版本的)的那些人。

更多的这方面的参考资料在 Richard Gooch's的站点上。


asm/uaccess.h
如果你要使用put_user或get_user你就需要 #include它。

get_user
在2.2版本中,get_user同时接收用户内存的指针和用来 设置信息的内核内存中变量的内存指针。变化的原因是因为当我们读取的变量是二或四个字节长的时候, get_user也可以读取二或四个字节长的变量。

file_operations
改结构体现在有了一个可以在open和 close之间进行的刷新操作函数。

close in file_operations
2.2版本中,close返回整形值,所以可以检测是否失败。

read,write in file_operations
这些函数的头文件改变了。它们现在返回ssize_t而不是整形值, 且它们的参数表也变了。inode 不再是一个参数,文件中的偏移量也一样。

proc_register_dynamic
该函数已经不复存在。你应该使用用0作为inode参数的proc_register 函数来替代它。

Signals
在 task 结构体中的signals不再是一个32位整形变量,而是一个为 _NSIG_WORDS 整形的数组。

queue_task_irq
即使你想在中断处理内部调度一个任务,你也应该使用 queue_task而不是queue_task_irq。

Module Parameters
你不必在将模块参数声明为全局变量。在2.2中,使用 MODULE_PARM去声明模块参数。这是一个进步, 这样就允许模块接受以数字开头的参数名而不会被弄糊涂。

Symmetrical Multi-Processing
内核本省已不再是一个spinlock,意味着你的模块也应该考虑 SMP的问题。



Appendix B. Where To Go From Here
为什么这样写?
我其实可以给这本书再加入几章,例如如何为实现新的文件系统加上一章,或是添加一个新的协议栈(如果有这样的必要的话, 想找到Linux不支持的网络协议已经是非常的困难的了)。我还可以解释一下我们尚未接触到的内核实现机制,像系统的引导自举, 或磁盘存储。

但是,我决定否。我写本书的目的是提供基本的,入门的对神秘的内核模块编程的认识和这方面的常用技巧。 对于那些非常热衷与内核编程的人,我推荐Juan-Mariano de Goyeneche的 内核资源列表 。 同样,就同Linus本人说的那样,学习内核最好的方法是自己阅读内核源代码。

如果你对更多的短小的示例内核模块感兴趣,我向你推荐 Phrack magazine 这本杂志。 即使你不关心安全问题,作为一个程序员你还是应该时时考虑这个问题的。这些内核模块代码都很短,不需要费多大劲就能读懂。

我希望我满足了你希望成为一个更优秀的程序员的要求,至少在学习技术的过程中体会到了乐趣。 如果你真的写了一些非常有用的模块,我希望你使用GPL许可证发布你的模块,这样我也就可以使用它们了。

论坛徽章:
0
15 [报告]
发表于 2006-11-07 04:38 |只看该作者
转这篇文章我也是事出有因详细请看下面
http://bbs.chinaunix.net/viewthread.php?tid=838790
总之能解决问题的文章那就是好文章我也给大家献上
为了让大家看着顺眼编辑了差不多一个多少时娃娃头终于全没了希望大家伙能用得上^_^

论坛徽章:
0
16 [报告]
发表于 2006-11-07 09:09 |只看该作者
留个名先,谢了

论坛徽章:
0
17 [报告]
发表于 2006-11-07 09:17 |只看该作者
好好学习,天天向上!
THS

论坛徽章:
0
18 [报告]
发表于 2006-11-07 10:30 |只看该作者
谢谢楼主,很辛苦呀

论坛徽章:
0
19 [报告]
发表于 2006-11-07 13:56 |只看该作者
好文

论坛徽章:
0
20 [报告]
发表于 2006-11-07 20:20 |只看该作者
如果有人愿意把这做成pdf版的供给大家下载就最好不过了或者教我怎么做也行(我正想学学呢^_^)
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP