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

驱动全流程:

以基于设备树、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开始搜索空闲的fdfd = files->next_fd;if (fd max_fds)//寻找空闲的fd,返回值为空闲的fdfd = 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);elseFD_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.cdo_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.cdo_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.cdo_sys_open->do_sys_openat2->do_filp_open->path_openatstatic 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;elseerror = -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.cdo_sys_open->do_sys_openat2->do_filp_open->path_openat->do_open->vfs_openint 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 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;}

在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.hqwc.cn/news/626518.html

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

相关文章

Linux上的可执行文件在Windows上是不能运行的

一、概要 1、可执行文件的格式 Linux上的可执行文件是elf格式的 Windows上的可执行文件是exe格式的 Linux上的可执行文件在Windows上是不能运行的 2、程序的普通构建与静态构建 普通构建&#xff1a; 一个.c文件&#xff0c;用gcc命令编译成可执行文件(程序)&#xff0c…

强化学习(四)基于蒙特卡罗算法 Monte-Calo 的求解

文章目录 1. 免模型学习的强化学习问题2. 利用蒙特卡洛法求解最优价值函数2.1 策略评估&#xff08;预测&#xff09;2.2 策略迭代&#xff08;控制&#xff09; 在《强化学习&#xff08;三&#xff09;基于动态规划 Dynamic Programming 的求解方法》的文末中提到&#xff0c…

网络攻防演练:一场针锋相对的技术博弈与安全防护实践

随着ChatGPT5的即将上线&#xff0c;其安全防护能力面临更为严峻的考验。网络攻防演练作为检验系统安全性能、提升防御体系的关键环节&#xff0c;对于确保ChatGPT5的安全稳定运行具有重要意义。本文将深入探讨网络攻击与防守之间的动态关系&#xff0c;并提供在网络攻防演练中…

发布!DolphinDB 白皮书正式上线官网!

对广大数据库用户而言&#xff0c;白皮书是极具参考价值的使用指南和学习手册。白皮书不但能深入剖析数据库的基础概念与架构&#xff0c;协助用户了解数据库的工作原理和应用技巧&#xff0c;更提供了丰富的实践案例&#xff0c;帮助用户从中汲取经验&#xff0c;避免在实际应…

C++ | Leetcode C++题解之第28题找出字符串中第一个匹配项的下标

题目&#xff1a; 题解&#xff1a; class Solution { public:int strStr(string haystack, string needle) {int n haystack.size(), m needle.size();if (m 0) {return 0;}vector<int> pi(m);for (int i 1, j 0; i < m; i) {while (j > 0 && needl…

做好项目精准估算 7大注意事项

项目精准估算对项目非常重要&#xff0c;它不仅影响到项目的成本和收入&#xff0c;还关系到项目资源分配以及风险管理。精准的估算有助于提高项目开发效率&#xff0c;降低风险。如果项目估算不准确&#xff0c;导致资源分配不当&#xff0c;容易造成成本超出预算&#xff0c;…

设计和实施适应不同文化的客户服务策略:满足全球客户需求

随着全球化的发展&#xff0c;企业越来越需要提供适应不同文化的客户服务&#xff0c;以满足全球客户的需求。然而&#xff0c;设计和实施这样的客户服务策略并非易事。文章将探讨如何设计和实施适应不同文化的客户服务策略&#xff0c;以帮助企业更好地满足全球客户的需求。 1…

【数字人】AIGC技术引领数字人革命:从制作到应用到全景解析

AIGC技术引领虚拟数字人革命&#xff1a;从制作到应用的全景解析 一、AIGC技术为虚拟数字人注入智能灵魂二、AIGC型虚拟人制作流程实例分析1、采集数据2、建模3、内容生成 三、AIGC在虚拟数字人应用中的案例分析四、总结与展望 在科技的浪潮中&#xff0c;AIGC&#xff08;人工…

树莓派驱动开发--搭建环境篇(保姆级)

前言&#xff1a;树莓派的环境搭建关系到之后的驱动开发&#xff0c;故一个好的环境能让你顺手完成驱动开发&#xff01;我使用的是64位树莓派4b&#xff01;有显示屏的前提&#xff01;&#xff01;&#xff01;&#xff08;因为wifi连接太刁钻了&#xff09; 一、ubantu相关 …

The O-one:开源语言模型计算机的革命

在人工智能的浪潮中&#xff0c;The O-one作为一个创新的开源项目&#xff0c;正以其独特的功能和开放性吸引着全球开发者和科技爱好者的目光。这个项目不仅仅是一个简单的语言模型&#xff0c;它是一个能够通过语音交互与计算机进行对话的智能系统&#xff0c;极大地提升了人机…

3D目标检测实用技巧(二)- 实现点云(or 体素)向图像平面的投影并可视化

一、引言 受Focals Conv的启发&#xff0c;该论文中通过将点云投影到图片中清晰展现出点云学习后的情况&#xff1a; 本次实现的是体素向图像投影并显示&#xff0c;实现出来的效果如下&#xff1a; 二、 实现细节 1、体素投影到图像坐标系 这里我们参考的是VirConv的投影函…

基于Java办公用品管理系统设计实现

基于JavaWeb开发的办公用品管理系统设计实现 &#x1f345; 作者主页 央顺技术团队 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; &#x1f345; 查看下方微信号获取联系方式 承接各种定制系统 &#x1…