Linux内核模块编程指南(四)

翻译来自:
http://tldp.org/LDP/lkmpg/2.6/html/lkmpg.html
本系列文章还有:
Linux内核模块编程指南(一)
Linux内核模块编程指南(二)
Linux内核模块编程指南(三)
Linux内核模块编程指南(四)

第10章更换Printks

替换printk

在1.2.1.2节中 ,我说X和内核模块编程不混合。 这对于开发内核模块是正确的,但在实际使用中,您希望能够将消息发送到加载模块的命令[ t ] [15] 。

这样做的方法是使用current ,一个指向当前正在运行的任务的指针,来获取当前任务的tty结构。 然后,我们查看tty结构内部以找到指向字符串写入函数的指针,我们将其用于将字符串写入tty。

例10-1。 print_string.c

/* 
 *  print_string.c - Send output to the tty we're running on, regardless if it's
 *  through X11, telnet, etc.  We do this by printing the string to the tty
 *  associated with the current task.
 */
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched.h>    /* For current */
#include <linux/tty.h>      /* For the tty declarations */
#include <linux/version.h>  /* For LINUX_VERSION_CODE */

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Peter Jay Salzman");

static void print_string(char *str)
{
    struct tty_struct *my_tty;

    /* 
     * tty struct went into signal struct in 2.6.6 
     */
#if ( LINUX_VERSION_CODE <= KERNEL_VERSION(2,6,5) )
    /* 
     * The tty for the current task 
     */
    my_tty = current->tty;
#else
    /* 
     * The tty for the current task, for 2.6.6+ kernels 
     */
    my_tty = current->signal->tty;
#endif

    /* 
     * If my_tty is NULL, the current task has no tty you can print to 
     * (ie, if it's a daemon).  If so, there's nothing we can do.
     */
    if (my_tty != NULL) {

        /* 
         * my_tty->driver is a struct which holds the tty's functions,
         * one of which (write) is used to write strings to the tty. 
         * It can be used to take a string either from the user's or 
         * kernel's memory segment.
         *
         * The function's 1st parameter is the tty to write to,
         * because the same function would normally be used for all 
         * tty's of a certain type.  The 2nd parameter controls 
         * whether the function receives a string from kernel
         * memory (false, 0) or from user memory (true, non zero). 
         * BTW: this param has been removed in Kernels > 2.6.9
         * The (2nd) 3rd parameter is a pointer to a string.
         * The (3rd) 4th parameter is the length of the string.
         *
         * As you will see below, sometimes it's necessary to use
         * preprocessor stuff to create code that works for different
         * kernel versions. The (naive) approach we've taken here 
         * does not scale well. The right way to deal with this 
         * is described in section 2 of 
         * linux/Documentation/SubmittingPatches
         */
        ((my_tty->driver)->write) (my_tty,  /* The tty itself */
#if ( LINUX_VERSION_CODE <= KERNEL_VERSION(2,6,9) )     
                       0,   /* Don't take the string 
                           from user space        */
#endif
                       str, /* String                 */
                       strlen(str));    /* Length */

        /* 
         * ttys were originally hardware devices, which (usually) 
         * strictly followed the ASCII standard.  In ASCII, to move to
         * a new line you need two characters, a carriage return and a
         * line feed.  On Unix, the ASCII line feed is used for both 
         * purposes - so we can't just use \n, because it wouldn't have
         * a carriage return and the next line will start at the
         * column right after the line feed.
         *
         * This is why text files are different between Unix and 
         * MS Windows.  In CP/M and derivatives, like MS-DOS and 
         * MS Windows, the ASCII standard was strictly adhered to,
         * and therefore a newline requirs both a LF and a CR.
         */

#if ( LINUX_VERSION_CODE <= KERNEL_VERSION(2,6,9) )     
        ((my_tty->driver)->write) (my_tty, 0, "\015\012", 2);
#else
        ((my_tty->driver)->write) (my_tty, "\015\012", 2);
#endif
    }
}

static int __init print_string_init(void)
{
    print_string("The module has been inserted.  Hello world!");
    return 0;
}

static void __exit print_string_exit(void)
{
    print_string("The module has been removed.  Farewell world!");
}

module_init(print_string_init);
module_exit(print_string_exit);

闪烁的键盘LED

在某些情况下,您可能希望以更简单,更直接的方式与外部世界进行通信。 闪烁的键盘LED可以是这样的解决方案:它是吸引注意力或显示状态条件的直接方式。 键盘LED存在于每个硬件上,它们始终可见,它们不需要任何设置,与写入tty或文件相比,它们的使用相当简单且非侵入性。

以下源代码说明了一个最小内核模块,在加载时,它开始闪烁键盘LED,直到它被卸载。

例10-2。 kbleds.c

/* 
 *  kbleds.c - Blink keyboard leds until the module is unloaded.
 */

#include <linux/module.h>
#include <linux/config.h>
#include <linux/init.h>
#include <linux/tty.h>      /* For fg_console, MAX_NR_CONSOLES */
#include <linux/kd.h>       /* For KDSETLED */
#include <linux/vt.h>
#include <linux/console_struct.h>   /* For vc_cons */

MODULE_DESCRIPTION("Example module illustrating the use of Keyboard LEDs.");
MODULE_AUTHOR("Daniele Paolo Scarpazza");
MODULE_LICENSE("GPL");

struct timer_list my_timer;
struct tty_driver *my_driver;
char kbledstatus = 0;

#define BLINK_DELAY   HZ/5
#define ALL_LEDS_ON   0x07
#define RESTORE_LEDS  0xFF

/*
 * Function my_timer_func blinks the keyboard LEDs periodically by invoking
 * command KDSETLED of ioctl() on the keyboard driver. To learn more on virtual 
 * terminal ioctl operations, please see file:
 *     /usr/src/linux/drivers/char/vt_ioctl.c, function vt_ioctl().
 *
 * The argument to KDSETLED is alternatively set to 7 (thus causing the led 
 * mode to be set to LED_SHOW_IOCTL, and all the leds are lit) and to 0xFF
 * (any value above 7 switches back the led mode to LED_SHOW_FLAGS, thus
 * the LEDs reflect the actual keyboard status).  To learn more on this, 
 * please see file:
 *     /usr/src/linux/drivers/char/keyboard.c, function setledstate().
 * 
 */

static void my_timer_func(unsigned long ptr)
{
    int *pstatus = (int *)ptr;

    if (*pstatus == ALL_LEDS_ON)
        *pstatus = RESTORE_LEDS;
    else
        *pstatus = ALL_LEDS_ON;

    (my_driver->ioctl) (vc_cons[fg_console].d->vc_tty, NULL, KDSETLED,
                *pstatus);

    my_timer.expires = jiffies + BLINK_DELAY;
    add_timer(&my_timer);
}

static int __init kbleds_init(void)
{
    int i;

    printk(KERN_INFO "kbleds: loading\n");
    printk(KERN_INFO "kbleds: fgconsole is %x\n", fg_console);
    for (i = 0; i < MAX_NR_CONSOLES; i++) {
        if (!vc_cons[i].d)
            break;
        printk(KERN_INFO "poet_atkm: console[%i/%i] #%i, tty %lx\n", i,
               MAX_NR_CONSOLES, vc_cons[i].d->vc_num,
               (unsigned long)vc_cons[i].d->vc_tty);
    }
    printk(KERN_INFO "kbleds: finished scanning consoles\n");

    my_driver = vc_cons[fg_console].d->vc_tty->driver;
    printk(KERN_INFO "kbleds: tty driver magic %x\n", my_driver->magic);

    /*
     * Set up the LED blink timer the first time
     */
    init_timer(&my_timer);
    my_timer.function = my_timer_func;
    my_timer.data = (unsigned long)&kbledstatus;
    my_timer.expires = jiffies + BLINK_DELAY;
    add_timer(&my_timer);

    return 0;
}

static void __exit kbleds_cleanup(void)
{
    printk(KERN_INFO "kbleds: unloading...\n");
    del_timer(&my_timer);
    (my_driver->ioctl) (vc_cons[fg_console].d->vc_tty, NULL, KDSETLED,
                RESTORE_LEDS);
}

module_init(kbleds_init);
module_exit(kbleds_cleanup);

如果本章中没有一个示例符合您的调试需求,可能还有其他一些尝试。 有没有想过make menuconfig中的 CONFIG_LL_DEBUG有什么用呢? 如果激活,则会获得对串行端口的低级访问权限。 虽然这本身可能听起来不是很强大,但您可以修补kernel / printk.c或任何其他必要的系统调用以使用printascii,从而可以跟踪几乎所有代码在串行线上执行的操作。 如果您发现自己将内核移植到一些新的和以前不受支持的体系结构中,这通常是应该实现的第一件事。 登录netconsole也可能值得一试。

虽然你已经看到了许多可以用来帮助调试的东西,但有一些事情需要注意。 调试几乎总是侵入性的。 添加调试代码可以改变情况,使错误似乎消失。 因此,您应该尽量将调试代码保持在最低限度,并确保它不会出现在生产代码中

第11章计划任务

计划任务

很多时候,我们有“家务”任务,这些任务必须在某个时间或每隔一段时间完成。 如果任务要由进程完成,我们将它放在crontab文件中。 如果任务要由内核模块完成,我们有两种可能性。 第一种方法是在crontab文件中放入一个进程,该文件将在必要时通过系统调用唤醒模块,例如打开文件。 然而,这是非常低效的 - 我们从crontab运行一个新进程,将新的可执行文件读取到内存,所有这些只是唤醒内存模块,无论如何都在内存中。

我们可以创建一个为每个定时器中断调用一次的函数,而不是这样做。 我们这样做的方法是创建一个以workqueue_struct结构保存的任务, 该结构将保存指向该函数的指针。 然后,我们使用queue_delayed_work将该任务放在名为my_workqueue的任务列表中,该列表是在下一个计时器中断时要执行的任务列表。 因为我们希望函数继续执行,所以每当调用它时,我们都需要将它放回my_workqueue ,以用于下一个定时器中断。

还有一点我们需要记住这里。 当rmmod删除模块时,首先检查其引用计数。 如果为零,则调用module_cleanup 。 然后,模块将从内存中删除其所有功能。 事情需要妥善关闭,否则会发生不好的事情。 请参阅下面的代码,了解如何以安全的方式完成此操作。

例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];

    /* 
     * We give all of our information in one go, so if 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);

    /* 
     * 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()
{
    /*
     * Create our /proc file
     */
    Our_Proc_File = create_proc_entry(PROC_ENTRY_FILENAME, 0644, NULL);

    if (Our_Proc_File == NULL) {
        remove_proc_entry(PROC_ENTRY_FILENAME, &proc_root);
        printk(KERN_ALERT "Error: Could not initialize /proc/%s\n",
               PROC_ENTRY_FILENAME);
        return -ENOMEM;
    }

    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;

    /* 
     * 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);


    printk(KERN_INFO "/proc/%s created\n", PROC_ENTRY_FILENAME);

    return 0;
}

/* 
 * 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");

第12章中断处理程序

中断处理程序

除了最后一章,我们在内核中所做的一切,我们已经完成了对要求它的进程的响应,通过处理特殊文件,发送ioctl()或发出系统调用。 但是内核的工作不仅仅是响应进程请求。 另一项同样重要的工作是与连接到机器的硬件对话。

CPU与计算机硬件的其余部分之间有两种类型的交互。 第一种类型是CPU向硬件发出命令,另一种是硬件需要告诉CPU的东西。 第二个称为中断,实现起来要困难得多,因为在方便硬件而不是CPU时必须处理它。 硬件设备通常具有非常少量的RAM,如果您在可用时未读取其信息,则会丢失。

在Linux下,硬件中断被称为IRQ( I nterrupt R e q eests) [16] 。 有两种类型的IRQ,短和长。 短IRQ是一个预计需要很短时间的IRQ,在此期间机器的其余部分将被阻止,并且不会处理其他中断。 长IRQ是一个可能需要更长时间的IRQ,并且在此期间可能发生其他中断(但不是来自同一设备的中断)。 如果可能的话,最好将中断处理程序声明为long。

当CPU接收到中断时,它会停止正在进行的任何操作(除非它正在处理更重要的中断,在这种情况下,只有当更重要的中断完成时才会处理这个中断),在堆栈上保存某些参数并调用中断处理程序。 这意味着中断处理程序本身不允许某些事情,因为系统处于未知状态。 这个问题的解决方案是让中断处理程序执行需要立即执行的操作,通常从硬件读取内容或将内容发送到硬件,然后在以后安排处理新信息(这称为“下半场” )并返回。 然后保证内核尽快调用下半部分 - 当它发生时,内核模块中允许的所有内容都将被允许。

实现这一点的方法是调用request_irq()以在收到相关IRQ时调用中断处理程序。 [17]该函数接收IRQ号,函数名,标志, / proc / interrupts的名称和传递给中断处理程序的参数。 通常有一定数量的IRQ可用。 有多少个IRQ依赖于硬件。 标志可以包括SA_SHIRQ以指示您愿意与其他中断处理程序共享IRQ(通常是因为许多硬件设备位于同一个IRQ上)和SA_INTERRUPT以指示这是一个快速中断。 只有在此IRQ上还没有处理程序,或者您愿意共享时,此功能才会成功。

然后,在中断处理程序中,我们与硬件通信,然后使用queue_work() mark_bh(BH_IMMEDIATE)来安排下半部分。

英特尔架构上的键盘

本章的其余部分完全针对英特尔。 如果您没有在英特尔平台上运行,它将无法运行。 甚至不要尝试在这里编译代码。

我在编写本章的示例代码时遇到了问题。 一方面,为了一个有用的例子,它必须在每个人的计算机上运行并产生有意义的结果。 另一方面,内核已经包含了所有常见设备的设备驱动程序,这些设备驱动程序将不会与我要编写的内容共存。 我发现的解决方案是为键盘中断写一些东西,并首先禁用常规键盘中断处理程序。 由于它被定义为内核源文件中的静态符号(特别是drivers / char / keyboard.c ),因此无法恢复它。 在insmod这个代码之前,做另一个终端睡眠120; 如果您重视文件系统,请重新启动

此代码将自身绑定到IRQ 1,IRQ 1是在英特尔架构下控制的键盘的IRQ。 然后,当它收到键盘中断时,它会读取键盘的状态(这是inb(0x64)的目的)和扫描码,这是键盘返回的值。 然后,只要内核认为它是可行的,就会运行got_char ,它会给出所用密钥的代码(扫描代码的前七位)以及是否已按下(如果第8位为零)或已释放(如果这是一个)。

例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(KERN_INFO "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");

第13章对称多处理

提高硬件性能的最简单和最便宜的方法之一是在板上放置多个CPU。 这可以通过使不同的CPU处理不同的作业(非对称多处理)或者使它们全部并行运行,执行相同的工作(对称多处理,也称为SMP)来完成。 有效地进行非对称多处理需要有关计算机应该执行的任务的专业知识,这在Linux等通用操作系统中是不可用的。 另一方面,对称多处理相对容易实现。

相对简单,我的意思是:不是说它真的很容易。 在对称的多处理环境中,CPU共享相同的内存,因此在一个CPU中运行的代码会影响另一个CPU使用的内存。 您不能再确定您在上一行中设置为某个值的变量仍然具有该值; 在你不看的时候,其他CPU可能已经玩过了。 显然,这样编程是不可能的。

在进程编程的情况下,这通常不是问题,因为进程通常一次只能在一个CPU上运行[18] 。 另一方面,内核可以由在不同CPU上运行的不同进程调用。

在2.0.x版本中,这不是问题,因为整个内核都在一个大的自旋锁中。 这意味着如果一个CPU在内核中并且另一个CPU想要进入,例如由于系统调用,则必须等到第一个CPU完成。 这使Linux SMP安全[19] ,但效率低下。

在2.2.x版本中,几个CPU可以同时在内核中。 这是模块编写者需要注意的事情。

第14章常见陷阱

在我发送你走向世界并编写内核模块之前,我需要提出一些警告你的事情。 如果我没有发出警告并且发生了不好的事情,请向我报告问题,以便全额退还我为您的图书副本支付的金额。

使用标准库
你不能这样做。 在内核模块中,您只能使用内核函数,这些函数可以在/ proc / kallsyms中看到 。

禁用中断
您可能需要在短时间内执行此操作,这是正常的,但如果您之后未启用它们,系统将会卡住,您将不得不关闭它。

把头伸进大食肉动物里面
我可能不必警告你这件事,但我想我还是会的,以防万一。

附录

Symbols
/etc/conf.modules, How Do Modules Get Into The Kernel?
/etc/modules.conf, How Do Modules Get Into The Kernel?
/proc filesystem, The /proc File System
/proc/interrupts, Interrupt Handlers
/proc/kallsyms, Functions available to modules, Name Space, Common Pitfalls
/proc/meminfo, The /proc File System
/proc/modules, How Do Modules Get Into The Kernel?, The /proc File System
2.6 changes, Changes between 2.4 and 2.6
_IO, Talking to Device Files (writes and IOCTLs)
_IOR, Talking to Device Files (writes and IOCTLs)
_IOW, Talking to Device Files (writes and IOCTLs)
_IOWR, Talking to Device Files (writes and IOCTLs)
__exit, Hello World (part 3): The __init and __exit Macros
__init, Hello World (part 3): The __init and __exit Macros
__initdata, Hello World (part 3): The __init and __exit Macros
__initfunction(), Hello World (part 3): The __init and __exit Macros

猜你喜欢

转载自blog.csdn.net/yeshennet/article/details/82315921