目录
内核模块
内核模块的概念
内核模块程序的一般形式
内核模块的相关工具
内核模块参数
内核模块和普通应用程序区别
字符设备驱动
字符设备驱动基础
字符设备驱动框架
linux是 宏内核(单内核)的操作系统的典型代表,它和微内核(典型代表window操作系统)的最大区别在于所有的内核功能都能被整体编译在一起,形成一个单独的内核镜像文件,显著的优点就是效率非常高,内核中各功能模块的交互通过直接的函数调用来进行,而微内核只实现内核中相当关键和核心的一部分,其他功能模块被单独编译,功能模块之间的交互需通过微内核提供的通信机制来建立。
但对于linux这类宏内核,缺点很明显,如果要增加,删除,修改内核的某个功能,不得不重新编译整个内核,然后重启整个系统。为了弥补这一缺点,Linux引入了内核模块。
内核模块就是被单独编译的一段代码,它可以在需要的时候动态的加载到内核,从而动态的增加内核的功能,不需要的时候可以动态卸载,从而减少内核的功能,并节约一部分内存,加载或卸载都不需要重启整个系统,所有在进行驱动开发的时候非常好用。内核模块这一特点也有助于减少内核镜像文件体积,自然也减少了内核占用的内存空间。
//1.头文件
#include
#include
//2.模块初始化
static int init_module(void)
{
printk("module init\n");
return 0;
}
//3.模块清除
static void cleanup_module(void)
{
printk("cleanup module\n");
}
//4.注册
module_init(init_module);
module_exit(cleanup_module);
//5.信息
MODULE_LICENSE("GPL");
模块初始化函数一般在模块被动态加载到内核时调用,返回0表示初始化成功,通常返回一个负值表示失败,一般在模块初始化中还会对某些对象进行初始化,比如对内存进行分配对驱动进行注册等等。模块的清除函数在模块从内核中卸载时被调用。
MODULE_LICENSE("GPL")
MODULE_LICENSE是一个宏,里面的参数是一个字符串,代表相应的许可证协议(GPL协议是将linux内核源码进行任意的修改和再发布的同时必须将修改后的源码发布,因为linux是一个开源项目)
1.模块加载
insmod mymod.ko
insmod:加载指定目录下的一个.ko文件到内核
modprobe:自动加载模块到内核;前提条件是模块要执行安装操作,运行前最好运行depmod来更新模块依赖信息(不需指定路径和后缀)
2.模块信息
modinfo:查看模块信息
3.模块卸载
rmmod:将模块从内核中卸载
模块的初始化函数在模块被加载时调用,但是该函数不接受参数,但如果我们想在加载时对模块的行为进行控制,比如我们编写了一个串口驱动,想要在加载时波特率由命令行参数设定,这种方式我们叫做模块参数。
在模块的加载过程中中,加载程序会得到命令行参数,并转换成相应类型的值,然后赋值给对应变量,这个过程发生在调用模块初始化函数之前。(bool,invbool,charp字符串指针short,int,long,ushort,uint,ulong)
module_param(变量名,变量类型,权限)
module_param(变量名,数组元素类型,数组元素个数的指针(可选),权限)
#include
#include
int val =1;
int val2 = 2;
module_param(val,int,0000);
module_param(val2,int,0000);
static int mod_init(void)
{
printk("mod_init val=%d,val2=%d\n",val,val2);
return 0;
}
static void mod_exit(void)
{
printk("mod_exit\n");
;
}
module_init(mod_init);
module_exit(mod_exit);
MODULE_LICENSE("GPL");
1.内核模块运行在内核空间;应用程序运行在用户空间。
2.内核模块是被动的被调用,应用程序是顺序执行。
3.内核模块处于c函数库之下,不能调用C库函数,应用程序可以任意调用
4.内核模块产生非法访问可能导致整个系统崩溃,而应用程序通常只影响自己。
5.内核模块的并发更多,而应用程序一般只考虑多线程多进程
6.内核空间的调用链上只有4kb或8kb的栈,相对于应用程序很小,如果需要较大内存空间需要动态分配。
7.printk不支持浮点数打印
linux系统中根据驱动程序实现的模型框架将设备的驱动分为:字符设备驱动,块设备驱动,网络设备驱动。
在linux系统中,一切皆文件,应用程序要对设备进行访问,最终就会转化为对文件的访问,这样可以统一对上层的接口。设备文件通常在/dev目录下
ls -l /dev //可以看到目录下的设备文件
属性中第一个字符表示文件属性:b为块设备,c为字符设备,设备文件会比普通文件多出两个数字,这两个数字分别是主设备号和次设备号。这两个号是设备在内核中的身份和标志,是内核区分不同设备的唯一信息,通常内核用主设备号区分一类设备,次设备号区分同一类设备的不同个体或不同分区。
设备文件(设备节点)在linux系统中往往是自动创建的,但我们也可以通过mknod命令来手动创建,但是设备号不能冲突。
mknod /dev/myled c(设备属性) 253(主设备号) 0(次设备号)
cat /proc/devices //查看设备信息
要实现一个字符设备驱动,需要先构造一个cdev对象,并让cdev同设备号和设备的操作方法集合相关联,然后将该cdev结构对象添加到内核的cdev_map散列表中。首先我们需要在驱动中注册设备号,有静态注册和动态注册两种方法,为了防止设备冲突注册失败,我们一般选择动态注册。
下面我们列出相关函数:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
//动态注册
//dev:主设备号
//baseminor:次设备号,第一个次设备号
//count : 设备个数
//name:设备名
//返回值:成功返回0,失败返回错误码(负值)
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
//字符设备初始化
//cdev: cdev设备
//fops: 字符设备操作集
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
//注册字符设备到内核
//p: cdev设备
//dev: 设备号
//count:个数
//返回值:失败返回负值
void unregister_chrdev_region(dev_t from, unsigned count)
//释放设备号
void cdev_del(struct cdev *p);
//从内核中删除
//p: cdev设备
上述函数顺序也是我们在模块加载和卸载的顺序,先分配设备号,然后将字符设备初始化,再将字符设备注册到内核中。
我们再介绍几个宏
MAJOR:从设备号中取出主设备号
MINOR:从设备号中取出次设备号
描述字符设备的结构体:
struct cdev { //描述字符设备结构体
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
//字符设备操作集
//一般会根据实际情况,实现对应的操作
struct file_operations {
struct module *owner;
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*mmap) (struct file *, struct vm_area_struct *);
long (*unlocked_ioctl)(struct file *filp, unsigned int cmd, unsigned long arg);
int (*open) (struct inode *, struct file *); /* 打开设备 */
int (*release) (struct inode *, struct file *); /* 关闭设备 */
int (*flush) (struct file *, fl_owner_t id);
loff_t (*llseek) (struct file *, loff_t, int);
int (*fasync) (int, struct file *, int);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
...
};
我们看一段示例代码:
//1.头文件
#include
#include
#include
#include
#define LED_ON 1
#define LED_OFF 2
//2.模块加载与卸载
static dev_t devnum;//设备号
static char *name = "myled"; //设备名
static struct cdev mycdev; //字符设备
long led_ioctl(struct file *pf, unsigned int cmd, unsigned long args);
static struct file_operations myfops={
.unlocked_ioctl = led_ioctl,
};
static int mod_init(void)
{
//与内核相关
int ret;
//1.分配设备号
ret = alloc_chrdev_region(&devnum, 0, 1, name);
if(ret!=0){
return ret;
}
//2.初始化字符设备cdev
cdev_init(&mycdev,&myfops);
//3.注册到内核
ret = cdev_add(&mycdev,devnum,1);
if(ret<0){
return ret;
}
printk("register success %d,%d\n",MAJOR(devnum),MINOR(devnum));
//与硬件相关
return 0;
}
static void mod_exit(void)
{
//1.从内核中删除cdev
cdev_del(&mycdev);
//2.释放设备号
unregister_chrdev_region(devnum,1);
}
/*********IO操作***********/
long led_ioctl(struct file *pf, unsigned int cmd, unsigned long args)
{
printk("led_ioctl:%d\n",cmd);
switch (cmd)
{
case LED_ON: printk("do_led_on\n");break;
case LED_OFF: printk("do_led_off\n");break;
default: break;
}
return 0;
}
//3.注册
module_init(mod_init);
module_exit(mod_exit);
//4.模块信息(许可证)
MODULE_LICENSE("GPL");