浅析Linux字符设备驱动程序内核机制

news2025/1/13 10:28:48

前段时间在学习linux设备驱动的时候,看了陈学松著的《深入Linux设备驱动程序内核机制》一书。说实话,这是一本很好的书,作者不但给出了在设备驱动程序开发过程中的所需要的知识点(如相应的函数和数据结构),还深入到linux内核里去分析了这些函数或数据结构的原理,对设备驱动开发的整个过程和原理都分析的很到位。但可能是由于知识点太多,原理也比较深的原因,这本书在知识点的排版上跨度有些大,所以读起来显得有点吃力,但是如果第一遍看的比较认真的话,再回头看第二次就真的能够很好地理解作者的写作思路了。第二章字符设备驱动程序我也是看了两遍才理解过来,趁着这热度,就按照自己的思路总结一下,以便下次再看的话,就可以按照自己比较好理解的方式去看了。

一、字符设备驱动程序框架

在深入讨论字符设备驱动程序之前,先给出一个设备驱动程序典型框架结构,以便于对字符设备驱动程序有个初步的理解。

<span style="background-color: rgb(255, 255, 255);">/*字符设备驱动程序源代码*/
/*demo_chr_dev.c*/
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<linux/cdev.h>
</span>
static struct cdev chr_dev;//定义一个字符设备对象
static dev_t ndev;//字符设备节点的设备号
 
static int chr_open(struct inode *nd,struct file *filp)  //打开设备
{
	int major=MAJOR(nd->i_rdev);
	int minor=MINOR(nd->i_rdev);
	printk("chr_open,major=%d,minor=%d\n",major,minor);
	return 0;
}
 
static ssize_t chr_read(struct file *f,char __user *u,size_t sz,loff_t *off) //读取设备文件内容
{
	printk("In the chr_read() function!\n");
	return 0;
}
 
//关键数据结构
struct file_operations chr_ops=
{
	.owner=THIS_MODULE,
	.open=chr_open,
	.read=chr_read,
};
 
static int demo_init(void)  //模块初始化函数
{
	int ret;
	cdev_init(&chr_dev,&chr_ops);//初始化字符设备对象,chr_ops定义在上面
	ret=alloc_chardev_region(&ndev,0,1,"char_dev");//分配设备号
	if(ret<0)
		return ret;
	printk("demo_init():major=%d,minor=%d\n",MAJOR(ndev),MINOR(ndev));
	ret=cdev_add(&chr_dev,ndev,1);//将字符设备对象chr_dev注册到系统中
	if(ret<0)
		return ret;
	return  0;
}
 
static void demo_exit(void)
{
	printk("Removing chr_dev module...\n");
	cdev_del(&chr_dev);//将字符设备对象chr_dev从系统中注销
	unregister_chr_region(ndev,1);//释放分配的设备号
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");

编译后可以看内核模块demo_chr_dev.ko

对驱动程序框架的总体理解:

(1)在linux系统中,各种设备都是以文件的形式存在,因此设备驱动程序包含了用于操作字符设备文件的函数,如打开,读、写等操作函数。如chr_open(),chr_read()等。这些函数都要由程序员自己实现。

(2)驱动程序中包含了类型为struct file_operations的结构体对象如chr_ops,该结构体对象用于存放针对字符设备的各种操作函数。

(3)设备驱动程序作为内核模块.ko安装到系统中,因此在程序框架中,必须要调用module_init()函数完成模块的安装;调用module_exit()函数完成模块的卸载。

(4)在模块初始化函数中完成字符设备对象的初始化,这个初始化过程中调用了程序员定义的数据结构chr_ops作为参数;同时在初始化函数中还完成了分配设备号,设备对象注册等工作。

(5)在模块的卸载函数中,会将对应的字符设备对象从系统中注销掉,并释放已分配的设备号。

【文章福利】小编推荐自己的Linux内核技术交流群:【 865977150】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100名进群领取,额外赠送一份价值 699的内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:最新Linux内核源码资料文档+视频资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

二,字符设备驱动程序内核机制详解

为了更容易理解驱动程序,我们结合前一步中给出的框架驱动程序中对应的函数和数据结构进行分析与解释。

(1)结构体structfile_operations

该结构体定义在文件<include/linux/fs.h>中,具体如下:

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, struct dentry *, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
};

可以看到struct file_operations的成员变量计划全是函数指针。现实中,字符设备驱动程序的编写,基本上是围绕着如何实现struct file_operations中的那些函数指针成员而展开的。应用程序对文件类函数的调用如read()/open()等,在linux内核的机制下,最终会转到structfile_operations中对应的函数指针成员上。

(2)THIS_MODULE

这是个宏定义#defineTHIS_MODULE(&__this_module)

__this_module内核模块的编译工具链为当前模块产生 的struct module 类对象,所以THIS_MODULE实际上是当前模块对象的指针。

(3)字符设备的抽象struct cdev

字符设备驱动程序管理的核心对象是字符设备,内核为字符设备抽象出了一个具体的数据结构struct cdev,它定义在文件<include/linux/cdev.h>中,如下:

struct cdev {
	struct kobject kobj;				//内嵌的内核对象
	struct module *owner;				//驱动程序所在的内核模块对象指针
	const struct file_operations *ops;  //存放各种操作函数的结构体对象
	struct list_head list;				//字符设备链表
	dev_t dev;							//字符设备的设备号,由主设备号和次设备号组成
	unsigned int count;					//隶属于同一个主设备号的次设备号的个数
};

需要注意的是,内核引入struct cdev数据结构作为字符设备的抽象,仅仅是为了满足系统 对字符设备驱动程序框架结构设计的需要,现实中一个具体的字符硬件设备的数据结构可能更复杂,在这种情况下,struct cdev常常作为一种内嵌的成员变量出现在设备的数据结构中,如:

struct my_keypad_dev
{
	//硬件相关的成员变量
	int a ,*p;
	...
	//内嵌的struct cdev结构对象
	struct cdev c_dev;
}

设备驱动程序中可以使用两种方式来产生struct cdev对象:

静态方式:static struct cdev chr_dev;

动态方式:static struct cdev *p=kmalloc(sizeof(struct cdev),GFP_KERNEL);

(4)初始化函数cdev_init

在(3)中讨论了如何产生一个struct cdev对象,接下来就讨论一下如何初始化一个cdev对象。为此,内核提供了相应的初始化函数cdev_init,定义在<fs/char_dev.c>中,如下:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;
}

参数说明:

  • cdev:指向需要初始化的设备对象
  • fops:包含了针对该字符设备的操作函数的结构体指针

5)设备号

在linux系统中,一个设备号由主设备号和次设备号构成。内核使用主设备号来定位对应的设备驱动程序。而次设备号则由驱动程序使用,用于标识它所管理的若干同类设备。设备号是系统管理设备的有效资源。Linux中使用 dev_t(32位无符号整数)来表示一个设备号。

A、内核提供了以下三个宏用于操作设备号:<include/linux/kdev_t.h>

#define MAJOR(dev) ((unsignedint)((dev)>>MINORBITS))   //提取主设备号
#define MINOR(dev) ((unsignedint)((dev)&MINORBITS))  //提取次设备号       
#define MKDEV(ma,mi)(((ma)<<MINORBITS)|(mi))              //合成设备号

B、为了有效的管理字符设备的设备号,内核定义了一个全局性指针数组chrdevs,该数组中的每一项都是一个指向struct char_device_struct类型的指针。系统中已分配的字符设备号都存放在该数组中。该指针数组定义如下:<fs/char_dev.c>

static struct char_device_struct {
	struct char_device_struct *next;
	unsigned int major;
	unsigned int baseminor;
	int minorct;
	char name[64];
	struct cdev *cdev;		/* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

C、另外内核还提供了两个函数用于分配和管理设备号,定义在<fs/char_dev.c>中

alloc_chrdev_region()函数:该函数用于分配设备号,分配的主设备号范围将在1~254之间。定义如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)
{
	struct char_device_struct *cd;
	cd = __register_chrdev_region(0, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	*dev = MKDEV(cd->major, cd->baseminor);
	return 0;
}

这个函数的核心是调用__register_chr_dev_region,而且第一个参数为0,这样将导致

__register_chr_dev_region执行下面的逻辑:

static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
			   int minorct, const char *name)
{jkjkhujhklkljkljmn
	struct char_device_struct *cd, **cp;
	int ret = 0;
	int i;
 
	cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
	if (cd == NULL)
		return ERR_PTR(-ENOMEM);
 
	mutex_lock(&chrdevs_lock);
 
	/* temporary */
	if (major == 0) {
		for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
			if (chrdevs[i] == NULL)
				break;
		}
 
		if (i == 0) {
			ret = -EBUSY;
			goto out;
		}
		major = i;
		ret = major;
	}
	...
}

这段代码的原理是:它在for循环中,从chrdevs数字的最后一项一次向前扫描,如果发现该数组中的某项,比如第i项对应的数值为NULL,那么就把该项对应的索引值i作为分配的主设备号返回给驱动程序,并将其加入到chrdevs[i]对应的哈希链表中。如果分配成功,所分配的主设备号将记录在structchar_device_struct对象cd中(数组存放的都是这种对象),并将cd返回给alloc_chrdev_region函数,后者通过*dev=MKDEV(cd->major,cd->baseminor) 将新分配的设备号返回给函数调用者。

register_chrdev_region()函数:函数原型如下:

int register_chrdev_region(dev_t from,unsigned count,const char *name){}

参数说明:

from:表示设备号;count:连续设备编号的个数;name:设备或者驱动的名称;

该函数的作用就是将当前设备驱动程序要使用的设备号记录到chrdev数组中,用于跟踪系统设备号的使用情况,从而避免设备号的冲突。在使用这个函数时,要事先知道它所使用的设备号。

(6)字符设备的注册

在一个字符设备初始化完之后,就可以把它加入系统中,这样别的模块才可以使用它。把一个字符加入系统中需要调用函数cdev_add。其定义如下:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
	p->dev = dev;
	p->count = count;
	return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}

cdev_add的核心功能通过kobj_map()函数来实现。调用cdev_add后,把要注册的字符设备对象的指针嵌入到了一个类型为struct probe的节点之中,然后再把该节点加入到cdev_map所实现的哈希链表中。有关struct probe和cdev_map的定义如下:

<fs/char_dev.c> static sturct kobj_map *cdev; //这是一个struct kobj_map指针类型的全局变量。在Linux系统启动期间由chrdev_init函数负责初始化。struct kobj_map定义如下:

struct kobj_map {
	struct probe {
		struct probe *next;
		dev_t dev;
		unsigned long range;
		struct module *owner;
		kobj_probe_t *get;
		int (*lock)(dev_t, void *);
		void *data;
	} *probes[255];
	struct mutex *lock;
};

kobj_map()函数过程:通过要加入系统的设备的主设备号major来获得probes数组的索引值i,然后把一个类型为struct probe的节点对象加入到probe[i]所管理的链表中。其中probe节点中包含了设备的主设备号,以及指向字符设备对象的指针。如下图:

通过调用cdev_add后,就意味着一个字符设备对象已经加入到了系统中,在需要的时候,系统就可以找到它了。

在cdev_add()函数中,动态分配了struct probe类型的节点。当设备对象从系统中移除时,需要将它们从链表中删除并释放节点所占用的内存空间。这就是cdev_del()函数的作用,定义如下:

void cdev_del(struct cdev *p)
{
	cdev_unmap(p->dev, p->count);
	kobject_put(&p->kobj);
}

对于以内核模块形式存在的驱动程序,作为通用规则,模块的卸载函数应负责调用这个函数来将所管理的设备对象从系统中移除。

(7)设备文件节点的生成

在linux系统中,硬件设备都是以文件的形式存在于/dev/下的。即对应/dev/下的每个文件节点都代表了一个设备。在linux系统中,每个文件都有两种不同的表示方式。对于任意一个文件,在用户空间一般用文件名来识别如demodev。在内核空间中,一般用inode来表示。如168。它们实际上指向的都是同一个文件。对于设备文件,有:

inode->i_fop=&def_chr_fops;
inode->i_rdev=rdev;

(8)字符设备文件的打开操作

作为例子,这里假定前面对应于/dev/demodev设备节点的驱动程序已经实现了如下的struct file_operations对象chr_fops和打开函数chr_open。

struct file_operations chr_ops=

{

       .owner=THIS_MODULE,

       .open=chr_open,

       .read=chr_read,

};

static int chr_open(struct inode *nd,structfile *filp)

{

       intmajor=MAJOR(nd->i_rdev);

       intminor=MINOR(nd->i_rdev);

       printk("chr_open,major=%d,minor=%d\n",major,minor);

       return0;

}

用户空间应用程序的open函数原型为:

int open(constchar *filename,int flages,mode_t mode);
位于内核空间的驱动程序中open函数的原型为:
structfile_operations

{     ...
       int(*open)(struct inode *,struct file *) ;
       …
}

接下来我们见描述用户态的open函数是如何一步步调用到驱动程序提供的open函数的(在本例子中即chr_open函数)。由前面的三个函数可以看出:用户态open函数返回的是文件描述符fd(整形)。而驱动程序中的参数类型为struct file*file。显然内核需要在打开设备文件时为fd与file建立某种关系,并且为file与驱动程序中的fops建立关联。

用户空间调用open函数,将发起一个系统调用,通过sys_open函数进入内核空间。调用关系如下:

1)do_sys_open函数首先通过get_unused_flags为本次的open操作 分配一个未使用过的文件描述符fd。

2)do_sys_open函数随后调用do_filp_open函数,该函数会查找 “/dev/demodev”设备文件对应的inode。查找到inode之后,接着调用函数get_empty_filp函数为打开的文件分配一个新的struct file类型的内存空间(返回指针)。内核用struct file对象来描述进程打开的每个文件。struct file的定义如下 :

struct file

{     ....

       Const structfile_operations *f_op ;

       ….

}

从struct file的定义可以看出,struct file对象中包含了struct file_operations类型的指针。

3)linux系统为每个进程都维护了一个文件描述符表。进程已打开文件的文件描述符(fd)就是文件描述符表的索引值。文件描述符表中的每一个表项都有一个指向已打开文件的指针。这个指针就是struct file 类型的指针。即:在描述符表中,通过fd索引只可以找到对应的表项,该表项的值就是filp,它指向了内核为刚刚打开的文件所分配的struct file类型空间。

4)在do_sys_open函数的后半部分,调用函数__dentry_open函数将“/dev/demodev”对应节点的inode中的i_fop赋值给filp->f_op。由(7)中节点创建可以知道,inode->i_fop=&def_chr_fops;因此进行赋值操作filp->fop=inode->i_fop后,filp->fop=&def_chr_fops;即file结构成员*fop指向了设备驱动程序中的struct file_operations型数据结构。从而可以调用驱动程序的open函数。

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

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

相关文章

从零开始的MySQL(2)

目录1.数据库约束1.1 unique1.2 not null1.3 default1.5 primary key1.6 foreign key2. 将A的记录插入到B中3.聚合函数3.1 count()函数3.2 sum()函数3.3 avg()函数3.4 max()函数3.5 MIN()函数3.6 group by4.联合查询4.1 内连接4.2 外连接4.3 自连接4.4 子连接1.数据库约束 约束…

【简单易上手】昇思MindSpore邀你定制专属Diffusion模型

昇思MindSpore首个可训练diffusion模型DDPM马上要和大家见面了&#xff0c;操作简单&#xff0c;可训练推理&#xff0c;单卡即可运行&#xff0c;欢迎广大产学研开发者使用启智社区免费Ascend NPU算力体验 最近爆火的AI绘图&#xff0c;相信大家并不陌生了。 从AI绘图软件生成…

知识图谱的知识表示:向量表示方法

目录 从词向量到实体向量 知识图谱向量表示学习模型 TransE TransH TransR TransD TransX系列总结 DisMult 从词向量到实体向量 知识图谱的向量表示。有一类词是代表实体的&#xff0c;假如对这类实体词的向量做一些计算&#xff0c;比如用Rome向量减去Italy的向量&am…

使用界面组件Telerik ThemeBuilder研发主题,只需要这七步!

Telerik DevCraft包含一个完整的产品栈来构建您下一个Web、移动和桌面应用程序。它使用HTML和每个.NET平台的UI库&#xff0c;加快开发速度。Telerik DevCraft提供最完整的工具箱&#xff0c;用于构建现代和面向未来的业务应用程序。 ThemeBuilder是一个web应用程序&#xff0…

前端基础(一)_初识JavaScript

最开始的时候&#xff0c;浏览器只能显示文本和图片&#xff0c;并不能做各种动态的操作。 一、JavaScript的历史 1、网景公司的布兰登艾奇开发了js 2、借鉴了java的数据管理机制、c的函数 3、Js的创建是用来解决浏览器和用户之间互动的问题&#xff08;表单提交的问题&#…

LeanCloud 快速实现服务端

1. 实现与服务器交互平台 1.1 Google 平台的 Firebase (需要科学网络) Firebasehttps://firebase.google.cn/ 1.2 LeanCloud 平台 LeanCloudhttps://www.leancloud.cn/ 2. 配置信息 2.1 在 LeanCloud 控制台创建应用, 根据 SDK下载 开发指南配置应用 2.2 配置文件 build…

拉伯杠杆平台|新冠药龙头跳水,万亿产业开启新阶段!

依照此前发表的时间表&#xff0c;新修订的《体育法》将于2023年1月1日起正式施行。 12月27日早盘&#xff0c;A股前期大热的抗疫体裁呈现显着回落。熊去氧胆酸、新冠药、肝炎概念、新冠检测、生物疫苗等体裁跌幅较大。熊去氧胆酸、新冠药两大板块跌幅超过4%&#xff0c;抢手龙…

外贸采购管理对业务的影响及解决方案

在外贸企业中&#xff0c;采购环节的把控对整个业务环节都有影响。像是采购供应商是否能够按时到货&#xff0c;会直接影响生产进度&#xff1b;采购质量的好坏直接影响产品的生产进度及质量&#xff1b;采购成本的高低&#xff0c;直接影响产品的成本及利润&#xff1b;采购供…

NSUM 问题

目录标题两数之和两数之和 2&#xff08;多个结果集去重&#xff09;15. 三数之和两数之和 问题描述 给定一个整数数组 nums 和一个目标值 target&#xff0c;请你在该数组中找出和为目标值的那 两个 整数&#xff0c;并返回他们的数组下标。 你可以假设每种输入只会对应一个答…

基于Java(JSP)+MySQL实现(Web)学生成绩综合管理系统【100010065】

Java 课程设计:师生交流系统 1.1 课设题目 信息商务学院学生成绩综合管理系统的综合与开发 2.1 课设目的 《Java 程序设计》课程是计算机专业的一门专业必修课程&#xff0c;针对课程进行为期两周的实践教学&#xff0c;旨在提升本专业学生对此课程中的理论知识的综合应用能…

[Python学习系列] 走进Django框架

文章目录1. 安装django2. 创建项目&#xff08;命令的方式&#xff09;在第一次创建django项目需要做的&#xff1a;每一次创建django项目需要做的&#xff1a;3. 创建app4. 启动运行django项目程序5. 模板&静态文件的存放地址6. django中的模板语法7. django中常见的请求和…

ArcGIS基础实验操作100例--实验4矢量要素的镜像复制,缩放

实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 基础编辑篇--实验4 矢量要素的镜像复制&#xff0c;缩放 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&#xff09;加载【Mirror Features】工具 &#x…

VMware之安装Windows10系统

系统下载 下载地址&#xff1a;原版软件 (itellyou.cn) 创建虚拟机系统框架 在菜单栏中选择文件下的新建虚拟机 选择自定义&#xff0c;然后点击下一步 直接下一步 选择稍后安装操作系统&#xff0c;然后点击下一步 因为安装的系统是Windows&#xff0c;这里注意选择Microsof…

使用eBPF追踪Linux内核

简介 BPF&#xff0c;及伯克利包过滤器Berkeley Packet Filter&#xff0c;最初构想提出于 1992 年&#xff0c;其目的是为了提供一种过滤包的方法&#xff0c;并且要避免从内核空间到用户空间的无用的数据包复制行为。它最初是由从用户空间注入到内核的一个简单的字节码构成&…

浏览器:理解HTTP无状态与Cookie的使用

一、理解HTTP无状态 1.1、理解http无状态 http无状态是指协议对于用户身份、用户状态、用户权限、交互场景等没有记忆能力。简单讲就是不能识别用户。 1.2、http无状态的优点&#xff1a; 可以更快地处理大量的事务&#xff0c;确保协议的可伸缩性&#xff0c;减少服务器的…

一文搞定十大排序算法(动画图解)

排序的定义 排序&#xff0c;就是重新排列表中的元素&#xff0c;使表中的元素满足按关键字递增或递减的过程。为了査找方便&#xff0c;通常要求计算机中的表是按关键字有序的。 排序的确切定义如下&#xff1a; 算法的稳定性&#xff1a; 若待排序表中有两个元素 Ri 和 Rj&am…

包装类和简单认识泛型

目录 1.包装类 1.1基本数据类型和对应的包装类 1.2装箱和拆箱 1.3自动装箱和自动拆箱 2.什么是泛型 3.引出泛型 3.1语法 4.泛型类的使用 4.1语法 4.2类型推导 5.裸类型 5.1说明 6.泛型如何编译的 6.1擦除机制 6.2为什么不能实例化泛型类型的数组 7.泛型的上界 …

MaxKey单点登录认证系统3.5.12发布,重要漏洞修复

业界领先的IAM/IDaas身份管理和认证产品 概述 MaxKey单点登录认证系统&#xff0c;谐音马克思的钥匙寓意是最大钥匙,是业界领先的IAM/IDaas身份管理和认证产品,支持OAuth 2.x/OpenID Connect、SAML 2.0、JWT、CAS、SCIM等标准协议&#xff0c;提供安全、标准和开放的用户身份…

STM32/51单片机实训day3——点亮LED灯、闪烁LED灯(二)实践

内 容&#xff1a;编写代码实现LED灯的点亮功能 学 时&#xff1a;2学时 知识点&#xff1a;分析原理图、LED灯控制原理 重点&#xff1a;GPIO参数配置、LED原理图分析 难点&#xff1a;编写 GPIO参数配置函数、LED点亮函数 时间&#xff1a;2022年12月21日 9:00&#xff5e;…

如何轻松做数据治理?开源技术栈告诉你答案

搭建一套数据治理体系耗时耗力&#xff0c;但或许我们没有必要从头开始搞自己的数据血缘项目。本文分享如何用开源、现代的 DataOps、ETL、Dashboard、元数据、数据血缘管理系统构建大数据治理基础设施。 元数据治理系统 元数据治理系统是一个提供了所有数据在哪、格式化方式…