libuv进程通信与管道描述符

libuv 提供了大量的子进程管理,抽象了平台差异并允许使用流或命名管道与子进程进行通信。Unix 中的一个常见习惯是每个进程只做一件事,并且把它做好。在这种情况下,一个进程通常会使用多个子进程来完成任务(类似于在 shell 中使用管道)。与具有线程和共享内存的多进程模型相比,具有消息的多进程模型也可能更容易推理。

对基于事件的程序的常见限制是它们无法利用现代计算机中的多核。在多线程程序中,内核可以进行调度,将不同的线程分配给不同的内核,从而提高性能。但事件循环只有一个线程。解决方法可以是启动多个进程,每个进程运行一个事件循环,并且每个进程被分配给一个单独的 CPU 核心。

在这里插入图片描述

生成子进程

最简单的情况是当您只想启动一个进程并知道它何时退出时。这是使用 uv_spawn 实现的。

int main() {
    loop = uv_default_loop();
    char* args[3];
    args[0] = "mkdir";
    args[1] = "test-dir";
    args[2] = NULL;

    options.exit_cb = on_exit;
    options.file = "mkdir";
    options.args = args;

    int r;
    if ((r = uv_spawn(loop, &child_req, &options))) {
        fprintf(stderr, "%s\n", uv_strerror(r));
        return 1;
    } else {
        fprintf(stderr, "Launched process with ID %d\n", child_req.pid);
    }

    return uv_run(loop, UV_RUN_DEFAULT);
}

options 隐式地用零初始化,因为它是一个全局变量。如果将 options 更改为局部变量,请记住将其初始化以清空所有未使用的字段:

uv_process_options_t options = {0};

uv_process_t 结构仅充当句柄,所有选项均通过 uv_process_options_t 设置。要简单地启动进程,您只需设置 fileargs 字段。 file 是要执行的程序。由于 uv_spawn 在内部使用 execvp,因此无需提供完整路径。最后,根据底层约定,参数数组必须比参数数量大 1,最后一个元素为 NULL

调用 uv_spawn 后, uv_process_t.pid 将包含子进程的进程 ID。将使用退出状态和导致退出的信号类型来调用退出回调。请注意,不要在退出回调之前调用 uv_close ,重要!。

void on_exit(uv_process_t *req, int64_t exit_status, int term_signal) {
    fprintf(stderr, "Process exited with status %lld, signal %d\n", exit_status, term_signal);
    uv_close((uv_handle_t*)req, NULL);
}

进程退出后需要关闭进程观察器。

改变进程参数

在子进程启动之前,您可以使用 uv_process_options_t 中的字段控制执行环境。

在这里插入图片描述

改变执行目录

uv_process_options_t.cwd 设置为相应的目录。

设置环境变量

uv_process_options_t.env 是一个以 null 结尾的字符串数组,每个 VAR=VALUE 形式用于设置进程的环境变量。将其设置为 NULL 以从父(此)进程继承环境。

选项与标志

uv_process_options_t.flags 设置为以下标志的按位或,可以修改子进程的行为:

  1. UV_PROCESS_SETUID - 将子进程的执行用户 ID 设置为 uv_process_options_t.uid

  2. UV_PROCESS_SETGID - 将子级的执行组 ID 设置为 uv_process_options_t.gid

    仅 Unix 支持更改 UID/GIDuv_spawn 在 Windows 上将失败,并显示 UV_ENOTSUP

  3. UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS - Windows 上不会引用或转义 uv_process_options_t.args 。在 Unix 上被忽略。

  4. UV_PROCESS_DETACHED - 在新会话中启动子进程,该子进程将在父进程退出后继续运行。请参阅下面的示例。

分离进程

传递标志 UV_PROCESS_DETACHED 可用于启动守护进程或独立于父进程的子进程,以便父进程退出不会影响它。

int main() {
    loop = uv_default_loop();

    char *args[3];
    args[0] = "sleep";
    args[1] = "100";
    args[2] = NULL;

    options.exit_cb = NULL;
    options.file = "sleep";
    options.args = args;
    options.flags = UV_PROCESS_DETACHED;

    int r;
    if ((r = uv_spawn(loop, &child_req, &options))) {
        fprintf(stderr, "%s\n", uv_strerror(r));
        return 1;
    }
    fprintf(stderr, "Launched sleep with PID %d\n", child_req.pid);
    uv_unref((uv_handle_t*) &child_req);

    return uv_run(loop, UV_RUN_DEFAULT);

}

请记住,句柄仍在监视子进程,因此您的程序不会退出。如果您想要更加彻底分离,请使用 uv_unref()

向进程发送信号

libuv 包装了 Unix 上的标准 kill(2) 系统调用,并在 Windows 上实现了具有类似语义的系统调用,但有一个警告:所有 SIGTERMSIGINTSIGKILL 的签名是:

uv_err_t uv_kill(int pid, int signum);

对于使用 libuv 启动的进程,您可以使用 uv_process_kill 代替,它接受 uv_process_t 观察程序作为第一个参数,而不是 pid。在这种情况下,请记住在调用退出回调之后在观察器上调用 uv_close

信号

libuv 提供了 Unix 信号的包装器以及一些 Windows 支持。

使用 uv_signal_init() 初始化句柄并将其与循环关联。要侦听该处理程序上的特定信号,请将 uv_signal_start() 与处理程序函数一起使用。每个处理程序只能与一个信号号关联,随后对 uv_signal_start() 的调用会覆盖先前的关联。使用 uv_signal_stop() 停止观看。这是一个小例子,展示了各种可能性:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <uv.h>

uv_loop_t* create_loop()
{
    uv_loop_t *loop = malloc(sizeof(uv_loop_t));
    if (loop) {
      uv_loop_init(loop);
    }
    return loop;
}

void signal_handler(uv_signal_t *handle, int signum)
{
    printf("Signal received: %d\n", signum);
    uv_signal_stop(handle);
}

// two signal handlers in one loop
void thread1_worker(void *userp)
{
    uv_loop_t *loop1 = create_loop();

    uv_signal_t sig1a, sig1b;
    uv_signal_init(loop1, &sig1a);
    uv_signal_start(&sig1a, signal_handler, SIGUSR1);

    uv_signal_init(loop1, &sig1b);
    uv_signal_start(&sig1b, signal_handler, SIGUSR1);

    uv_run(loop1, UV_RUN_DEFAULT);
}

// two signal handlers, each in its own loop
void thread2_worker(void *userp)
{
    uv_loop_t *loop2 = create_loop();
    uv_loop_t *loop3 = create_loop();

    uv_signal_t sig2;
    uv_signal_init(loop2, &sig2);
    uv_signal_start(&sig2, signal_handler, SIGUSR1);

    uv_signal_t sig3;
    uv_signal_init(loop3, &sig3);
    uv_signal_start(&sig3, signal_handler, SIGUSR1);

    while (uv_run(loop2, UV_RUN_NOWAIT) || uv_run(loop3, UV_RUN_NOWAIT)) {
    }
}

int main()
{
    printf("PID %d\n", getpid());

    uv_thread_t thread1, thread2;

    uv_thread_create(&thread1, thread1_worker, 0);
    uv_thread_create(&thread2, thread2_worker, 0);

    uv_thread_join(&thread1);
    uv_thread_join(&thread2);
    return 0;
}

uv_run(loop, UV_RUN_NOWAIT)uv_run(loop, UV_RUN_ONCE) 类似,它只处理一个事件。如果没有待处理事件,UV_RUN_ONCE 会阻塞,而 UV_RUN_NOWAIT 将立即返回。我们使用NOWAIT,这样其中一个循环就不会因为另一个循环没有待处理的活动而陷入饥饿状态。

SIGUSR1 发送到进程,您会发现处理程序被调用 4 次,每个 uv_signal_t 调用一次。处理程序只是停止每个句柄,以便程序退出。这种向所有处理程序的分派非常有用。使用多个事件循环的服务器可以确保在终止之前安全地保存所有数据,只需在每个循环中添加 SIGINT 的观察程序即可。

子进程I/O

一个正常的、新生成的进程有自己的一组文件描述符,其中 0、1 和 2 分别是 stdinstdoutstderr 。有时您可能想与孩子共享文件描述符。例如,也许您的应用程序启动了一个子命令,并且您希望将所有错误记录在日志文件中,但忽略 stdout 。为此,您希望子级的 stderr 与父级的 stderr 相同。在这种情况下,libuv支持继承文件描述符。在此示例中,我们调用测试程序,即:

#include <stdio.h>

int main()
{
    fprintf(stderr, "This is stderr\n");
    printf("This is stdout\n");
    return 0;
}

实际程序 proc-streams 在仅共享 stderr 的同时运行此程序。子进程的文件描述符是使用 uv_process_options_t 中的 stdio 字段设置的。首先将 stdio_count 字段设置为正在设置的文件描述符的数量。 uv_process_options_t.stdiouv_stdio_container_t 的数组,即:

typedef struct uv_stdio_container_s {
    uv_stdio_flags flags;

    union {
        uv_stream_t* stream;
        int fd;
    } data;
} uv_stdio_container_t;

其中标志可以有多个值。如果不使用,请使用 UV_IGNORE 。如果前三个 stdio 字段标记为 UV_IGNORE 它们将重定向到 /dev/null

由于我们想要传递现有的描述符,因此我们将使用 UV_INHERIT_FD 。然后我们将 fd 设置为 stderr

int main() {
    loop = uv_default_loop();

    /* ... */

    options.stdio_count = 3;
    uv_stdio_container_t child_stdio[3];
    child_stdio[0].flags = UV_IGNORE;
    child_stdio[1].flags = UV_IGNORE;
    child_stdio[2].flags = UV_INHERIT_FD;
    child_stdio[2].data.fd = 2;
    options.stdio = child_stdio;

    options.exit_cb = on_exit;
    options.file = args[0];
    options.args = args;

    int r;
    if ((r = uv_spawn(loop, &child_req, &options))) {
        fprintf(stderr, "%s\n", uv_strerror(r));
        return 1;
    }

    return uv_run(loop, UV_RUN_DEFAULT);
}

如果运行 proc-stream ,您将看到仅显示“This is stderr”行。尝试将 stdout 标记为继承并查看输出。

将这种重定向应用于流非常简单。通过将 flags 设置为 UV_INHERIT_STREAM 并将 data.stream 设置为父进程中的流,子进程可以将该流视为标准 I/O。这可以用来实现 CGI 之类的东西。

示例 CGI 脚本/可执行文件是:

#include <stdio.h>
#include <unistd.h>

int main() {
    int i;
    for (i = 0; i < 10; i++) {
        printf("tick\n");
        fflush(stdout);
        sleep(1);
    }
    printf("BOOM!\n");
    return 0;
}

CGI 服务器结合了网络的概念,以便向每个客户端发送十个时钟周期,然后关闭连接。

void on_new_connection(uv_stream_t *server, int status) {
    if (status == -1) {
        // error
        return;
    }
    uv_tcp_t *client = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
    uv_tcp_init(loop, client);
    if (uv_accept(server, (uv_stream_t*)client) == 0) {
        invoke_cgi_script(client);
    } else {
        uv_close((uv_handle_t*) client, NULL);
    }
}

这里我们只是接受 TCP 连接并将套接字(流)传递给 invoke_cgi_script

int main() {
    loop = uv_default_loop();

    uv_tcp_t server;
    uv_tcp_init(loop, &server);

    struct sockaddr_in bind_addr;
    uv_ip4_addr("0.0.0.0", 7000, &bind_addr);
    uv_tcp_bind(&server, (const struct sockaddr *)&bind_addr, 0);
    int r = uv_listen((uv_stream_t*) &server, 128, on_new_connection);
    if (r) {
        fprintf(stderr, "Listen error %s\n", uv_err_name(r));
        return 1;
    }
    return uv_run(loop, UV_RUN_DEFAULT);
}

CGI 脚本的 stdout 设置为套接字,以便我们的刻度脚本打印的任何内容都会发送到客户端。通过使用进程,我们可以将读/写缓冲卸载到操作系统,因此就便利性而言,这非常好。请注意,创建流程是一项成本高昂的任务。

父子进程IPC

父级和子级可以通过将 uv_stdio_container_t.flags 设置为 UV_CREATE_PIPEUV_READABLE_PIPEUV_WRITABLE_PIPE 。读/写标志是从子进程的角度来看的。在这种情况下, uv_stream_t* stream 字段必须设置为指向已初始化、未打开的 uv_pipe_t 实例。

新的 stdio 管道

uv_pipe_t 结构不仅仅表示 pipeline (或 | ),还支持任何类似流文件的对象。在 Windows 上,该描述的唯一对象是命名管道。在 Unix 上,这可以是任何 Unix 域套接字,或者派生自 mkfifo,或者它实际上可以是管道。当 uv_spawn 由于 UV_CREATE_PIPE 标志而初始化 uv_pipe_t 时,它会选择创建套接字对。

这样做的目的是允许多个 libuv 进程与 IPC 进行通信。这将在下面讨论。

任意进程IPC

由于域套接字可以具有众所周知的名称和文件系统中的位置,因此它们可以用于不相关进程之间的 IPC。开源桌面环境使用的 D-BUS 系统使用域套接字进行事件通知。当联系人上线或检测到新硬件时,各种应用程序可以做出反应。 MySQL 服务器还运行一个域套接字,客户端可以在该套接字上与其交互。

使用域套接字时,通常遵循客户端-服务器模式,套接字的创建者/所有者充当服务器。初始设置后,消息传递与 TCP 没有什么不同,因此我们将重新使用 echo 服务器示例。

void remove_sock(int sig) {
    uv_fs_t req;
    uv_fs_unlink(loop, &req, PIPENAME, NULL);
    exit(0);
}

int main() {
    loop = uv_default_loop();

    uv_pipe_t server;
    uv_pipe_init(loop, &server, 0);

    signal(SIGINT, remove_sock);

    int r;
    if ((r = uv_pipe_bind(&server, PIPENAME))) {
        fprintf(stderr, "Bind error %s\n", uv_err_name(r));
        return 1;
    }
    if ((r = uv_listen((uv_stream_t*) &server, 128, on_new_connection))) {
        fprintf(stderr, "Listen error %s\n", uv_err_name(r));
        return 2;
    }
    return uv_run(loop, UV_RUN_DEFAULT);
}

我们将套接字命名为 echo.sock ,这意味着它将在本地目录中创建。就流 API 而言,此套接字现在的行为与 TCP 套接字没有什么不同。您可以使用 socat 测试该服务器:

$ socat - /path/to/socket

想要连接到域套接字的客户端将使用:

void uv_pipe_connect(uv_connect_t *req, uv_pipe_t *handle, const char *name, uv_connect_cb cb);

其中 name 将是 echo.sock 或类似的。在 Unix 系统上, name 必须指向有效文件(例如 /tmp/echo.sock )。在 Windows 上, name 遵循 \\?\pipe\echo.sock 格式。

通过管道发送文件描述符

域套接字的一个很酷的事情是,可以通过域套接字发送文件描述符,从而在进程之间交换文件描述符。这允许进程将其 I/O 移交给其他进程。应用程序包括负载平衡服务器、工作进程和其他充分利用 CPU 的方法。 libuv 目前仅支持通过管道发送 TCP 套接字或其他管道。

为了进行演示,我们将查看一个回显服务器实现,该实现以循环方式将客户端交给工作进程。这个程序有点复杂,虽然书中只包含了一些片段,但建议阅读完整的代码以真正理解它。

工作进程非常简单,因为文件描述符是由主进程移交给它的。

uv_loop_t *loop;
uv_pipe_t queue;
int main() {
    loop = uv_default_loop();

    uv_pipe_init(loop, &queue, 1 /* ipc */);
    uv_pipe_open(&queue, 0);
    uv_read_start((uv_stream_t*)&queue, alloc_buffer, on_new_connection);
    return uv_run(loop, UV_RUN_DEFAULT);
}

queue 是连接到另一端主进程的管道,新的文件描述符沿着该管道发送。将 uv_pipe_init 的 ipc 参数设置为 1 非常重要,以指示此管道将用于进程间通信!由于主节点会将文件句柄写入工作节点的标准输入,因此我们使用 uv_pipe_open 将管道连接到 stdin

void on_new_connection(uv_stream_t *q, ssize_t nread, const uv_buf_t *buf) {
    if (nread < 0) {
        if (nread != UV_EOF)
            fprintf(stderr, "Read error %s\n", uv_err_name(nread));
        uv_close((uv_handle_t*) q, NULL);
        return;
    }

    uv_pipe_t *pipe = (uv_pipe_t*) q;
    if (!uv_pipe_pending_count(pipe)) {
        fprintf(stderr, "No pending count\n");
        return;
    }

    uv_handle_type pending = uv_pipe_pending_type(pipe);
    assert(pending == UV_TCP);

    uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));
    uv_tcp_init(loop, client);
    if (uv_accept(q, (uv_stream_t*) client) == 0) {
        uv_os_fd_t fd;
        uv_fileno((const uv_handle_t*) client, &fd);
        fprintf(stderr, "Worker %d: Accepted fd %d\n", getpid(), fd);
        uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read);
    }
    else {
        uv_close((uv_handle_t*) client, NULL);
    }
}

首先,我们调用 uv_pipe_pending_count() 以确保句柄可供读出。如果您的程序可以处理不同类型的句柄,则可以使用 uv_pipe_pending_type() 来确定类型。虽然这段代码中的 accept 看起来很奇怪,但它实际上是有道理的。 accept 传统上所做的是从另一个文件描述符(监听套接字)获取一个文件描述符(客户端)。这正是我们在这里所做的。从 queue 获取文件描述符 ( client )。从这一点开始,工作人员执行标准的回显服务器工作。

现在转向master,让我们看看如何启动worker以实现负载平衡。

struct child_worker {
    uv_process_t req;
    uv_process_options_t options;
    uv_pipe_t pipe;
} *workers;

child_worker 结构包装了进程以及主进程和单个进程之间的管道。

void setup_workers() {
    round_robin_counter = 0;

    // ...

    // launch same number of workers as number of CPUs
    uv_cpu_info_t *info;
    int cpu_count;
    uv_cpu_info(&info, &cpu_count);
    uv_free_cpu_info(info, cpu_count);

    child_worker_count = cpu_count;

    workers = calloc(cpu_count, sizeof(struct child_worker));
    while (cpu_count--) {
        struct child_worker *worker = &workers[cpu_count];
        uv_pipe_init(loop, &worker->pipe, 1);

        uv_stdio_container_t child_stdio[3];
        child_stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE;
        child_stdio[0].data.stream = (uv_stream_t*) &worker->pipe;
        child_stdio[1].flags = UV_IGNORE;
        child_stdio[2].flags = UV_INHERIT_FD;
        child_stdio[2].data.fd = 2;

        worker->options.stdio = child_stdio;
        worker->options.stdio_count = 3;

        worker->options.exit_cb = close_process_handle;
        worker->options.file = args[0];
        worker->options.args = args;

        uv_spawn(loop, &worker->req, &worker->options); 
        fprintf(stderr, "Started worker %d\n", worker->req.pid);
    }
}

在设置worker时,我们使用漂亮的libuv函数 uv_cpu_info 来获取CPU的数量,这样我们就可以启动相同数量的worker。同样重要的是初始化充当 IPC 通道的管道,第三个参数为 1。然后我们指示子进程的 stdin 将是一个可读管道(从子进程的角度来看) )。到这里为止一切都很简单。工作线程启动并等待文件描述符写入其标准输入。

正是在 on_new_connection 中(TCP 基础设施在 main() 中初始化),我们接受客户端套接字并将其传递给循环中的下一个工作程序。

void on_new_connection(uv_stream_t *server, int status) {
    if (status == -1) {
        // error!
        return;
    }

    uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));
    uv_tcp_init(loop, client);
    if (uv_accept(server, (uv_stream_t*) client) == 0) {
        uv_write_t *write_req = (uv_write_t*) malloc(sizeof(uv_write_t));
        dummy_buf = uv_buf_init("a", 1);
        struct child_worker *worker = &workers[round_robin_counter];
        uv_write2(write_req, (uv_stream_t*) &worker->pipe, &dummy_buf, 1, (uv_stream_t*) client, NULL);
        round_robin_counter = (round_robin_counter + 1) % child_worker_count;
    }
    else {
        uv_close((uv_handle_t*) client, NULL);
    }
}

uv_write2 调用处理所有抽象,只需将句柄 ( client ) 作为正确参数传递即可。这样我们的多进程回显服务器就可以运行了。即使在发送句柄时 uv_write2() 也需要非空缓冲区。

作者:岬淢箫声
日期:2023年11月2日
版本:1.0
链接:http://caowei.blog.csdn.net

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

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

相关文章

OpenCV实战——OpenCV.js介绍

OpenCV实战——OpenCV.js介绍 0. 前言1. OpenCV.js 简介2. 网页编写3. 调用 OpenCV.js 库4. 完整代码相关链接 0. 前言 本节介绍如何使用 JavaScript 通过 OpenCV 开发计算机视觉算法。在 OpenCV.js 之前&#xff0c;如果想要在 Web 上执行一些计算机视觉任务&#xff0c;必须…

【实战Flask API项目指南】之七 用JWT进行用户认证与授权

实战Flask API项目指南之 用JWT进行用户认证与授权 本系列文章将带你深入探索实战Flask API项目指南&#xff0c;通过跟随小菜的学习之旅&#xff0c;你将逐步掌握 Flask 在实际项目中的应用。让我们一起踏上这个精彩的学习之旅吧&#xff01; 前言 当小菜踏入Flask后端开发…

【卷积神经网络】YOLO 算法原理

在计算机视觉领域中&#xff0c;目标检测&#xff08;Object Detection&#xff09;是一个具有挑战性且重要的新兴研究方向。目标检测不仅要预测图片中是否包含待检测的目标&#xff0c;还需要在图片中指出它们的位置。2015 年&#xff0c;Joseph Redmon, Santosh Divvala 等人…

防火墙日志记录和分析

防火墙监控进出网络的流量&#xff0c;并保护部署防火墙的网络免受恶意流量的侵害。它是一个网络安全系统&#xff0c;它根据一些预定义的规则监控传入和传出的流量&#xff0c;它以日志的形式记录有关如何管理流量的信息&#xff0c;日志数据包含流量的源和目标 IP 地址、端口…

Everything结合内网穿透搭建在线资料库,一秒实现随时随地访问

Everythingcpolar搭建在线资料库&#xff0c;实现随时随地访问 文章目录 Everythingcpolar搭建在线资料库&#xff0c;实现随时随地访问前言1.软件安装完成后&#xff0c;打开Everything2.登录cpolar官网 设置空白数据隧道3.将空白数据隧道与本地Everything软件结合起来总结 前…

在Ubuntu上安装Redis并学习使用get、set和keys命令

目录 安装Redis切换到root用户搜索redis相关软件包安装redis修改配置文件重启服务器使用redis客户端连接服务器 get与set命令keys 安装Redis 我们在Ubuntu20.04上进行Redis的安装 切换到root用户 使用su命令&#xff1a; 在终端中&#xff0c;输入su并按回车键。然后输入roo…

C++ 输入输出流

iostream库&#xff0c;包含两个基础类型istream(输入流)和ostream(输出流)。一个流就是一个字符序列。 流 输入输出产生的字符串称为流。 被称为流的原因&#xff1a;所有的字符都在缓冲区中&#xff0c;从缓冲区拿/放都是顺序进行的&#xff0c;字符串的消耗&#xff0c;像…

恒驰服务 | 华为云数据使能专家服务offering之大数据建设

恒驰大数据服务主要针对客户在进行智能数据迁移的过程中&#xff0c;存在业务停机、数据丢失、迁移周期紧张、运维成本高等问题&#xff0c;通过为客户提供迁移调研、方案设计、迁移实施、迁移验收等服务内容&#xff0c;支撑客户实现快速稳定上云&#xff0c;有效降低时间成本…

vue封装独立组件:实现分格密码输入框/验证码输入框

目录 第一章 实现效果 第二章 核心实现思路 第三章 封装组件代码实现 第一章 实现效果 为了方便小编的父组件随便找了个页面演示的通过点击按钮&#xff0c;展示子组件密码输入的输入框通过点击子组件输入框获取焦点&#xff0c;然后输入验证码数字即可子组件的确定按钮是验…

MySQL Error 1215: Cannot add foreign key constraint

首先确保中介表中被设置外键的字段不能被设置为主键 第二步确保外键字段的属性与要连接的表的字段属性相同 第三步&#xff0c;设置表的选项 修改引擎为 InnoDB 三个表的引擎都要修改 最后就是运行代码 SET OLD_FOREIGN_KEY_CHECKSFOREIGN_KEY_CHECKS; SET FOREIGN_KEY_…

Android Icon 添加水印 Python脚本

源代码 # -*- coding: utf-8 -*- from PIL import Image 图片合成def mergePictureLXJ():commonIcon Image.open("icon.png")markIcon Image.open("领现金.png")markLayer Image.new(RGBA, commonIcon.size, (0, 0, 0, 0))markLayer.paste(markIcon, (0…

魔术般的速度,焕然一新的磁盘空间 - Magic Disk Cleaner for Mac 2023

在当今这个信息时代&#xff0c;我们的磁盘空间无时无刻不在被各种文件和数据所填满。无论是工作文件&#xff0c;还是日常生活的照片、视频&#xff0c;亦或是下载的各种应用程序&#xff0c;都在不断地蚕食着我们的磁盘空间。面对这种情况&#xff0c;一款高效、便捷的磁盘垃…

大厂面试题-为什么Netty线程池默认大小为CPU核数的2倍

目录 1、分析原因 2、如何衡量性能指标 3、总结与使用建议 1、分析原因 我们都知道使用多线程的本质是为了提升程序的性能&#xff0c;总体来说有两个最核心的指标&#xff0c;一个延迟&#xff0c;一个吞吐量。延迟指的是发出请求到收到响应的时间&#xff0c;吞吐量指的是…

Cannot resolve class ‘DruidDataSource‘

无法配置 DataSource&#xff1a;未指定“url”属性&#xff0c;并且无法配置嵌入数据源。 原因&#xff1a;无法确定合适的驱动程序类 原因是数据库没有配置或者没事错误 1配置配置文件报错 没有依赖 pom.xml加入 <dependency><groupId>com.alibaba</grou…

Oracle-执行计划生成及查看的几种方法

1. EXPLAIN FOR 语法&#xff1a; EXPLAIN PLAN FOR SQL语句SELECT * FROM TABLE(dbms_xplan.display());优点&#xff1a; 无需真正执行SQL 缺点&#xff1a; 没有输出相关的统计信息&#xff0c;例如产生了多少逻辑读、物理读、递归调用等情况无法判断处理了多少行无法判断…

单元测试反射注解

单元测试 就是针对最小的功能单元(方法)&#xff0c;编写测试代码对其进行正确性测试。 咱们之前是如何进行单元测试的&#xff1f;有啥问题 &#xff1f; Junit单元测试框架 可以用来对方法进行测试&#xff0c;它是由Junit公司开源出来的 具体步骤 Junit框架的常见注解…

前端埋点方式

前言&#xff1a; 想要了解用户在系统中所做的操作&#xff0c;从而得出用户在本系统中最常用的模块、在系统中停留的时间。对于了解用户的行为、分析用户的需求有很大的帮助&#xff0c;想实现这种需求可以通过前端埋点的方式。 埋点方式&#xff1a; 1.什么是埋点&#xff1f…

批量发送邮件时怎么使用蜂邮EDM与Outlook?

批量发送邮件时使用蜂邮EDM和Outlook的方法&#xff1f;群发电子邮件的技巧有哪些&#xff1f; 电子邮件仍然是最常用的沟通工具之一&#xff0c;无论是企业还是个人用户&#xff0c;都希望能够高效地一次性将邮件发送给多个收件人。在本文中&#xff0c;将深入探讨蜂邮EDM和O…

无限上下文,多级内存管理!突破ChatGPT等大语言模型上下文限制

目前&#xff0c;ChatGPT、Llama 2、文心一言等主流大语言模型&#xff0c;因技术架构的问题上下文输入一直受到限制&#xff0c;即便是Claude 最多只支持10万token输入&#xff0c;这对于解读上百页报告、书籍、论文来说非常不方便。 为了解决这一难题&#xff0c;加州伯克利…

循环神经网络(RNN)与长短期记忆网络(LSTM)

前言&#xff1a; 通过前面的学习&#xff0c;我们以BP神经网络为基础&#xff0c;认识到了损失函数&#xff0c;激活函数&#xff0c;以及梯度下降法的原理&#xff1b;而后学习了卷积神经网络&#xff0c;知道图像识别是如何实现的。今天这篇文章&#xff0c;讲述的就是计算机…