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

news2024/11/15 7:02:52

一,从/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_register
    device_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 */
    else
        printk(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 while
           we 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.coloradmin.cn/o/1646078.html

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

相关文章

牛客热题:单链表排序

&#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;力扣刷题日记 &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 文章目录 牛客热题&#xff1a;单链表排序题目链接方法一&…

设计网页用什么软件

在设计网页时&#xff0c;可以使用多种软件来完成不同的任务。以下是一些常用的网页设计软件&#xff0c;以及它们的特点和用途。 1. Adobe Photoshop&#xff1a; Adobe Photoshop 是一款功能强大的图像编辑软件。在网页设计中&#xff0c;它常用于创建和编辑网页所需的图像、…

[Unity常见小问题]打包ios后无法修改模型透明度

问题 在Editor下可以使用如下代码去修改模型的材质的透明度&#xff0c;但是打包ios后无法对透明度进行修改且没有任何warning和error using System.Collections; using System.Collections.Generic; using UnityEngine;public class NewBehaviourScript : MonoBehaviour {[R…

第三篇、利用潜空间生成超稳定动画

1、使用temporal-kit&#xff0c;生成拼接的图片 sides填写3&#xff0c;Height Resolution要填写原视频高度 * sides ,这里也就是三倍 因为原视频动作很快&#xff0c;frames per keyframe填写了2 发现在temp1目录的Input目录下生成了 3* 3的拼接图片 2、到图生图界面&#…

AVL树浅谈

前言 大家好&#xff0c;我是jiantaoyab&#xff0c;本篇文章给大家介绍AVL树。 基本概念 AVL树&#xff08;Adelson-Velsky和Landis树&#xff09;是一种自平衡的二叉搜索树&#xff0c;得名于其发明者G. M. Adelson-Velsky和E. M. Landis。在AVL树中&#xff0c;任何节点的…

【云原生】Pod 的生命周期(一)

【云原生】Pod 的生命周期&#xff08;一&#xff09;【云原生】Pod 的生命周期&#xff08;二&#xff09; Pod 的生命周期&#xff08;一&#xff09; 1.Pod 生命期2.Pod 阶段3.容器状态3.1 Waiting &#xff08;等待&#xff09;3.2 Running&#xff08;运行中&#xff09;3…

蓝牙连接手机播放音乐的同时传输少量数据,那些蓝牙芯片可以实现呢

简介 蓝牙连接手机播放音乐的同时连接另一蓝牙芯片传输少量数据&#xff0c;那些蓝牙芯片可以实现呢&#xff1f; 这个需求&#xff0c;其实就是双模的需求 简单描述就是:播放音乐的同时&#xff0c;还可以连接ble&#xff0c;进行数据的传输。二者同时进行&#xff0c;互不…

Apache.commons.lang3 的 isNumber 将会在 lang 4 的时候丢弃

在判断输入的字符串是不是一个数字的时候&#xff0c;我们通常用的最多的方法就是 &#xff1a; NumberUtils.isNumber("12"); 但是这个方法将会在 Lang 4.0 版本中被丢弃。 可以使用的替代方法为&#xff1a;isCreatable(String) 通过查看源代码&#xff0c;我们…

ASP.NET网上图书预约系统的设计

摘 要 《网上图书预约系统的设计》是以为读者提供便利为前提而开发的一个信息管理系统&#xff0c;它不仅要求建立数据的一致性和完整性&#xff0c;而且还需要应用程序功能的完备、易用等特点。系统主要采用VB.NET作为前端的应用开发工具&#xff0c;利用SQL Server2000数据…

7zip如何只压缩文件不带上级目录?

在使用7zip进行文件压缩的时候&#xff0c;如果直接选择要压缩的文件进行压缩&#xff0c;得到的压缩包则会多包含一层顶层目录&#xff0c;解压缩之后需要点击两次才能进入到实际目录中&#xff0c;为了解决这个问题&#xff0c;本文根据探索找到了一种解决办法。 如下是一个演…

【星海随笔】windows 上跑MySQL

step one 使用 WSL 在 Windows 上安装 Linux wsl官方文档 在管理员模式下打开 PowerShell windows上安装wsl wsl --install查看哪些是可用的 wsl --list --onlineC:\Windows\System32\drivers\hosts docker-desktop下载官网&#xff1a;Install Docker Desktop on Windows …

Java苍穹外卖04-

一、缓存菜品 1.问题说明 2.实现思路 就是点击到这个分类的时候就可以展示相应的菜品数据 3.代码实现 在user的菜品的contoller中&#xff1a;增加判断redis中是否存在所需数据&#xff0c;不存在添加&#xff0c;存在直接取得 这里注意&#xff1a;你放进去用的是List<Di…

数码管的显示

静态数码管显示 数码管有两种一种的负电压促发,一种是正电压促发,上图是单数码管的引脚 上图是数码管模组的引脚,采用了引脚复用技术 咱们这个单片机由8个单数码管,所以要用上38译码器,如下图 74138使能端,单片机上电直接就默认接通了 74HC245的作用是稳定输入输出,数据缓冲作…

memory consistency

memory consistency model 定义了对于programmer和implementor来说&#xff0c;访问shared memory system的行为&#xff1b; 对于programmer而言&#xff0c;他知道期望值是什么&#xff0c; 知道会返回什么样的数据&#xff1b;&#xff1b; 对于implementro而言&#xff0c;…

npy文件如何追加数据?

.npy 文件是 NumPy 库用于存储数组数据的二进制格式&#xff0c;它包含一个描述数组结构的头部信息和实际的数据部分。直接追加数据到现有的 .npy 文件并不像文本文件那样直接&#xff0c;因为需要手动修改文件头部以反映新增数据后的数组尺寸&#xff0c;并且要确保数据正确地…

电子信息工程专业就业前景怎么样

电子信息工程专业的就业前景十分广阔&#xff0c;主要得益于现代社会对信息技术的依赖不断加深以及科技的快速发展&#xff0c;以下是上大学网&#xff08;www.sdaxue.com&#xff09;对该专业就业前景的具体分析&#xff0c;供大家参考&#xff01; 行业需求广泛&#xff1a;随…

Partisia Blockchain 生态zk跨链DEX上线,加密资产将无缝转移

在 5 月 1 日&#xff0c;由 Partisia Blockchain 与 zkCross 创建合作推出的 Partisia zkCrossDEX 在 Partisia Blockchain 生态正式上线。Partisia zkCrossDEX 是 Partisia Blockchain 上重要的互操作枢纽&#xff0c;其融合了 zkCross 的 zk 技术跨链互操作方案&#xff0c;…

vue2实现生成二维码和复制保存图片功能(复制的同时会给图片加文字)

<template><divstyle"display: flex;justify-content: center;align-items: center;width: 100vw;height: 100vh;"><div><!-- 生成二维码按钮和输入二维码的输入框 --><input v-model"url" placeholder"输入链接" ty…

重写muduo之EPollPoller

1、EPollPoller.h EPollPoller的主要实现&#xff1a;作为poller的派生类&#xff0c;把基类给派生类保留的这些纯虚函数的接口实现出来。 override表示在派生类里面&#xff0c;这些方法是覆盖方法。必须由编译器来保证在基类里面一定有这些函数的接口的声明。在派生类要重写…

[蓝桥杯2024]-PWN:ezheap解析(堆glibc2.31,glibc2.31下的double free)

查看保护 查看ida 大致就是只能创建0x60大小的堆块&#xff0c;并且uaf只能利用一次 完整exp&#xff1a; from pwn import* #context(log_leveldebug) pprocess(./ezheap2.31)def alloc(content):p.sendlineafter(b4.exit,b1)p.send(content) def free(index):p.sendlineaft…