linux字符设备

news2024/11/15 22:57:12

目录

设计字符设备

文件系统调用系统IO的内核处理过程

硬件层原理

驱动层原理

文件系统层原理

设备号的组成与哈希表

Hash Table(哈希表、散列表,数组和链表的混合使用)

设备号管理

关键的数据结构:char_device_struct(存放在内核/fs/char_dev.c)

关键的函数:__register_chrdev_region(存放在内核/fs/char_dev.c)

保存file_operation结构体

关键数据结构:字符设备管理对象cdev(存放在内核/include/linux/cdev.h)

关键数据结构:kobj_map(与哈希表有关,存放在内核/drivers/base/map.c)

创建一个设备文件

mknod命令:创建指定类型的特殊文件

操作系统存在的好处

内存管理单元MMU

LED字符设备驱动实验

实验步骤

驱动模块初始化

chrdev.c文件

执行过程


设计字符设备

文件系统调用系统IO的内核处理过程

inode索引节点是文件系统中的一种数据结构,用于存储文件的元数据信息,包括文件的大小、访问权限、创建时间、修改时间等。每个文件在文件系统中都对应着一个唯一的inode节点,通过inode节点可以查找到文件的实际数据块的位置。inode节点通常存储在磁盘的inode表中,文件系统通过inode号来访问和管理文件。

file_operation结构体是函数指针表,用于定义文件的操作方法。当应用程序通过文件描述符打开文件时,内核会根据文件描述符找到对应的inode节点,并获取与inode节点关联的file_operation表。通过file_operation表中的函数指针,内核可以调用相应的函数来执行文件操作,如open、read、write、close等。不同内核可以有不同的file_operation表,因为不同的内核可能有不同的文件操作方法和特性。

task_struct结构体用于描述和管理进程,内容很多很复杂。里面有个成员变量是struct files_struct *files,用于存储与进程相关的文件描述符表的信息(文件描述符表记录了进程打开的文件以及相应的操作权限等信息)。要想获取进程的文件描述符相关信息,需要通过访问task_struct结构体的files指针来获取files_struct结构体,进而访问文件描述符表的信息。

files_struct结构体用于跟踪和管理进程打开的文件。fd_array[]为指针数组,用于存储进程打开的文件描述符的信息,即每个文件描述符都对应一个files_struct。通过fd_array数组可以快速访问和操作这些文件描述符,数组索引值对应着文件描述符的值。

硬件层原理

思路:把底层寄存器配置操作放在文件操作接口里,新建一个文件绑定该文件操作接口,应用程序通过操作指定文件来配置底层寄存器。

基本接口实现:查原理图,数据手册,确定底层需要配置的寄存器。类似于裸机开发。实现一个文件的底层操作接口,这是文件的基本特征。

struct file_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 (*read_iter) (struct kiocb *, struct iov_iter *);
        ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
        int (*iterate) (struct file *, struct dir_context *);
        int (*iterate_shared) (struct file *, struct dir_context *);
        __poll_t (*poll) (struct file *, struct poll_table_struct *);
        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 *);
        unsigned long mmap_supported_flags;
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *, fl_owner_t id);
        int (*release) (struct inode *, struct file *);
        int (*fsync) (struct file *, loff_t, loff_t, 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 **, void **);
        long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
        void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
        unsigned (*mmap_capabilities)(struct file *);
#endif
        ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int);
        int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64);
        int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t, u64);
        int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

几乎所有成员都是函数指针,用来实现文件的具体操作。 

驱动层原理

设备号(dev_t)是uint32_t类型,主设备号(高12位)+次设备号(低20位)。主设备号用于标识该驱动所管理的设备类型,次设备号用于标识同一类型设备的具体设备实例。

cdev_init()函数存放在内核/fs/char_dev.c

cdev_init()  //把用户构建的file_operations结构体记录在内核驱动的基本对象cdev

把file_operations文件操作接口注册到内核,内核通过主次设备号来记录它cdev_init()用于初始化字符设备驱动中的struct cdev结构体。struct cdev结构体是字符设备驱动的核心数据结构,描述了字符设备驱动的属性和操作(file_operations)。

当开发者编写字符设备驱动时,需要先调用cdev_init()函数来初始化struct cdev结构体。这个函数会将struct cdev的各个成员初始化为合适的值,并建立与字符设备驱动相关的关联。

cdev_init()函数通常在字符设备驱动加载时init函数中调用,确保驱动模块的正确初始化。

cdev_add()函数存放在内核/fs/char_dev.c

int cdev_add(struct cdev *p, dev_t dev, unsigned int count);
参数:
    p:指定的字符设备对象
    dev:设备号
    count:设备号数量

根据哈希函数保存cdev到probes哈希表中,方便内核查找file_operation使用cdev_add()函数用于向字符设备驱动注册字符设备,即将一个已初始化的字符设备对象(struct cdev)和一个设备号(dev_t)进行关联,并将其注册到内核中,使其能够被用户程序访问和使用。

cdev_add()函数通常在字符设备驱动初始化阶段调用来注册字符设备,在此之前,需要先通过cdev_init()初始化。注意:需要正确初始化之后才调用,否则可能导致注册失败或出现意外的行为。

两个Hash表(帮助找到cdev结构体)

chrdevs:登记设备号。

cdev_map->probe:保存驱动基本对象struct cdev。

文件系统层原理

mknod + 主次设备号

构建一个新的设备文件,通过主次设备号在cdev_map中找到cdev->file_operations,把cdev->file_operations绑定到新的设备文件中。

到这一步,应用程序就可以使用open()、write()、read()等函数来控制设备文件了。

设备号的组成与哈希表

内核/include/linux/kdev_t.h描述了设备号的具体构成。

/* 截取部分代码,关于设备号的描述 */
#define MINORBITS    20
#define MINORMASK    ((1U << MINORBITS) -1)
 
#define MAJOR(dev)   ((unsigned int)((dev) >> MINORBITS))
#define MINOR(dev)   ((unsigned int)((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

理论取值范围:

主设备号:2^12=4K

次设备号:2^20=1M

cat /proc/devices:查看已注册的设备号。

内核是希望一个设备驱动(file_operation)可以独自占有一个主设备号和多个次设备号,而通常一个设备文件绑定一个主设备号和一个次设备号,所以设备驱动与设备文件是一对一或者一对多的关系。

Hash Table(哈希表、散列表,数组和链表的混合使用)

以主设备号为编号,使用哈希函数f(major)= major % 255 来计算主设备号的对应数组下标。

主设备号冲突(如0、255,都挂载在数组0下标),则以次设备号为比较值来排序链表节点。

哈希函数的设计目标:链表节点尽量平均分布在各个数组元素中,提高查询效率。

设备号管理

关键的数据结构: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;                  //内核字符对象(已丢弃)
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

关键的函数:__register_chrdev_region(存放在内核/fs/char_dev.c)

static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name)
{
        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);

        if (major == 0) {            /* 主设备号为0,从chadevs哈希表中查找一个空闲位置 */
                ret = find_dynamic_major();
                if (ret < 0) {
                        pr_err("CHRDEV \"%s\" dynamic allocation region is full\n", name);
                        goto out;
                }          /* 返回主设备号 */
                major = ret;  
     }

        if (major >= CHRDEV_MAJOR_MAX) {
                pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n", name, major, CHRDEV_MAJOR_MAX-1);
                ret = -EINVAL;
                goto out;
        }
     /* 保存参数 */
        cd->major = major;
        cd->baseminor = baseminor;
        cd->minorct = minorct;
        strlcpy(cd->name, name, sizeof(cd->name));
     /* 哈希函数,计算哈希表的位置 */
        i = major_to_index(major);     /* 链表排序,按主设备号从小到大排序。如果主设备号相等,按次设备号从小到大排序,要考虑次设备号的最大值 */
        for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
                if ( (*cp)->major > major || ( (*cp)->major == major && ( ( (*cp)->baseminor >= baseminor ) || ( (*cp)->baseminor + (*cp)->minorct > baseminor) ) ) )  break;

        /* 如果主设备号相等,检查次设备号是否存在冲突  */
        if (*cp && (*cp)->major == major) {          /* 获取链表节点的次设备号范围 */
                int old_min = (*cp)->baseminor;
                int old_max = (*cp)->baseminor + (*cp)->minorct - 1;          /* 获取新设备的次设备号范围 */
                int new_min = baseminor;
                int new_max = baseminor + minorct - 1;

                /* 判断新设备的次设备号最大值是否位于链表节点的次设备号范围  */
                if (new_max >= old_min && new_max <= old_max) {               /* 确定冲突,返回错误 */
                        ret = -EBUSY;
                        goto out;
                }

                /* 判断新设备的次设备号最小值是否位于链表节点的次设备号范围  */
                if (new_min <= old_max && new_min >= old_min) {               /* 确定冲突,返回错误 */
                        ret = -EBUSY;
                        goto out;
                }
          /* 判断新设备的次设备号是否跨越链表节点的次设备号范围 */
                if (new_min < old_min && new_max > old_max) {               /* 确定冲突,返回错误 */
                        ret = -EBUSY;
                        goto out;
                }
        }
     /* 插入新设备的链表节点 */
        cd->next = *cp;
        *cp = cd;
        mutex_unlock(&chrdevs_lock);
        return cd;
out:
        mutex_unlock(&chrdevs_lock);
        kfree(cd);
        return ERR_PTR(ret);
}

上诉函数主设备号相等,判断新旧次设备号三种错误图如下。

该函数用于注册字符设备驱动,保存新注册的设备号到chrdevs哈希表中,防止设备号冲突。

主设备为0时,需要动态分配设备号(优先使用255~234,其次使用511~384),函数会从字符设备哈希表中找到一个空闲的位置,分配主设备号,并将该主设备号保存到字符设备的数据结构中。主设备号最大为512。

然后函数将传入的参数保存到字符设备结构体cd中,并计算出在字符设备哈希表中的位置。如果遍历字符设备哈希表的链表,按主设备号从小到大排序。

如果遍历过程中,主设备号和传入的主设备号冲突或次设备号范围与传入的次设备号范围有重叠,函数会返回错误。否则函数会将新字符设备的节点插入到链表中,并返回字符设备结构体cd。

该函数一般在字符设备驱动加载时调用,用于注册字符设备。通过注册字符设备,内核可以识别和管理相应的设备,并提供相应的接口供用户空间程序进行读写操作。

保存file_operation结构体

关键数据结构:字符设备管理对象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;                           //表示32位设备号
        unsigned int count;                  //用于跟踪该设备对象的引用计数
} __randomize_layout;                        //告诉编译器随机化结构体的布局,以提高安全性

关键数据结构:kobj_map(与哈希表有关,存放在内核/drivers/base/map.c)

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;
};

创建一个设备文件

mknod命令:创建指定类型的特殊文件

 用法:mknod [选项]... 名称 类型 [主设备号 次设备号]

以指定的 <名称> 创建指定 <类型> 的特殊文件。

当 <类型> 为 b、c 或 u 时必须指定 <主设备号> 和 <次设备号>,当 <类型>为 p 时不得指定 <主设备号> 和 <次设备号>。

如果 <主设备号> 或 <次设备号>以 0x 或 0X 开头,则将它们视为十六进制数进行解析;如果以 0 开头,则视为八进制数;其余情况下,视为十进制数。<类型> 可以是:

    b          创建一个(带缓冲的)块特殊文件
    c, u      创建一个(不带缓冲的)字符特殊文件
    p          创建一个 FIFO 文件

举例:mkmod /dev/test c 2 0

原理分析

init_special_inode函数定义在ebf-buster-linux/fs/inode.c。主要内容是判断文件的inode类型,如果是字符设备类型,则把def_chr_fops作为该文件的操作接口,并把设备号记录在inode->i_rdev。

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);
}

inode上的file_operation并不是自己构造的file_operation(保存在内核的字符设备驱动哈希表里),而是字符设备通用的def_chr_fops。

自己构造的file_operation会在chrdev_open函数那里才会绑定到我们的文件上。所以自己构建的file_operation等在应用程序调用open函数后才会绑定在文件上。

操作系统存在的好处

首先操作系统完成了多任务并发;

其次操作系统为我们提供了内存管理机制,32位操作系统可以让每个进程都能够独立访问4GB的内存空间;

对于应用程序来说,应用程序使用统一的系统调用接口来访问各种设备,通过write()、read()等函数读写文件就可以访问各种字符设备和块设备,而不用管设备的具体类型和工作方式。

在linux环境直接访问物理内存是很危险的,如果用户不小心修改了内存中的数据,很有可能造成错误甚至系统崩溃。为了解决这些问题内核引入了内存管理单元MMU。

内存管理单元MMU

MMU是一个实际的硬件,位于CPU和主存之间,负责处理计算机内存的管理和访问。主要作用是将虚拟地址转换成物理地址,同时管理和保护内存,并且实现了虚拟内存技术。

程序中所写的变量地址是虚拟内存的地址,倘若处理器想访问这个地址,MMU就实现了虚拟地址转换成物理地址。

不同进程有各自的虚拟地址空间,某个进程中的程序不能修改另外一个进程所使用的物理地址,使得进程之间胡不干扰,相互隔离。CPU可以运行在虚拟的内存上,虚拟内存一般比实际的物理内存大很多,使得CPU可以运行比较大的应用程序。

现在很多实时操作系统都可以运行在无MMU的CPU上,比如uCOS、FreeRTOS、uCLinux。以前想让CPU运行在Linux系统必须要改CPU具备MMU。

MMU给指定的内存块设置了rwx权限,权限信息存储在页表中,MMU会检查CPU当前所处的是特权模式还是用户模式。如果和操作系统所设置的权限匹配则可以访问。

物理地址:内存单元的绝对地址。无论CPU怎么处理,它的访问目标都是物理地址。

虚拟地址:当CPU开启了MMU时,CPU发出的地址(虚拟地址)将被送入到MMU,然后MMU根据虚拟地址访问页表地址寄存器,之后在内存中找到页表(假设只有一级页表)的条目,从而转换成物理地址。

对于I.MX 6ULL 32bitCPU,虚拟地址空间共有4G(232)。一旦CPU开启了MMU,无论何时CPU发出的地址都是虚拟地址,为了实现虚拟地址到物理地址间的映射,MMU内部有一个专门存放页表的页表地址寄存器(存放着页表的具体位置)。用ioremap映射一段地址意味着用户空间的一段地址关联到了设备内存上,使得只要程序在被分配的虚拟地址范围内进行读写操作,实际上就是对设备(寄存器)的访问。

LED字符设备驱动实验

驱动模块 = 内核模块(.ko)+ 驱动接口(file_operations) 

实验步骤

  1. 在内核模块入口函数里获取GPIO相关寄存器并初始化。
  2. 构造file_operations接口,并注册到内核。
  3. 创建设备文件,绑定自定义file_operations接口。
  4. 应用程序echo通过写设备文件控制硬件led。

驱动模块初始化

虚拟地址映射和取消映射

GPIO寄存器物理地址和虚拟地址映射。函数ioremap存放于ebf-buster-linux/arch/arm/include/asm/io.h。

void __iomem *ioremap(resource_size_t res_cookie, size_t size);
参数:
    res_cookie:物理地址
    size:映射长度
返回值:
    void *类型的指针,指向被映射的虚拟地址
    __iomem 主要是用于编译器的检查地址在内核空间的有效性

void iounmap(void *addr);
参数:
    addr:需要取消ioremap映射的起始地址(虚拟地址)
返回值:
    无

虚拟地址读写

旧机制:

readl() / writel()

新机制:

unsigned int ioread32(void __iomem *addr);  //读取32位

void iowrite32(u32 b, void __iomem *addr);   //写入32位

检查CPU大小端,调整字节序,以提高驱动的可移植性。

自定义led的file_operation接口

static struct file_operation led_fops = {
        .owner = THIS_MODULE,
        .open = led_open,
        .read = led_read,
        .write = led_write,
        .release = led_release
};

owner:设置驱动接口关联的内核模块,防止驱动程序运行时内核模块被卸载。

release:文件引用数为0时调用。

拷贝数据函数copy_from_user(存放于ebf-buster-linux/include/linux/uaccess.h)

static __always_inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)

参数:
    *to:将数据拷贝到内核的地址
    *from:需要拷贝数据的用户空间地址
    n:拷贝数据的长度(字节)
返回值:
    成功:0
    失败:没有被拷贝的字节数

注册字符设备驱动 

层次由高到低:register_chadev -> __register_chadev -> __register_chadev_region(该功能实现了cdev_init函数和cdev_add函数功能,但是在__register_chadev函数中规定了次设备号从0开始,有256个),即一个file_operations对应256个设备文件。

chrdev.c文件

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

#define DEV_MAJOR       0               /* 动态申请主设备号 */
#define DEV_NAME        "red_led"       /* LED设备名字 */

/* GPIO虚拟地址映射 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO04;
static void __iomem *SW_PAD_GPIO1_IO04;
static void __iomem *GPIO1_GDIR;
static void __iomem *GPIO1_DR;

static int led_open(struct inode *inode, struct file *flip)
{
        return 0;
}

static ssize_t led_read(struct file *flip, char __user *buf, size_t cnt, loff_t *offt)
{
        return -EFAULT;
}

static ssize_t led_write(struct file *flip, const char __user *buf, size_t cnt, loff_t *offt)
{
        unsigned char databuf[10];

        if(cnt >10)     cnt = 10;

        /* 从用户空间拷贝数据到内核空间 */
        if(copy_from_user(databuf, buf, cnt)){
                return -EIO;
        }

        if(!memcmp(databuf, "on", 2)){
                iowrite32(0<<4, GPIO1_DR);
        }else if(!memcmp(databuf, "off", 3)){
                iowrite32(1<<4, GPIO1_DR);
        }

        /* 写成功后,返回写入的字节数 */
        return cnt;
}

static int led_release(struct inode *inode, struct file *flip)
{
        return 0;
}

/* 自定义LED的file_operations接口 */
static struct file_operations led_fops = {
        .owner = THIS_MODULE,
        .open = led_open,
        .read = led_read,
        .write = led_write,
        .release = led_release
};

int major = 0;
static int __init led_init(void){
        /* GPIO相关寄存器操作 */
        IMX6U_CCM_CCGR1 = ioremap(0x20c406c, 4);
        SW_MUX_GPIO1_IO04 = ioremap(0x20e006c, 4);
        SW_PAD_GPIO1_IO04 = ioremap(0x20e02f8, 4);
        GPIO1_GDIR = ioremap(0x0209c004, 4);
        GPIO1_DR = ioremap(0x0209c000, 4);

        /* 使能GPIO1时钟 */
        iowrite32(0xffffffff, IMX6U_CCM_CCGR1);

        /* 设置GPIO1_IO04复用为普通GPIO */
        iowrite32(5, SW_MUX_GPIO1_IO04);

        /* 设置GPIO属性 */
        iowrite32(0x10b0, SW_PAD_GPIO1_IO04);

        /* 设置GPIO1_IO04为输出功能 */
        iowrite32(1<<4, GPIO1_GDIR);

        /* LED输出高电平 */
        iowrite32(1<<4, GPIO1_DR);

        /* 注册字符设备驱动 */
        major = register_chrdev(DEV_MAJOR, DEV_NAME, &led_fops);
        printk(KERN_ALERT "led major:%d\n", major);

        return 0;
}
static void __exit led_exit(void){
        /* 取消映射 */
        iounmap(IMX6U_CCM_CCGR1);
        iounmap(SW_MUX_GPIO1_IO04);
        iounmap(SW_PAD_GPIO1_IO04);
        iounmap(GPIO1_GDIR);
        iounmap(GPIO1_DR);

        /* 注销字符设备驱动 */
        unregister_chrdev(major, DEV_NAME);
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL2");
MODULE_AUTHOR("couvrir");
MODULE_DESCRIPTION("led module");
MODULE_ALIAS("led module");

执行过程

虚拟机:执行makemake copy。生成.ko文件。(此时编译报错,提示编译器版本太高,然后我把虚拟机的ubuntu版本从22.04 换到 18.04,编译成功)

开发板(在挂载目录下执行):

sudo insmod chrdev.ko

打印设备号:cat /proc/devices。

创建一个设备文件:sudo mknod /dev/xxx c 244 0

查看字符设备文件:ls -l /dev/xxx

开LED:sudo sh -c "echo on > /dev/xxx"

关LED:sudo sh -c "echo off > /dev/xxx"

sudo rmmod chrdev

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

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

相关文章

Python应用工具-Jupyter Notebook

工具简介 Jupyter Notebook是 基于 网页的用于交互计算的 应用程序&#xff0c;以网页的形式打开&#xff0c;可以在网页页面中直接编写代码和运行代码&#xff0c;代码的运行结果也会直接在代码块下 显示&#xff0c;文档是保存为后缀名为 . ipynb 的 JSON 格式文件。 操作指令…

学习笔记:Opencv实现限制对比度得自适应直方图均衡CLAHE

2023.8.19 为了完成深度学习的进阶&#xff0c;得学习学习传统算法拓展知识面&#xff0c;记录自己的学习心得 CLAHE百科&#xff1a; 一种限制对比度自适应直方图均衡化方法&#xff0c;采用了限制直方图分布的方法和加速的插值方法 clahe&#xff08;限制对比度自适应直方图…

AI搜索引擎助力科学家创新

开发者希望通过帮助科学家从大量文献中发现联系从而解放科学家&#xff0c;让他们专注于发现和创新。 图片来源&#xff1a;The Project Twins 对于专注于历史的研究者Mushtaq Bilal来说&#xff0c;他在未来科技中投入了大量时间。 Bilal在丹麦南部大学&#xff08; Universit…

畅享个性海报创作——探索免费开源的在线自动生成海报项目魅力

我们的生活越来越离不开各种创意和宣传&#xff0c;而其中一个常见的需求就是制作精美的海报。然而&#xff0c;对许多人来说&#xff0c;制作海报可能并不是一件轻松的事情&#xff0c;往往需要专业的设计技能或者花费不少时间去请人帮忙。今天了我给大家介绍一款开源的可私有…

SQL助你面大厂(连续N天登录)

在腾讯、网易或者一些游戏类大厂中&#xff0c;他们经常关注的就是用户上线人数以及天数&#xff0c;那么给我们一个数据库&#xff0c;我们怎么样才能快速的查询那个用户的连续N天登录&#xff1f; 那我们用案例来说明&#xff0c;再多的语言在现实面前总是那么苍白无力&…

mongodb 数据库管理(数据库、集合、文档)

目录 一、数据库操作 1、创建数据库 2、删除数据库 二、集合操作 1、创建集合 2、删除集合 三、文档操作 1、创建文档 2、 插入文档 3、查看文档 4、更新文档 1&#xff09;update() 方法 2&#xff09;replace() 方法 一、数据库操作 1、创建数据库 创建数据库…

HCIP——VLAN实验2

一.实验要求 1.PC1/3的接口均为access模式&#xff0c;且属于van2&#xff0c;在同一网段 2.PC2/4/5/6的IP地址在同一网段&#xff0c;与PC1/3不在同一网段 3.PC2可以访问4/5/6&#xff0c;PC4不能访问5/6&#xff0c;PC5不能访问PC6 4.所有PC通过DHCP获取ip地址&#xff0c;PC…

第 7 章 排序算法(1)

7.1排序算法的介绍 排序也称排序算法(Sort Algorithm)&#xff0c;排序是将一组数据&#xff0c;依指定的顺序进行排列的过程。 7.2排序的分类&#xff1a; 内部排序: 指将需要处理的所有数据都加载到**内部存储器(内存)**中进行排序。外部排序法&#xff1a; 数据量过大&am…

深入探索:Kali Linux 网络安全之旅

目录 前言 访问官方网站 导航到下载页面 启动后界面操作 前言 "Kali" 可能指的是 Kali Linux&#xff0c;它是一种基于 Debian 的 Linux 发行版&#xff0c;专门用于渗透测试、网络安全评估、数字取证和相关的安全任务。Kali Linux 旨在提供一系列用于测试网络和…

【数据结构】吃透单链表!!!(详细解析~)

目录 前言&#xff1a;一.顺序表的缺陷 && 介绍链表1.顺序表的缺陷2.介绍链表&#xff08;1&#xff09;链表的概念&#xff08;2&#xff09;链表的结构&#xff08;3&#xff09;链表的功能 二.单链表的实现1.创建节点的结构2.头文件函数的声明3.函数的实现&#xff…

一、docker及mysql基本语法

文章目录 一、docker相关命令二、mysql相关命令 一、docker相关命令 &#xff08;1&#xff09;拉取镜像&#xff1a;docker pull <镜像ID/image> &#xff08;2&#xff09;查看当前docker中的镜像&#xff1a;docker images &#xff08;3&#xff09;删除镜像&#x…

golang云原生项目之:etcd服务注册与发现

服务注册与发现&#xff1a;ETCD 1直接调包 kitex-contrib&#xff1a; 上面有实现的案例&#xff0c;直接cv。下面是具体的理解 2 相关概念 EtcdResolver: etcd resolver是一种DNS解析器&#xff0c;用于将域名转换为etcd集群中的具体地址&#xff0c;以便应用程序可以与et…

【深度学习 | 数据可视化】 视觉展示分类边界: Perceptron模型可视化iris数据集的决策边界

&#x1f935;‍♂️ 个人主页: AI_magician &#x1f4e1;主页地址&#xff1a; 作者简介&#xff1a;CSDN内容合伙人&#xff0c;全栈领域优质创作者。 &#x1f468;‍&#x1f4bb;景愿&#xff1a;旨在于能和更多的热爱计算机的伙伴一起成长&#xff01;&#xff01;&…

Spring Boot通过企业邮箱发件被Gmail退回的解决方法

这两天给我们开发的Chrome插件&#xff1a;Youtube中文配音 增加了账户注册和登录功能&#xff0c;其中有一步是邮箱验证&#xff0c;所以这边会在Spring Boot后台给用户的邮箱发个验证信息。如何发邮件在之前的文章教程里就有&#xff0c;这里就不说了&#xff0c;着重说说这两…

升级鸿蒙3后的超级快充 Turbo,如何开启和关闭?

nova 10 和 nova 9 、nova 9 Pro 也支持超级快充 Turbo 模式充电啦&#xff01;发布会后&#xff0c;许多人艳羡 nova 11 的超级快充 Turbo 模式充电&#xff0c;这不就来了&#xff01;超级快充 Turbo 加持&#xff0c;充电速度更快&#xff0c;心情更 UP&#xff01; 超级快充…

深度学习环境配置教程(保姆教程)

深度学习环境配置教程(保姆教程&#xff09; 目录1.Anaconda安装2.Anaconda环境操作相关1.显示所有环境2.新建虚拟环境3.激活虚拟环境4.在对应的虚拟环境中安装库&#xff08;tensorflow与torch的安装&#xff09;1. Tensorflow的CPU与GPU安装示例如下&#xff1a;2. pytorch的…

RK3399平台开发系列讲解(内核调试篇)Valgrind使用案例

🚀返回专栏总目录 文章目录 一、使用未初始化的内存案例二、内存泄露三、在内存被释放后进行读/写案例四、从已分配内存块的尾部进行读/写案例五、两次释放内存案例沉淀、分享、成长,让自己和他人都能有所收获!😄 📢Valgrind 是一个开源的内存调试和性能分析工具,用于…

【SoC基础】从[存储器]到[内存]再到[闪存],一次性解释清楚!

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

【小沐学NLP】Python进行统计假设检验

文章目录 1、简介1.1 假设检验的定义1.2 假设检验的基本步骤 2、测试数据2.1 鸢尾花&#xff08;Iris plants dataset&#xff09; 3、正态分布检验3.1 直方图判断3.2 KS检验&#xff08;scipy.stats.kstest&#xff09;3.3 Shapiro-Wilk test&#xff08;scipy.stats.shapiro&…

Docker 练习2 安装MySQL

一、实验要求 1、使用mysql:5.6和 owncloud 镜像&#xff0c;构建一个个人网盘。 2、安装搭建私有仓库 Harbor 3、编写Dockerfile制作Web应用系统nginx镜像&#xff0c;生成镜像nginx:v1.1&#xff0c;并推送其到私有仓库。具体要求如下&#xff1a; &#xff08;1&#xff09…