设计师找灵感的网站,百度指数免费添加,手机网站开发服务,asp.net做报名网站Processes
libuv提供了相当多的子进程管理函数#xff0c;并且是跨平台的#xff0c;还允许使用stream#xff0c;或者说pipe完成进程间通信。
在UNIX中有一个共识#xff0c;就是进程只做一件事#xff0c;并把它做好。因此#xff0c;进程通常通过创建子进程来完成不…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和argsfile是要执行的程序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为结尾的字符串数组其中每一个字符串的形式都是VARVALUE。这些值用来设置进程的环境变量。如果子进程想要继承父进程的环境变量就将uv_process_options_t.env设为null。
Option flags
通过使用下面标识的按位或的值设置uv_process_options_t.flags的值可以定义子进程的行为 UV_PROCESS_SETUID-将子进程的执行用户idUID设置为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上实现了一个类似用法的调用但要注意所有的SIGTERMSIGINT和SIGKILL都会导致进程的中断。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初始化handleuv_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.huv_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
一个正常的新产生的进程都有自己的一套文件描述符映射表例如012分别对应stdinstdout和stderr。有时候父进程想要将自己的文件描述符映射表分享给子进程。例如你的程序启动了一个子命令并且把所有的错误信息输出到log文件中但是不能使用stdout。因此你想要使得你的子进程和父进程一样拥有stderr。在这种情形下libuv提供了继承文件描述符的功能。在下面的例子中我们会调用这么一个测试程序
proc-streams/test.c
#include stdio.hint 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.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.hint 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.flags为UV_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。