Linux驱动学习笔记
本文是作者的Linux驱动学习相关笔记。
目录
一.Linux驱动基础介绍
二.第一个内核驱动模块
三.内核模块的参数
四.第一个字符设备驱动
五.给字符驱动设备添加读写功能
六.自动创建设备节点
一.Linux驱动基础介绍
Linux驱动模块简单来说就是运行在Linux内核里的,直接控制硬件和虚拟设备的内核模块代码。而与其对应就是运行在用户态的应用程序。下面区分下用户态和内核态的主要区别:
- 用户态使用普通用户,权限受限;内核态使用root用户,是最高权限。
- 用户态使用glibc标准库函数;内核态使用Linux内核的标准API。
- 用户态的应用程序是main()函数是程序入口,exit()函数是程序出口;内核态的驱动模块是module_init是程序入口,module_exit是程序出口。
- 用户态的应用程序使用gcc进行编译链接;内核态的驱动模块使用Makefile进行编译,同时使用insmod进行模块的安装,rmmod卸载模块。
驱动设备分为三种,分别是字符设备,块设备,网络设备。下面介绍这三种设备:
- 字符设备:按字节流读写,顺序存取。主要包括键盘,鼠标,串口,LED,传感器等。
- 块设备:按数据块读写,有缓存,可随机读写。主要包括硬盘,U盘,Flash等。
- 网络设备:不在/dev文件系统下,专门负责收发网络数据包。主要有网卡,虚拟网卡等。
二.第一个内核驱动模块
下面将介绍第一个内核模块的编写。首先创建一个独立文件夹,然后在里面创建一个hello.c文件和一个Makefile文件。目录如下:
/module1 |---hello.c |---Makefile下面是C文件的内容。首先是必须添加的头文件。
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h>然后是模块的开源声明,主要负责告诉内核模块开源,遵守GPL协议。
MODULE_LICENSE("GPL");然后就是内核的初始化和退出函数的编写,这里要注意使用了printk()函数,相当于用户态的printf()函数,也是用于打印信息。然后信息的字符串前可以加上描述的宏。
int hello_init(void){ printk(KERN_INFO "Hello kernel!\n"); return 0; } void hello_exit(void){ printk(KERN_INFO "Goodbye kernel!\n"); }最后就是使用宏函数进行模块入口和出口的声明。
module_init(hello_init); module_exit(hello_exit);下面是Makefile文件的内容。首先是让内核编译系统,将hello.c编译成可加载的内核模块。然后是执行make可以自动编译出.ko内核模块。然后执行clean可以清除所有编译产生的中间垃圾文件。
obj-m += hello.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: rm -f *.o *.mod.o *.mod *.mod.c Module.symvers modules.order .*.cmd *.symvers然后下面就是具体的编译部分了。首先执行下面的命令就可以编译并删除中间文件。
make && make clean然后是切换成root超级管理员账号。这里需要输入密码。
sudo su然后首先要清除内核环形缓冲区日志,防止一会日志内容繁琐。
dmesg -c然后就可以进行内核模块的安装了。后面的参数需要使用相对应的内核模块名称。
insmod hello.ko这样就将这个内核模块安装了,然后使用下面的模块进行查看内核的日志,就可以看到hello_init的信息了。
dmesg也可以使用下面的命令加管道进行驱动模块的查看,管道后面的名字要注意更换。
lsmod | grep hello最后就是卸载模块了,后面的模块的名字也需要改变。
rmmod hello然后可以使用下面的命令查看日志,就可以看都hello_exit的信息了。
dmesg最后附上完整的hello.c的代码:
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> MODULE_LICENSE("GPL"); int hello_init(void){ printk(KERN_INFO "Hello kernel!\n"); return 0; } void hello_exit(void){ printk(KERN_INFO "Goodbye kernel!\n"); } module_init(hello_init); module_exit(hello_exit);恭喜你,这样就完成了第一个内核模块啦!
三.内核模块的参数
下面要在前面的基础程序上加上参数的输入。可以输入的参数的类型包括short,int,long,charp,array类型。基础类型的输入可以使用module_param这个宏函数。数组可以使用module_param_array这个宏函数,下面要进行参数输入的演示:
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> MODULE_LICENSE("GPL"); short myshort = 10; module_param(myshort,short,S_IRUGO); int myint = 20; module_param(myint,int,S_IRUGO); long mylong = 30; module_param(mylong,long,S_IRUGO); char* mychar = "Hello"; module_param(mychar,charp,S_IRUGO); int myarray[4] = {1,2,3,4}; int num = 4; module_param_array(myarray,int,&num,S_IRUGO); static int hello_init(void){ printk(KERN_INFO "Hello kernel!\n"); printk("myshort=%d\n",myshort); printk("myint=%d\n",myint); printk("mylong=%ld\n",mylong); printk("mychar=%s\n",mychar); printk("myarray[0]=%d\n",myarray[0]); printk("myarray[1]=%d\n",myarray[1]); printk("myarray[2]=%d\n",myarray[2]); printk("myarray[3]=%d\n",myarray[3]); return 0; } static void hello_exit(void){ printk(KERN_INFO "Goodbye kernel!\n"); } module_init(hello_init); module_exit(hello_exit);然后使用下面的命令进行参数的输入,参数可以改变。如果不输入参数,就会输入默认参数。
insmod hello.ko myshort=10 myint=20 mylong=30 mychar="guan" myarray=5,6,7,8然后同样用dmesg查看内核日志。
四.第一个字符设备驱动
之前介绍有三种设备驱动,首先我们开始介绍字符设备。Linux系统使用一个设备号表示字符设备。设备号包括主设备号和次设备号,主设备号负责识别是哪类驱动,次设备号负责识别是这类驱动的哪个设备。一般使用MKDEV宏函数合成设备号。
Linux字符设备使用cdev结构体,一个cdev结构体就可以代表一个字符设备,其中设备号就是这个结构体的一个成员,cdev其中还包含file_operations结构体类型的指针,而file_operations结构体就是字符设备的操作函数的函数指针的集合。
下面我们就要从字符设备开始编写。我们的目的是在内核中编写一个字符设备的驱动,然后在用户态用一个应用程序使用这个字符设备。下面是我们的文件夹的目录结构,其中mychar.c是驱动设备源文件,test.c是应用层的调用文件,当然还有Makefile内核编译文件。
/module1 |---mychar.c |---Makefile |---test.c然后开始编写mychar.c文件。首先就是定义主设备号和次设备号,然后用cdev定义自己的字符设备。然后用file_operations定义字符设备的行为,因为这个是最基础的,所以只定义open和release两个函数。定义好后在file_operarions里输入函数名进行注册即可。
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/cdev.h> int major = 500; // 主设备号 int minor = 0; // 次设备号 struct cdev cdev; static int open_char(struct inode *inode,struct file *filp){ printk("The char device opened!\n"); return 0; } static int release_char(struct inode *inode,struct file *filp){ printk("The char device closed!\n"); return 0; } static struct file_operations fops = { .owner = THIS_MODULE, .open = open_char, .release = release_char, };然后就是模块的初始化和退出函数。初始化函数首先要使用MKDEV创建起始字符设备号,然后要用register_chrdev_region告诉内核要创建几个这样的设备,这样次设备号就会顺序延续下去。然后就用cdev_init将这个设备初始化,同时用cdev_add在内核中添加该字符设备。而在退出函数中就要首先使用cdev_del在内核中删除这个设备,然后使用unregister_chrdev_region释放设备号,这里要注意初始化和退出的顺序是反的,不要弄错。
static int char_init(void) { dev_t devno = MKDEV(major, minor); int ret = register_chrdev_region(devno, 1, "mychar"); // 创建字符设备号 if (ret < 0) { printk("fail to get devno!\n"); return ret; } cdev_init(&cdev, &fops); // 字符设备初始化 cdev.owner = THIS_MODULE; ret = cdev_add(&cdev, devno, 1); // 添加字符设备 if (ret < 0) { printk("fail to add cdev!\n"); return ret; } printk("char drive install seccess!\n"); return 0; } static void char_exit(void) { dev_t devno = MKDEV(major, minor); cdev_del(&cdev); // 删除字符设备 unregister_chrdev_region(devno, 1); // 释放字符设备号 printk("char drive uninstall success!\n"); } MODULE_LICENSE("GPL"); module_init(char_init); module_exit(char_exit);下面是对test.c的编写,这个是应用层调用我们写的字符设备的程序,只是用open打开设备文件,然后输出这个设备的文件描述符,最后在关闭文件。还有不要忘记更改Makefile中的文件名字。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(int argc, char *argv[]){ int fd = open("/dev/mychar",O_RDWR); if(fd < 0){ perror("open"); exit(-1); } printf("fd=%d\n",fd); close(fd); return 0; }下面是编译过程,首先是使用Makefile编译,清空日志,然后就是安装模块并查看。
make && make clean sudo su dmesg -c insmod mychar.ko lsmod | grep mychar然后就是使用mknod命令在/dev下创建设备文件驱动节点,这样在用户态才能找到这个设备,写入指定的设备类型和主设备号和次设备号,然后就是使用gcc编译test.c文件,最后运行就可以看到这设备的文件描述符了。然后这个也可以使用dmesg查看内核日志。
mknod /dev/mychar c 500 0 ls -l /dev/mychar gcc test.c -o test ./test dmesg最后就是卸载模块了,再查看后发现字符设设备消失了,同时设备号也没有了。
rmmod mychar lsmod | grep mychar cat /proc/devices这样你就完成第一个字符设备的驱动编写了。
五.给字符驱动设备添加读写功能
刚才我们已经完成了第一个字符驱动程序的编写,但是这个字符设备只有打开关闭的功能,下面我们要丰富这个设备的功能,我们要给这个设备添加读写功能。
首先要讲下读写的基本过程。首先用memset在用户态的缓冲区内存中填充数据,然后使用write从内存里写入内核缓冲区,这里用来模拟真实硬件寄存器中的采样值。而其实内核则调用file_operations里的write函数指针,然后在write对应的函数里使用copy_from_user才将数据拷贝到驱动程序里定义的缓冲区。然后是在用户态使用read将数据从内核缓冲区里读出,然后用printf打印。其实内核也是调用file_operations里的read函数指针,然后在驱动中的read对应的函数中使用copy_to_user才将数据从内核缓冲区拷贝到用户缓冲区,才能打印。下面是驱动需要添加的两个函数,其他代码全都保留,同时要再file_operations里注册两个新函数。
char dribuf[128] = {0}; static ssize_t read_char(struct file *filp,char __user *usrbuf,size_t count,loff_t *off){ ssize_t ret = 0; ret = copy_to_user(usrbuf,dribuf,count); if(ret < 0){ printk("驱动写入应用程序失败!\n"); return ret; } else{ printk("driver write %ld byte\n",count); } return ret; } static ssize_t write_char(struct file *filp,const char __user *usrbuf,size_t count,loff_t *off){ ssize_t ret = 0; ret = copy_from_user(dribuf,usrbuf,count); if(ret < 0){ printk("驱动读应用程序失败!\n"); return ret; } else{ printk("driver read %ld byte\n",count); } return ret; } static struct file_operations fops = { .owner = THIS_MODULE, .open = open_char, .release = release_char, .read = read_char, .write = write_char, };下面是对应的应用层代码,使用了write和read函数进行缓冲区的读写。
int main(int argc, char *argv[]){ char buf[128] = {0}; int fd = open("/dev/mychar",O_RDWR); if(fd < 0){ perror("open"); exit(-1); } printf("fd=%d\n",fd); memset(buf,'O',sizeof(buf)-1); int ret = write(fd,buf,sizeof(buf)); if(ret < 0){ perror("write"); exit(-1); } memset(buf,0,sizeof(buf)); ret = read(fd,buf,sizeof(buf)); if(ret < 0){ perror("read"); exit(-1); } printf("buf=%s\n",buf); close(fd); return 0; }然后就是编译驱动程序和应用程序,运行应用程序后,再使用dmesg查看内核日志了。这样就完成字符设备的读写功能了。
六.自动创建设备节点
在刚才的案例中,我们使用了mknod在用户态创建字符设备的节点,才能在/dev目录访问设备。但是在实际的Linux中,由于设备很多,设备号不固定,热插拔频繁等问题。导致手动创建设备节点非常不方便,也不规范。所以下面我们将使用Linux提供的设备类和设备实例,同时配合udev守护进程来创建设备节点。主要用到下面的四个函数。
//创建设备类 struct class *class_create(struct module *owner,const char *name); //创建设备实例 struct device *device_create(struct class *class,struct device *parent, dev_t devt,void *drvdata,const char *fmt, ...); void device_destroy(struct class *class,dev_t devt);//销毁设备实例 void class_destroy(struct class *cls);//销毁设备类下面是具体使用方法,其他的都不需要改动,这样就不需要使用mknod手动创建设备节点了。首先在init中,先完成字符设备注册,再创建 class,最后创建 device。在exit中,先销毁 device,再销毁 class,最后注销字符设备。
struct class *class; static int char_init(void) { dev_t devno = MKDEV(major, minor); int ret = register_chrdev_region(devno, 1, "mychar"); if (ret < 0) { printk("fail to get devno!\n"); return ret; } cdev_init(&cdev, &fops); cdev.owner = THIS_MODULE; ret = cdev_add(&cdev, devno, 1); if (ret < 0) { printk("fail to add cdev!\n"); return ret; } class = class_create("hello"); if((void*)class == NULL){ printk("class_create failed\n"); return -EFAULT; } device_create(class,NULL,devno,NULL,"hello"); printk("char drive install seccess!\n"); return 0; } static void char_exit(void) { dev_t devno = MKDEV(major, minor); device_destroy(class,devno); class_destroy(class); cdev_del(&cdev); unregister_chrdev_region(devno, 1); printk("char drive uninstall success!\n"); }到这里就完成一个基础的字符设备的驱动框架了。
