libuv库学习笔记-processes

Processes

libuv提供了相当多的子进程管理函数,并且是跨平台的,还允许使用stream,或者说pipe完成进程间通信。

在UNIX中有一个共识,就是进程只做一件事,并把它做好。因此,进程通常通过创建子进程来完成不同的任务(例如,在shell中使用pipe)。 一个多进程的,通过消息通信的模型,总比多线程的,共享内存的模型要容易理解得多。

当前一个比较常见的反对事件驱动编程的原因在于,其不能很好地利用现代多核计算机的优势。一个多线程的程序,内核可以将线程调度到不同的cpu核心中执行,以提高性能。但是一个event-loop的程序只有一个线程。实际上,工作区可以被分配到多进程上,每一个进程执行一个event-loop,然后每一个进程被分配到不同的cpu核心中执行。

Spawning child processes

一个最简单的用途是,你想要开始一个进程,然后知道它什么时候终止。需要使用uv_spawn完成任务:

spawn/main.c

uv_loop_t *loop;
uv_process_t child_req;
uv_process_options_t options;
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);
}
Note

由于上述的options是全局变量,因此被初始化为0。如果你在局部变量中定义options,请记得将所有没用的域设为0

uv_process_options_t options = {0};

uv_process_t只是作为句柄,所有的选择项都通过uv_process_options_t设置,为了简单地开始一个进程,你只需要设置file和args,file是要执行的程序,args是所需的参数(和c语言中main函数的传入参数类似)。因为uv_spawn在内部使用了execvp,所以不需要提供绝对地址。遵从惯例,实际传入参数的数目要比需要的参数多一个,因为最后一个参数会被设为NULL

在函数uv_spawn被调用之后,uv_process_t.pid会包含子进程的id。

回调函数on_exit()会在被调用的时候,传入exit状态和导致exit的信号。

spawn/main.c

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

在进程关闭后,需要回收handler。

Changing process parameters

在子进程开始执行前,你可以通过使用uv_process_options_t设置运行环境。

Change execution directory

设置uv_process_options_t.cwd,更改相应的目录。

Set environment variables

uv_process_options_t.env的格式是以null为结尾的字符串数组,其中每一个字符串的形式都是VAR=VALUE。这些值用来设置进程的环境变量。如果子进程想要继承父进程的环境变量,就将uv_process_options_t.env设为null。

Option flags

通过使用下面标识的按位或的值设置uv_process_options_t.flags的值,可以定义子进程的行为:

  • UV_PROCESS_SETUID-将子进程的执行用户id(UID)设置为uv_process_options_t.uid中的值。
  • UV_PROCESS_SETGID-将子进程的执行组id(GID)设置为uv_process_options_t.gid中的值。
    只有在unix系的操作系统中支持设置用户id和组id,在windows下设置会失败,uv_spawn会返回UV_ENOTSUP
  • UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS-在windows上,uv_process_options_t.args参数不要用引号包裹。此标记对unix无效。
  • UV_PROCESS_DETACHED-在新会话(session)中启动子进程,这样子进程就可以在父进程退出后继续进行。请看下面的例子:

Detaching processes

使用标识UV_PROCESS_DETACHED可以启动守护进程(daemon),或者是使得子进程从父进程中独立出来,这样父进程的退出就不会影响到它。

detach/main.c

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

记住一点,就是handle会始终监视着子进程,所以你的程序不会退出。uv_unref()会解除handle。

Sending signals to processes

libuv打包了unix标准的kill(2)系统调用,并且在windows上实现了一个类似用法的调用,但要注意:所有的SIGTERMSIGINTSIGKILL都会导致进程的中断。uv_kill函数如下所示:

uv_err_t uv_kill(int pid, int signum);

对于用libuv启动的进程,应该使用uv_process_kill终止,它会以uv_process_t作为第一个参数,而不是pid。当使用uv_process_kill后,记得使用uv_close关闭uv_process_t

Signals

libuv对unix信号和一些windows下类似的机制,做了很好的打包。

使用uv_signal_init初始化handle(uv_signal_t ),然后将它与loop关联。为了使用handle监听特定的信号,使用uv_signal_start()函数。每一个handle只能与一个信号关联,后续的uv_signal_start会覆盖前面的关联。使用uv_signal_stop终止监听。下面的这个小例子展示了各种用法:

signal/main.c

#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;
}
Note

uv_run(loop, UV_RUN_NOWAIT)uv_run(loop, UV_RUN_ONCE)非常像,因为它们都只处理一个事件。但是不同在于,UV_RUN_ONCE会在没有任务的时候阻塞,但是UV_RUN_NOWAIT会立刻返回。我们使用NOWAIT,这样才使得一个loop不会因为另外一个loop没有要处理的事件而挨饿。

当向进程发送SIGUSR1,你会发现signal_handler函数被激发了4次,每次都对应一个uv_signal_t。然后signal_handler调用uv_signal_stop终止了每一个uv_signal_t,最终程序退出。对每个handler函数来说,任务的分配很重要。一个使用了多个event-loop的服务器程序,只要简单地给每一个进程添加信号SIGINT监视器,就可以保证程序在中断退出前,数据能够安全地保存。

Child Process I/O

一个正常的新产生的进程都有自己的一套文件描述符映射表,例如0,1,2分别对应stdinstdoutstderr。有时候父进程想要将自己的文件描述符映射表分享给子进程。例如,你的程序启动了一个子命令,并且把所有的错误信息输出到log文件中,但是不能使用stdout。因此,你想要使得你的子进程和父进程一样,拥有stderr。在这种情形下,libuv提供了继承文件描述符的功能。在下面的例子中,我们会调用这么一个测试程序:

proc-streams/test.c

#include <stdio.h>int main()
{fprintf(stderr, "This is stderr\n");printf("This is stdout\n");return 0;
}

实际的执行程序 proc-streams在运行的时候,只向子进程分享stderr。使用uv_process_options_tstdio域设置子进程的文件描述符。首先设置stdio_count,定义文件描述符的个数。uv_process_options_t.stdio是一个uv_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;

上边的flag值可取多种。比如,如果你不打算使用,可以设置为UV_IGNORE。如果与stdio中对应的前三个文件描述符被标记为UV_IGNORE,那么它们会被重定向到/dev/null

因为我们想要传递一个已经存在的文件描述符,所以使用UV_INHERIT_FD。因此,fd被设为stderr。

proc-streams/main.c

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-streams,也就是在main中产生一个执行test的子进程,你只会看到“This is stderr”。你可以试着设置stdout也继承父进程。

同样可以把上述方法用于流的重定向。比如,把flag设为UV_INHERIT_STREAM,然后再设置父进程中的data.stream,这时子进程只会把这个stream当成是标准的I/O。这可以用来实现,例如CGI。

一个简单的CGI脚本的例子如下:

cgi/tick.c

#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服务器用到了这章和网络那章的知识,所以每一个client都会被发送10个tick,然后被中断连接。

cgi/main.c

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

上述代码中,我们接受了连接,并把socket(流)传递给invoke_cgi_script

cgi/main.c

  args[1] = NULL;/* ... finding the executable path and setting up arguments ... */options.stdio_count = 3;uv_stdio_container_t child_stdio[3];child_stdio[0].flags = UV_IGNORE;child_stdio[1].flags = UV_INHERIT_STREAM;child_stdio[1].data.stream = (uv_stream_t*) client;child_stdio[2].flags = UV_IGNORE;options.stdio = child_stdio;options.exit_cb = cleanup_handles;options.file = args[0];options.args = args;// Set this so we can close the socket after the child process exits.child_req.data = (void*) client;int r;if ((r = uv_spawn(loop, &child_req, &options))) {fprintf(stderr, "%s\n", uv_strerror(r));

cgi的stdout被绑定到socket上,所以无论tick脚本程序打印什么,都会发送到client端。通过使用进程,我们能够很好地处理读写并发操作,而且用起来也很方便。但是要记得这么做,是很浪费资源的。

Pipes

libuv的uv_pipe_t结构可能会让一些unix程序员产生困惑,因为它像魔术般变幻出|pipe(7)。但这里的uv_pipe_t并不是IPC机制里的 匿名管道(在IPC里,pipe是 匿名管道,只允许父子进程之间通信。FIFO则允许没有亲戚关系的进程间通信,显然llibuv里的uv_pipe_t不是第一种)。uv_pipe_t背后有unix本地socket或者windows 具名管道的支持,可以实现多进程间的通信。下面会具体讨论。

Parent-child IPC

父进程与子进程可以通过单工或者双工管道通信,获得管道可以通过设置uv_stdio_container_t.flagsUV_CREATE_PIPEUV_READABLE_PIPE或者UV_WRITABLE_PIPE的按位或的值。上述的读/写标记是对于子进程而言的。

Arbitrary process IPC

既然本地socket具有确定的名称,而且是以文件系统上的位置来标示的(例如,unix中socket是文件的一种存在形式),那么它就可以用来在不相关的进程间完成通信任务。被开源桌面环境使用的D-BUS系统也是使用了本地socket来作为事件通知的,例如,当消息来到,或者检测到硬件的时候,各种应用程序会被通知到。mysql服务器也运行着一个本地socket,等待客户端的访问。

当使用本地socket的时候,客户端/服务器模型通常和之前类似。在完成初始化后,发送和接受消息的方法和之前的tcp类似,接下来我们同样适用echo服务器的例子来说明。

pipe-echo-server/main.c

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, "echo.sock"))) {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);
}

我们把socket命名为echo.sock,意味着它将会在本地文件夹中被创造。对于stream API来说,本地socekt表现得和tcp的socket差不多。你可以使用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。

Sending file descriptors over pipes

最酷的事情是本地socket可以传递文件描述符,也就是说进程间可以交换文件描述符。这样就允许进程将它们的I/O传递给其他进程。它的应用场景包括,负载均衡服务器,分派工作进程等,各种可以使得cpu使用最优化的应用。libuv当前只支持通过管道传输TCP sockets或者其他的pipes

为了展示这个功能,我们将来实现一个由循环中的工人进程处理client端请求,的这么一个echo服务器程序。这个程序有一些复杂,在教程中只截取了部分的片段,为了更好地理解,我推荐你去读下完整的代码。

工人进程很简单,文件描述符将从主进程传递给它。

multi-echo-server/worker.c

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很关键,因为它标明了这个管道将被用来做进程间通信。因为主进程需要把文件handle赋给了工人进程作为标准输入,因此我们使用uv_pipe_open把stdin作为pipe(别忘了,0代表stdin)。

multi-echo-server/worker.c

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来确定从handle中可以读取出数据。如果你的程序能够处理不同类型的handle,这时uv_pipe_pending_type就可以用来决定当前的类型。虽然在这里使用accept看起来很怪,但实际上是讲得通的。accept最常见的用途是从其他的文件描述符(监听的socket)获取文件描述符(client端)。这从原理上说,和我们现在要做的是一样的:从queue中获取文件描述符(client)。接下来,worker可以执行标准的echo服务器的工作了。

我们再来看看主进程,观察如何启动worker来达到负载均衡。

multi-echo-server/main.c

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

child_worker结构包裹着进程,和连接主进程和各个独立进程的管道。

multi-echo-server/main.c

void setup_workers() {round_robin_counter = 0;// ...// launch same number of workers as number of CPUsuv_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(sizeof(struct child_worker), cpu_count);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);}
}

首先,我们使用酷炫的uv_cpu_info函数获取到当前的cpu的核心个数,所以我们也能启动一样数目的worker进程。再次强调一下,务必将uv_pipe_init的ipc参数设置为1。接下来,我们指定子进程的stdin是一个可读的管道(从子进程的角度来说)。接下来的一切就很直观了,worker进程被启动,等待着文件描述符被写入到他们的标准输入中。

在主进程的on_new_connection中,我们接收了client端的socket,然后把它传递给worker环中的下一个可用的worker进程。

multi-echo-server/main.c

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作为一个参数即可完成传输。现在,我们的多进程echo服务器已经可以运转起来啦。

感谢Kyle指出了uv_write2需要一个不为空的buffer。

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

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

相关文章

微信小程序 background-image直接设置本地图片路径,编辑器正常显示,真机运行不显示解决方法

项目场景 微信小程序&#xff0c;设置background-image直接设置本地图片路径。 问题描述 编辑器正常显示&#xff0c;真机运行不显示 原因分析 background-image只能用网络url或者base64图片编码。 解决方案 1、将本地图片转为网络url后设置到background-image上 例如&…

nestjs+typeorm+mysql基本使用学习

初始化项目 安装依赖 npm i -g nest/cli 新建项目 nest new project-name 命令行创建 创建Controller&#xff1a;nest g co test 创建Module&#xff1a;nest g mo test 创建Service&#xff1a;nest g service test 请求创建 123123 接口文档swagger 安装依赖 npm…

【Golang 接口自动化03】 解析接口返回XML

目录 解析接口返回数据 定义结构体 解析函数&#xff1a; 测试 优化 资料获取方法 上一篇我们学习了怎么发送各种数据类型的http请求&#xff0c;这一篇我们来介绍怎么来解析接口返回的XML的数据。 解析接口返回数据 定义结构体 假设我们现在有一个接口返回的数据resp如…

❤ yarn 和npm 的使用

❤ yarn 和npm 的使用 yarn 版本1的使用 yarn 简介 Yarn是facebook发布的一款取代npm的包管理工具。 yarn特点&#xff1a; 1&#xff0c;速度超快。 Yarn 缓存了每个下载过的包&#xff0c;所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率&#xff0c;因…

C# 反射

反射的概念&#xff1a;C#通过类型&#xff08;Type&#xff09;来创建对象&#xff0c;调用对象中的方法&#xff0c;属性等信息&#xff1b;B超就是利用了反射原理将超声波打在人的肚子上&#xff0c;然后通过反射波进行体内器官的成员&#xff1b; 反射提供的类&#xff1a;…

【代理模式】了解篇:静态代理 动态代理~

目录 1、什么是代理模式&#xff1f; 2、静态代理 3、动态代理 3.1 JDK动态代理类 3.2 CGLIB动态代理类 4、JDK动态代理和CGLIB动态代理的区别&#xff1f; 1、什么是代理模式&#xff1f; 定义&#xff1a; 代理模式就是为其他对象提供一种代理以控制这个对象的访问。在某…

强引用和弱引用

什么是弱引用和强引用 强引用&#xff1a; JavaScript 中强引用&#xff1a;对象的引用在 JavaScript 中是强引用&#xff0c;也就是将一个引用对象通过变量或常量保存时&#xff0c;那么这个变量或常量就是强引用&#xff0c;这个对象就不会被回收。 弱引用&#xff1a; JavaS…

P1123 取数游戏(dfs,嘎嘎清晰)

1分析&#xff1a;这一题每个数是否选择会影响后面的选择情况&#xff0c;所以需要用一个数组来保存 所以状态为当前选到那个数&#xff0c;之前选的数的和以及之前每个数是否选了 之后直接搜索即可。尽管复杂度较高&#xff0c;但因为存在大量的不合法情况所以可以通过 时间复…

华为nat64配置

1.前期环境准备 环境拓扑 拓扑分为两个区域,左边为trust区域,使用IPv4地址互访,右边为untrust区域,使用IPv6地址互访 2.接口地址配置 pc1地址配置 pc2地址配置 FW接口配置 (1)首先进入防火墙配置界面 注:防火墙初始账号密码为user:admin,pwd:Admin@123,进入之后…

web攻击面试|网络渗透面试(三)

Web攻击大纲 常见Web攻击类型&#xff1a; SQL注入攻击&#xff1a;介绍SQL注入攻击的概念、原理和常见的攻击方式&#xff0c;如基于错误消息的注入、基于布尔盲注的注入等。解释攻击者如何利用SQL注入漏洞获取敏感信息或者对数据库进行恶意操作&#xff0c;并提供防御措施&a…

8.docker仓库

文章目录 Docker仓库本地私有仓库Docker HarborDocker harbor部署访问页面创建用户下载私有仓库镜像harbor同步 Docker仓库 本地私有仓库 ##先下载 registry 镜像docker pull registry##修改配置文件&#xff0c;在 daemon.json 文件中添加私有镜像仓库地址vim /etc/dock…

SQL-每日一题【1070. 产品销售分析 III】

题目 销售表 Sales&#xff1a; 产品表 Product&#xff1a; 编写一个 SQL 查询&#xff0c;选出每个销售产品 第一年 销售的 产品 id、年份、数量 和 价格。 结果表中的条目可以按 任意顺序 排列。 查询结果格式如下例所示&#xff1a; 示例 1&#xff1a; 解题思路 前置知…

【设计模式】 策略模式

策略模式&#xff08;Strategy Pattern&#xff09;是一种行为型设计模式&#xff0c;它定义了一系列算法&#xff0c;将每个算法封装起来&#xff0c;使它们可以相互替换&#xff0c;让客户端代码和算法的具体实现解耦。这样&#xff0c;客户端可以根据不同的需求选择不同的算…

Python爬虫的urlib的学习(学习于b站尚硅谷)

目录 一、页面结构的介绍  1.学习目标  2.为什么要了解页面&#xff08;html&#xff09;  3. html中的标签&#xff08;仅介绍了含表格、无序列表、有序列表、超链接&#xff09;  4.本节的演示 二、Urllib  1.什么是互联网爬虫&#xff1f;  2.爬虫核心  3.爬虫…

Visual Studio 2015编译器 自动生成 XXX_EXPORTS宏

XXX_EXPORTS宏 XXX_EXPORTS宏是由Visual Studio 2015编译器自动生成的。这个宏用于标识当前项目是一个导出符号的动态链接库&#xff08;DLL&#xff09;项目。在使用Visual Studio 2015创建Win32项目时&#xff0c;编译器会自动添加这个宏到项目的预定义宏中。 这个宏的作用…

【MySQL】复合查询

复合查询目录 一、基本查询二、多表查询三、自连接四、子查询4.1 单行子查询4.2 多行子查询4.3 多列子查询4.4 在from子句中使用子查询4.5 合并查询4.5.1 union4.5.2 union all 五、实战OJ 一、基本查询 --查询工资高于500或岗位为MANAGER的雇员&#xff0c;同时还要满足他们的…

web APIs-练习二

轮播图点击切换&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name"viewport" content"…

LLaMA模型论文《LLaMA: Open and Efficient Foundation Language Models》阅读笔记

文章目录 1. 简介2.方法2.1 预训练数据2.2 网络架构2.3 优化器2.4 高效的实现 3.论文其余部分4. 参考资料 1. 简介 LLaMA是meta在2023年2月开源的大模型&#xff0c;在这之后&#xff0c;很多开源模型都是基于LLaMA的&#xff0c;比如斯坦福大学的羊驼模型。 LLaMA的重点是比…

从Vue层面 - 解析发布订阅模式和观察者模式区别

目录 前言一、发布订阅模式什么是发布订阅模式&#xff1f;应用场景 二、观察者模式1&#xff09;什么是观察者模式&#xff1f;2&#xff09;应用场景3&#xff09;vue中的观察者模式观察者&#xff08;订阅者&#xff09; - Watcher目标者&#xff08;发布者&#xff09; - D…

剑指 Offer 46.! 把数字翻译成字符串(动态规划,青蛙跳台问题的变形)

剑指 Offer 46. 把数字翻译成字符串 中等 588 相关企业 给定一个数字&#xff0c;我们按照如下规则把它翻译为字符串&#xff1a;0 翻译成 “a” &#xff0c;1 翻译成 “b”&#xff0c;……&#xff0c;11 翻译成 “l”&#xff0c;……&#xff0c;25 翻译成 “z”。一个数字…