Linux中用户通过系统调用实现硬件驱动全流程

news2024/11/26 20:45:36

驱动全流程:

以基于设备树、Pinctrl、gpio子系统,打开一个字符设备为例:

1、通过系统调用open进入内核

        当我们在用户空间调用open之后,应用程序会使用系统调用指令(在上图中可看到,ARM架构中软中断汇编指令为svc指令,X86架构中为int0X80)触发一个软中断,保存中断上下文后切换用户栈到内核栈,陷入内核空间,将控制权转移到操作系统内核。

2、内核调用sys_open服务函数

        内核中的中断处理程序sys_call通过系统调用号(EABI形式中,系统调用号通过通用寄存器R7传递)查找系统调用表,也就是sys_call_table数组,它是一个函数指针数组,每一个函数指针都指向其系统调用的封装例程,有NR_syscalls个表项,第n个表项包含系统调用号为n的服务例程的地址来调用相应的系统调用处理函数,在此示例中也就是sys_open函数,sys_open是经过宏替换定义的,源码在fs/open.c中。

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
	if (force_o_largefile())
		flags |= O_LARGEFILE;

	return do_sys_open(AT_FDCWD, filename, flags, mode);
}

展开SYSCALL_DEFINE3(open, const char __user , filename, int, flags, int, mode)函数原型如下:

asmlinkage long sys_open(const char __user* filename, int flags, int mode)

do_sys_open

在sys_open里面继续调用do_sys_open完成 open操作,该函数主要分为如下几个步骤来完成打开文件的操作:
1.将文件名参数从用户态拷贝至内核,调用函数get_name();
2.从进程的文件表中找到一个空闲的文件表指针也就是文件句柄,调用了函数get_unused_fd_flgas();
3.完成真正的打开操作,调用函数do_filp_open();
4.将打开的文件添加到进程的文件表数组中,调用函数fd_install();

long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{
	/*从进程地址空间读取该文件的路径名*/
	char *tmp = getname(filename);
	int fd = PTR_ERR(tmp);
	if (!IS_ERR(tmp)) {
		/*在内核中,每个打开的文件由一个文件描述符表示该描述符在特定于进程的数组中充当位置索引(数组是
		task_struct->files->fd_arry),该数组的元素包含了file结构,其中包括每个打开文件的所有必要信息。因此,调用下面
		函数查找一个未使用的文件描述符,返回的是上面说的数组的下标*/
		fd = get_unused_fd_flags(flags);
		if (fd >= 0) {
			/*fd获取成功则开始打开文件,此函数是主要完成打开功能的函数*/
			//如果分配fd成功,则创建一个file对象
			struct file *f = do_filp_open(dfd, tmp, flags, mode, 0);
			if (IS_ERR(f)) {
				put_unused_fd(fd);
				fd = PTR_ERR(f);
			}
		}
	}
} else {
		/*文件如果打开成功,调用fsnoTIfy_open()函数,根据inode所指定的信息进行打开
		函数(参数为f)将该文件加入到文件监控的系统中。该系统是用来监控文件被打开,创建,
		读写,关闭,修改等操作的*/
		fsnotify_open(f->f_path.dentry);
		/*将文件指针安装在fd数组中
		将struct file *f加入到fd索引位置处的数组中。如果后续过程中,有对该文件描述符的
		操作的话,就会通过查找该数组得到对应的文件结构,而后在进行相关操作。*/
		fd_install(fd, f);
	}
}
	putname(tmp);
	return fd;
}

getname()

        其中getname函数主要的任务是将文件名filename从用户态拷贝至内核态

char * getname(const char __user * filename)
{
	char *tmp, *result;
	result = ERR_PTR(-ENOMEM);
	tmp = __getname(); //从内核缓存中分配空间;
	if (tmp)  {
		//将文件名从用户态拷贝至内核态;
		int retval = do_getname(filename, tmp);
		result = tmp;
		if (retval){
			__putname(tmp);
			result = ERR_PTR(retval);
		}
	}
	audit_getname(result);
	return result;
}

get_unused_fd_flags

        get_unused_fd_flags实际调用的是alloc_fd,该函数为需要打开的文件在当前进程内分配一个空闲的文件描述符fd,该fd就是open()系统调用的返回值

#define get_unused_fd_flags(flags) alloc_fd(0, (flags))
/*
* allocate a file descriptor, mark it busy.
*/
int alloc_fd(unsigned start, unsigned flags)
{
	struct files_struct *files = current->files;//获得当前进程的files_struct 结构
	unsigned int fd;
	int error;
	struct fdtable *fdt;
	spin_lock(&files->file_lock);
	repeat:
	fdt = files_fdtable(files);
	fd = start;
	if (fd next_fd) //从上一次打开的fd的下一个fd开始搜索空闲的fd
		fd = files->next_fd;
	if (fd max_fds)//寻找空闲的fd,返回值为空闲的fd
		fd = find_next_zero_bit(fdt->open_fds->fds_bits,
	fdt->max_fds, fd);
	//如果有必要,即打开的fd超过max_fds,则需要expand当前进程的fd表;
	//返回值error<0表示出错,error=0表示无需expand,error=1表示进行了expand;
	error = expand_files(files, fd);
	if (error)
		goto out;

	/*
	* If we needed to expand the fs array we
	* might have blocked - try again.
	*/
	//error=1表示进行了expand,那么此时需要重新去查找空闲的fd;
	if (error)
		goto repeat;
	//设置下一次查找的起始fd,即本次找到的空闲的fd的下一个fd,记录在files->next_fd中;
	if (start <= files->next_fd)
	files->next_fd = fd + 1;
	FD_SET(fd, fdt->open_fds);
	if (flags & O_CLOEXEC)
		FD_SET(fd, fdt->close_on_exec);
	else
		FD_CLR(fd, fdt->close_on_exec);
	error = fd;
#if 1
/* Sanity check */
if (rcu_dereference(fdt->fd[fd]) != NULL) {
	printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
	rcu_assign_pointer(fdt->fd[fd], NULL);
}
#endif
out:
	spin_unlock(&files->file_lock);
	return error;
}
do_filp_open

do_filp_open函数的一个重要作用就是根据传递进来的权限进行分析,并且分析传递进来的路径名字,根据路径名逐个解析成dentry,并且通过dentry找到inode,inode就是记录着该文件相关的信息, 包括文件的创建时间和文件属性所有者等等信息,根据这些信息就可以找到对应的文件操作方法。在这个过程当中有一个临时的结构体用于保存在查找过程中的相关信息

fs/namei.c

do_sys_open->do_sys_openat2->do_filp_open
struct file *do_filp_open(int dfd, struct filename *pathname,
        const struct open_flags *op)
{
    struct nameidata nd;
    int flags = op->lookup_flags;
    struct file *filp;

    set_nameidata(&nd, dfd, pathname);
    filp = path_openat(&nd, op, flags | LOOKUP_RCU);
    if (unlikely(filp == ERR_PTR(-ECHILD)))
        filp = path_openat(&nd, op, flags);
    if (unlikely(filp == ERR_PTR(-ESTALE)))
        filp = path_openat(&nd, op, flags | LOOKUP_REVAL);
    restore_nameidata();
    return filp;
}

do_file_open 函数的处理如下, 主要调用了path_openat 函数去执行真正的open 流程:

fs/namei.c

do_sys_open->do_sys_openat2->do_filp_open
struct file *do_filp_open(int dfd, struct filename *pathname,
        const struct open_flags *op)
{
    struct nameidata nd;
    int flags = op->lookup_flags;
    struct file *filp;

    set_nameidata(&nd, dfd, pathname);
    filp = path_openat(&nd, op, flags | LOOKUP_RCU);
    if (unlikely(filp == ERR_PTR(-ECHILD)))
        filp = path_openat(&nd, op, flags);
    if (unlikely(filp == ERR_PTR(-ESTALE)))
        filp = path_openat(&nd, op, flags | LOOKUP_REVAL);
    restore_nameidata();
    return filp;
}

        path_openat: 执行open的核心流程

(1) 申请 file 结构体, 并做初始化

(2) 找到路径的最后一个分量

(3) 对于最后一个分量进行处理, 这里面会去查找文件是否存在,如果不存在则看条件创建

(4) 执行open的最后步骤, 例如调用open 回调

fs/namei.c

do_sys_open->do_sys_openat2->do_filp_open->path_openat

static struct file *path_openat(struct nameidata *nd,
            const struct open_flags *op, unsigned flags)
{
    struct file *file;
    int error;

    file = alloc_empty_file(op->open_flag, current_cred());          /*    1      */
    if (IS_ERR(file))
        return file;

    if (unlikely(file->f_flags & __O_TMPFILE)) {
        error = do_tmpfile(nd, flags, op, file);
    } else if (unlikely(file->f_flags & O_PATH)) {
        error = do_o_path(nd, flags, file);
    } else {
        const char *s = path_init(nd, flags);
        while (!(error = link_path_walk(s, nd)) &&                   /*      2        */
               (s = open_last_lookups(nd, file, op)) != NULL)        /*      3        */
            ;
        if (!error)
            error = do_open(nd, file, op);                          /*        4        */
        terminate_walk(nd);
    }
    if (likely(!error)) {
        if (likely(file->f_mode & FMODE_OPENED))
            return file;
        WARN_ON(1);
        error = -EINVAL;
    }
    fput(file);
    if (error == -EOPENSTALE) {
        if (flags & LOOKUP_RCU)
            error = -ECHILD;
        else
            error = -ESTALE;
    }
    return ERR_PTR(error);
}
(1) 申请 file 结构体, 并做初始化
(2) 找到路径的最后一个分量
(3) 对于最后一个分量进行处理, 这里面会去查找文件是否存在,如果不存在则看条件创建
(4) 执行open的最后步骤, 例如调用open 回调

我们使用的open函数在内核中对应的是sys_open函数,sys_open函数又会调用do_sys_open函数。在do_sys_open函数中,首先调用函数get_unused_fd_flags来获取一个未被使用的文件描述符fd,该文件描述符就是我们最终通过open函数得到的值。紧接着,又调用了do_filp_open函数,该函数通过调用函数get_empty_filp得到一个新的file结构体,之后的代码做了许多复杂的工作,如解析文件路径,查找该文件的文件节点inode等,最后来到了do_dentry_open函数,如下所示:

do_sys_open->do_sys_openat2->do_filp_open->path_openat->do_open->vfs_open->do_dentry_open

fs/open.c

do_sys_open->do_sys_openat2->do_filp_open->path_openat->do_open->vfs_open

int vfs_open(const struct path *path, struct file *file)
{
    file->f_path = *path;
    return do_dentry_open(file, d_backing_inode(path->dentry), NULL);
}


static int do_dentry_open(struct file *f,
              struct inode *inode,
              int (*open)(struct inode *, struct file *))
{
    static const struct file_operations empty_fops = {};
    int error;

    path_get(&f->f_path);
    f->f_inode = inode;
    f->f_mapping = inode->i_mapping;
    f->f_wb_err = filemap_sample_wb_err(f->f_mapping);
    f->f_sb_err = file_sample_sb_err(f);                  /*            1          */

    if (unlikely(f->f_flags & O_PATH)) {
        f->f_mode = FMODE_PATH | FMODE_OPENED;
        f->f_op = &empty_fops;
        return 0;
    }

    if (f->f_mode & FMODE_WRITE && !special_file(inode->i_mode)) {
        error = get_write_access(inode);
        if (unlikely(error))
            goto cleanup_file;
        error = __mnt_want_write(f->f_path.mnt);
        if (unlikely(error)) {
            put_write_access(inode);
            goto cleanup_file;
        }
        f->f_mode |= FMODE_WRITER;
    }

    /* POSIX.1-2008/SUSv4 Section XSI 2.9.7 */
    if (S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode))
        f->f_mode |= FMODE_ATOMIC_POS;

    f->f_op = fops_get(inode->i_fop);                /*取该文件节点inode的成员变量i_fop*/
    if (WARN_ON(!f->f_op)) {
        error = -ENODEV;
        goto cleanup_all;
    }

    error = security_file_open(f);
    if (error)
        goto cleanup_all;

    error = break_lease(locks_inode(f), f->f_flags);
    if (error)
        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);                      /*               3            */
        if (error)
            goto cleanup_all;
    }
    f->f_mode |= FMODE_OPENED;
    if ((f->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ)
        i_readcount_inc(inode);
    if ((f->f_mode & FMODE_READ) &&
         likely(f->f_op->read || f->f_op->read_iter))
        f->f_mode |= FMODE_CAN_READ;
    if ((f->f_mode & FMODE_WRITE) &&
         likely(f->f_op->write || f->f_op->write_iter))
        f->f_mode |= FMODE_CAN_WRITE;

    f->f_write_hint = WRITE_LIFE_NOT_SET;
    f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);

    file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);

    /* NB: we're sure to have correct a_ops only after f_op->open */
    if (f->f_flags & O_DIRECT) {
        if (!f->f_mapping->a_ops || !f->f_mapping->a_ops->direct_IO)
            return -EINVAL;
    }
    /*
     * XXX: Huge page cache doesn't support writing yet. Drop all page
     * cache for this file before processing writes.
     */
    if ((f->f_mode & FMODE_WRITE) && filemap_nr_thps(inode->i_mapping))
        truncate_pagecache(inode, 0);
    return 0;

cleanup_all:
    if (WARN_ON_ONCE(error > 0))
        error = -EINVAL;
    fops_put(f->f_op);
    if (f->f_mode & FMODE_WRITER) {
        put_write_access(inode);
        __mnt_drop_write(f->f_path.mnt);
    }
cleanup_file:
    path_put(&f->f_path);
    f->f_path.mnt = NULL;
    f->f_path.dentry = NULL;
    f->f_inode = NULL;
    return error;
}

def_chr_fops结构体(位于内核源码/fs/char_dev.c文件)
const struct file_operations def_chr_fops = {
	.open = chrdev_open,
	.llseek = noop_llseek,
};
(1) (2) 设置file结构体的一些成员
(3) 找到open 回调, 并执行
以上代码中的使用fops_get函数来获取该文件节点inode的成员变量i_fop,在上图中我们使用mknod创建字符设备文件时,将def_chr_fops结构体赋值给了该设备文件inode的i_fop成员。到了这里,我们新建的file结构体的成员f_op就指向了def_chr_fops。

得到的file 结构体如下图所示:

此处的f_pos是文件的偏移地址,即read函数读文件的开始位置。而file结构体的位置如下图所示:

每个进程都有对应的 task_struct 结构体

3、执行最底层open

最终,会执行file_operation中的open函数,也就是驱动程序中的chrdev_open函数可以理解为一个字符设备的通用初始化函数,根据字符设备的设备号,找到相应的字符设备,从而得到操作该设备的方法,代码实现如下。chrdev_open函数(位于内核源码/fs/char_dev.c文件)

注:可以自己在自定义驱动程序中定义drv_open,drv_open函数执行具体的寄存器操作,完成硬件驱动,其中如果引入Pinctrl、gpio子系统,将由gpio子系统指定硬件资源,这工作一般芯片厂家会提前做好,Pinctrl子系统设置gpio的功能,驱动程序可以直接使用gpio函数接口完成gpio的访问,所以具体的寄存器操作将由pinctrl、gpio子系统代劳。底层platform_driver结构体匹配设备节点时调用probe函数(记录引脚信息,创建设备节点)后,将硬件信息传给drv_open硬件操作函数)

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

在Linux内核中,使用结构体cdev来描述一个字符设备。在以上代码中的第14行,inode->i_rdev中保存了字符设备的设备编号,通过函数kobj_lookup函数便可以找到该设备文件cdev结构体的kobj成员,再通过函数container_of便可以得到该字符设备对应的结构体cdev。函数container_of的作用就是通过一个结构变量中一个成员的地址找到这个结构体变量的首地址。同时,将cdev结构体记录到文件节点inode中的i_cdev,便于下次打开该文件。继续阅读第36~45行代码,我们可以发现,函数chrdev_open最终将该文件结构体file的成员f_op替换成了cdev对应的ops成员,并执行ops结构体中的open函数。
  最后,调用上图的fd_install函数,完成文件描述符和文件结构体file的关联,之后我们使用对该文件描述符fd调用read、write函数,最终都会调用file结构体对应的函数,实际上也就是调用cdev结构体中ops结构体内的相关函数。

背景知识:

系统函数调用和常规函数调用的不同

在典型的 Linux 内核源代码中,用户调用 open 系统调用后,实际上会调用内核中的 sys_open 函数。但是,这个过程并不是通过常规的函数调用方式实现的。用户态的 open 系统调用会触发一个软中断(或者是通过系统调用指令),使得处理器从用户模式切换到内核模式,然后内核会根据中断号来执行相应的中断服务例程。在 Linux 内核中,这个中断服务例程会调用 sys_open 函数来完成实际的文件打开操作。

在典型的 Linux 内核源代码中,sys_open 函数通常被实现在一个文件中,例如 fs/open.c 或者类似的文件中。虽然你可以通过跳转到定义(jump to definition)的方式查看 open 函数的定义,但是在用户空间的代码中并不能直接看到 sys_open 函数的定义。这是因为 sys_open 是在内核空间中实现的,而用户空间的代码无法直接访问或查看内核空间的函数定义

因此,虽然用户可以在代码中调用 open 系统调用,但是 sys_open 函数的具体实现对于用户是不可见的。用户只需要知道调用 open 函数即可发起文件打开操作,而具体的系统调用实现细节是由操作系统内核来处理的。

所以这也解答了笔者的疑惑,在查看源代码时,根据对open函数的jump to  definition操作回溯到的open函数定义并不能显示出调用sys_open的具体过程,原因就是系统调用方式和常规的函数调用不同。

OABI 和 EABI

在 arm 平台架构中,存在两种不同的 ABI 形式,OABI 和 EABI,OABI 中的 O 是 old 的意思,表示旧有的 ABI,而 EABI 是基于 OABI 上的改进,或者说它更适合目前大多数的硬件,OABI 和 EABI 的区别主要在于浮点的处理和系统调用,浮点的区别不做过多讨论,对于系统调用而言,OABI 和 EABI 最大的区别在于,OABI 的系统调用指令需要传递参数来指定系统调用号,而 EABI 中将系统调用号保存在 r7 中.

所以在系统调用的源码实现中,尽管大多数情况下都是使用 EABI 的系统调用方式,也会保持对 OABI 的兼容。

SVC指令

SVC(Supervisor Call)指令是一种特权指令,用于触发软中断或异常,进入supervisor模式,使得处理器从用户模式切换到supervisor模式,以便执行特权操作,例如系统调用。

ARM架构中的特权模式包括以下几种:

  1. 用户模式(User mode):也称为非特权模式,用户空间应用程序通常在该模式下运行。在用户模式下,应用程序只能访问受限资源,无法直接执行特权指令或访问特权寄存器。

  2. 特权模式(Privileged mode):也称为特权级或特权状态。在特权模式下,处理器可以执行特权指令、访问特权寄存器,并且可以执行一些受限制的操作。操作系统内核通常在特权模式下运行,以便执行特权操作,例如处理中断、管理内存、执行系统调用等。

在ARM架构中,特权模式可以进一步细分为以下几种:

  • 中断模式(Interrupt mode):用于处理中断请求。当处理器接收到中断请求时,会从当前模式切换到中断模式,并执行相应的中断处理程序。

  • 监管者模式(Supervisor mode):也称为超级用户模式。在监管者模式下,操作系统内核执行大部分特权操作,包括管理进程、调度任务、执行系统调用等。监管者模式是操作系统内核的主要执行模式。

  • 其他特权模式:ARM架构还包括一些其他特权模式,如快速中断模式(FIQ mode)和异常模式(Abort mode)。这些模式通常用于处理特定类型的中断或异常,以提高系统的响应速度和稳定性。

保存中断上下文

  1. 保存寄存器状态:处理器中的通用寄存器和特殊寄存器的状态需要保存下来,以便在系统调用完成后能够正确地恢复。通用寄存器保存的是用户空间应用程序的状态,而特殊寄存器保存的是处理器的状态,如程序计数器(PC)堆栈指针(SP)等。

  2. 保存堆栈状态:当前用户空间的堆栈状态也需要保存下来。这通常包括保存当前堆栈指针(SP)的值,以及将堆栈指针移动到内核空间的堆栈区域

  3. 保存程序计数器:程序计数器(PC)是用于指示下一条要执行的指令的寄存器。在系统调用触发的过程中,需要保存当前用户空间应用程序的程序计数器的值,以便在系统调用完成后能够正确地返回到用户空间继续执行。

  4. 保存其他状态信息:根据具体的架构和实现,可能还需要保存其他的一些状态信息,如标志寄存器状态等。

参考博文:linux设备驱动模型一字符设备open系统调用流程_open是怎么一步一步调用到cdev的?-CSDN博客

Linux ARM系统调用过程分析(三)——Linux中open系统调用实现原理_sys_open-CSDN博客

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

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

相关文章

浏览器工作原理与实践--浏览上下文组:如何计算Chrome中渲染进程的个数

经常有朋友问到如何计算Chrome中渲染进程个数的问题&#xff0c;那么今天就来完整地解答这个问题。 在前面“04 | 导航流程”这一讲中我们介绍过了&#xff0c;在默认情况下&#xff0c;如果打开一个标签页&#xff0c;那么浏览器会默认为其创建一个渲染进程。不过我们在“04 |…

搜维尔科技:【工业仿真】煤矿机械安全事故VR警示教育系统

产品概述 搜维尔科技 煤矿机械安全事故VR警示教育系统 系统内容&#xff1a; 系统采用虚拟现实技术模拟矿井井下机械安全技术及事故&#xff0c;展现井下常见机械伤害事故&#xff0c;表现伤害事故的隐患点&#xff0c;能够模拟事故发生和发展过程&#xff1b;营造井下灾害发…

C#基于SSE传递消息给Vue前端实现即时单向通讯

一、简述 通常前端调用后端的API&#xff0c;调用到了&#xff0c;等待执行完&#xff0c;拿到返回的数据&#xff0c;进行渲染&#xff0c;流程就完事了。如果想要即时怎么办&#xff1f;如果你想问什么场景非要即时通讯&#xff0c;那可就很多了&#xff0c;比如在线聊天、实…

HQL,SQL刷题,尚硅谷(中级)

目录 相关表结构&#xff1a; 1、order_info表 2、order_detail表 题目及思路解析&#xff1a; 第一题&#xff0c;查询各品类销售商品的种类数及销量最高的商品 第二题 查询用户的累计消费金额及VIP等级 第三题 查询首次下单后第二天连续下单的用户比率 总结归纳&#xff1a…

C#版Facefusion:让你的脸与世界融为一体!-02 获取人脸关键点

C#版Facefusion&#xff1a;让你的脸与世界融为一体&#xff01;-02 获取人脸关键点 目录 说明 效果 模型信息 项目 代码 下载 说明 C#版Facefusion一共有如下5个步骤&#xff1a; 1、使用yoloface_8n.onnx进行人脸检测 2、使用2dfan4.onnx获取人脸关键点 3、使用arcfa…

【MATLAB源码-第36期】matlab基于BD,SVD,ZF,MMSE,MF,SLNR预编码的MIMO系统误码率分析。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 1. MIMO (多输入多输出)&#xff1a;这是一个无线通信系统中使用的技术&#xff0c;其中有多个发送和接收天线。通过同时发送和接收多个数据流&#xff0c;MIMO可以增加数据速率和系统容量&#xff0c;同时提高信号的可靠性。…

算法1: 素数个数统计

统计n以内的素数个数 素数&#xff1a;只能被1和自身整除的自然数&#xff0c;0和1除外&#xff1b; 举例&#xff1a; 输入&#xff1a;100 输出&#xff1a;25 import java.util.*; class Test1{public static void main(String[] args){int a 100; //输入数字//…

41、二叉树-二叉树的层序遍历

思路&#xff1a; 层序遍历就是从左到右依次遍历。这个时候就可以使用队列的方式。例如先把头节点入队&#xff0c;然后遍历开始&#xff0c;首先计算队列长度&#xff0c;第一层&#xff0c;长度为了&#xff0c;遍历一次&#xff0c;依次出队&#xff0c;头结点出队&#xff…

Redis的RedisObject和对外可见的5种数据结构

目录 RedisObject Redis的编码方式 对外可见的5种数据结构 1.string string结构的源码 为什么是小于44字节会采用embstr编码&#xff1f; embstr和raw区别 2.list list结构的源码 3.set set结构的源码 4.zset zset结构的源码 5.hash hash结构的源码 Redis中…

淘宝客订单产品设计:连接商家与推广者的智能桥梁

随着电商行业的迅速发展&#xff0c;淘宝客作为一种常见的推广方式&#xff0c;为商家引流、提升销量发挥了重要作用。而淘宝客订单产品的设计&#xff0c;则是连接商家与推广者的智能桥梁&#xff0c;本文将对其进行探讨与分析。 ### 1. 淘宝客订单产品的定义 淘宝客订单产品…

梯度提升树(Gradient Boosting Trees)

通过5个条件判定一件事情是否会发生&#xff0c;5个条件对这件事情是否发生的影响力不同&#xff0c;计算每个条件对这件事情发生的影响力多大&#xff0c;写一个梯度提升树&#xff08;Gradient Boosting Trees&#xff09;模型程序,最后打印5个条件分别的影响力。 示例一 梯…

【目标检测】Focal Loss

Focal Loss用来解决正负样本不平衡问题&#xff0c;并提升训练过程对困难样本的关注。 在一阶段目标检测算法中&#xff0c;以YOLO v3为例&#xff0c;计算置信度损失&#xff08;图中第3、4项&#xff09;时有目标的点少&#xff0c;无目标的点多&#xff0c;两者可能相差百倍…

WSL(Ubuntu)、PC物理机,linux开发板三个设备通讯,镜像模式

文章目录 一、前言二、使用2.1 需要的系统信息2.2 添加 .wslconfig 文件 三、如何从局域网访问WSL中的服务 一、前言 最近在使用Linux开发板的环境下&#xff0c;由于使用的 WSL的子系统&#xff0c;并不是虚拟机&#xff0c;导致 网络传输 这方面不是很方便&#xff0c;由于 W…

AGM AG32 MCU在汽车UWB应用方案

AG32的汽车UWB应用方案 汽车电子产品的日益成熟&#xff0c;包括ADAS和车载信息娱乐&#xff0c;正在推动对CPLD的需求。例如&#xff0c;利用安装在车上的各种传感器&#xff08;如雷达、摄像头和激光雷达等&#xff09;来感知周围环境&#xff0c;实现实时监测和数据处理。这…

docker容器技术篇:数据卷的常用操作

Docker数据卷的使用 在docker中&#xff0c;为了方便查看容器内产生的数据或者将多个容器中的数据实现共享&#xff0c;就涉及到容器数据卷管理&#xff0c;那什么是数据卷呢&#xff0c;往下看&#xff01;&#xff01;&#xff01; 1 数据卷概念 数据卷是一个共给容器使用…

一款挺不错网站维护页面HTML源码

一款挺不错网站维护页面源码&#xff0c;单HTML不需要数据库&#xff0c;上传到你的虚拟机就可以用做维护页面还不错&#xff0c;用处多。。 源码下载 一款挺不错网站维护页面源码

C# - 反射动态添加/删除Attribute特性

API: TypeDescriptor.AddAttributes TypeDescriptor.GetAttributes 注意&#xff1a;TypeDescriptor.AddAttributes添加的特性需要使用 TypeDescriptor.GetAttributes获取 根据api可以看到&#xff0c;该接口不仅可以给指定类&#xff08;Type&#xff09;添加特性&#xf…

设计模式——模版模式21

模板方法模式在超类中定义了一个事务流程的框架&#xff0c; 允许子类在不修改结构的情况下重写其中一个或者多个特定步骤。下面以ggbond的校招礼盒发放为例。 设计模式&#xff0c;一定要敲代码理解 模版抽象 /*** author ggbond* date 2024年04月18日 17:32* 发送奖品*/ p…

华为框式交换机S12700E系列配置CSS集群

搭建集群环境 a.为两台交换机上电&#xff0c;按照数据规划分别对两台框式交换机进行配置 <HUAWEI> system-view [HUAWEI] sysname Switch1 [Switch1] set css id 1 [Switch1] set css priority 150 //框1的集群优先级配置为150 [Switch1] interface css-port 1 [Sw…

后端-MySQL-week11 多表查询

tips: distinct————紧跟“select”之后&#xff0c;用于去重 多表查询 概述 一对多&#xff08;多对一&#xff09; 多对多 一对一 多表查询概述 分类 连接查询 内连接 外连接 自连接 必须起别名&#xff01; 联合查询-union&#xff0c;union all 子查询 概念 分类 …