:
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是内核中用来表示设备编号的数据类型。
------------------------------------------------------------------------
该头文件声明了在内核代码和用户空间之间移动数据的函数。
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只会唤醒那些可中断休眠的进程
|