Linux驱动开发:内核模块和字符设备驱动

目录

内核模块

内核模块的概念

内核模块程序的一般形式

内核模块的相关工具

内核模块参数

内核模块和普通应用程序区别

字符设备驱动

字符设备驱动基础

字符设备驱动框架


内核模块

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");

你可能感兴趣的:(linux驱动开发,驱动开发,linux,运维)