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开始搜索空闲的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博客

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Qt】:网络编程

网络编程 一.UDP Socket1.回显服务器2.回显客户端 二.TCP Socket1.回显服务器2.回显客户端 三.HTTP Client1.常见的APL2.给服务器发送⼀个GET请求. 四.音视频 前置知识&#xff1a;网络。如果不了解&#xff0c;可以看我的博客网络部分。 在进⾏⽹络编程之前,需要在项⽬中的 .p…

使用两台主机实现博客的搭建

1.运行环境 这里的主机IP是自己虚拟器的IP。 主机主机名系统服务192.168.179.128Server-WebLinuxWeb192.168.179.129Server-NFSDNSLinuxNFS/DNS 2.基础配置 1.配置主机名&#xff0c;静态IP地址 2.开启防火墙并配置 3.部分开启SElinux并配置 4.服务器之间使用同ntp.aliyun.com…

设计模式之状态模式(下)

3&#xff09;共享状态 1.概述 在某些情况下&#xff0c;多个环境对象可能需要共享同一个状态&#xff0c;如果希望在系统中实现多个环境对象共享一个或多个状态对象&#xff0c;那么需要将这些状态对象定义为环境类的静态成员对象。 2.案例 背景&#xff1a;要求两个开关对…

本地做好准备上传到Git分支,发现git上已经更新了,上传到dev分支

git add . git commit -m 备注 git pull --rebase origin dev 拉取dev上的代码合并到本地 git push -u origin dev推到远程dev上&#xff08;注意着可能不是最后一步&#xff0c;先看完&#xff09; 如果报错&#xff0c;意思是本地没有dev分支&#xff0c;没办法上传到git上…

获取钉钉群的 chatId

1、地址 获取地址 在代码发钉钉的地方找到 corpId。 填上后&#xff0c;会出现一个二维码&#xff0c;使用钉钉扫描二维码&#xff0c;就会出现你所在的群&#xff0c;点击&#xff0c;就能获取到 chatId

数据结构——单链表(C语言版)

文章目录 一、链表的概念及结构二、单链表的实现SList.h链表的打印申请新的结点链表的尾插链表的头插链表的尾删链表的头删链表的查找在指定位置之前插入数据在指定位置之后插入数据删除pos结点删除pos之后的结点销毁链表 三、完整源代码SList.hSList.ctest.c 一、链表的概念及…

自定义鼠标软件 SteerMouse最新完整激活版

SteerMouse是一款实用的Mac OS X系统辅助工具&#xff0c;可以帮助用户自定义鼠标和触控板的设置&#xff0c;提高使用效率。它提供了多种功能&#xff0c;如自定义按钮、滚轮和光标速度&#xff0c;以及调整灵敏度等&#xff0c;使用户能够根据自己的需求和习惯进行优化。 Ste…

振弦式裂缝计安装指南:使用灌浆锚头安装法

振弦式表面裂缝计作为一种精密的测量设备&#xff0c;在土木工程、建筑结构监测等领域发挥着重要的作用。为了确保裂缝计能够准确、稳定地工作&#xff0c;其安装过程尤为重要。本文将详细介绍振弦式表面裂缝计灌浆锚头的安装步骤&#xff0c;帮助大家更好地完成安装工作。 步骤…

启明智显技术分享|HMI工业级芯片Model3(简称M3芯片)PSRAM使用指南及PSRAM溢出如何进行问题定位

Model3芯片简介&#xff1a; 启明智显发布的HMI工业级芯片Model3&#xff08;简称M3芯片&#xff09;是一款高性能的显示交互和智能控制 MCU&#xff0c;采用国产自主高算力 RISC-V 内核&#xff0c;内置片上 1MB 大容量 SRAM 以及 64Mb PSRAM&#xff0c;并提供丰富的互联外设…

李彦宏官宣第二届“文心杯”创业大赛,最高投资奖励翻5倍达5000万

4月16日&#xff0c;百度创始人、董事长兼首席执行官李彦宏在Create 2024百度AI开发者大会上宣布&#xff0c;第二届“文心杯”创业大赛正式启动&#xff0c;参赛选手有机会获得最高5000万人民币投资。 李彦宏在Create 2024百度AI开发者大会的演讲主题是“人人都是开发者”&…

伦敦站:电子科技大学2024年全球人才推介会诚邀学者报名参会!

2024年4月24日&#xff0c;电子科技大学访英代表团一行将在伦敦举办人才推介交流会。届时&#xff0c;电子科技大学嘉宾将现场推介学校办学和人才队伍建设情况&#xff0c;宣讲学校人才引进政策&#xff0c;并与参会学者进行互动交流与洽谈。现热忱欢迎伦敦及周边地区学者报名参…

怎么申请OV证书

不同于DV SSL证书申请只需要验证域名所有权&#xff0c;申请OV SSL证书除了会验证域名之外&#xff0c;同时还会对申请企业的组织信息进行验证。本篇就给大家介绍一下如何申请OV SSL证书。 目前DV SSL证书和OV SSL证书的区别还是比较大的&#xff0c;DV和OV的区别&#xff1a;…

红帽认证考试流程指导

参加红帽认证考试涉及以下三个流程帐号和证件的准备 考试信息的填写 证书关联与下载 帐号和证件的准备RHN 帐号注册 在参加红帽官方培训和认证考试前需要您提前注册好红帽帐号(RHN) 访问 此页面 &#xff0c;随后点击 Register for a Red Hat account 链接进行注册 注册时以下条…

BoostCompass( 查找功能实现 )

阅读导航 一、查找功能基本思路二、详细代码三、代码介绍四、运行结果 一、查找功能基本思路 通过实现一个基于倒排索引的搜索引擎&#xff0c;来提供高效、准确的搜索服务。其核心在于快速准确地从大量文档中检索出与用户查询关键词相关的文档&#xff0c;并按照相关性对结果…

【计算机考研】「软件工程」VS「电子信息」专硕有什么不同?

就今年的24国考来说&#xff0c;计算机技术&#xff08;085404&#xff09;能报的只是比计算机科学与技术少那么一点点&#xff08;因为“计算机类”它都可以报&#xff0c;只有写计算机科学与技术的报不了&#xff09;相对于其他天坑专业来说还是好很多的&#xff01; 本人双…

制造企业研发设计资源用共享云桌面集中管控有哪些优势?

在制造企业上云的过程中&#xff0c;因为它们多用3D设计软件&#xff0c;所以选择一款高效、稳定、安全的云桌面产品显得尤为重要。云飞云共享云桌面作为一种新型的云桌面产品&#xff0c;正逐渐受到越来越多制造企业的青睐。那么&#xff0c;制造企业为什么要选云飞云共享云桌…

PaddleOCR训练自己模型(2)----参数配置及训练

一、介绍 paddleocr分为文字定位(Det)和文字识别(Rec)两个部分 二、定位模型训练 &#xff08;1&#xff09;Det预训练模型下载&#xff1a;https://paddleocr.bj.bcebos.com/PP-OCRv4/chinese/ch_PP-OCRv4_det_train.tar &#xff08;2&#xff09;下载完之后&#xff0c;…

(十一)C++自制植物大战僵尸游戏客户端更新实现

植物大战僵尸游戏开发教程专栏地址http://t.csdnimg.cn/cFP3z 更新检查 游戏启动后会下载服务器中的版本号然后与本地版本号进行对比&#xff0c;如果本地版本号小于服务器版本号就会弹出更新提示。让用户选择是否更新客户端。 在弹出的更新对话框中有显示最新版本更新的内容…

React-hooks:useRef

useRef文档 useRef 是一个ReactHook&#xff0c;它能帮助引用一个不需要渲染的值。 const ref useRef(initialValue)参数 initialValue&#xff1a;ref对象的 current 属性的初始值&#xff0c;可以是任意类型的值&#xff0c;这个参数在首次渲染后被忽略。 返回值 useRe…

Day99:云上攻防-云原生篇K8s安全实战场景攻击Pod污点Taint横向移动容器逃逸

目录 云原生-K8s安全-横向移动-污点Taint 云原生-K8s安全-Kubernetes实战场景 知识点&#xff1a; 1、云原生-K8s安全-横向移动-污点Taint 2、云原生-K8s安全-Kubernetes实战场景 云原生-K8s安全-横向移动-污点Taint 如何判断实战中能否利用污点Taint&#xff1f; 设置污点…