Linux·eventfd 原理与实践

news2024/12/25 2:36:12

1. eventfd/timerfd 简介

目前越来越多的应用程序采用事件驱动的方式实现功能,如何高效地利用系统资源实现通知的管理和送达就愈发变得重要起来。在Linux系统中,eventfd是一个用来通知事件的文件描述符,timerfd是的定时器事件的文件描述符。二者都是内核向用户空间的应用发送通知的机制,可以有效地被用来实现用户空间的事件/通知驱动的应用程序。

简而言之,就是eventfd用来触发事件通知,timerfd用来触发将来的事件通知。

开发者使用eventfd相关的系统调用,需要包含头文件;对于timerfd,则是。

系统调用eventfd/timerfd自linux 2.6.22版本加入内核,由Davide Libenzi最初实现和维护。

2. 接口及参数介绍

eventfd

对于eventfd,只有一个系统调用接口

int eventfd(unsigned int initval, int flags);

创建一个eventfd对象,或者说打开一个eventfd的文件,类似普通文件的open操作。

该对象是一个内核维护的无符号的64位整型计数器。初始化为initval的值。

flags可以以下三个标志位的OR结果:

  • EFD_CLOEXEC:FD_CLOEXEC,简单说就是fork子进程时不继承,对于多线程的程序设上这个值不会有错的。
  • EFD_NONBLOCK:文件会被设置成O_NONBLOCK,一般要设置。
  • EFD_SEMAPHORE:(2.6.30以后支持)支持semophore语义的read,简单说就值递减1。

这个新建的fd的操作很简单:

read(): 读操作就是将counter值置0,如果是semophore就减1。

write(): 设置counter的值。

注意,还支持epoll/poll/select操作,当然,以及每种fd都都必须实现的close。

timerfd

对于timerfd,有三个涉及的系统调用接口

int timerfd_create(int clockid, int flags);
int timerfd_settime(int fd, int flags,
                           const struct itimerspec *new_value,
                           struct itimerspec *old_value);
int timerfd_gettime(int fd, struct itimerspec *curr_value);

timerfd_create就是用来创建新的timerfd对象,clockid可以指定时钟的种类,比较常用的有两种:CLOCK_REALTIME(实时时钟)或 CLOCK_MONOTONIC(单调递增时钟)。实时时钟是指系统的时钟,它可以被手工修改。而后者单调递增时钟则是不会被系统时钟的人为设置的不连续所影响的。通常选择后者。而flags的选择,TFD_CLOEXEC和TFD_NONBLOCK的意义就比较直接了。

timerfd_settime函数用来设置定时器的过期时间expiration。itmerspec结构定义如下:

struct timespec {
    time_t tv_sec;                /* Seconds */
    long   tv_nsec;               /* Nanoseconds */
};
struct itimerspec {
    struct timespec it_interval;  /* Interval for periodic timer */
    struct timespec it_value;     /* Initial expiration */
};

该结构包含两个时间间隔:it_value是指第一次过期时间,it_interval是指第一次到期之后的周期性触发到期的间隔时间,(设为0的话就是到期第一次)。

old_value如果不为NULL,将会用调用时间来更新old_value所指的itimerspec结构对象。

timerfd_gettime():返回当前timerfd对象的设置值到curr_value指针所指的对象。

read():读操作的语义是:如果定时器到期了,返回到期的次数,结果存在一个8字节的整数(uint64_6);如果没有到期,则阻塞至到期,或返回EAGAIN(取决于是否设置了NONBLOCK)。

另外,支持epoll,同eventfd。

3. 使用实例 - 实现高性能消费者线程池

生产者-消费者设计模式是常见的后台架构模式。本实例将实现多个生产者和多个消费者的事件通知框架,用以阐释eventfd/timerfd在线程通信中作为通知实现的典型场景。

本实例采用以下设计:生产者创建eventfd/timerfd并在事件循环中注册事件;消费者线程池中的线程共用一个epoll对象,每个消费者线程并行地进行针对eventfd或timerfd触发的事件循环的轮询(epoll_wait)。

eventfd对应实现

typedef struct thread_info {
    pthread_t thread_id;
    int rank;
    int epfd;
} thread_info_t;

static void *consumer_routine(void *data) {
    struct thread_info *c = (struct thread_info *)data;
    struct epoll_event *events;
    int epfd = c->epfd;
    int nfds = -1;
    int i = -1;
    uint64_t result;

    log("Greetings from [consumer-%d]", c->rank);
    events = calloc(MAX_EVENTS_SIZE, sizeof(struct epoll_event));
    if (events == NULL) handle_error("calloc epoll events\n");

    for (;;) {
        nfds = epoll_wait(epfd, events, MAX_EVENTS_SIZE, 1000); // poll every second
        for (i = 0; i < nfds; i++) {
            if (events[i].events & EPOLLIN) {
                log("[consumer-%d] got event from fd-%d", c->rank, events[i].data.fd);
                // consume events (reset eventfd)
                read(events[i].data.fd, &result, sizeof(uint64_t));
                close(events[i].data.fd);   // NOTE: need to close here
            }
        }
    }
}

static void *producer_routine(void *data) {
    struct thread_info *p = (struct thread_info *)data;
    struct epoll_event event;
    int epfd = p->epfd;
    int efd = -1;
    int ret = -1;

    log("Greetings from [producer-%d]", p->rank);
    while (1) {
        sleep(1);
        // create eventfd (no reuse, create new every time)
        efd = eventfd(1, EFD_CLOEXEC|EFD_NONBLOCK);
        if (efd == -1) handle_error("eventfd create: %s", strerror(errno));
        // register to poller
        event.data.fd = efd;
        event.events = EPOLLIN | EPOLLET;    // Edge-Triggered
        ret = epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &event);
        if (ret != 0) handle_error("epoll_ctl");
        // trigger (repeatedly)
        write(efd, (void *)0xffffffff, sizeof(uint64_t));
    }
}

int main(int argc, char *argv[]) {
    struct thread_info *p_list = NULL, *c_list = NULL;
    int epfd = -1;
    int ret = -1, i = -1;
    // create epoll fd
    epfd = epoll_create1(EPOLL_CLOEXEC);
    if (epfd == -1) handle_error("epoll_create1: %s", strerror(errno));
    // producers
    p_list = calloc(NUM_PRODUCERS, sizeof(struct thread_info));
    if (!p_list) handle_error("calloc");
    for (i = 0; i < NUM_PRODUCERS; i++) {
        p_list[i].rank = i;
        p_list[i].epfd = epfd;
        ret = pthread_create(&p_list[i].thread_id, NULL, producer_routine, &p_list[i]);
        if (ret != 0) handle_error("pthread_create");
    }
    // consumers
    c_list = calloc(NUM_CONSUMERS, sizeof(struct thread_info));
    if (!c_list) handle_error("calloc");
    for (i = 0; i < NUM_CONSUMERS; i++) {
        c_list[i].rank = i;
        c_list[i].epfd = epfd;
        ret = pthread_create(&c_list[i].thread_id, NULL, consumer_routine, &c_list[i]);
        if (ret != 0) handle_error("pthread_create");
    }
    // join and exit
    for (i = 0; i < NUM_PRODUCERS; i++) {
        ret = pthread_join(p_list[i].thread_id, NULL);
        if (ret != 0) handle_error("pthread_join");
    }
    for (i = 0; i < NUM_CONSUMERS; i++) {
        ret = pthread_join(c_list[i].thread_id, NULL);
        if (ret != 0) handle_error("pthread_join");
    }
    free(p_list);
    free(c_list);
    return EXIT_SUCCESS;
}

执行过程(2个生产者,4个消费者):

[1532099804] Greetings from [producer-0]
[1532099804] Greetings from [producer-1]
[1532099804] Greetings from [consumer-0]
[1532099804] Greetings from [consumer-1]
[1532099804] Greetings from [consumer-2]
[1532099804] Greetings from [consumer-3]
[1532099805] [consumer-3] got event from fd-4
[1532099805] [consumer-3] got event from fd-5
[1532099806] [consumer-0] got event from fd-4
[1532099806] [consumer-0] got event from fd-4
[1532099807] [consumer-1] got event from fd-4
[1532099807] [consumer-1] got event from fd-5
[1532099808] [consumer-3] got event from fd-4
[1532099808] [consumer-3] got event from fd-5
^C

结果符合预期(附:源码链接)

注意,推荐在eventfd在打开时设置NON_BLOCKING,并在注册至epoll监听对象时设为EPOLLET(尽管一次8字节的read就可以读完整个计数器到用户空间),因为毕竟,只有采用了非阻塞IO和边沿触发,epoll的并发能力才能完全发挥极致。

另外,本实例中的eventfd消费地非常高效,fd号几乎不会超过5(前四个分别为stdin/stdout/stderr/eventpoll),但实际应用中往往在close前会执行一些事务,随着消费者线程的增加,eventfd打开的文件也会增加(这个数值得上限由系统的ulimit -n决定)。然而,eventfd打开、读写和关闭都效非常高,因为它本质并不是文件,而是kernel在内核空间(内存中)维护的一个64位计数器而已。

timerfd对应实现

main函数和consumer线程实现几乎一致,而producer线程创建timerfd,并注册到事件循环中。

timer的it_value设为1秒,即第一次触发为1秒以后;it_interval设为3秒,即后续每3秒再次触发一次。

注意,timerfd_settime函数的位置与之前eventfd的write的相同,二者达到了类似的设置事件的作用,只不过这次是定时器事件。

static void *producer_routine(void *data) {
    struct thread_info *p = (struct thread_info *)data;
    struct epoll_event event;
    int epfd = p->epfd;
    int tfd = -1;
    int ret = -1;
    struct itimerspec its;
    its.it_value.tv_sec = 1;    // initial expiration
    its.it_value.tv_nsec = 0;
    its.it_interval.tv_sec = 3; // interval
    its.it_interval.tv_nsec = 0;

    log("Greetings from [producer-%d]", p->rank);
    // create timerfd
    tfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC|TFD_NONBLOCK);
    if (tfd == -1) handle_error("timerfd create: %s", strerror(errno));
    // register to poller
    event.data.fd = tfd;
    event.events = EPOLLIN | EPOLLET;    // Edge-Triggered
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &event);
    if (ret != 0) handle_error("epoll_ctl");
    // register timer expired in future
    ret = timerfd_settime(tfd, 0, &its, NULL);
    if (ret != 0) handle_error("timerfd settime");
    return (void *)0;
}

执行过程(2个生产者,4个消费者):

[1532099143] Greetings from [producer-1]
[1532099143] Greetings from [consumer-1]
[1532099143] Greetings from [consumer-2]
[1532099143] Greetings from [consumer-3]
[1532099143] Greetings from [consumer-0]
[1532099143] Greetings from [producer-0]
[1532099144] [consumer-3] got event from fd-4
[1532099144] [consumer-3] got event from fd-5
[1532099147] [consumer-3] got event from fd-4
[1532099147] [consumer-3] got event from fd-5
[1532099150] [consumer-0] got event from fd-4
[1532099150] [consumer-0] got event from fd-5
[1532099153] [consumer-1] got event from fd-4
[1532099153] [consumer-1] got event from fd-5
^C

从上图可以看出,运行时打开的fd-4和fd-5两个文件描述符即是timerfd。

结果符合预期(附:源码链接)

4. 典型应用场景及优势

引用eventfs的Manual中NOTE段落的第一句话:

Applications can use an eventfd file descriptor instead of a pipe in all cases where a pipe is used simply to signal events.

在信号通知的场景下,相比pipe有非常大的资源和性能优势。其根本在于counter(计数器)和channel(数据信道)的区别。

  • 第一,是打开文件数量的巨大差别。由于pipe是半双工的传统IPC方式,所以两个线程通信需要两个pipe文件,而用eventfd只要打开一个文件。众所周知,文件描述符可是系统中非常宝贵的资源,linux的默认值也只有1024而已。那开发者可能会说,1相比2也只节省了一半嘛。要知道pipe只能在两个进程/线程间使用,并且是面向连接(类似TCP socket)的,即需要之前准备好两个pipe;而eventfd是广播式的通知,可以多对多的。如上面的NxM的生产者-消费者例子,如果需要完成全双工的通信,需要NxMx2个的pipe,而且需要提前建立并保持打开,作为通知信号实在太奢侈了,但如果用eventfd,只需要在发通知的时候瞬时创建、触发并关闭一个即可。
  • 第二,是内存使用的差别。eventfd是一个计数器,内核维护几乎成本忽略不计,大概是自旋锁+唤醒队列(后续详细介绍),8个字节的传输成本也微乎其微。但pipe可就完全不是了,一来一回数据在用户空间和内核空间有多达4次的复制,而且更糟糕的是,内核还要为每个pipe分配至少4K的虚拟内存页,哪怕传输的数据长度为0。
  • 第三,对于timerfd,还有精准度和实现复杂度的巨大差异。由内核管理的timerfd底层是内核中的hrtimer(高精度时钟定时器),可以精确至纳秒(1e-9秒)级,完全胜任实时任务。而用户态要想实现一个传统的定时器,通常是基于优先队列/二叉堆,不仅实现复杂维护成本高,而且运行时效率低,通常只能到达毫秒级。

所以,第一个最佳实践法则:当pipe只用来发送通知(传输控制信息而不是实际数据),放弃pipe,放心地用eventfd/timerfd,"in all cases"。

另外一个重要优势就是eventfd/timerfd被设计成与epoll完美结合,比如支持非阻塞的读取等。事实上,二者就是为epoll而生的(但是pipe就不是,它在Unix的史前时代就有了,那时不仅没有epoll连Linux都还没诞生)。应用程序可以在用epoll监控其他文件描述符的状态的同时,可以“顺便“”一起监控实现了eventfd的内核通知机制,何乐而不为呢?

所以,第二个最佳实践法则:eventfd配上epoll才更搭哦。

5. 内核实现细节

eventfd在内核源码中,作为syscall实现在内核源码的 fs/eventfd.c下。从Linux 2.6.22版本引入内核,在2.6.27版本以后加入对flag的支持。以下分析参考Linux 2.6.27源码。

内核中的数据结构:eventfd_ctx

该结构除了包括之前所介绍的一个64位的计数器,还包括了等待队列头节点(较新的kernel中还加上了一个kref)。

定义和初始化过程核心代码如下,比较直接:内核malloc,设置count值,创建eventfd的anon_inode。

struct eventfd_ctx {
        wait_queue_head_t wqh;
        __u64 count;
};

以下为创建eventfd的函数的片段,比较直接。

SYSCALL_DEFINE2(eventfd2, unsigned int, count, int, flags) {
        // ...
	ctx = kmalloc(sizeof(*ctx), GFP_KERNEL);
	if (!ctx)
		return -ENOMEM;
	init_waitqueue_head(&ctx->wqh);
	ctx->count = count;
	fd = anon_inode_getfd("[eventfd]", &eventfd_fops, ctx,
			      flags & (O_CLOEXEC | O_NONBLOCK));
        // ...
}

稍提一下,等待队列是内核中的重要数据结构,在进程调度、异步通知等多种场景都有很多的应用。其节点结构并不复杂,即自带自旋锁的双向循环链表的节点,如下:

struct __wait_queue_head {
	spinlock_t lock;
	struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

等待队列中存放的是task(内存中对线程的抽象)的结构。

操作等待队列的函数主要是和调度相关的函数,如:wake_up和schedule,它们位于sched.c中,前者即唤醒当前等待队列中的task,后者为当前task主动让出CPU时间给等待队列中的其他task。这样,便通过等待队列实现了多个task在运行中(TASK_RUNNING)和IO等待(TASK_INTERRUPTABLE)中的状态切换。

让我们一起复习下,系统中进程的状态转换:

  • TASK_RUNNING: 正在在CPU上运行,或者在执行队列(run queue)等待被调度执行。
  • TASK_INTERRUPTIBLE: 睡眠中等待默写事件出现,task可以被信号打断,一旦接收到信号或显示调用了wake-up,转为TASK_RUNNING状态。常见于IO等待中。

清楚了task的两种状态以及run queue / wait queue原理,read函数就不难理解了。

以下是read函数的实现:

static ssize_t eventfd_read(struct file *file, char __user *buf, size_t count,
			    loff_t *ppos)
{
	struct eventfd_ctx *ctx = file->private_data;
	ssize_t res;
	__u64 ucnt;
	DECLARE_WAITQUEUE(wait, current);

	if (count < sizeof(ucnt))
		return -EINVAL;
	spin_lock_irq(&ctx->wqh.lock);
	res = -EAGAIN;
	ucnt = ctx->count;
	if (ucnt > 0)
		res = sizeof(ucnt);
	else if (!(file->f_flags & O_NONBLOCK)) {
		__add_wait_queue(&ctx->wqh, &wait);
		for (res = 0;;) {
			set_current_state(TASK_INTERRUPTIBLE);
			if (ctx->count > 0) {
				ucnt = ctx->count;
				res = sizeof(ucnt);
				break;
			}
			if (signal_pending(current)) {
				res = -ERESTARTSYS;
				break;
			}
			spin_unlock_irq(&ctx->wqh.lock);
			schedule();
			spin_lock_irq(&ctx->wqh.lock);
		}
		__remove_wait_queue(&ctx->wqh, &wait);
		__set_current_state(TASK_RUNNING);
	}
	if (res > 0) {
		ctx->count = 0;
		if (waitqueue_active(&ctx->wqh))
			wake_up_locked(&ctx->wqh);
	}
	spin_unlock_irq(&ctx->wqh.lock);
	if (res > 0 && put_user(ucnt, (__u64 __user *) buf))
		return -EFAULT;

	return res;
}

read操作目的是要将count值返回用户空间并清零。ctx中的count值是共享数据,通过加irq自旋锁实现对其的独占安全访问,spin_lock_irq函数可以禁止本地中断和抢占,在SMP体系中也是安全的。从源码可以看出,如果是对于(通常的epoll中的,也是上面实例中的)非阻塞读,count大于0则直接返回并清零,count等于0则直接返回EAGAIN。

对于阻塞读,如果count值为0则加入等待队列并阻塞,直到值不为0时(被其他线程更新)返回。阻塞是如何实现的呢?是通过TASK_INTERRUPTABLE状态下的循环加schedule。注意,schedule前释放了自旋锁,意味着允许其他线程更新值,只要值被更新大于0且又再次获得cpu时间,那么就可以跳出循环继续执行而返回了。

考虑一个情景,两个线程几乎同时read请求,那么:两个都会被加入到等待队列中,当第一个抢到自旋锁,返回了大于1的res并重置了count为0,此时它会(在倒数第二个if那里) 第一时间唤醒等待队列中的其他线程,此时第二个线程被调度到,于是开始了自己的循环等待。即实现了:事件只会通知到第一个接收到的线程。

那么问题来了:我们知道在其他线程write后,阻塞的read线程是马上返回的。那么如何能在count置一旦不为0时,等待的调度的阻塞读线程可以尽快地再次获得cpu时间,从而继续执行呢?关键在于write函数也有当确认可以成功返回时,主动调用wakeup_locked的过程,这样就能实现write后立即向等待队列通知的效果了。

write操作与read操作过程非常相似,不在此展开。

关于poll操作的核心代码如下:

	// ...
        spin_lock_irqsave(&ctx->wqh.lock, flags);
	if (ctx->count > 0)
		events |= POLLIN;
	if (ctx->count == ULLONG_MAX)
		events |= POLLERR;
	if (ULLONG_MAX - 1 > ctx->count)
		events |= POLLOUT;
	spin_unlock_irqrestore(&ctx->wqh.lock, flags);

在count值大于0时,返回了设置POLLIN标志的事件,使得用户层的应用可以通过epoll监控 eventfd的可读事件状态。

6. 本篇小结

通过对eventfd/timerfd的接口和实现的了解,可以看出其不仅功能实用,而且调用方式简单。另外,其实现是非常精巧高效的,构建于内核众多系统基础核心功能之上,为用户态的应用封装了十分高效简单的事件通知机制。

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

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

相关文章

防火墙(三)

firewalld防火墙 一、firewalld概述firewalld与iptables的区别firewalld区域firewalld数据处理流程 二、firewalld防火墙的使用配置方法常用的firewalld-cmd命令选项 三、操作小实验 一、firewalld概述 firewalld防火墙是Centos 7 系统默认的防火墙管理工具&#xff0c;取代了…

AWS设备自定义身份认证

AWS设备自定义身份认证需要通过lambda服务实现&#xff0c;具体来说&#xff0c;首先需要创建一个lambda函数&#xff0c;在函数中实现具体的认证逻辑&#xff0c;然后Iot在调用授权方时&#xff0c;将触发lambda函数&#xff0c;返回认证结果。 1.输入参数说明 授权方在调用…

Qt编程基础 | 使用VS创建空白Qt项目

一、使用VS创建空白Qt项目 使用VS创建空白Qt项目&#xff0c;如下&#xff1a; 步骤一&#xff1a;新建一个空白Qt项目 步骤二&#xff1a;手动添加需要的文件 头文件代码&#xff0c;如下&#xff1a; #include <QtWidgets/QApplication> #include <QWidget>int…

C++11 异常

文章目录 &#x1f356;异常是什么&#x1f32d;概念&#x1f32d;实现方式 &#x1f356;异常的使用和注意事项&#x1f32d;注意事项&#x1f32d;异常的重新抛出&#x1f32d;异常安全 &#x1f356;异常的规范&#x1f356;异常带来的优缺点 &#x1f356;异常是什么 &…

jQurey-基本知识点总结

&#xff08;一&#xff09;jQurey基础知识 1、官网下载&#xff1a;jQuery jQurey是一个js文件&#xff0c;直接存到项目文件中&#xff0c;然后跟平常文件js导入一致&#xff1a; <script src"js/jquery-3.7.0.js"></script> 2、jQurey语法 jQure…

邹检验,结构变化识别及其R语言实现

在描述多维数据的维度关系时&#xff0c;线性模型无疑应用最多。然而某些情况下&#xff0c;我们关心随着时间变化或随着样本分组&#xff0c;线性关系的具体参数是否发生了变化&#xff0c;即是否发生结构变化Structural break。邹检验Chow test提供了最基本的一种结构变化显著…

Solaris Network:去中心化金融(DeFi)的未来

近年来&#xff0c;金融世界经历了一场范式转变&#xff0c;区块链技术在实现无障碍和反审计的去中心化金融服务方面发挥了关键作用。在这样的背景下&#xff0c;Solaris Network应运而生&#xff0c;它创建了一个基于Web 3.0技术的去中心化合成资产生态系统。 什么是Solaris N…

制作网上投票链接制作可以投票的链接制作制作一个投票链接

现在来说&#xff0c;公司、企业、学校更多的想借助短视频推广自己。 通过微信投票小程序&#xff0c;网友们就可以通过手机拍视频上传视频参加活动&#xff0c;而短视频微信投票评选活动既可以给用户发挥的空间激发参与的热情&#xff0c;又可以让商家和企业实现推广的目的&am…

FinClip小程序统计能力重磅上线!数据统计分析更精准

不妨让我们看看在本月的产品与市场发布亮点&#xff0c;看看它们如何帮助您实现目标。 产品方面的相关动向&#x1f447;&#x1f447;&#x1f447; 全新版本的小程序统计能力 ​ 全新版本的⼩程序统计功能已经在近期上线了&#xff0c;我们计划在 2023 年 5 ⽉ 23 ⽇的「价…

git fsmonitor--daemon 占用目录,导致无法修改

当我通过命令 git clone 目录然后导入 IDE 操作时&#xff0c;由于想修改目录名&#xff0c;就退了 IDE&#xff0c;再修改目录名&#xff0c;系统提示我文件夹正在使用&#xff1a; 通过 LockHunter (或者PowerToys) 发现占用该目录的进程&#xff0c;右键打开。 打开后如下…

03. 数据结构之链表

前言 链表是相区别于数组的&#xff0c;另一种典型的线性表数据结构。也是学习后面复杂的数据结构的基础&#xff0c;我们后面很多结构比如树&#xff0c;有向图等都可以用链表很方便的存储管理。 1. 概念 链表&#xff08;linked list&#xff09;是一种在物理上非连续、非…

CryoEM - 冷冻电镜 EMPIAR 数据集的下载过程

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/130812925 EMPIAR: Electron Microscopy Public Image Archive&#xff0c;电镜公开图像存档。 IBM Aspera Connect 是一款高效的文件传…

如何在两个月内考过软考高级

如何在两个月内考过软考高级 前言本人情况备考经历一些备考关键点考试中结果相关资料获取 前言 高级软考的作用这里不用多说&#xff0c;本人在2022年9月初开始备考&#xff0c;在11月初顺利通过高级系统架构师&#xff0c;期间的经历这里与大家分享一下。本人之前并没有考过其…

使用Jmeter连接MySQL测试实战

01、连接MQSQL数据库 1、jmeter要连接mysql数据库 首先得下载mysql jdbc驱动包&#xff0c;尽量保证其版本和你的数据库版本一致&#xff0c;至少不低于数据库版本&#xff0c;否则可能有问题。 官网下载地址为&#xff1a;https://dev.mysql.com/downloads/connector/j/ 下…

数据结构课程设计——哈夫曼编/译码器

数据结构课程设计任务书 学生姓名&#xff1a; 专业班级&#xff1a;软件工程 指导教师&#xff1a; 工作单位&#xff1a; 题 目: 哈夫曼编/译码器 基础要求&#xff1a; &#xff08;1&#xff09;熟悉各种…

HTML- 标签学习之- 表单

input 系列&#xff0c; 类型根据type 区分&#xff0c;所有效果如下&#xff1a; 注意点&#xff1a; 单/多选框&#xff1a; name &#xff1a; 相同name属性的单选值为一组&#xff0c;遗嘱中只能有一个被选中 checked&#xff1a; 默认选中 性别<i…

stm32的IIC驱动0.96OLED

IIC原理介绍&#xff1a; IIC是一个总线的结构但不支持总线协议 OLED介绍&#xff1a; 一、0.96寸OLED屏幕介绍 本文采用的是4针的0.96寸OLED显示进行讲解&#xff0c;采用的是SPI协议&#xff0c;速度会比采用I2C协议的更快&#xff0c;但这两者的显示驱动都一样&#xf…

AIGC功能在线制作思维导图?

ProcessOn思维导图软件是一款功能强大的在线制作思维导图的工具&#xff0c;它提供了丰富的模板和图标&#xff0c;可以帮助用户快速制作出高质量的思维导图。其中&#xff0c;AIGC(人工智能图形识别)功能是 ProcessOn软件中的一大特色&#xff0c;它可以帮助用户更加高效地制…

permiere的字幕自动转录功能

permiere2022.3后就能够离线字幕转录了&#xff0c;这个是个很好的消息&#xff08;但也要温馨的提示大家&#xff0c;这个功能对于非标注发音或者环境嘈杂的环境下的语音识别很不友好&#xff0c;当然&#xff0c;如果你想利用它来识别歌词&#xff0c;那就乘早死了这条心&…

chatgpt赋能Python-pythonslam

Pythonslam&#xff1a;实现SLAM技术的Python库 在机器人领域&#xff0c;SLAM&#xff08;Simultaneous Localization and Mapping&#xff09;技术是非常重要的。SLAM技术使得机器人能够在未知环境中构建地图并同时确定自己的位置。然而&#xff0c;SLAM算法往往需要强大的计…