Linux字符设备驱动(二) - 与设备驱动模型的关系

一,从/dev目录说起

从事Linux嵌入式驱动开发的人,都很熟悉下面的一些基础知识, 比如,对于一个char类型的设备,我想对其进行read wirte 和ioctl操作,那么:

1)我们通常会在内核驱动中实现一个file_operations结构体,然后分配主次设备号,调用cdev_add函数进行注册。

2)从/proc/devices下面找到注册的设备的主次设备号,在用mknod /dev/char_dev c major minor命令行创建设备节点。

3)在用户空间open /dev/char_dev这个设备,然后进行各种操作。

OK,字符设备模型就这么简单,很多ABC教程都是一个类似的实现。

然后我们去看内核代码时,突然一脸懵逼。。。怎么内核代码里很多常用的驱动的实现不是这个样子的?没看到有file_operations结构体,我怎么使用这些驱动?看到了/dev目录下有需要的char设备,可是怎么使用呢?

Linux驱动模型的一个重要分界线

linux2.6版本以前,普遍的用法就像我上面说的一样。但是linux2.6版本以后,引用了Linux设备驱动模型,开始使用了基于sysfs的文件系统,出现让我们不是太明白的那些Linux内核驱动了。

也就是说,我们熟悉的那套驱动模式是2.6版本以前的(当然这是基础,肯定要会的)

我们不熟悉的驱动模型是2.6版本以后的。

二,cdev_map对象

//fs/char_dev.c 
27 static struct kobj_map *cdev_map;

内核中关于字符设备的操作函数的实现放在"fs/char_dev.c"中,打开这个文件,首先注意到就是这个在内核中不常见的静态全局变量cdev_map(27),我们知道,为了提高软件的内聚性,Linux内核在设计的时候尽量避免使用全局变量作为函数间数据传递的方式,而建议多使用形参列表,而这个结构体变量在这个文件中到处被使用,所以它应该是描述了系统中所有字符设备的某种信息,带着这样的想法,我们可以在"drivers/base/map.c"中找到kobj_map结构的定义:

//drivers/base/map.c
19 struct kobj_map {     
20         struct probe {
21                 struct probe *next;
22                 dev_t dev;
23                 unsigned long range;
24                 struct module *owner;
25                 kobj_probe_t *get;
26                 int (*lock)(dev_t, void *);
27                 void *data;
28         } *probes[255];  
29         struct mutex *lock;
30 };

从中可以看出,kobj_map的核心就是一个struct probe类型、大小为255的数组,而在这个probe结构中,第一个成员next(21)显然是将这些probe结构通过链表的形式连接起来,dev_t类型的成员dev显然是设备号,get(25)和lock(26)分别是两个函数接口,最后的重点来了,void作为C语言中的万金油类型,在这里就是我们cdev结构(通过后面的分析可以看出),所以,这个cdev_map是一个struct kobj_map类型的指针,其中包含着一个struct probe*类型、大小为255的数组,数组的每个元素指向的一个probe结构封装了一个设备号和相应的设备对象(这里就是cdev),下图中体现两种常见的对设备号和cdev管理的方式,其一是一个cdev对象对应这一个/多个设备号的情况, 在cdev_map中, 一个probes对象就对应一个主设备号,多个设备号对应一个cdev时,其实只是次设备号在变,主设备号还是一样的,所以是同一个probes对象;其二是当主设备号超过255时,会进行probe复用,此时probe->next就派上了用场,比如probe[200],可以表示设备号200,455...3895等所有对255取余是200的数字, 参见下文的kobj_map--58--。

三,cdev_add()

1,cdev_add()

了解了cdev_map的功能,我们就可以一探cdev_add()。从中可以看出,其工作显然是交给了kobj_map()

cdev_add()

--460-->就是将我们之前获得设备号和设备号长度填充到cdev结构中,

--468-->kobject_get()将kobject的计数减一,并返回struct kobject*

//fs/char_dev.c
456 int cdev_add(struct cdev *p, dev_t dev, unsigned count)    
457 {
458         int error;
459
460         p->dev = dev;
461         p->count = count;
462
463         error = kobj_map(cdev_map, dev, count, NULL,
464                          exact_match, exact_lock, p);
465         if (error)
466                 return error;
467
468         kobject_get(p->kobj.parent);
469
470         return 0;
471 }

2,kobj_map()

这个函数在内核的设备管理中占有重要的地位,这里我们只从字符设备的角度分析它的功能,这个函数的设计也很单纯,就是封装好一个probe结构并将它的地址放入probes数组进而封装进cdev_map,。

kobj_map()

--48-55-->根据传入的设备号的个数,将设备号和cdev依次封装到kmalloc_array()分配的n个probe结构中

--57-63-->就是遍历probs数组,直到找到一个值为NULL的元素,再将probe的地址存入probes, 将设备号对255取余后与probes的下标对应。至此,我们就将我们的cdev放入的内核的数据结构。

//drivers/base/map.c
32 int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
33              struct module *module, kobj_probe_t *probe,
34              int (*lock)(dev_t, void *), void *data)
35 {
36         unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
37         unsigned index = MAJOR(dev);
38         unsigned i;
39         struct probe *p;...
44         p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL);...
48         for (i = 0; i < n; i++, p++) {    
49                 p->owner = module;
50                 p->get = probe;
51                 p->lock = lock;
52                 p->dev = dev;
53                 p->range = range;
54                 p->data = data;
55         }
56         mutex_lock(domain->lock);
57         for (i = 0, p -= n; i < n; i++, p++, index++) {
58                 struct probe **s = &domain->probes[index % 255];
59                 while (*s && (*s)->range < range)
60                         s = &(*s)->next;
61                 p->next = *s;        
62                 *s = p;
63         }
64         mutex_unlock(domain->lock);  
65         return 0;
66 }

3,cdev_add函数的实质

kobj_map()函数:用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里 。当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。

此处只是简单的一个保存过程,并没有将cdev和inode关联起来。

有了这个关联之后,当我们使用mknod 命令,就会创建一个inode节点,并且通过 dev_t将inode和cdev_map里面的cdev联系起来了。

四,cdev的创建

1,手动创建cdev

手动创建设备文件就是使用mknod /dev/xxx 设备类型 主设备号 次设备号的命令创建,

所以首先需要使用cat /proc/devices查看设备的主设备号并通过源码找到设备的次设备号。

2,自动创建cdev

class_create()/device_create() -- 导出相应的设备信息到"/sys"/* 在/sys中导出设备类信息 */cls = class_create(THIS_MODULE,DEV_NAME);/* 在cls指向的类中创建一组(个)设备文件 */for(i= minor;i<(minor+cnt);i++){devp = device_create(cls,NULL,MKDEV(major,i),NULL,"%s%d",DEV_NAME,i);}

3,mknod

device_registerdevice_add // 其中包含2个关键函数// 将相关信息添加到/sys文件系统中device_create_file// 将相关信息添加到/devtmpfs文件系统中devtmpfs_create_node

devtmpfs_create_node()函数的核心是调用了内核的 vfs_mknod()函数,这样就在devtmpfs系统中创建了一个设备节点,

当devtmpfs被内核mount到/dev目录下时,该设备节点就存在于/dev目录下,比如/dev/char_dev之类的。

devtmpfs_create_node函数流程:

devtmpfs_create_node(dev);
----devtmpfs_submit_req(&req, tmp);
--------wake_up_process(thread);
------------thread = kthread_run(devtmpfsd, &err, "kdevtmpfs");
----------------devtmpfs_work_loop();
--------------------handle(req->name, req->mode, req->uid, req->gid, req->dev);
------------------------handle_create(name, mode, uid, gid, dev);
----------------------------vfs_mknod(d_inode(path.dentry), dentry, mode, dev->devt);
--------------------------------dir->i_op->mknod(dir, dentry, mode, dev); //kernel\fs\namei.c//D:\work\source_code\msm-kernel\msm_kernel\fs\hostfs\hostfs_kern.c
static const struct inode_operations hostfs_dir_iops = {.create        = hostfs_create,.lookup        = hostfs_lookup,.link        = hostfs_link,.unlink        = hostfs_unlink,.symlink    = hostfs_symlink,.mkdir        = hostfs_mkdir,.rmdir        = hostfs_rmdir,.mknod        = hostfs_mknod,.rename        = hostfs_rename2,.permission    = hostfs_permission,.setattr    = hostfs_setattr,
};

mknod函数流程:

.mknod
----hostfs_mknod
--------init_special_inode(inode, mode, dev); //kernel\fs\inode.c
------------if (S_ISCHR(mode)) {
----------------inode->i_fop = &def_chr_fops;
--------do_mknod(name, mode, MAJOR(dev), MINOR(dev));
------------mknod(file, mode, os_makedev(major, minor));
----------------sys_mknod(path, mode, dev);
--------------------my_syscall4(__NR_mknodat, AT_FDCWD, path, mode, dev);
------------------------__SYSCALL(__NR_mknodat, sys_mknodat)
----------------------------do_mknodat(dfd, filename, mode, dev);
--------------------------------user_path_create(dfd, filename, &path, lookup_flags);
------------------------------------filename_create(dfd, getname(pathname), path, lookup_flags);SYSCALL_DEFINE4(mknodat, int, dfd, const char __user *, filename, umode_t, mode,unsigned int, dev)
{return do_mknodat(dfd, filename, mode, dev);
}SYSCALL_DEFINE3(mknod, const char __user *, filename, umode_t, mode, unsigned, dev)
{return do_mknodat(AT_FDCWD, filename, mode, dev);
}

init_special_inode函数的实现:

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{inode->i_mode = mode;if (S_ISCHR(mode)) {inode->i_fop = &def_chr_fops;inode->i_rdev = rdev;} else if (S_ISBLK(mode)) {inode->i_fop = &def_blk_fops;inode->i_rdev = rdev;} else if (S_ISFIFO(mode))inode->i_fop = &pipefifo_fops;else if (S_ISSOCK(mode));    /* leave it no_open_fops */elseprintk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"" inode %s:%lu\n", mode, inode->i_sb->s_id,inode->i_ino);
}
EXPORT_SYMBOL_NS(init_special_inode, ANDROID_GKI_VFS_EXPORT_ONLY);/*
* Dummy default file-operations: the only thing this does
* is contain the open that then fills in the correct operations
* depending on the special file...
*/
const struct file_operations def_chr_fops = {.open = chrdev_open,.llseek = noop_llseek,
};

如果node是一个char设备,会给i_fop 赋值一个默认的def_chr_fops,也就是说对该node节点,有一个默认的操作。

在open一个字符设备文件时,最终总会调用chrdev_open,然后调用各个char设备自己的file_operations 定义的open函数。

通过上面的分析,我们知道当device_add()注册device时,会调用devtmpfs_create_node()

但是这个调用是有个约束条件的, 这个约束条件是device中必须定义了devt这个设备号。

所以,对于很多的平台设备platform_devices(也就是那些在dts文件中定义的devices),在进行platform_device_add()时,并不会在/dev下面出现inode节点。

五,chrdev_open()

虽然我们有了字符设备的设备文件,inode也被构造并初始化了,但是在第一次调用chrdev_open()之前,这个inode和具体的chr_dev对象并没有直接关系,

而只是通过设备号建立的"间接"关系。在第一次调用chrdev_open()之后, inode->i_cdev才被根据设备号找到的cdev对象赋值,此后inode才和具体的cdev对象直接联系在了一起。

1,应用层怎么才能精确的调用到底层的驱动程序

  • 在Linux文件系统中,每个文件都用一个struct inode结构体来描述,这个结构体里面记录了这个文件的所有信息,例如:文件类型,访问权限等。

  • 在Linux操作系统中,每个驱动程序在应用层的/dev目录下都会有一个设备文件和它对应,并且该文件会有对应的主设备号和次设备号。

  • 在Linux操作系统中,每个驱动程序都要分配一个主设备号,字符设备的设备号保存在struct cdev结构体中。

  • 在Linux操作系统中,每打开一次文件,Linux操作系统在VFS层都会分配一个struct file结构体来描述打开的这个文件。该结构体用于维护文件打开权限、文件指针偏移值、私有内存地址等信息。

注意:

常常我们认为struct inode描述的是文件的静态信息,即这些信息很少会改变。而struct file描述的是动态信息,即在对文件的操作的时候,

struct file里面的信息经常会发生变化。典型的是struct file结构体里面的f_pos(记录当前文件的位移量),每次读写一个普通文件时f_ops的值都会发生改变。

这几个结构体关系如下图所示:

 

通过上图我们可以知道,如果想访问底层设备,就必须打开对应的设备文件。也就是在这个打开的过程中,Linux内核将应用层和对应的驱动程序关联起来。

1)当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备)。还会分配一个struct file结构体。

2)根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备有一个struct cdev结构体。此结构体描述了字符设备所有的信息,其中最重要一项的就是字符设备的操作函数接口。

3)找到struct cdev结构体后,Linux内核就会将struct cdev结构体所在的内存空间首地记录在struct inode结构体的i_cdev成员中。将struct cdev结构体的中记录的函数操作接口地址记录在struct file结构体的f_op成员中。

4)任务完成,VFS层会给应用层返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层的应用程序就可以通过fd来找到strut file,然后在由struct file找到操作字符设备的函数接口了。

2,struct inode 与 struct file

struct inode:

/*
* Keep mostly read-only and often accessed (especially for
* the RCU path lookup and 'stat' data) fields at the beginning
* of the 'struct inode'
*/
struct inode {umode_t            i_mode;unsigned short        i_opflags;kuid_t            i_uid;kgid_t            i_gid;unsigned int        i_flags;const struct inode_operations    *i_op;dev_t            i_rdev;loff_t            i_size;union {const struct file_operations    *i_fop;    /* former ->i_op->default_file_ops */void (*free_inode)(struct inode *);};struct list_head    i_devices;union {struct pipe_inode_info    *i_pipe;struct block_device    *i_bdev;struct cdev        *i_cdev;char            *i_link;unsigned        i_dir_seq;};void            *i_private; /* fs or device private pointer */
} __randomize_layout;

struct file:

struct file {struct path        f_path;struct inode        *f_inode;    /* cached value */const struct file_operations    *f_op;/** Protects f_ep_links, f_flags.* Must not be taken from IRQ context.*/spinlock_t        f_lock;enum rw_hint        f_write_hint;atomic_long_t        f_count;unsigned int         f_flags;fmode_t            f_mode;struct mutex        f_pos_lock;loff_t            f_pos;struct fown_struct    f_owner;u64            f_version;/* needed for tty driver, and maybe others */void            *private_data;} __randomize_layout__attribute__((aligned(4)));    /* lest something weird decides that 2 is OK */

3,open()代码流程

user space API:

int open(const char *pathname, int flags, mode_t mode);

open函数在kernel中的流程:

//kernel\fs\open.c
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
----do_sys_open(AT_FDCWD, filename, flags, mode);
--------do_sys_openat2(dfd, filename, &how);
------------struct file *f = do_filp_open(dfd, tmp, &op);
----------------filp = path_openat(&nd, op, flags | LOOKUP_RCU);
--------------------file = alloc_empty_file(op->open_flag, current_cred());
--------------------do_open(nd, file, op);
------------------------vfs_open(&nd->path, file);
----------------------------do_dentry_open(file, d_backing_inode(path->dentry), NULL);

do_dentry_open:

static int do_dentry_open(struct file *f,struct inode *inode,int (*open)(struct inode *, struct file *))
{f->f_op = fops_get(inode->i_fop);if (WARN_ON(!f->f_op)) {error = -ENODEV;goto cleanup_all;}... .../* normally all 3 are set; ->open() can clear them if needed */f->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;if (!open)open = f->f_op->open;if (open) {error = open(inode, f);if (error)goto cleanup_all;}f->f_mode |= FMODE_OPENED;
}

chrdev_open:

/*
* Called every time a character special file is opened
*/
static int chrdev_open(struct inode *inode, struct file *filp)
{const struct file_operations *fops;struct cdev *p;struct cdev *new = NULL;int ret = 0;spin_lock(&cdev_lock);p = inode->i_cdev;if (!p) {struct kobject *kobj;int idx;spin_unlock(&cdev_lock);//根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);if (!kobj)return -ENXIO;new = container_of(kobj, struct cdev, kobj);spin_lock(&cdev_lock);/* Check i_cdev again in case somebody beat us to it whilewe dropped the lock. */p = inode->i_cdev;if (!p) {inode->i_cdev = p = new;list_add(&inode->i_devices, &p->list);new = NULL;} else if (!cdev_get(p))ret = -ENXIO;} else if (!cdev_get(p))ret = -ENXIO;spin_unlock(&cdev_lock);cdev_put(new);if (ret)return ret;ret = -ENXIO;fops = fops_get(p->ops);if (!fops)goto out_cdev_put;replace_fops(filp, fops);if (filp->f_op->open) {ret = filp->f_op->open(inode, filp);if (ret)goto out_cdev_put;}return 0;out_cdev_put:cdev_put(p);return ret;
}

实现私有的open函数,filp->f_op->open(inode, filp):

将设备属性封装成结构体后,在编写open函数时,将该结构体作为私有数据添加到设备文件中,如下:

//open函数
static int test_open(struct inode *inode, struct file *filp)
{filp->private_data = &testdev;    //设置私有数据return 0; //在私有数据设置好后,在write、read、close等函数中直接读取privata_data就可以访问设备结构体
}

六,总结

Linux中几乎所有的"设备"都是"device"的子类,无论是平台设备还是i2c设备还是网络设备,但唯独字符设备不是,cdev并不是继承自device,注册一个cdev对象到内核其实只是将它放到cdev_map中,直到对device_create的分析才知道此时才创建device结构并将kobj挂接到相应的链表,所以,基于历史原因,当下cdev更合适的一种理解是一种接口(使用mknod时可以当作设备),而不是而一个具体的设备,和platform_device,i2c_device有着本质的区别。

参考链接:

https://www.cnblogs.com/xiaojiang1025/p/6196198.html

https://www.cnblogs.com/schips/p/linux_device_model_and_cdev_miscdev.html

一文带你掌握 Linux 字符设备架构-linux中字符设备

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

tomcat-以服务的方式重启tomcat

背景 双击tomcat的bin目录下面的startup.bat&#xff0c;会留下一个cmd的窗口&#xff0c;很不优雅 使用service服务的方式启动&#xff0c;并且设置为自动启动 找到tomcat的bin目录输入cmd&#xff0c;按Enter&#xff0c;进入命令行界面。执行“service.bat install” 。&…

文件删了,回收站清空了怎么恢复?文件恢复软件一览

在日常生活和工作中&#xff0c;我们常常会遇到误删除文件的情况&#xff0c;有时甚至会因为清空了回收站而无法找回这些文件。这些文件可能包含重要的工作数据、个人照片或其他珍贵的回忆。那么&#xff0c;在这种情况下&#xff0c;我们该如何恢复这些被删除且清空回收站的文…

【项目分享】用 Python 写一个桌面倒计日程序!

事情是这样的&#xff0c;我们班主任想委托我做一个程序&#xff0c;能显示还有几天考试。我立即理解了这个意思&#xff0c;接下了这个项目。 话不多说&#xff0c;来看看这个项目吧—— 项目简介 仓库地址&#xff1a;https://gitee.com/yaoqx/desktop-countdown-day 这是 …

为何美国多IP服务器搭建蜘蛛池SEO更经济?

为何美国多IP服务器搭建蜘蛛池SEO更经济? 随着网络时代的不断演进&#xff0c;搜索引擎优化(SEO)已经成为企业和个人提升网站曝光度的必经之路。在这个过程中&#xff0c;蜘蛛池(Spider Pool)服务被广泛应用。但是有趣的是&#xff0c;美国多IP服务器搭建蜘蛛池SEO服务却相对…

计算机网络-ICMPv6基础概念

前面我们学习了IPv6的基础概念以及IPv6地址的格式与分类&#xff0c;在IPv4中我们通过ARP、广播、ICMP进行地址冲突检测、网络连通性&#xff0c;但是在IPv6中是没有广播和ARP的&#xff0c;都是通过ICMPv6来实现其功能&#xff0c;所以这里我们需要了解下ICMPv6。 一、ICMP协议…

一个基于ComfuUI Api的 AIGC自动绘画实现方案

工作流程图 基本原理已经弄通&#xff0c;下一步要开始编码搬砖了。整个自动绘画的流程如下&#xff0c;暂就不整高深U什么L了&#xff0c;写个简单明了能容易看懂的流程图。UI借用了下墨刀里的AI绘画公开原型 部署节点 整个系统的后端服务典型部署需要3类节点 Aigc Server&…

mac/windows下安装docker,minikube

1、安装docker Get Started | Docker 下载安装docker 就行 启动后&#xff0c;就可以正常操作docker了 使用docker -v 验证是否成功就行 2、安装minikube&#xff0c;是基于docker-desktop的 2.1、点击设置 2.2、选中安装&#xff0c;这个可能需要一点时间 这样安装后&…

【研发管理】产品经理知识体系-组合管理

导读&#xff1a;新产品开发的组合管理是一个重要的过程&#xff0c;它涉及到对一系列新产品开发项目进行策略性选择、优先级排序、资源分配和监控。这个过程旨在确保企业能够最大化地利用有限的资源&#xff0c;以实现其战略目标。 目录 1、组合管理、五大目标 2、组合管理的…

OpenCV的周期性噪声去除滤波器(70)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇:OpenCV如何通过梯度结构张量进行各向异性图像分割(69) 下一篇 :OpenCV如何为我们的应用程序添加跟踪栏(71) 目录 目标 理论 如何消除傅里叶域中的周期性噪声&#xff1f; 源代码 解释 结果 目…

详解基于 RAG 的 txt2sql 全过程

前文 本文使用通义千问大模型和 ChromaDB 向量数据库来实现一个完整的 text2sql 的项目&#xff0c;并基于实际的业务进行效果的展示。 准备 在进行项目之前需要准备下面主要的内容&#xff1a; python 环境通义千问 qwen-max 模型的 api-keyChromaDB 向量数据库acge_text_…

Sharding Capital: 为什么投资全链流动性基础设施 Entangle ?

写在前面&#xff1a;Entangle 项目的名称取自于量子纠缠(Quantum entanglement)&#xff0c;体现了项目对于构建连接、关联和互通的愿景。就像量子纠缠将不同的粒子联系在一起&#xff0c;Entangle 旨在通过其跨链流动性和合成衍生品的解决方案将不同的区块链网络连接在一起&a…

django设计模式理解FBV和CBV

在 Web 开发中&#xff0c;FBV&#xff08;Function-Based Views&#xff09;和 CBV&#xff08;Class-Based Views&#xff09;是两种常见的视图设计模式&#xff0c;用于处理 HTTP 请求并生成相应的响应。下面是它们的简要解释&#xff1a; Function-Based Views (FBV) 在 …

激发创新活力,泸州老窖锻造人才“铁军”(内附长江酒道短评)

执笔 | 姜 姜 编辑 | 古利特 刚刚站上300亿元新台阶&#xff0c;泸州老窖再次传来喜讯。 <<<左右滑动查看更多>>> 4月28日&#xff0c;四川省庆祝“五一”国际劳动节大会在成都召开。泸州老窖股份有限公司工业4.0项目秘书长赵丙坤、泸州老窖酿酒有限责任公…

VS Code 远程连接 SSH 服务器

文章目录 一、安装 Remote - SSH 扩展并连接远程主机二、免密连接远程主机1. 生成 SSH 密钥对2. 将公钥复制到远程服务器3. 配置 SSH 客服端4. 连接测试 随着技术的不断迭代更新&#xff0c;在 Linux 系统中使用 Vim、nano 等基于 Shell 终端的编辑器&#xff08;我曾经也是个 …

利用AI大模型和Echarts 绘制知识图谱,实现文本信息提取和图数据库操作

引言 随着信息时代的到来&#xff0c;海量的文本数据成为了我们获取知识的重要来源。然而&#xff0c;如何从这些文本数据中提取出有用的信息&#xff0c;并将其以可视化的方式展示出来&#xff0c;一直是一个具有挑战性的问题。近年来&#xff0c;随着人工智能技术的发展&…

热敏电阻符号与常见术语详细解析

热敏电阻是一种电阻器&#xff0c;其特点是电阻值随温度的变化而显著变化&#xff0c;这使得它们成为非常有用的温度传感器。它们可以由单晶、多晶或玻璃、塑料等半导体材料制成&#xff0c;并分为两大类&#xff1a;正温度系数热敏电阻&#xff08;#PTC热敏电阻#&#xff09;和…

纯血鸿蒙APP实战开发——短视频切换实现案例

短视频切换实现案例 介绍 短视频切换在应用开发中是一种常见场景&#xff0c;上下滑动可以切换视频&#xff0c;十分方便。本模块基于Swiper组件和Video组件实现短视频切换功能。 效果图预览 使用说明 上下滑动可以切换视频。点击屏幕暂停视频&#xff0c;再次点击继续播放…

场外个股期权和场内个股期权的优缺点是什么?

场外个股期权和场内个股期权的优缺点 场外个股期权是指在沪深交易所之外交易的个股期权&#xff0c;其本质是一种金融衍生品&#xff0c;允许投资者在股票交易场所外以特定价格买进或卖出证券。场内个股期权是以单只股票作为标的资产的期权合约&#xff0c;其内在价值是基于标…

深度学习-线性回归+基础优化算法

目录 线性模型衡量预估质量训练数据参数学习训练损失最小化损失来学习参数显式解 总结基础优化梯度下降选择学习率 小批量随机梯度下降选择批量大小 总结线性回归的从零开始实现实现一个函数读取小批量效果展示这里可视化看一下 线性回归从零开始实现线性回归的简洁实现效果展示…

HCIP第二节

OSPF&#xff1a;开放式最短路径协议&#xff08;属于IGP-内部网关路由协议&#xff09; 优点&#xff1a;相比与静态可以实时收敛 更新方式&#xff1a;触发更新&#xff1a;224.0.0.5/6 周期更新&#xff1a;30min 在华为设备欸中&#xff0c;默认ospf优先级是10&#…