libuv进程
1、创建进程
调用uv_spawn()来启动一个进程:
#include <stdio.h> #include <uv.h> uv_loop_t* loop; uv_process_t child_req; uv_process_options_t options; //全局变量会自动初始化int类型为0,改为局部变量的话需要将所有没用的域设为0 char worker_path[500] = {0}; void on_exit(uv_process_t* req, int64_t exit_status/*main()返回值*/, int term_signal/*导致进程exit的信号*/) { fprintf(stderr, "Process exited with status %d, signal %d\n", exit_status, term_signal); uv_close((uv_handle_t*)req, NULL); //进程关闭后回收资源 } int main() { loop = uv_default_loop(); //通过uv_process_options_t设置启动子进程的选项 options.exit_cb = on_exit; //进程结束后的回调 char* args[2]; strcpy_s(worker_path, sizeof(worker_path), "D:\\Study\\C++\\libuv\\test\\x64\\Debug\\worker"); args[0] = worker_path; args[1] = NULL; //遵从惯例:实际传入参数的数目要比需要的参数多一个,因为最后一个参数会被设为NULL options.file = args[0]; //要执行的程序 options.args = args; //参数 options.env = nullptr; //设置环境变量,格式是以null为结尾的字符串数组,其中每一个字符串的形式都是VAR=VALUE,nullptr表示继承父进程的环境变量 //options.cwd; //更改目录 //options.flags; //定义子进程的行为,多个行为的话使用或运算。UV_PROCESS_DETACHED表示启动守护进程,即父进程的退出不会影响子进程(在Windows上使用此标志的话发现启动的子进程不能继承父进程的文件描述符) //下面对uv_process_options_t的设置将父进程的标准输出和标准出错分享给了子进程,这样子进程中的标准输出和标准出错就继承了父进程的 options.stdio_count = 3; //文件描述符的个数 uv_stdio_container_t child_stdio[3]; child_stdio[0].flags = UV_IGNORE; //不打算使用 child_stdio[1].flags = UV_INHERIT_FD; //使用一个已经存在的文件描述符 child_stdio[1].data.fd = 1; //标准输出 child_stdio[2].flags = UV_INHERIT_FD; child_stdio[2].data.fd = 2; //标准错误 options.stdio = child_stdio; //设置子进程的文件描述符 int r; if ((r = uv_spawn(loop, &child_req, &options))) { //启动进程 fprintf(stderr, "%s\n", uv_strerror(r)); return 1; } fprintf(stderr, "Launched process with ID %d\n", child_req.pid); return uv_run(loop, UV_RUN_DEFAULT); }
2、继承文件描述符
一个新产生的进程都有自己的一套文件描述符映射表,例如0,1,2分别对应stdin,stdout和stderr。父进程想要将自己的文件描述符映射表分享给子进程的话,可以像上面那样使用,使用uv_process_options_t::stdio域设置子进程的文件描述符,这样子进程就继承了父进程的stdout和stderr,子进程对标准输出和标准出错写就是对父进程的标准输出和标准出错进行写入。
也可以通过uv_process_options_t::stdio来设置流的重定向,如下所示我们在TCP服务收到连接并accept后,调用invoke_cgi_script()来开启一个CGI程序,uv_process_options_t::stdio中的相关设置就可以将连接socket流重定向到CGI的stdio中,即在CGI程序中向标准输出写的话就相当于是向socket写(CGI 程序接收信息有三种途径:环境变量、命令行和标准输入):
//连接回调 void on_new_connection(uv_stream_t* server, int status) { ...... if (uv_accept(server, (uv_stream_t*)client) == 0) { invoke_cgi_script(client); } ...... } //打开CGI程序 void invoke_cgi_script(uv_tcp_t* client) { ...... 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 = uv_spawn(loop, &child_req, &options); ...... } //CGI程序: include <stdio.h> #include <unistd.h> int main() { for (int i = 0; i < 10; i++) { printf("tick\n"); fflush(stdout); sleep(1); } printf("BOOM!\n"); return 0; }
2、Signal
可以调用uv_signal_init()和uv_signal_start()来将指定的信号与loop管理,进程收到该消息的话就会触发制定的回调,如下所示,我们向进程发送SIGUSR1的话,进程内所有线程都会收到该消息,signal_handler()方法会被调用两次:
#include <stdio.h> #include <uv.h> uv_loop_t* create_loop() { uv_loop_t* loop = (uv_loop_t*)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); //停止监听 } void thread1_worker(void* userp) { uv_loop_t* loop = create_loop(); uv_signal_t sig; uv_signal_init(loop, &sig); //初始化signal handle(uv_signal_t),将其关联到一个loop uv_signal_start(&sig, signal_handler, SIGUSR1); //监听指定的信号 uv_run(loop, UV_RUN_DEFAULT); } void thread2_worker(void* userp) { uv_loop_t* loop = create_loop(); uv_signal_t sig; uv_signal_init(loop, &sig); uv_signal_start(&sig, signal_handler, SIGUSR1); uv_process_kill() uv_run(loop, UV_RUN_DEFAULT); } int main() { 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); }
调用uv_kill(int pid, int signum)或者uv_process_kill(uv_process_t*, int signum)可以向进程发送消息,对于libuv启动的进程,可以使用uv_process_kill()来向其发送消息,因为其第一个参数为uv_process_t类型。SIGTERM,SIGINT和SIGKILL都会导致进程的中断,一个使用了多个event-loop的服务器程序,只要给每一个进程添加信号SIGINT监视器,就可以保证程序在中断退出前对数据进行安全的处理。
3、IPC与multi-echo-server
父子进程之间可以通过管道来进行通信,uv_pipe_t背后有unix本地socket或者windows实名管道的支持。进程间可以交换文件描述符,目前libuv只支持通过管道传输TCP sockets或者其他的pipes。如下所示,我们实现了一个多进程的服务端以利用多核计算机的优势,每个进程执行一个event-loop,主进程将连接socket分配到工作进程中去处理IO,父进程通过管道进行通信来将连接socket发送给子进程,工作进程的数量与CPU核心数相关:
/***************主进程***************/ #include <stdio.h> #include <uv.h> uv_loop_t* loop; uv_buf_t dummy_buf; char worker_path[500]; struct child_worker { uv_process_t req; uv_process_options_t options; uv_pipe_t pipe; } *workers; int round_robin_counter; //全局变量默认初始化为0 int child_worker_count; void close_process_handle(uv_process_t* req, int64_t exit_status, int term_signal) { fprintf(stderr, "Process exited with status %d, signal %d\n", exit_status, term_signal); uv_close((uv_handle_t*)req, NULL); } void on_new_connection(uv_stream_t* server, int status) { if (status < 0) { fprintf(stderr, "New connection error: %s\n", uv_strerror(status)); 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); //uv_write2()需要一个不为空的buffer 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); //调用uv_write2()来向管道发送连接socket round_robin_counter = (round_robin_counter + 1) % child_worker_count; //轮流发送 } uv_close((uv_handle_t*)client, NULL); } void setup_workers() { size_t path_size = 500; uv_exepath(worker_path, &path_size); //获得当前目录,格式范例:"D:\Study\C++\libuv\test\x64\Debug\multi-server.exe" strcpy_s(worker_path + (strlen(worker_path) - strlen("multi-server.exe")), strlen("multi-server.exe"), "worker.exe"); //"D:\Study\C++\libuv\test\x64\Debug\worker.exe" char* args[2]; args[0] = worker_path; args[1] = NULL; uv_cpu_info_t* info; int cpu_count; uv_cpu_info(&info, &cpu_count); //获得CPU数量 uv_free_cpu_info(info, cpu_count); child_worker_count = cpu_count; //工作进程数与CPU数量相同 workers = (child_worker*)calloc(sizeof(struct child_worker), cpu_count); //工作进程相关信息 while (cpu_count--) { struct child_worker* worker = &workers[cpu_count]; uv_pipe_init(loop, &worker->pipe, 1/*该参数设为1表示管道将被用来IPC*/); //每个工作进程对应一个管道 //工作进程继承父进程的管道来进行IPC uv_stdio_container_t child_stdio[3]; child_stdio[0].flags = (uv_stdio_flags)(UV_CREATE_PIPE | UV_READABLE_PIPE); //子进程的标准输入是一个可读的pipe child_stdio[0].data.stream = (uv_stream_t*)&worker->pipe; //指定子进程的stdin是上面创建的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; int r; if ((r = uv_spawn(loop, &worker->req, &worker->options))) { //启动工作进程 fprintf(stderr, "%s\n", uv_strerror(r)); return; } fprintf(stderr, "Started worker %d\n", worker->req.pid); } } int main() { loop = uv_default_loop(); setup_workers();//创建工作进程 uv_tcp_t server; uv_tcp_init(loop, &server); sockaddr_in addr; uv_ip4_addr("0.0.0.0", 6000, &addr); uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0); int r = uv_listen((uv_stream_t*)&server, SOMAXCONN, on_new_connection); if (r) { fprintf(stderr, "Listen error: %s\n", uv_strerror(r)); return 1; } return uv_run(loop, UV_RUN_DEFAULT); }
父进程将PIPE共享给工作进程并作为其标准输入stdin,所以工作进程从标准输入:
/***************工作进程***************/ #include <stdio.h> #include <uv.h> uv_loop_t* loop; uv_pipe_t queue; typedef struct { uv_write_t req; uv_buf_t buf; } write_req_t; void free_write_req(uv_write_t* req) { write_req_t* wr = (write_req_t*)req; free(wr->buf.base); free(wr); } void on_write(uv_write_t* req, int status) { if (status) { fprintf(stderr, "Write error %s\n", uv_err_name(status)); } free_write_req(req); } void on_read(uv_stream_t* client, ssize_t nread, const uv_buf_t* buf) { if (nread > 0) { write_req_t* req = (write_req_t*)malloc(sizeof(write_req_t)); req->buf = uv_buf_init(buf->base, nread); uv_write((uv_write_t*)req, client, &req->buf, 1, on_write); //向socket原样发回读取到的数据 return; } if (nread < 0) { if (nread != UV_EOF) fprintf(stderr, "Read error %s\n", uv_err_name(nread)); uv_close((uv_handle_t*)client, NULL); } free(buf->base); } void alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { buf->base = (char*)malloc(suggested_size); buf->len = suggested_size; } 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); //只支持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) { //这里accept()的功能是从pipe中获取socket 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, on_read); //从socket中读取数据 } else { uv_close((uv_handle_t*)client, NULL); } } int main() { loop = uv_default_loop(); uv_pipe_init(loop, &queue, 1 /*该值为1表示pipe将被用来做IPC*/); //初始化pipe uv_pipe_open(&queue, 0/*关联stdin到pipe*/); uv_read_start((uv_stream_t*)&queue, alloc_buffer, on_new_connection); //从pipe读取数据 return uv_run(loop, UV_RUN_DEFAULT); }