驱动-兼容不同设备-container_of

news2025/5/9 17:14:20

驱动兼容不同类型设备
在 Linux 驱动开发中,container_of 宏常被用来实现一个驱动兼容多种不同设备的架构。这种设计模式在 Linux 内核中非常常见,特别
是在设备驱动模型中。linux内核的主要开发语言是C,但是现在内核的框架使用了非常多的面向对象的思想,这就面临了一个用C语言
来实现面向对象编程的问题

文章目录

  • 参考资料
  • 典型应用场景
  • 原理:利用结构体中元素指针获取结构体指针
  • container_of 函数
  • 理解
    • C程序例子
  • 实验
    • 源码程序 file.c
      • 源码分析
        • 基本步骤
      • 知识点
        • container_of 当前使用分析
        • read、write
    • Makefile 编译脚本
    • 测试程序app.c
    • 加载驱动 insmod file.ko
    • 生成的驱动设备
    • 运行程序 ./app
  • 总结


参考资料

在字符设备这块内容,所有知识点都是串联起来的,需要整体来理解,缺一不可,建议多了解一下基础知识
驱动-申请字符设备号
驱动-注册字符设备
驱动-创建设备节点
驱动-字符设备驱动框架
驱动-杂项设备
驱动-内核空间和用户空间数据交换
驱动-文件私有数据

典型应用场景

  • 同一厂商的不同型号设备
  • 功能相似但寄存器布局不同的设备
  • 需要维护设备特定数据的场合

原理:利用结构体中元素指针获取结构体指针

Kobject是linux设备驱动模型的基础,也是设备模型中抽象的一部分。

linux内核为了兼容各种形形色色的设备,就需要对各种设备的共性进行抽象,抽象出一个基类,其余的设备只需要继承此基类就可以了。

而此基类就是kobject(暂且把它看成是一个类),但是C语言没有面向对象语法。

在C++中这样的操作非常简单,继承基类就可以了,而在C语言中需要将基类的结构体指针嵌入到派生的类中,那么为什么将基类指针嵌入就可以得到派生类的指针呢?

这个实现是一个宏:container_of。

container_of 函数

container_of 在 Linux 内核中是一个常用的宏, 用于从包含在某个结构中的指针获得结构本
身的指针, 通俗地讲就是通过结构体变量中某个成员的首地址进而获得整个结构体变量的首地
址。 那么可以使用这个函数获取不同设备的地址, 来对不同的设备进行操作, 从而一个驱动可
以兼容不同的设备。

container_of 

函数原型:
    container_of(ptr,type,member)
函数作用:
   通过结构体变量中某个成员的首地址获取到整个结构体变量的首地址。
参数含义:
  ptr 是结构体变量中某个成员的地址。
  type 是结构体的类型
  member 是该结构体变量的具体名字

container_of 宏的作用是通过结构体内某个成员变量的地址和该变量名, 以及结构体类型。
找到该结构体变量的地址

理解

C程序例子

 #include <stdio.h>
 
 struct Base{                    //定义一个Base类
     int var;
     char *string;
 };
 
 struct Derived{                 //定义一个派生类
     int var;
     struct Base base;           //派生类中包含了struct Base类
 };

 #define offsetof(TYPE, MEMBER)	((size_t)&((TYPE *)0)->MEMBER)       //offset_of宏
 #define container_of(ptr, type, member) ({				\            //阉割版container_of,省去了类型检查
     void *__mptr = (void *)(ptr);                     \
     ((type *)(__mptr - offsetof(type, member))); })
 
 struct Base *base_p;                  
 struct Derived test_derived;       
 
 
 struct Derived *get;
 int main()
 {
    base_p = &test_derived.base;                                     //赋值给指针
    printf("Derived addr = %x\n",(unsigned int)&test_derived);
    printf("=================================\n");
    get = container_of(base_p,struct Derived,base);                 //使用base_p指针获取派生类的首地址
    printf("get Derived addr = %x\n",(unsigned int)get);

    return 0;
 }

输出结果:


输出如下:
Derived addr = 601050
=================================
get Derived addr = 601050

源码分析:最重要的就是理解 container_of 函数

    get = container_of(base_p,struct Derived,base);                 //使用base_p指针获取派生类的首地址

  
  


  • base_p :ptr 是结构体变量中某个成员的地址。 这个base_p 是怎么定义的 struct Base *base_p; Base 又是在哪里定义的呢? 作为派生类Deviced 的成员变量 struct Base base

  • struct Derived: type 是结构体的类型,不就是派生类的结构体类型吗? 这个函数就是要返回这个结构体类型的指针

  • base: member 是该结构体变量的具体名字。 就是派生类里面 定义的成员变量名字。

我们这里讨论和接下来讨论的目的就是要理解这个函数container_of的作用,理解参数和返回类型 以及理解实际应用的价值。

实验

使用 container_of 函数编写一个驱动兼容不同设备

源码程序 file.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

struct device_test
{

    dev_t dev_num;         // 设备号
    int major;             // 主设备号
    int minor;             // 次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   // 类
    struct device *device; // 设备
    char kbuf[32];
};

struct device_test dev1; // 定义一个device_test结构体变量dev1
struct device_test dev2; // 定义一个device_test结构体变量dev2

/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    dev1.minor = 0; // 设置dev1的次设备号为0
    dev2.minor = 1; // 设置dev2的次设备号为1

    // inode->i_rdev 为该 inode 的设备号,使用container_of函数找到结构体变量dev1 dev2的地址
    // 然后设置私有数据
    file->private_data = container_of(inode->i_cdev, struct device_test, cdev_test);
    printk("This is cdev_test_open\r\n");

    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    struct device_test *test_dev = (struct device_test *)file->private_data;

    // 如果次设备号是0,则为dev1
    if (test_dev->minor == 0)
    {

        if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
        {
            printk("copy_from_user error\r\n");
            return -1;
        }
        printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);
    }
    // 如果次设备号是1,则为dev2
    else if (test_dev->minor == 1)
    {
        if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
        {
            printk("copy_from_user error\r\n");
            return -1;
        }
        printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);
    }
    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{

    struct device_test *test_dev = (struct device_test *)file->private_data;

    if (copy_to_user(buf, test_dev->kbuf, strlen(test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }

    printk("This is cdev_test_read\r\n");
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    printk("This is cdev_test_release\r\n");
    return 0;
}

/*设备操作函数,定义file_operations结构体类型的变量cdev_test_fops*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE,         // 将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open,       // 将open字段指向chrdev_open(...)函数
    .read = cdev_test_read,       // 将open字段指向chrdev_read(...)函数
    .write = cdev_test_write,     // 将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, // 将open字段指向chrdev_release(...)函数
};

static int __init chr_fops_init(void) // 驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号,,这里注册2个设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 2, "alloc_name"); // 动态分配设备号
    if (ret < 0)
    {
        printk("alloc_chrdev_region is error\n");
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); // 获取主设备号
    dev1.minor = MINOR(dev1.dev_num); // 获取次设备号

    printk("major is %d \r\n", dev1.major); // 打印主设备号
    printk("minor is %d \r\n", dev1.minor); // 打印次设备号

    // 对设备1进行操作
    /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
    cdev_add(&dev1.cdev_test, dev1.dev_num, 1);

    /*4 创建类*/
    dev1.class = class_create(THIS_MODULE, "test1");

    /*5 创建设备*/
    dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test1");

    dev2.major = MAJOR(dev1.dev_num + 1); // 获取主设备号
    dev2.minor = MINOR(dev1.dev_num + 1); // 获取次设备号

    printk("major is %d \r\n", dev2.major); // 打印主设备号
    printk("minor is %d \r\n", dev2.minor); // 打印次设备号

    // 对设备2进行操作
    /*2 初始化cdev*/
    dev2.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev2.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
    cdev_add(&dev2.cdev_test, dev1.dev_num + 1, 1);

    /*4 创建类*/
    dev2.class = class_create(THIS_MODULE, "test2");

    /*5  创建设备*/
    dev2.device = device_create(dev2.class, NULL, dev1.dev_num + 1, NULL, "test2");

    return 0;
}

static void __exit chr_fops_exit(void) // 驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1);     // 注销设备号
    unregister_chrdev_region(dev1.dev_num + 1, 1); // 注销设备号
    cdev_del(&dev1.cdev_test);                     // 删除cdev
    cdev_del(&dev2.cdev_test);                     // 删除cdev
    device_destroy(dev1.class, dev1.dev_num);      // 删除设备
    device_destroy(dev2.class, dev1.dev_num + 1);  // 删除设备
    class_destroy(dev1.class);                     // 删除类
    class_destroy(dev2.class);                     // 删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

源码分析

这个代码程序就是在一个程序驱动程序适配了两个设备,里面有两套字符设备,所以字符设备驱动实践的基本步骤都是有的:

基本步骤

这里虽然在一个驱动程序里面,但是两个字符设备在里面生成,基本步骤是有的。

  • 动态申请设备号:alloc_chrdev_region
  • 初始化cdev:cdev_init
  • 添加一个cdev,完成字符设备注册到内核:cdev_add
  • 创建类:class_create
  • 创建device device_create

知识点

  • private_data:这里也用到了之前的私有数据结构体,file 相关的:file->private_data;
  • device_test: 匹配private_data 就有了一个结构体,作为变量的封装。 搭配private_data 使用。 这里也体现了私有数据 private_data 和 结构体的意义。封装:设备号 dev_t dev_num ; 主 次设备号:major、minor;字符设备:cdev ;class:字符类 ;device 创建的字符设备:device
  • alloc_chrdev_region(&dev1.dev_num, 0, 2, “alloc_name”): 看参数数据,动态申请设备号时候,主设备号一样的,从0 开始,申请两个次设备号来使用。
container_of 当前使用分析
  // ,使用container_of函数找到结构体变量dev1 dev2的地址
    // 然后设置私有数据
    
file->private_data = container_of(inode->i_cdev, struct device_test, cdev_test);

参数分析:

  • struct device_test:struct device_test,不就是封装的结构体类型吗? 说明生成的结果就是封装结构体类型的指针,然后放到private_data里面。 这样在 系统调用中的read / write 就特别方便使用了。
  • cdev_test : 第三个参数要求是 派生类里面,定义的基类的对象的名称。 在 结构体里面定义的如下: struct cdev cdev_test; // cdev 不就是动态注册的字符设备嘛。
  • inode->i_cdev:第一个参数本身需要派生类中某个成员变量的的地址就可以了。 这里用如下 inode 就是传递过来节点的设备号。它本身也是一个结构体指针要,简要如说也如下所示:
 cdev_test_open(struct inode *inode, struct file *file)
struct inode {
    // ...
    union {
        struct pipe_inode_info *i_pipe;
        struct block_device *i_bdev;
        struct cdev *i_cdev;    // 指向字符设备结构的指针
        char *i_link;
    };
    // ...
};

所以,这里在open 方法中,使用了cdev_test_open 函数,最终将 封装的结构体存储在私有数据里面,传递不同的设备过来 比如打开不同设备节点,那么就会生成不同的封装的结构体类型的指针了。

read、write

核心逻辑还是上面的 知识点 container_of 函数的分析。既然封装的结构体指针都获取到了,那么在read/write 就可以通过不同的属性来适配不同的字符设备了。

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    struct device_test *test_dev = (struct device_test *)file->private_data;

    // 如果次设备号是0,则为dev1
    if (test_dev->minor == 0)
    {

        if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
        {
            printk("copy_from_user error\r\n");
            return -1;
        }
        printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);
    }
    // 如果次设备号是1,则为dev2
    else if (test_dev->minor == 1)
    {
        if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
        {
            printk("copy_from_user error\r\n");
            return -1;
        }
        printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);
    }
    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{

    struct device_test *test_dev = (struct device_test *)file->private_data;

    if (copy_to_user(buf, test_dev->kbuf, strlen(test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }

    printk("This is cdev_test_read\r\n");
    return 0;
}

Makefile 编译脚本

#!/bin/bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
obj-m += file.o
KDIR :=/home/wfc123/Linux/rk356x_linux/kernel
PWD ?= $(shell pwd)
all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean

测试程序app.c

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

int main(int argc, char *argv[])
{
    int fd1;  //定义设备1的文件描述符
    int fd2;  //定义设备2的文件描述符
    char buf1[32] = "nihao /dev/test1";   //定义写入缓存区buf1
    char buf2[32] = "nihao /dev/test2";   //定义写入缓存区buf2
    fd1 = open("/dev/test1", O_RDWR);  //打开设备1:test1
    if (fd1 < 0)
    {
        perror("open error \n");
        return fd1;
    }
    write(fd1,buf1,sizeof(buf1));  //向设备1写入数据
    close(fd1); //取消文件描述符到文件的映射

    fd2= open("/dev/test2", O_RDWR); //打开设备2:test2
    if (fd2 < 0)
    {
        perror("open error \n");
        return fd2;
    }
    write(fd2,buf2,sizeof(buf2));  //向设备2写入数据
    close(fd2);   //取消文件描述符到文件的映射

    return 0;
}

编译生成可执行程序:


aarch64-linux-gnu-gcc app.c -o app

加载驱动 insmod file.ko

如下,生成了两个不同的次设备号,主设备号一样的。
在这里插入图片描述

生成的驱动设备

驱动加载成功之后会生成/dev/test1 和/dev/test2 设备驱动文件,如下:

root@topeet:/mnt/sdcard]# ls /dev/test1 -al
crw------- 1 root root 236, 0 Jan 12 08:18 /dev/test1
[root@topeet:/mnt/sdcard]# ls /dev/test2 -al
crw------- 1 root root 236, 1 Jan 12 08:18 /dev/test2

运行程序 ./app

结果如下所示,一切按照程序逻辑来执行的。
在这里插入图片描述

总结

本篇其实还是对以前技术的总结,这里着重用到了函数 container_of,有点面向对象的意思。 这里适配不同的驱动设备只是一个案例而已。

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

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

相关文章

MySQLQ_数据库约束

目录 什么是数据库约束约束类型NOT NULL 非空约束UNIQUE 唯一约束PRIMARY KEY主键约束FOREIGN KEY外键约束CHECK约束DEFAULT 默认值(缺省)约束 什么是数据库约束 数据库约束就是对数据库添加一些规则&#xff0c;使数据更准确&#xff0c;关联性更强 比如加了唯一值约束&#…

责任链设计模式(单例+多例)

目录 1. 单例责任链 2. 多例责任链 核心区别对比 实际应用场景 单例实现 多例实现 初始化 初始化责任链 执行测试方法 欢迎关注我的博客&#xff01;26届java选手&#xff0c;一起加油&#x1f498;&#x1f4a6;&#x1f468;‍&#x1f393;&#x1f604;&#x1f602; 最近在…

林纳斯·托瓦兹:Linux系统之父 Git创始人

名人说&#xff1a;路漫漫其修远兮&#xff0c;吾将上下而求索。—— 屈原《离骚》 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 林纳斯托瓦兹&#xff1a;Linux之父、Git创始人 一、传奇人物的诞生 1. 早年生活与家…

8. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 “发布确认” 的功能

8. RabbitMQ 消息队列 结合配合 Spring Boot 框架实现 “发布确认” 的功能 文章目录 8. RabbitMQ 消息队列 结合配合 Spring Boot 框架实现 “发布确认” 的功能1. RabbitMQ 消息队列 结合配合 Spring Boot 框架实现 “发布确认” 的功能1.1 回退消息 2.备用交换机3. API说…

维港首秀!沃飞长空AE200亮相香港特别行政区

4月13日-16日&#xff0c;第三届香港国际创科展在香港会议展览中心盛大举办。 作为国内领先、国际一流的eVTOL主机厂&#xff0c;沃飞长空携旗下AE200批产构型登陆国际舞台&#xff0c;以前瞻性的创新技术与商业化应用潜力&#xff0c;吸引了来自全球17个国家及地区的行业领袖…

redis6.2.6-prometheus监控

一、软件及系统信息 redis&#xff1a;redis-6.2.6 redis_exporter&#xff1a;redis_exporter-v1.50.0.linux-amd64.tar.gz # cat /etc/anolis-release Anolis OS release 8.9 granfa; 7.5.3 二、下载地址 https://github.com/oliver006/redis_exporter/releases?page…

如何在idea中快速搭建一个Spring Boot项目?

文章目录 前言1、创建项目名称2、勾选需要的依赖3、在setting中检查maven4、编写数据源5、开启热启动&#xff08;热部署&#xff09;结语 前言 Spring Boot 凭借其便捷的开发特性&#xff0c;极大提升了开发效率&#xff0c;为 Java 开发工作带来诸多便利。许多大伙伴希望快速…

itext7 html2pdf 将html文本转为pdf

1、将html转为pdf需求分析 经常会看到爬虫有这样的需求&#xff0c;将某一个网站上的数据&#xff0c;获取到了以后&#xff0c;进行分析&#xff0c;然后将需要的数据进行存储&#xff0c;也有将html转为pdf进行存储&#xff0c;作为原始存档&#xff0c;当然这里看具体的需求…

docker compose搭建博客wordpress

一、前言 docker安装等入门知识见我之前的这篇文章 https://blog.csdn.net/m0_73118788/article/details/146986119?fromshareblogdetail&sharetypeblogdetail&sharerId146986119&sharereferPC&sharesourcem0_73118788&sharefromfrom_link 1.1 docker co…

代码随想录算法训练营Day30

力扣452.用最少数量的箭引爆气球【medium】 力扣435.无重叠区间【medium】 力扣763.划分字母区间【medium】 力扣56.合并区间【medium】 一、力扣452.用最少数量的箭引爆气球【medium】 题目链接&#xff1a;力扣452.用最少数量的箭引爆气球 视频链接&#xff1a;代码随想录 题…

无感改造,完美监控:Docker 多阶段构建 Go 应用无侵入观测

作者&#xff1a;牧思 背景 随着云原生的普及&#xff0c;Golang 编程语言变得越来越热门。相比 Java&#xff0c;Golang 凭借其轻量&#xff0c;易学习的特点得到了越来越多工程师的青睐&#xff0c;然而由于 Golang 应用需要被编译成二进制文件再进行运行&#xff0c;Golan…

006.Gitlab CICD流水线触发

文章目录 触发方式介绍触发方式类型 触发方式实践分支名触发MR触发tag触发手动人为触发定时任务触发指定文件变更触发结合分支及文件变更触发正则语法触发 触发方式介绍 触发方式类型 Gitlab CICD流水线的触发方式非常灵活&#xff0c;常见的有如下几类触发方式&#xff1a; …

512天,倔强生长:一位技术创作者的独白

亲爱的读者与同行者&#xff1a; 我是倔强的石头_&#xff0c;今天是我在CSDN成为创作者的第512天。当系统提示我写下这篇纪念日文章时&#xff0c;我恍惚间想起了2023年11月19日的那个夜晚——指尖敲下《开端——》的标题&#xff0c;忐忑又坚定地按下了“发布”键。那时的我…

【目标检测】【YOLO综述】YOLOv1到YOLOv10:最快速、最精准的实时目标检测系统

YOLOv1 to YOLOv10&#xff1a; The fastest and most accurate real-time object detection systems YOLOv1到YOLOv10&#xff1a;最快速、最精准的实时目标检测系统 论文链接 0.论文摘要 摘要——本文是对YOLO系列系统的全面综述。与以往文献调查不同&#xff0c;本综述文…

日常学习开发记录-slider组件

日常学习开发记录-slider组件 从零开始实现一个优雅的Slider滑块组件前言一、基础实现1. 组件结构设计2. 基础样式实现3. 基础交互实现 二、功能增强1. 添加拖动功能2. 支持范围选择3. 添加垂直模式 三、高级特性1. 键盘操作支持2. 禁用状态 五、使用示例六、总结 从零开始实现…

Windows 系统如何使用Redis 服务

前言 在学习过程中&#xff0c;我们长期接触到的是Mysql 关系型数据库&#xff0c;也是够我们平时练习项目用的&#xff0c;但是后面肯定会有大型数据的访问就要借助新的新的工具。 一、什么是Redis Redis&#xff08;Remote Dictionary Server&#xff09;是一个基于内存的 键…

【unity游戏开发入门到精通——UGUI】CanvasScaler画布缩放器组件

注意&#xff1a;考虑到UGUI的内容比较多&#xff0c;我将UGUI的内容分开&#xff0c;并全部整合放在【unity游戏开发——UGUI】专栏里&#xff0c;感兴趣的小伙伴可以前往逐一查看学习。 文章目录 一、CanvasScaler画布缩放器组件是什么二、CanvasScaler的三种适配模式1、Cons…

Hugging Face 模型:AI 模型的“拥抱”与开源革命!!!

&#x1f310; Hugging Face 模型&#xff1a;AI 模型的“拥抱”与开源革命 用表情符号、图表和代码&#xff0c;探索开源模型生态的底层逻辑与应用场景&#xff01; &#x1f31f; 名字由来&#xff1a;为什么叫 Hugging Face&#xff1f; “Hugging”&#xff1a;象征 开放…

关于 人工智能(AI)发展简史 的详细梳理,按时间阶段划分,涵盖关键里程碑、技术突破、重要人物及挑战

以下是关于 人工智能&#xff08;AI&#xff09;发展简史 的详细梳理&#xff0c;按时间阶段划分&#xff0c;涵盖关键里程碑、技术突破、重要人物及挑战&#xff1a; 字数&#xff1a;约2500字 逻辑结构&#xff1a;时间线清晰&#xff0c;分阶段描述技术突破、关键事件与挑战…

微服务即时通信系统---(四)框架学习

目录 ElasticSearch 介绍 安装 安装kibana ES客户端安装 头文件包含和编译时链接库 ES核心概念 索引(Index) 类型(Type) 字段(Field) 映射(mapping) 文档(document) ES对比MySQL Kibana访问ES测试 创建索引库 新增数据 查看并搜索数据 删除索引 ES…