嵌入式驱动初级-字符设备驱动基础

news2025/7/22 16:48:47

文章目录

  • 前言
  • 一、驱动学习预备知识
    • 1.什么是设备驱动程序
    • 2.向内核添加新功能方法
      • 2.1新功能源码与Linux内核源码不在同目录下
      • 2.2在Ubuntu下加载和删除ko文件步骤
      • 2.3在开发板下加载和删除ko文件步骤
      • 2.4内核模块基础代码解析
  • 二、字符设备驱动框架
    • 2.1Linux内核对设备的分类
    • 2.2字符设备驱动开发步骤
    • 2.3字符设备驱动代码编写
    • 2.4字符设备应用层代码编写
    • 2.5字符设备驱动添加读写


前言

记录嵌入式驱动学习笔记

一、驱动学习预备知识

学习驱动前要了解的基础内容和命令。

1.什么是设备驱动程序

一种添加到操作系统中的特殊程序,主要作用是协助操作系统完成应用程序与对应硬件设备之间数据传送的功能。简言之,设备驱动程序就是操作系统中“驱动”对应硬件设备使之能正常工作的代码。

一个驱动程序主要完成如下工作:

  1. 初始化设备,让设备做好开始工作的准备
  2. 读数据:将设备产生的数据传递给上层应用程序
  3. 写数据:将上层应用程序交付过来的数据传递给设备
  4. 获取设备信息:协助上层应用程序获取设备的属性、状态信息
  5. 设置设备信息:让上层应用程序可以决定设备的一些工作属性、模式
  6. 其它相关操作:如休眠、唤醒、关闭设备等
    其中最核心的工作就是设备数据的输入和输出,因此计算机外部设备(外设)也被称为IO设备

2.向内核添加新功能方法

向内核添加新功能的方法分为两种,一种静态加载方法,一种动态加载方法。
静态加载方法思想:即新功能源码与内核其它代码一起编译进uImage文件内,在学习驱动的阶段我们经常采用动态加载方法因此不过多介绍该方法。
动态加载方法思想:即新功能源码与内核其它源码不一起编译,而是独立编译成内核的插件(被称为内核模块)文件.ko

2.1新功能源码与Linux内核源码不在同目录下

  1. cd ~/fs4412
  2. mkdir mydrivercode
  3. cd mydrivercode
  4. cp …/linux-3.14/drivers/char/myhello.c .
  5. vim Makefile
  6. make (生成的ko文件适用于主机ubuntu linux)
  7. make ARCH=arm (生成的ko文件适用于开发板linux,注意此命令执行前,开发板的内核源码已被编译)

#file命令可以查看指定ko文件适用于哪种平台,用法:
file ko文件
#结果带x86字样的适用于主机ubuntu linux,带arm字样的适用于开发板linux

2.2在Ubuntu下加载和删除ko文件步骤

sudo insmod ./???.ko  
#此处为内核模块文件名,将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
lsmod 
#查看已被插入的内核模块有哪些,显示的是插入内核后的模块名
sudo rmmod ??? 
#此处为插入内核后的模块名,此时将已被插入的内核模块从内核中移除 ----- 相当于卸载插件
sudo dmesg -C  #清除内核已打印的信息
dmesg #查看内核的打印信息

2.3在开发板下加载和删除ko文件步骤

#先将生成的ko文件拷贝到/opt/4412/rootfs目录下:
cp ????/???.ko  /opt/4412/rootfs
#在串口终端界面开发板Linux命令行下执行
insmod ./???.ko  #将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
lsmod #查看已被插入的内核模块有哪些
rmmod ??? #将已被插入的内核模块从内核中移除 ----- 相当于卸载插件
内核随时打印信息,我们可以在串口终端界面随时看到打印信息,不需要dmesg命令查看打印信息

2.4内核模块基础代码解析

Linux内核的插件机制——内核模块
类似于浏览器、eclipse这些软件的插件开发,Linux提供了一种可以向正在运行的内核中插入新的代码段、在代码段不需要继续运行时也可以从内核中移除的机制,这个可以被插入、移除的代码段被称为内核模块。

代码如下(示例):

#include <linux/module.h> //包含内核编程最常用的函数声明,如printk
#include <linux/kernel.h> //包含模块编程相关的宏定义,如:MODULE_LICENSE

/*该函数在模块被插入进内核时调用,主要作用为新功能做好预备工作
  被称为模块的入口函数
  
  __init的作用 : 
1. 一个宏,展开后为:__attribute__ ((__section__ (".init.text")))   实际是gcc的一个特殊链接标记
2. 指示链接器将该函数放置在 .init.text区段
3. 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置
*/
int __init myhello_init(void)
{
    /*内核是裸机程序,不可以调用C库中printf函数来打印程序信息,
    Linux内核源码自身实现了一个用法与printf差不多的函数,命名为printk (k-kernel)
    printk不支持浮点数打印*/
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("myhello is running\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	printk("#####################################################\n");
	return 0;
}

/*该函数在模块从内核中被移除时调用,主要作用做些init函数的反操作
  被称为模块的出口函数
  
  __exit的作用:
1.一个宏,展开后为:__attribute__ ((__section__ (".exit.text")))   实际也是gcc的一个特殊链接标记
2.指示链接器将该函数放置在 .exit.text区段
3.在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置
*/
void __exit myhello_exit(void)
{
	printk("myhello will exit\n");
}

/*
MODULE_LICENSE(字符串常量);
字符串常量内容为源码的许可证协议 可以是"GPL" "GPL v2"  "GPL and additional rights"  "Dual BSD/GPL"  "Dual MIT/GPL" "Dual MPL/GPL"等, "GPL"最常用

其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证
在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL协议,如果发现不遵循GPL,则在插入模块时打印抱怨信息:
	myhello:module license 'unspecified' taints kernel
	Disabling lock debugging due to kernel taint
也会导致新模块没法使用一些内核其它模块提供的高级功能
*/
MODULE_LICENSE("GPL");

/*
module_init 宏
1. 用法:module_init(模块入口函数名) 
2. 动态加载模块,对应函数被调用
3. 静态加载模块,内核启动过程中对应函数被调用
4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。
5. 对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名
*/
module_init(myhello_init);

/*
module_exit宏
1.用法:module_exit(模块出口函数名)
2.动态加载的模块在卸载时,对应函数被调用
3.静态加载的模块可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略
4.对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。
5.对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名
*/
module_exit(myhello_exit);

模块三要素:入口函数 出口函数 MODULE__LICENSE
Makefile文件用于生成ko模块,代码如下

ifeq ($(KERNELRELEASE),)
ifeq ($(ARCH),arm)
KERNELDIR ?= 目标板linux内核源码顶层目录的绝对路径
ROOTFS ?= 目标板根文件系统顶层目录的绝对路径
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
	rm -rf  *.o  *.ko  .*.cmd  *.mod.*  modules.order  Module.symvers   .tmp_versions
else
obj-m += hello.o
endif

内核接口头文件查询

  1. 首先在include/linux 查询指定函数:grep 名称 ./ -r -n
  2. 找不到则更大范围的include目录下查询,命令同上

二、字符设备驱动框架

2.1Linux内核对设备的分类

Linux内核按驱动程序实现模型框架的不同,将设备分为三类:

  1. 字符设备:按字节流形式进行数据读写的设备,一般情况下按顺序访问,数据量不大,一般不设缓存
  2. 块设备:按整块进行数据读写的设备,最小的块大小为512字节(一个扇区),块的大小必须是扇区的整数倍,Linux系统的块大小一般为4096字节,随机访问,设缓存以提高效率。块设备不直接面向应用程序,一般使用一个文件系统去对接。
  3. 网络设备:针对网络数据收发的设备

在这里插入图片描述

2.2字符设备驱动开发步骤

A:设备号
为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t 的数据类型表示设备号。dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型,,其中高 12 位为主设备号,低 20 位为次设备号。应用程序打开一个设备文件时,通过设备号来查找定位内核中管理的设备。
在这里插入图片描述
MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号,用法:

	dev_t devno;
	int major = 251;//主设备号
	int minor = 2;//次设备号
	devno = MKDEV(major,minor);

MAJOR宏用来从32位设备号中分离出主设备号,用法:

	dev_t devno = MKDEV(249,1);
	int major = MAJOR(devno);

MINOR宏用来从32位设备号中分离出次设备号,用法:

	dev_t devno = MKDEV(249,1);
	int minor = MINOR(devno);

如果已知一个设备的主次设备号,应用层指定好设备文件名,那么可以用mknod命令在/dev目录创建代表这个设备的文件,即此后应用程序对此文件的操作就是对其代表的设备操作,mknod用法如下:

//其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个
//字符设备,“200”是设备的主设备号,“0”是设备的次设备号。
	mknod /dev/chrdevbase c 200 0

B:申请和注销设备号
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数有两套如下所示:

	int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号
参数:
	from:自己指定的设备号
	count:申请的设备数量
	name:/proc/devices文件中与该设备对应的名字,方便用户层查询主设备号
返回值:
	成功为0,失败负数,绝对值为错误码

int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count, const char *name)
功能:动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
参数:
	dev:分配设备号成功后用来存放分配到的设备号
	baseminior:起始的次设备号,一般为0
	count:申请的设备数量
	name:/proc/devices文件中与该设备对应的名字,方便用户层查询主次设备号
返回值:
	成功为0,失败负数,绝对值为错误码

分配成功后在/proc/devices 可以查看到申请到主设备号和对应的设备名,mknod时参数可以参考查到的此设备信息

void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放设备号
参数:
	from:已成功分配的设备号将被释放
	count:申请成功的设备数量

释放后/proc/devices文件对应的记录消失

C:注册字符设备
在 Linux 中使用 cdev 结构体表示一个字符设备,在 cdev 中有两个重要的成员变量:ops 和 dev,这两个就是字符设备文件操作函数集合file_operations 以及设备号 dev_t。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个变量就表示一个字符设备。

1.定义 cdev 结构体变量

struct cdev test_cdev;

2.用 cdev_init 函数对其进行初始化
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化,cdev_init 函数原型如下

//参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

3.cdev_add 函数用于向 Linux 系统添加字符设备
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数,完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。cdev_add 函数原型如下

//参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参
//数 count 是要添加的设备数量。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

4.使用 cdev_del 函数从 Linux 内核中删除相应的字符设备
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备,cdev_del函数原型如下

//参数 p 就是要删除的字符设备。如果要删除字符设备
void cdev_del(struct cdev *p)

使用 cdev_init 函数初始化 cdev 变量的示例代码如下:

struct cdev testcdev;
/* 设备操作函数 */
	static struct file_operations test_fops = {
	.owner = THIS_MODULE,
	 /* 其他具体的初始项 */
	};
 testcdev.owner = THIS_MODULE;
 cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
 cdev_add(&testcdev, devid, 1);/* 添加字符设备 */

小结:

字符设备驱动开发步骤:

  1. 如果设备有自己的一些控制数据,则定义一个包含struct cdev cdev成员的结构体struct mydev,其它成员根据设备需求,设备简单则直接用struct cdev
  2. 定义一个struct mydev或struct cdev的全局变量来表示本设备;也可以定义一个struct mydev或struct cdev的全局指针(记得在init时动态分配)
  3. 定义三个全局变量分别来表示主设备号、次设备号、设备数
  4. 定义一个struct file_operations结构体变量,其owner成员置成THIS_MODULE
  5. module init函数流程:a. 申请设备号 b. 如果是全局设备指针则动态分配代表本设备的结构体元素 c. 初始化struct cdev成员 d. 设置struct cdev的owner成员为THIS_MODULE e. 添加字符设备到内核
  6. module exit函数:a. 注销设备号 b. 从内核中移除struct cdev c. 如果如果是全局设备指针则释放其指向空间
  7. 编写各个操作函数并将函数名初始化给struct file_operations结构体变量

验证操作步骤:

  1. 编写驱动代码mychar.c
  2. make生成ko文件
  3. insmod内核模块
  4. 查阅字符设备用到的设备号(主设备号):cat /proc/devices | grep 申请设备号时用的名字
  5. 创建设备文件(设备节点) : mknod /dev/??? c 上一步查询到的主设备号 代码中指定初始次设备号
  6. 编写app验证驱动(testmychar_app.c)
  7. 编译运行app,dmesg命令查看内核打印信息

2.3字符设备驱动代码编写

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
//主设备号
int major = 11;
//次设备号
int minor = 0;
//注册设备数量
int mychar_num = 1;
//设备名
char mycharname[] ="mychar";
//创建cdev类型的对象 cdevinit函数要用到
struct cdev mydev;
int mychar_open(struct inode *pnode, struct file *pfile) //打开设备
{
   printk("mychar_open is called\n");
   return 0;
}
int mychar_close(struct inode *pnode, struct file *pfile) //关闭设备
{
   printk("mychar_close is called\n");
   return 0;
}

//内核驱动操作函数集合 cdevinit函数要用到
struct file_operations myops =
{
   .owner=THIS_MODULE,
   .open=mychar_open,
   .release=mychar_close,
};

int __init mychar_init(void)
{
   //用来接收注册设备函数的返回值
   int ret = 0;
   //主设备号和次设备号合并
   dev_t devno = MKDEV(major,minor);
   /*1.申请设备号*/
   //静态分配 手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号
   //ret = register_chrdev_region(devno,mychar_num,"mychar");
   ret = register_chrdev_region(devno,mychar_num,mycharname);
   //分配失败返回负数
   if(ret)
   {
      //动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
      //ret = alloc_chrdev_region(&devno,minor,mychar_num,"mychar");
      ret = alloc_chrdev_region(&devno,minor,mychar_num,mycharname);
      if(ret)
      {
         printk("get devno failed\n");
         return -1;
      }
      //如果是动态分配说明自己设置的设备号被占用了 系统会更换主设备号 重新获取一下主设备号
      major = MAJOR(devno);
   }
   /*2.将struct cdev对象添加到内核对应的数据结构里*/
   //初始化cdev 给struct cdev对象指定操作函数集
   cdev_init(&mydev,&myops);
   mydev.owner=THIS_MODULE;
   //将字符设备添加进内核 哈希管理列表
   cdev_add(&mydev,devno,1);
   
 
   return 0;
}
   
void __exit mychar_exit(void)
{
   //主设备号和次设备号合并
   dev_t devno = MKDEV(major,minor);
   //注销字符设备驱动 入口参数:设备号 设备名称
   //unregister_chrdev(devno,"mychar");
   //将字符设备从内核移除
   cdev_del(&mydev);
   unregister_chrdev_region(devno,mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);

2.4字符设备应用层代码编写

#include <stdio.h>
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"

int main(int argc, char *argv[])
{
	int fd = -1;
	if(argc < 2){
		printf("Error Usage!\r\n");
		return 1;
	}
	fd = open(argv[1],O_RDONLY);
	if(fd < 0){
		printf("Can't open file %s\r\n", argv[1]);
		return 2;
	}
	close(fd);
	fd = -1;
	return 0;
}

2.5字符设备驱动添加读写

ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);
/*
完成功能:读取设备产生的数据
参数:
    filp:指向open产生的struct file类型的对象,表示本次read对应的那次open
    pbuf:指向用户空间一块内存,用来保存读到的数据
    count:用户期望读取的字节数
    ppos:对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置
 返回值:
    本次成功读取的字节数,失败返回-1
*/
unsigned long copy_to_user (void __user * to, const void * from, unsigned long n)

ssize_t xxx_write (struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos);  
/*
完成功能:向设备写入数据
参数:
    filp:指向open产生的struct file类型的对象,表示本次write对应的那次open
    pbuf:指向用户空间一块内存,用来保存被写的数据
    count:用户期望写入的字节数
    ppos:对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置
 返回值:
    本次成功写入的字节数,失败返回-1
*/
unsigned long copy_from_user (void * to, const void __user * from, unsigned long n)

添加读写的字符设备驱动代码

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>

#define BUF_LEN 100
//主设备号
int major = 11;
//次设备号
int minor = 0;
//注册设备数量
int mychar_num = 1;
//设备名
char mycharname[] ="mychar";
//创建cdev类型的对象 cdevinit函数要用到
struct cdev mydev;
//全局数组 字符设备产生的数据存放
char mydev_buf[BUF_LEN];
//数组下标从零开始
int curlen=0;
int mychar_open(struct inode *pnode, struct file *pfile) //打开设备
{
   printk("mychar_open is called\n");
   return 0;
}
int mychar_close(struct inode *pnode, struct file *pfile) //关闭设备
{
   printk("mychar_close is called\n");
   return 0;
}
//读数据是对于应用层来说 把内核数据拷贝到应用层来读取
/*
ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);
完成功能:读取设备产生的数据
参数:
    filp:指向open产生的struct file类型的对象,表示本次read对应的那次open
    pbuf:指向用户空间一块内存,用来保存读到的数据
    count:用户期望读取的字节数
    ppos:对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置
 返回值:
    本次成功读取的字节数,失败返回-1
*/
ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos)
{
   int ret = 0;
   //要读的数据大于数据的长度 修改要读数据的长度
   int size = 0;
   if(count > curlen)
   {
      size = curlen;
   }else
   {
      size=count;
   }
   //将内核空间数据复制到用户空间 用户空间:puser 内核空间:mydev_buf
   ret = copy_to_user(puser,mydev_buf,size);
   if(ret)
   {
      printk("copy_to_user failed\n");
      return -1;
   }
   //将剩下的数据移动到数组起始 即删除已读数据
   memcpy(mydev_buf,mydev_buf + size,curlen - size);
   curlen = curlen - size;
   return size;
}
//写数据对于应用层来说 把数据写进驱动
/*
ssize_t xxx_write (struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos);  
完成功能:向设备写入数据
参数:
    filp:指向open产生的struct file类型的对象,表示本次write对应的那次open
    pbuf:指向用户空间一块内存,用来保存被写的数据
    count:用户期望写入的字节数
    ppos:对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置
 返回值:
    本次成功写入的字节数,失败返回-1
*/
ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos)
{
   int size = 0;
   int ret = 0;
   //如果剩余数组长度小于要写入的数据长度 则把要写入的数据修改 
   if(count > BUF_LEN - curlen)
   {
      size = BUF_LEN - curlen;
   }else
   {
      size = count;
   }
   //从用户空间拷贝数据到内核空间
   ret = copy_from_user(mydev_buf + curlen,puser,size);
   if(ret)
   {
      printk("copy_from_user failed\n");
      return -1;
   }
   curlen = curlen + size;
   return size;
}
//内核驱动操作函数集合 cdevinit函数要用到
struct file_operations myops =
{
   .owner=THIS_MODULE,
   .open=mychar_open,
   .release=mychar_close,
   .read=mychar_read,
   .write=mychar_write,
};

int __init mychar_init(void)
{
   //用来接收注册设备函数的返回值
   int ret = 0;
   //主设备号和次设备号合并
   dev_t devno = MKDEV(major,minor);
   /*1.申请设备号*/
   //静态分配 手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号
   //ret = register_chrdev_region(devno,mychar_num,"mychar");
   ret = register_chrdev_region(devno,mychar_num,mycharname);
   //分配失败返回负数
   if(ret)
   {
      //动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
      //ret = alloc_chrdev_region(&devno,minor,mychar_num,"mychar");
      ret = alloc_chrdev_region(&devno,minor,mychar_num,mycharname);
      if(ret)
      {
         printk("get devno failed\n");
         return -1;
      }
      //如果是动态分配说明自己设置的设备号被占用了 系统会更换主设备号 重新获取一下主设备号
      major = MAJOR(devno);
   }
   /*2.将struct cdev对象添加到内核对应的数据结构里*/
   //初始化cdev 给struct cdev对象指定操作函数集
   cdev_init(&mydev,&myops);
   mydev.owner=THIS_MODULE;
   //将字符设备添加进内核 哈希管理列表
   cdev_add(&mydev,devno,1);
   
 
   return 0;
}
   
void __exit mychar_exit(void)
{
   //主设备号和次设备号合并
   dev_t devno = MKDEV(major,minor);
   //注销字符设备驱动 入口参数:设备号 设备名称
   //unregister_chrdev(devno,"mychar");
   //将字符设备从内核移除
   cdev_del(&mydev);
   unregister_chrdev_region(devno,mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/35597.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

css点击文字(非按钮) 能自动改变颜色。

实现功能说明&#xff1a; 如下图&#xff0c;点击不同的文字能够切换不同的页面&#xff0c;同时这个tab的文字能够相应的变色&#xff0c;其他未选中的状态默认为灰色。 方案一&#xff1a; 使用css的伪类。 :active&#xff0c;元素被点击时变色&#xff0c;但颜色在点击后…

为什么 NGINX 的 reload 命令不是热加载?

这段时间在 Reddit 看到一个讨论&#xff0c;为什么 NGINX 不支持热加载&#xff1f;乍看之下很反常识&#xff0c;作为世界第一大 Web 服务器&#xff0c;不支持热加载&#xff1f;难道大家都在使用的 nginx -s reload 命令都用错了&#xff1f;带着这个疑问&#xff0c;让我们…

DiffusionDet: Diffusion Model for Object Detection

paper: https://arxiv.org/abs/2211.09788 code&#xff1a;https://github.com/ShoufaChen/DiffusionDet 探索了扩散模型在非生成类任务&#xff08;目标检测&#xff09;中的应用。相较于传统Coarse-to-fine方法&#xff0c;基于扩散思路训练有两个特点&#xff1a;1&…

网络编程/计算机网络

目录 一、网络基本概念 1.网络 2.互联网 3.ip地址 4.MAC地址 5.端口号Port 6.网络协议 二.网络分层模型 1.数据链路层 2.网络层 3.传输层 4.应用层 三、网络应用程序通信流程 四、socket 网络编程 1.主机字节序列和网络字节序列 2.套接字地址结构 &#xff08…

离群点检测和新颖性检测

引言 在异常检测领域中&#xff0c;我们常常需要决定新观测点是否属于与现有观测点相同的分布&#xff0c; &#xff08;则称它们为inlier),或被认为是不同的(outlier). 在这里&#xff0c;必须做出两个重要的区别&#xff1a; 异常值检测&#xff1a;outlier detection 训练…

List接口与实现类

目录 一、List接口 1.特点&#xff1a; 2.方法&#xff1a; 二、List接口的使用 三、List实现类 1.ArrayList&#xff08;重点&#xff09; &#xff08;1&#xff09;特点 &#xff08;2&#xff09;示例 &#xff08;3&#xff09;源码分析 2.Vector &#xff08;1…

Python列表推导式——List

目录 遍历列表 range函数 列表推导式(★★★★★)五星技能点 五万次【列表推导式】与【for】遍历计算消耗时间对照表&#xff1a; 列表推导式逻辑判断 遍历列表 通过for in循环来遍历集合列表 range函数 我们基础range函数是参数范围是range(start,stop)&#xff0c;在下…

利用天翎知识文档+群晖NAS搭建企业知识库,享用智能检索

编者按&#xff1a;检索是知识文档管理中常见切十分重要的功能&#xff0c;搭建企业知识库必不可少。本文分析了智能检索的特点&#xff0c;并介绍了其在知识文档管理系统中的实现。 关键词&#xff1a;智能检索&#xff0c;移动端&#xff0c;群晖NAS 知识管理中的检索功能 …

基于Java+SpringBoot+Mybaties+Vue 在线问卷调查系统设计与实现

一.项目介绍 本项目 可支持注册、登录 创建问卷、设置问卷内容&#xff08;支持单选、多选、问答题&#xff09; 分享问卷链接、分析问卷 查看问卷库、以及填写问卷内容 等功能 该项目采用 Springboot Mybaties Vue element UI 框架实现 二.环境需要 1.运行环境&#xff1…

教育科技创新即将崛起

随着“双减”政策的不断落实&#xff0c;真正具有创新精神和懂得教育科技的人&#xff0c;他们所创造的新模式、新生态即将崛起&#xff0c;属于教育科技和创新的春天即将到来。 那么&#xff0c;教育科技化和创新在不同的教育场景有什么体现呢&#xff1f; 我们先来看看目前…

【雷达通信】雷达探测项目仿真(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

OCR-paddleocr

PaddleOCR 分为 Detection&#xff08;文本检测&#xff09;、Direction classifier&#xff08;方向分类器&#xff09;和 Recognition&#xff08;文本识别&#xff09;三部分&#xff0c;因此需要三个模型。一、介绍 PaddleOCR是一款超轻量、中英文识别模型目标是打造丰富、…

【软件测试】测试人填坑?测试工作中的坑成长经历,填着填着就成了神......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 小刘&#xff1a; 之…

深度强化学习DRL现存问题和训练指南(D3QN(Dueling Double DQN))

目录 强化学习 问题 好用的算法标准 表现好 减小学习曲线的波动 On-Policy和off-Policy区别 学习方式 采样数据利用 学习本质 优缺点 DQN 创新点 优点 缺点 MDP和POMDP 结果图 DQN DDQN Dueling DQN 参数 iteration episode epoch Batch_Size Experime…

全新电商版本答案——全民拼购,越拼越得,白嫖产品不是梦

根据国家市场监督管理总局自2020年12月1日起发布的《规范促销暂行规定》第十二条的规定&#xff0c;为了推广自己的平台商城或商品&#xff0c;获取流量&#xff0c;采用新购买模式运营的平台通过附带给未被选中的参与者一定的奖金或者其他利益&#xff0c;属于有奖销售。 在此…

RS编码译码误码率性能matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB部分代码预览 4.完整MATLAB程序 1.算法描述 纠错编码技术在卫星通信、移动通信及数字存储等领域已获得了广泛的应用。RS码作为其中最重要的码类之一,具有优良的纠随机错误和突发错误的能力,被空间数据系统咨询委员会(CCSDS)作为一种…

记录安装Cytoscape的过程

一、安装前的准备工作 1、先java 看了大佬们的安装教程&#xff1a; 看到说先要安装对应的Java才能下载相应的cytoscape&#xff0c;因cytoscape是基于java的软件。 cytoscape安装教程1 cytoscape安装教程2 然后&#xff0c;自查了自己电脑上的Java版本,应该为jdk 11&#x…

【js】【爬虫】fetch + json-server 快速搭建爬虫服务器环境及数据后续处理(突破session缓存大小限制)

文章目录导读需求开发环境json-server使用安装json-server创建数据文件db.json运行服务器修改默认端口json-server提供的线上服务&#xff08;不推荐&#xff09;fetch上传数据到json-server服务器通过fetch新增数据报错Error: Insert failed, duplicate id数据后续处理打开导入…

ShellBrowser Delphi,Delphi组件功能和工具

ShellBrowser Delphi,Delphi组件功能和工具 ShellBrowser基本上被描述为集合的一部分&#xff0c;它能够为用户和开发人员提供Delphi的程序员&#xff0c;以便轻松灵活地访问windows shell性能。ShellBrowserComponents Delphi的使用基本上是能够模拟windows资源管理器的变体。…

阅读源码时:idea中如何使用todo标记、活动模板 (史上最全)

接下来&#xff0c; 尼恩要带大家 完成一个 超级牛逼的 大厂offer 收割机项目—— 100Wqps 三级组件 实操&#xff0c; 实操中&#xff0c;用到 caffeine 并且&#xff0c;尼恩要带大家 穿透式 、起底式的 学习 caffeine 的源码和架构&#xff0c; caffeine 的源码 特别复杂…