:
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
最重要的头文件之一。包含驱动程序使用的大部分内核API的定义,包括睡眠函数以及各种变量声明。
------------------------------------------------------------------------
struct task_struct *current;当前进程。current->pid、current->comm:当前进程的进程ID和命令名。
------------------------------------------------------------------------
#include
必需的头文件,必须包含在模块源代码中。
------------------------------------------------------------------------
#include
module_param(variable, type, perm);//perm表示该变量的用户许可。
用于创建模块参数的宏,用户可在装载模块时调整这些参数的值。参数类型可以是:bool、charp、int、invbool、long、short、ushort、uint、ulong或者intarray。
------------------------------------------------------------------------
#include
int printk(const char * fmt, ...);
函数printf的内核代码。
------------------------------------------------------------------------
#include
dev_t是内核中用来表示设备编号的数据类型。
------------------------------------------------------------------------
该头文件声明了在内核代码和用户空间之间移动数据的函数。
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到来后才会执行定时器函数 ... }
|