Linux驱动学习笔记

张开发
2026/4/18 8:35:16 15 分钟阅读

分享文章

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);恭喜你这样就完成了第一个内核模块啦三.内核模块的参数下面要在前面的基础程序上加上参数的输入。可以输入的参数的类型包括shortintlongcharparray类型。基础类型的输入可以使用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 myshort10 myint20 mylong30 mycharguan myarray5,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); }到这里就完成一个基础的字符设备的驱动框架了。

更多文章