1. 驱动开发 字符设备驱动
代码:
vser.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>#include <linux/fs.h>
/***** 设备相关信息 ******/
static unsigned int VSER_MAJOR = 256; //主设备号
static unsigned int VSER_MINOR = 0; //次设备号
#define VSER_DEV_CNT 1 //个数为1个
#define VSER_DEV_NAME "vser" //设备名称/***** 字符设备注册步骤 **************************************************** 初始化函数* 1.将主设备和次设备组合成设备号 MKDEV(主设备号,次设备号)* 2.将该设备号注册到内核 register_chrdev_region(设备号,设备个数,设备名称) 或 * 注销函数* 1.将申请的设备号注销 unregister_chrdev_region(设备号,设备个数) * * 跟文件系统下创建 设备文件 cat /proc/devices * 查看到设备信息 Character devices:1 mem256 vser 我们自己申请的设备信息4 /dev/vc/04 tty4 ttyS5 /dev/tty5 /dev/console * ***********************************************************************/static int __init vser_init(void)
{int ret; //用于获取函数返回值,并该函数是否执行成功dev_t dev; //用于存储设备号dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINORret = register_chrdev_region(dev,VSER_DEV_CNT,VSER_DEV_NAME);if(ret != 0){printk("静态申请设备号失败\n");ret = alloc_chrdev_region(&dev,VSER_MINOR,VSER_DEV_CNT,VSER_DEV_NAME);if(ret != 0){printk("动态申请设备号失败\n");goto register_err;}VSER_MAJOR = MAJOR(dev); //从设备号中提取主设备号VSER_MINOR = MINOR(dev); //从设备号中提取次设备号}return 0; //表示成功,直接结束函数并返回0register_err: /* 设备号申请失败 */return ret;}static void __exit vser_exit(void)
{dev_t dev; //用于存储设备号dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINORunregister_chrdev_region(dev,VSER_DEV_CNT);
}/****** 加入到内核 *******/
module_init(vser_init);
module_exit(vser_exit);
/****** 模块说明 ********/
MODULE_LICENSE("GPL"); /*开元许可协议:GPL协议*/
Makefile
#动态编译内核驱动生成.ko文件的Makeifle#自己的模块代码名
obj-m = vser.o #就会生成一个 vser.ko 文件#内核源代码路径
ifeq ($(ARCH),arm)KERNELDIR ?= /home/student/linux-5.4.31
elseKERNELDIR ?= /lib/modules/${shell uname -r}/build
endif#当前模块路径
PWD ?= $(shell pwd)#编译源码生成 .ko 文件 make all
all:${MAKE} -C ${KERNELDIR} M=${PWD} modules
#伪代码之清除垃圾
clean:rm Module.* modules.* *.mod *.ko
1. 命令: make (编译形成vser .ko 文件)
2. 命令 : mknod /dev/vser0 c 256 0 (创建一个字符设备)
解释
mknod == make node //创建一个节点
/dev/vser0 设备名称
c 字符设备
256 主设备号
0 次设备号
3. 命令 : ls -l /dev/vser0(设备查看存在)
4.命令: sudo rmmod vser (卸载模块 防止之前的模块没有卸载)
5. 命令 : sudo insmod vser.ko (加载 模块)
6. 命令 : lsmod ( 查看模块是否加载)
7. 命令: dmesg (查看 内核模块的输出 比如说 printk)(这个没有内核信息打印)
8. 过程多差不多!!!! (下面的 我将不写过程了,,除了一个驱动 多个设备)
2. 字符设备注册
代码:
vser.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>#include <linux/fs.h>
#include <linux/cdev.h>
/***** 设备相关信息 ******/
static unsigned int VSER_MAJOR = 256; //主设备号
static unsigned int VSER_MINOR = 0; //次设备号
#define VSER_DEV_CNT 1 //个数为1个
#define VSER_DEV_NAME "vser" //设备名称/**** 字符设备结构体 ******/
static struct cdev vser_cdev; //字符设备结构体/**** 设备操作相关函数 到时候就可以使用 open 打开该设备文件 *****/
int vser_open(struct inode *p_inode, struct file *p_file)
{printk("vser设备打开了\n");return 0;
}/**** 设备操作集 *****/
static struct file_operations vser_ops = { //设备操作集结构体.owner = THIS_MODULE,.open = vser_open,
};/***** 字符设备注册步骤 **************************************************** 初始化函数* 1.将主设备和次设备组合成设备号 MKDEV(主设备号,次设备号)* 2.将该设备号注册到内核 register_chrdev_region(设备号,设备个数,设备名称) 或 * 3.初始化字符设备 cdev_init(字符设备结构体地址,操作集结构体地址)* 4.将字符设备添加到内核散列表map中 cdev_add(字符设备结构体地址,设备号,设备个数)* 注销函数* 1.从内核散链表map中删除字符设备 cdev_del(字符设备结构体地址)* 2.将申请的设备号注销 unregister_chrdev_region(设备号,设备个数) * * 跟文件系统下创建 设备文件 cat /proc/devices * 查看到设备信息 Character devices:1 mem256 vser 我们自己申请的设备信息4 /dev/vc/04 tty4 ttyS5 /dev/tty5 /dev/console * ***********************************************************************/static int __init vser_init(void)
{int ret; //用于获取函数返回值,并该函数是否执行成功/********* 申请设备号 **************/dev_t dev; //用于存储设备号dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINORret = register_chrdev_region(dev,VSER_DEV_CNT,VSER_DEV_NAME);if(ret != 0){printk("静态申请设备号失败\n");ret = alloc_chrdev_region(&dev,VSER_MINOR,VSER_DEV_CNT,VSER_DEV_NAME);if(ret != 0){printk("动态申请设备号失败\n");goto register_err;}VSER_MAJOR = MAJOR(dev); //从设备号中提取主设备号VSER_MINOR = MINOR(dev); //从设备号中提取次设备号}/******** 申请字符设备 *********/cdev_init(&vser_cdev,&vser_ops);vser_cdev.owner = THIS_MODULE;ret = cdev_add(&vser_cdev,dev,VSER_DEV_CNT);if(ret != 0){printk("申请字符设备失败\n");goto cdev_err;}return 0; //表示成功,直接结束函数并返回0cdev_err: /* 申请字符设备失败 */unregister_chrdev_region(dev,VSER_DEV_CNT); //释放设备号register_err: /* 设备号申请失败 */return ret;}static void __exit vser_exit(void)
{dev_t dev; //用于存储设备号dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINORcdev_del(&vser_cdev); //1.从内核散列表中删除字符设备unregister_chrdev_region(dev,VSER_DEV_CNT); //2.释放设备号
}/****** 加入到内核 *******/
module_init(vser_init);
module_exit(vser_exit);
/****** 模块说明 ********/
MODULE_LICENSE("GPL"); /*开元许可协议:GPL协议*/
main.c (记得使用 gcc 编译 )
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{if(argc != 2){return -1;}int fd = open(argv[1],O_RDWR); //会调用内核的 struct file_operations 结构体中的 open 指针,指针指向 vser_open 函数if(fd < 0){perror("打开设备失败:");return -2;}close(fd);return 0;
}
Makefile
#动态编译内核驱动生成.ko文件的Makeifle#自己的模块代码名
obj-m = vser.o #就会生成一个 vser.ko 文件#内核源代码路径
ifeq ($(ARCH),arm)KERNELDIR ?= /home/student/linux-5.4.31
elseKERNELDIR ?= /lib/modules/${shell uname -r}/build
endif#当前模块路径
PWD ?= $(shell pwd)#编译源码生成 .ko 文件 make all
all:${MAKE} -C ${KERNELDIR} M=${PWD} modules
#伪代码之清除垃圾
clean:rm Module.* modules.* *.mod *.ko
1. 过程: gcc mian.c (把main.c 编译成 a.out)
2. sudo rmmod vser (卸载之前加载的模块)
3. make (编译 makefile 文件)
4. sudo insmod vser.ko (加载 模块)
5.命令: lsmod (查看是否加载)
6. dmesg (查看内核打印的信息, 这个没有内核信息打印)
3.虚拟串口
代码:
main_read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc,char *argv[])
{if(argc != 2){return -1;}int fd = open(argv[1],O_RDWR); //会调用内核的 struct file_operations 结构体中的 open 指针,指针指向 vser_open 函数if(fd < 0){perror("打开设备失败:");return -2;}char buf[10];while(1){memset(buf,0,sizeof(buf));read(fd,buf,10);printf("读取到:%s\n",buf);sleep(1);}close(fd);return 0;
}
main_write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc,char *argv[])
{if(argc != 2){return -1;}int fd = open(argv[1],O_RDWR); //会调用内核的 struct file_operations 结构体中的 open 指针,指针指向 vser_open 函数if(fd < 0){perror("打开设备失败:");return -2;}char buf[10];while(1){printf("请输入要写入的数据:");scanf("%s",buf);write(fd,buf,strlen(buf));}close(fd);return 0;
}
vser.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>#include <linux/fs.h>
#include <linux/cdev.h>
/***** 设备相关信息 ******/
static unsigned int VSER_MAJOR = 256; //主设备号
static unsigned int VSER_MINOR = 0; //次设备号
#define VSER_DEV_CNT 1 //个数为1个
#define VSER_DEV_NAME "vser" //设备名称/**** 字符设备结构体 ******/
static struct cdev vser_cdev; //字符设备结构体/**** 虚拟串口 ******/
#include <linux/kfifo.h>
DEFINE_KFIFO(vser_fifo,char,32); //创建一个名为 vser_fifo 的 char 类型 大小为 32 个的队列管道/**** 设备操作相关函数 到时候就可以使用 open 打开该设备文件 *****/
int vser_open(struct inode *p_inode, struct file *p_file)
{printk("vser设备打开了\n");return 0;
}int vser_close(struct inode *p_inode, struct file *p_file)
{printk("vser设备关闭了\n");return 0;
}ssize_t vser_read(struct file *p_file, char __user *user_buf, size_t user_size, loff_t *ps)
{ int copied = 0;kfifo_to_user(&vser_fifo,user_buf,user_size,&copied); //将内核队列管道数据拷贝到用户空间return copied;
}ssize_t vser_write(struct file *p_file, const char __user *user_buf, size_t user_size, loff_t *ps)
{int copied = 0;kfifo_from_user(&vser_fifo,user_buf,user_size,&copied);//将用户空间数据拷贝到内核队列管道中return copied;
}/**** 设备操作集 *****/
static struct file_operations vser_ops = { //设备操作集结构体.owner = THIS_MODULE,.release = vser_close,.open = vser_open,.read = vser_read,.write = vser_write,
};/***** 字符设备注册步骤 **************************************************** 初始化函数* 1.将主设备和次设备组合成设备号 MKDEV(主设备号,次设备号)* 2.将该设备号注册到内核 register_chrdev_region(设备号,设备个数,设备名称) 或 * 3.初始化字符设备 cdev_init(字符设备结构体地址,操作集结构体地址)* 4.将字符设备添加到内核散列表map中 cdev_add(字符设备结构体地址,设备号,设备个数)* 注销函数* 1.从内核散链表map中删除字符设备 cdev_del(字符设备结构体地址)* 2.将申请的设备号注销 unregister_chrdev_region(设备号,设备个数) * * 跟文件系统下创建 设备文件 cat /proc/devices * 查看到设备信息 Character devices:1 mem256 vser 我们自己申请的设备信息4 /dev/vc/04 tty4 ttyS5 /dev/tty5 /dev/console * ***********************************************************************/static int __init vser_init(void)
{int ret; //用于获取函数返回值,并该函数是否执行成功/********* 申请设备号 **************/dev_t dev; //用于存储设备号dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINORret = register_chrdev_region(dev,VSER_DEV_CNT,VSER_DEV_NAME);if(ret != 0){printk("静态申请设备号失败\n");ret = alloc_chrdev_region(&dev,VSER_MINOR,VSER_DEV_CNT,VSER_DEV_NAME);if(ret != 0){printk("动态申请设备号失败\n");goto register_err;}VSER_MAJOR = MAJOR(dev); //从设备号中提取主设备号VSER_MINOR = MINOR(dev); //从设备号中提取次设备号}/******** 申请字符设备 *********/cdev_init(&vser_cdev,&vser_ops);vser_cdev.owner = THIS_MODULE;ret = cdev_add(&vser_cdev,dev,VSER_DEV_CNT);if(ret != 0){printk("申请字符设备失败\n");goto cdev_err;}return 0; //表示成功,直接结束函数并返回0cdev_err: /* 申请字符设备失败 */unregister_chrdev_region(dev,VSER_DEV_CNT); //释放设备号register_err: /* 设备号申请失败 */return ret;}static void __exit vser_exit(void)
{dev_t dev; //用于存储设备号dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINORcdev_del(&vser_cdev); //1.从内核散列表中删除字符设备unregister_chrdev_region(dev,VSER_DEV_CNT); //2.释放设备号
}/****** 加入到内核 *******/
module_init(vser_init);
module_exit(vser_exit);
/****** 模块说明 ********/
MODULE_LICENSE("GPL"); /*开元许可协议:GPL协议*/
Makefile
#动态编译内核驱动生成.ko文件的Makeifle#自己的模块代码名
obj-m = vser.o #就会生成一个 vser.ko 文件#内核源代码路径
ifeq ($(ARCH),arm)KERNELDIR ?= /home/student/linux-5.4.31
elseKERNELDIR ?= /lib/modules/${shell uname -r}/build
endif#当前模块路径
PWD ?= $(shell pwd)#编译源码生成 .ko 文件 make all
all:${MAKE} -C ${KERNELDIR} M=${PWD} modules
#伪代码之清除垃圾
clean:rm Module.* modules.* *.mod *.ko
1. 编译所有的 .c 文件
gcc main_write.c -o write (写文件)
gcc main_read.c -o read (读文件)
make (编译 vser.ko 文件)
2. 命令: sudo rmmod vser (卸载之前加载的模块)
3. 命令: sudo vser.ko (加载现在的模块)
4. 命令: lsmod (查看模块是否加载)
5. 命令: sudo ./read /dev/vser0 (运行读文件 后面的/dev/vser0 是之前创建的字符设备)
sudo ./write /dev/vser0 (运行写文件 后面的/dev/vser0 是之前创建的字符设备)
6. 命令 : dmesg (查看内核信息)
4. 一个驱动支持多个设备
代码:
vser.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>#include <linux/fs.h>
#include <linux/cdev.h>
/***** 设备相关信息 ******/
static unsigned int VSER_MAJOR = 256; //主设备号
static unsigned int VSER_MINOR = 0; //次设备号
#define VSER_DEV_CNT 2 //个数为1个
#define VSER_DEV_NAME "vser" //设备名称// mknod /dev/vser0 c 256 0
// mknod /dev/vser1 c 256 1/**** 字符设备结构体 ******/
static struct cdev vser_cdev; //字符设备结构体/**** 虚拟串口 ******/
#include <linux/kfifo.h>
DEFINE_KFIFO(vser_fifo0,char,32); //创建一个名为 vser_fifo0 的 char 类型 大小为 32 个的队列管道
DEFINE_KFIFO(vser_fifo1,char,32); //创建一个名为 vser_fifo1 的 char 类型 大小为 32 个的队列管道
/**** 设备操作相关函数 到时候就可以使用 open 打开该设备文件 *****/
int vser_open(struct inode *p_inode, struct file *p_file)
{int minor = MINOR(p_inode->i_rdev); //获取设备的次设备号printk("vser设备打开了\n");printk("当前的次设备号;%d\n",minor);switch (minor){case 0:p_file->private_data = &vser_fifo0;break;case 1:p_file->private_data = &vser_fifo1;break;}printk("操作的FIFO = %p\n",p_file->private_data);return 0;
}int vser_close(struct inode *p_inode, struct file *p_file)
{printk("vser设备关闭了\n");return 0;
}ssize_t vser_read(struct file *p_file, char __user *user_buf, size_t user_size, loff_t *ps)
{ int copied = 0;struct kfifo *fifo = p_file->private_data;kfifo_to_user(fifo,user_buf,user_size,&copied); //将内核队列管道数据拷贝到用户空间return copied;
}ssize_t vser_write(struct file *p_file, const char __user *user_buf, size_t user_size, loff_t *ps)
{int copied = 0;struct kfifo *fifo = p_file->private_data;printk("操作的FIFO = %p\n",p_file->private_data);kfifo_from_user(fifo,user_buf,user_size,&copied);//将用户空间数据拷贝到内核队列管道中return copied;
}/**** 设备操作集 *****/
static struct file_operations vser_ops = { //设备操作集结构体.owner = THIS_MODULE,.release = vser_close,.open = vser_open,.read = vser_read,.write = vser_write,
};/***** 字符设备注册步骤 **************************************************** 初始化函数* 1.将主设备和次设备组合成设备号 MKDEV(主设备号,次设备号)* 2.将该设备号注册到内核 register_chrdev_region(设备号,设备个数,设备名称) 或 * 3.初始化字符设备 cdev_init(字符设备结构体地址,操作集结构体地址)* 4.将字符设备添加到内核散列表map中 cdev_add(字符设备结构体地址,设备号,设备个数)* 注销函数* 1.从内核散链表map中删除字符设备 cdev_del(字符设备结构体地址)* 2.将申请的设备号注销 unregister_chrdev_region(设备号,设备个数) * * 跟文件系统下创建 设备文件 cat /proc/devices * 查看到设备信息 Character devices:1 mem256 vser 我们自己申请的设备信息4 /dev/vc/04 tty4 ttyS5 /dev/tty5 /dev/console * ***********************************************************************/static int __init vser_init(void)
{int ret; //用于获取函数返回值,并该函数是否执行成功/********* 申请设备号 **************/dev_t dev; //用于存储设备号dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINORret = register_chrdev_region(dev,VSER_DEV_CNT,VSER_DEV_NAME);if(ret != 0){printk("静态申请设备号失败\n");ret = alloc_chrdev_region(&dev,VSER_MINOR,VSER_DEV_CNT,VSER_DEV_NAME);if(ret != 0){printk("动态申请设备号失败\n");goto register_err;}VSER_MAJOR = MAJOR(dev); //从设备号中提取主设备号VSER_MINOR = MINOR(dev); //从设备号中提取次设备号}/******** 申请字符设备 *********/cdev_init(&vser_cdev,&vser_ops);vser_cdev.owner = THIS_MODULE;ret = cdev_add(&vser_cdev,dev,VSER_DEV_CNT);if(ret != 0){printk("申请字符设备失败\n");goto cdev_err;}return 0; //表示成功,直接结束函数并返回0cdev_err: /* 申请字符设备失败 */unregister_chrdev_region(dev,VSER_DEV_CNT); //释放设备号register_err: /* 设备号申请失败 */return ret;}static void __exit vser_exit(void)
{dev_t dev; //用于存储设备号dev = MKDEV(VSER_MAJOR,VSER_MINOR); //组合成设备号 (VSER_MAJOR << 20) | VSER_MINORcdev_del(&vser_cdev); //1.从内核散列表中删除字符设备unregister_chrdev_region(dev,VSER_DEV_CNT); //2.释放设备号
}/****** 加入到内核 *******/
module_init(vser_init);
module_exit(vser_exit);
/****** 模块说明 ********/
MODULE_LICENSE("GPL"); /*开元许可协议:GPL协议*/
main_write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc,char *argv[])
{if(argc != 2){return -1;}int fd = open(argv[1],O_RDWR); //会调用内核的 struct file_operations 结构体中的 open 指针,指针指向 vser_open 函数if(fd < 0){perror("打开设备失败:");return -2;}char buf[10];while(1){printf("请输入要写入的数据:");scanf("%s",buf);write(fd,buf,strlen(buf));}close(fd);return 0;
}
main_read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc,char *argv[])
{if(argc != 2){return -1;}int fd = open(argv[1],O_RDWR); //会调用内核的 struct file_operations 结构体中的 open 指针,指针指向 vser_open 函数if(fd < 0){perror("打开设备失败:");return -2;}char buf[10];while(1){memset(buf,0,sizeof(buf));read(fd,buf,10);printf("读取到:%s\n",buf);sleep(1);}close(fd);return 0;
}
Makefile
#动态编译内核驱动生成.ko文件的Makeifle#自己的模块代码名
obj-m = vser.o #就会生成一个 vser.ko 文件#内核源代码路径
ifeq ($(ARCH),arm)KERNELDIR ?= /home/student/linux-5.4.31
elseKERNELDIR ?= /lib/modules/${shell uname -r}/build
endif#当前模块路径
PWD ?= $(shell pwd)#编译源码生成 .ko 文件 make all
all:${MAKE} -C ${KERNELDIR} M=${PWD} modules
#伪代码之清除垃圾
clean:rm Module.* modules.* *.mod *.ko
1. 命令: sudo /dev/vser* (卸载之前创建的字符设备模块 )
2. sudo mknod /dev/vser1 c 256 0 ( 创建字符设备)
sudo mknod /dev/vser2 c 256 1 (设备名称不同, 次设备号不同 , 自己看 vser.c 里面的 switch 的数字 决定次设备号)
3.命令: su (进入超级用户)
4. 命令:sudo rmmod vser
5.命令 : make
6. 命令: sudo insmod vser.ko
7. 命令: gcc main_read.c -o read
8. 命令: gcc main_write.c -o write
9. 命令: echo "我是" > /dev/vser1
10 . 命令: cat /dev/vser1
作业:
1.字符设备和块设备的区别不包括()
[A]字符设备按字节流进行访问,块设备按块大小进行访问
[B]字符设备只能处理可打印字符,块设备可以处理二进制数据
[C]多数字符设备不能随机访问,而块设备一定能随机访问
[D]字符设备通常没有页高速缓存,而块设备有
2.在3.14.25版本的内核中,主设备号占———位,次设备号占———位。
[A]8
[B] 16
[C] 12
[D]20
3.用于分配主次设备号的函数是()。
[A] register_chrdev_region
[B] MKDEV
[C]alloc_chrdev_region
[D] MAJOR
4、在字符设备驱动中,struct file_operations 结构中的函数指针成员不包含$,
[A] open
[B] close
[C] read
[D] show_fdinfo