Chinaunix首页 | 论坛 | 博客
  • 博客访问: 641205
  • 博文数量: 75
  • 博客积分: 7001
  • 博客等级: 少将
  • 技术积分: 1465
  • 用 户 组: 普通用户
  • 注册时间: 2007-07-11 17:39
文章分类

全部博文(75)

文章存档

2010年(1)

2009年(25)

2008年(49)

我的朋友

分类: LINUX

2008-07-01 20:10:44

The Linux Kernel Module Programming Guide笔记


 
1、通过lsmod来获得内核已加载了那些模块,这个命令是读取/proc/modules文件的内容来获得信息的。
 
2、内核模块管理守护进程kmod执行modprobe去加载内核模块。modprobe的功能和insmod类似,但是它除了装入指定模块外,还同时装入指定模块所依赖的其他模块。
 
3、如果内核中打开了CONFIG_MODVERSIONS选项,则为某个指定版本内核编译的模块将不能被另一版本的内核加载。所以在开发的工程中,最好将内核中的这个选项关闭。
 
4、建议在控制台下输入文档中的范例代码,编译然后加载模块,而不是在X下。这个可及时读取加载模块时的日志信息。
 
5、模块初始化函数(module_init)应该返回值为0,非0则表明初始化失败,该模块将不能被加载。
 
6、任一个内核模块需要包含linux/module.h我们仅仅需要包含linux/kernel.h当需要使用printk()记录级别的宏扩展时KERN_ALERN
 
7、printk()并不是设计用于用户交互的,它实际上用来为内核提供日志功能,记录模块信息和给出警告。它定义了八个优先级。我们可以使用KERN_ALERT这样的高优先级,来确保printk()将信息输出到控制台而不是添加到日志中。
 
8、关于宏__init和__exit。它们负责“初始化”和“清理收尾”的函数定义处的变化。如果模块是被编译到内核,而不是动态加载,__init会使初始化完成后丢弃该函数并收回所占的内存(__initdata的作用与__init类似,只不过对变量有效),__exit则将会忽略该收尾函数。
 
9、如果一个模块未定义清除函数,则内核不允许卸载该模块。
 
10、#include <linux/sched.h>
    最重要的头文件之一。包含驱动程序使用的大部分内核API的定义,包括睡眠函数以及各种变量声明。
------------------------------------------------------------------------
    struct task_struct *current;当前进程。current->pid、current->comm:当前进程的进程ID和命令名。
------------------------------------------------------------------------
    #include <linux/module.h>
    必需的头文件,必须包含在模块源代码中。
------------------------------------------------------------------------
    #include <linux/moduleparam.h>
    module_param(variable, type, perm);//perm表示该变量的用户许可。
    用于创建模块参数的宏,用户可在装载模块时调整这些参数的值。参数类型可以是:bool、charp、int、invbool、long、short、ushort、uint、ulong或者intarray。
------------------------------------------------------------------------
    #include <linux/kernel.h>
    int printk(const char * fmt, ...);
    函数printf的内核代码。
------------------------------------------------------------------------
    #include <linux/types.h>
    dev_t是内核中用来表示设备编号的数据类型。
------------------------------------------------------------------------
    #include
    该头文件声明了在内核代码和用户空间之间移动数据的函数。
    unsigned long copy_from_user(void *to, const void *from, unsigned long count);
    unsigned long copy_to_user(void *to, const void *from, unsigned long count);
 
11、kmalloc/kfree()
    原型:void kmalloc(size_t size, int priority);
    最大可开辟128k内存。priority可为GFP_KERNEL表示等待,GFP_ATOMIC表示不等待,如果分配不成功,返回0.
 
12、结构file_operations在头文件linux/fs.h中定义,用于存储驱动模块提供的对设备各种操作的函数指针。C99有这个结构的扩展,EX:
    struct file_operations xxx_fops = {
        .read = xxx_read,
        .write = xxx_write,
        .open = xxx_open,
        .release = xxx_release
    };
-----------------------------------------------------------------------------------
struct file结构:每一个设备文件都代表着内核中的一个file结构,该结构在头文件linux/fs.h中定义。指向该结构struct file的指针一般命名为filp。它内核在open时创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数。
-----------------------------------------------------------------------------------
struct inode结构:内核用inode结构在内部表示文件,而file结构表示打开的文件描述符。它包含了大量的有关文件的信息,但是只有以下两个字段对编写驱程有关:
dev_t i_rdev;//对表示设备文件的inode结构,包含了真正的设备编号。
struct cdev *i_cdev;//表示字符设备的内核的内部结构
 
13、请求一个字符设备编号。
    int register_chrdev_region(dev_t first, unsigned int count, char *name);
    first是要分配的设备编号范围的起始值。count是所请求的连续设备编号的个数。name是和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中。
    和大部分内核函数一样,register_chrdev_region的返回值在分配成功返回0.失败时返回一个负的错误码。
------------------------------------------------------------------------
    设备编号的动态分配
    int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
    dev为仅用于输出的参数。firstminor是被请求的第一个次设备号,通常为0.
------------------------------------------------------------------------
    释放设备编号
    void unregister_chrdev_region(dev_t first, unsigned int count);
    通常我们在模块的清除函数中调用该函数。
-----------------------------------------------------------------------------------
    一个通俗获取主设备号的代码如下:

if (xxx_major){
    dev = MKDEV(xxx_major, xxx_minor);
    result = register_chrdev_region(dev, xxx_nr_devs, "xxx");
} else {
    result = alloc_chrdev_region(&dev, xxx_minor, xxx_nr_devs, "xxx");
    xxx_major = MAJOR(dev);
}

if (result <0){

    printk(KERN_WARNING "xxx: can't get major %d\n", xxx_major);

    return result;

}

 
14、字符设备的注册。我们代码中应该包含头文件linux/cdev.h。其中的一个例子如下:

static void xxx_setup_cdev(struct xxx_dev *dev, int index)
{
    int err, devno = MKDEV(xxx_major, xxx_minor + index);
    cdev_init(&dev->cdev, &xxx_fops);
    dev->cdev.owner = THIS_MODULE;
    dev->cdev.ops = &xxx_fops;
    err = cdev_add(&dev->cdev, devno, 1);
    if (err)
        printk(KERN_NOTICE "Error %d adding xxx%d", err, index);
}

至于早期那种经典注册一个字符设备驱动程序则不应该再使用。
int register_chrdev(unsigned int major const char *name, struct file_operations *fops);
int unregister_chrdev(unsigned int major, const char *name);
 
15、信号量与互斥,头文件asm/semaphore.h。一个信号量本质上是一个整数值,它和一对函数联合使用,这对函数通常成为P和V。希望进入临界区的进程将在相关信号量上调用P。如果信号量的值大于0,则该值会减一,而进程得以继续;如果信号量的值为0,进程必须等待直到其他人释放该信号量。对信号量的解锁通过调用 V完成;该函数对信号量的值做加一操作,并在必要时唤醒等待的进程。
 
    信号量的初始化:

DECLARE_MUTEX(name);//一个称为name的信号量被初始化为1
DECLARE_MUTEX_LOCKED(name);一个称为name的信号量被初始化为0

    P函数称为down:

void down(struct semaphore *sem);//down减少信号量的值,并在必要时一直等待
int down_interruptible(struct semaphore *sem);//down_interruptible完成相同工作,它允许等待在某个信号量上的用户空间进程可被用户中断

int down_trylock(struct semaphore *sem);//down_trylock不会休眠,如果信号量在调用时不可获得,会立即返回一个非零值

作为通常的规则,我们不应该使用非中断操作。使用down_interruptible需要小心,如果操作被中断,该函数会返回非零值,而调用者不会拥有该信号量。对down_interruptible的正确使用需要始终检查返回值,并作成相应响应。

    V函数称为up:

void up(struct semaphore *sem);

16、自旋锁,头文件linux/spinlock.h。和信号量不同,自旋锁可在不能休眠的代码中使用,比如中断处理例程(因为信号量会引起休眠)。如果锁可用,则“锁定”位被设置,而代码继续进入临界区;相反,如果锁被其他人获得,则代码进入忙循环并重复检查该锁,直到该锁可用为止。“测试并设置”的操作必须以原子的方式完成。
 
    自旋锁的初始化:

spinlock_t xxx_lock = SPIN_LOCK_UNLOCKED;

    自旋锁的获得:

void spin_lock(spinlock_t *lock);//自旋锁的等待本质上不可中断,一旦调用spin_lock,在获得锁之前将一直处于自旋状态

void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
//在获得锁之前禁止中断,而之前的中断状态保存到flags

void spin_lock_irq(spinlock_t *lock);
//确保释放自旋锁时应该启用中断

void spin_lock_bh(spinlock_t *lock);//在获得锁之前禁止软件中断,但是会让硬件中断保持打开

    自旋锁的释放:

void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);

适用自旋锁的核心规则是:任何拥有自旋锁的代码都必须是原子的,它不能休眠。事实上,它不能因为任何原因放弃处理器,除了服务中断以外(某些情况下此时也不能放弃处理器)。任何时候,只要内核代码拥有自旋锁,在相关的处理器上的抢占就会被禁止。自旋锁必须在可能的最短时间内拥有。

17、休眠。永远不要在原子上下文中进入休眠。这意味着:我们的驱动程序不能在拥有自旋锁、seqlock或者RCU锁时休眠;如果我们已经禁止了中断,也不能休眠。同时我们对唤醒之后的状态不能做任何假定,因此必须检查以确保我们的等待的条件真正为真。

    初始化一个等待队列头:

DECLARE_WAIT_QUEUE_HEAD(name);

    当进程休眠时,它将期待某个条件会在未来成真。当一个休眠进程被唤醒时,它必须再次检查它所等待的条件的确为真。wait_event:

wait_event(queue, condition);//进程将被置于非中断休眠,通常不是我们所期望的。

wait_event_interruptible(quene, condition);
//可被中断信号打断。这可返回一个整数值,非零值表示休眠被某个信号打断。

wait_event_timeout(queue, condition, timeout);
//只会等待限定的时间

wait_event_interruptible_timeout(queue, condition, timeout);//只会等待限定的时间

我们的进程正在休眠中,用来唤醒休眠进程的基本函数是:wake_up

void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
//wake_up会唤醒等待在给定queue上的所有进程,wake_up_interruptible只会唤醒那些可中断休眠的进程

阅读(1592) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~