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 <asm/uaccess.h>
    该头文件声明了在内核代码和用户空间之间移动数据的函数。
    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。 和信号量不同,自旋锁可在不能休眠的代码中使用,比如中断处理例程(因为信号量会引起休眠)。如果锁可用,则“锁定”位被设置,而代码继续进入临界区;相反,如果锁被其他人获得,则代码进入忙循环并重复检查该锁,直到该锁可用为止。“测试并设置”的操作必须以原子的方式完成。
    任何拥有自旋锁的代码都必须是原子的,它不能休眠。它不能因为任何原因放弃处理器,除了服务中断以外。
    内核抢占的情况由自旋锁代码本身管理,任何时候,只要内核代码拥有自旋锁,在相关处理器上抢占就会被禁止。
 
    如果我们有一个自旋锁,它可以在中断处理例程中使用获得,则必须使用某个禁止中断的spin_lock(spin_lock_irq或spin_lock_irqsave),因为使用其他的锁定函数迟早会导致系统死锁。如果我们不会在硬件中断处理例程中访问自旋锁,则应该使用spin_lock_bh,以便在安全避免死锁的同时还能服务硬件中断。这些在编写硬件设备驱动应特别留意。
 
    如果我们拥有信号量和自旋锁的组合,则必须首先获得信号量;在拥有自旋锁时,调用down(可导致休眠)是个严重的错误。
 
    自旋锁的初始化:

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);
//可被中断信号打断。这可返回一个整数值,非零值表示休眠被某个信号打断,这个时候返回一个-ERESTARTSYS

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只会唤醒那些可中断休眠的进程

另外,我们常常看到有时候用以下一大段代码来代替一个简单的wait_event_interrupt(wq, condition),这是因为在schedule()切换进程之前,通过up(&dev->sem)来释放信号量,从而避免了死锁的发生。当多个等待队列、信号量等机制同时发生时,谨防死锁。参考例子见: FIFO特性的globalmem模块驱动

DECLARE_WAITQUEUE(wait, current);
down(&dev->sem);
add_wait_queue(&dev->r_wait, &wait);
...
__set_current_state(TASK_INTERRUPTIBLE);
//改变进程状态为睡眠

//事实上add_wait_queue、__set_current_state可用一条语句代替:prepare_to_wait(&dev->r_wait, &wait, TASK_INTERRUPTIBLE),详见prepare_to_wait()的实现
up(&dev->sem);
//释放信号量,让等待该信号量的进程得以唤醒
schedule();
//调度其他进程执行。通过改变当前状态,我们只是改变了调度器处理该进程的方式,但尚未使进程让出处理器
if (signal_pending(current))
//如果是因为信号唤醒
{...}
...
remove_wait_queue(&dev->r_wait, &wait);

 
 
18、异步通知与异步IO
    暂空,保留位置
 
19、中断与时钟
    Linux的中断处理架构分解为两个部分:顶半部和底半部。顶半部往往只是处理比较紧急的功能,随后就进行“登记中断”的工作。“登记中断”是将底半部处理程序挂到该设备的底半部执行队列中去。
    底半部机制的实现有tasklet、工作队列和软中断。软中断和tasklet仍然运行在中断上下文,而工作队列则运行于进程上下文。因此软中断和tasklet处理函数不能睡眠,而工作队列处理函数中允许睡眠。local_bh_disable()和local_bh_enable()是内核中用于禁止和使能软中断和tasklet底半部机制的函数。tasklet是基于软中断实现的。
 

//申请irq
int request_irq(unsigned int irq,
                void (*handler)(int irq, void *dev_id, struct pt_regs *regs),
                const char * devname,
                void *dev_id);
//释放irq
void free_irq(unsigned int irq, void *dev_id);

//对所有cpu屏蔽使能一个中断源
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);

//对本cpu内的所有中断屏蔽恢复
void local_irq_save(unsigned long flags);
void local_irq_disable(void);
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);

/*tasklet使用模板*/
void my_do_tasklet(unsigned long);
DECLARE_TASTLET(my_tasklet, my_do_tasklet, 0);

//中断处理底半部
void my_do_tasklet(unsigned long)
{
  ...
}

//中断处理顶半部
irqreturn_t my_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
  ...
  tasklet_schedule(&my_tasklet);//tasklet_schedule()调度的tasklet函数my_do_tasklet()在适当的时候得到执行
  ...
}

int __init my_init(void)
{
  ...
  result = request_irq(my_irq, my_interrupt, SA_INTERRUPT, "MY", NULL);
 ...
}

void __exit my_exit(void)
{
  ...
  free_irq(my_irq, my_interrupt);
  ...
}

/*工作队列*/
//与tasklet类型,如下只写个大概

struct work_struct my_wq;
void my_do_work(unsigned long);


irqreturn_t my_interrupt(int irq, void *dev_id, struct pt_regs *regs)
(
  ...
  schedule_work(&my_wq);
  ...
)


int __init my_init(void)
{
  ...
  result = request_irq(my_irq, my_interrupt, SA_INTERRUPT, "MY", NULL);
  ...
  INIT_WORK(&my_wq, (void (*)(void *))my_do_work, NULL);
  ...
}

    至于软中断,用softirq_action结构体表征一个软中断,这个结构体包含软中断处理函数指针和传递给该函数的参数。使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()可以触发一个软中断。

 
    时钟中断处理程序执行update_process_timer()函数,该函数调用run_local_timer()函数,这个函数处理TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。
    timer_list结构体的一个实例对应一个定时器。

struct timer_list {
  struct list_head entry;
  unsigned long expires;//定时器到期时间
  void (*function)(unsigned long);//定时器处理函数
  unsigned long data;//作为参数被传入定时器处理函数
  struct timer_base_s *base;
};

/*定时器使用模板*/
struct my_dev
{
  struct cdev cdev;
  ...
  timer_list my_timer;
};


static int my_funt(...)
{
  struct my_dev *dev = filp->private_data;
  ...
  init_timer(&dev->my_timer);//初始化定时器结构体,将成员entry的next初始化为NULL,并给base指针赋值
  dev->my_timer.function = &my_do_timer;//
  dev->my_timer.data = (unsigned long)dev;
  dev->my_timer.expires = jiffies + delay;//
  add_timer(&dev->my_timer);//注册定时器,将定时器加入到内核动态定时器链表中
  ...
}


static void timer_exit_func(...)
{
  struct my_dev *dev = filp->private_data;
  ...
  del_timer(&dev->my_timer;//
}


static void my_do_timer(unsigned long arg)
{
  struct my_dev *dev = (struct my_dev *)(arg);
  ...
  mod_timer(&dev->my_timer, jiffies + delay);//修改定时器的到期时间,在新的被传入的expires到来后才会执行定时器函数
  ...
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值