nginx继承socket 和 热代码替换

本文详细解析了nginx的热代码替换过程,涉及nginx如何通过ngx_add_inherited_sockets函数从环境变量中继承socket,以及在接收到USR2信号后,如何进行热启动,包括socket的保存、子进程的创建和execve的使用,确保socket文件描述符在进程间继承。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    在看nginx源码的时候,遇到这样一个函数:ngx_add_inherited_sockets, 这个函数在main函数里被调用。
  里面具体做的事情就是:从一个环境变量读出socket文件描述符,然后这个socket文件描述符就可以用了,不用调用socket()函数创建。具体可以看这位大神的博客:http://blog.csdn.net/livelylittlefish/article/details/7277607
 
  但我读完这篇博客还是有一个困惑的问题,可能是我智商捉急不能理解大神的文章:为什么socket从环境变量里读出来,就能用了?
 
    进过反复阅读代码终于懂了,这就是nginx热启动。
    
    来看一下这个热启动的执行过程。
    
    1:给nginx发一个USR2信号:在ngx_process.c文件里有一个signals信号数组,这个数组定义了nginx所支持的信号,
    
    其中一个,
    { ngx_signal_value(NGX_CHANGEBIN_SIGNAL),
      "SIG" ngx_value(NGX_CHANGEBIN_SIGNAL),
      "",
      ngx_signal_handler },
    
    ngx_config.h里面有
    #define NGX_CHANGEBIN_SIGNAL     USR2
    
    2:处理函数ngx_signal_hanler做了啥:
      ngx_process.c:
      
      case ngx_signal_value(NGX_CHANGEBIN_SIGNAL):
            if (getppid() > 1 || ngx_new_binary > 0) {

                /*
                 * Ignore the signal in the new binary if its parent is
                 * not the init process, i.e. the old binary's process
                 * is still running.  Or ignore the signal in the old binary's
                 * process if the new binary's process is already running.
                 */
                /* 这里给出了详细的注释,更通俗一点来讲,就是说,进程现在是一个
                * master(新的master进程),但是当他的父进程old master还在运行的话,
                * 这时收到了USR2信号,我们就忽略它,不然就成了新master里又要生成
                * master。。。另外一种情况就是,old master已经开始了生成新master的过程
                * 中,这时如果又有USR2信号到来,那么也要忽略掉。。。(不知道够不够通俗=.=)
                参考文档:http://blog.csdn.net/dingyujie/article/details/7192144
                */
                action = ", ignoring";
                ignore = 1;
                break;
            }

            //正常情况下,需要热代码替换,设置标志位
            ngx_change_binary = 1;
            action = ", changing binary";
            break;
            
      
       里面设置了ngx_change_binary = 1,这一句是设置热启动标志
       
    3:ngx_process_cycle.c里面有个 ngx_master_process_cycle 这里面会循环检测各种标志,其中就包括热启动
    
    //热代码替换
    if (ngx_change_binary) { // 信号处理函数里将这个设置成了1
      ngx_change_binary = 0;
      ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "changing binary");
      //进行热代码替换,这里是调用execve来执行新的代码
      ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);
    }
    
    4:ngx_exec_new_binary干了啥?
      
        1:现将socket写到环境变量里面
        2:pid = ngx_execute(cycle, &ctx)
        
    5:ngx_execute干了啥?
    
      ngx_pid_t
      ngx_execute(ngx_cycle_t *cycle, ngx_exec_ctx_t *ctx)
      {
        return ngx_spawn_process(cycle, ngx_execute_proc, ctx, ctx->name,
                             NGX_PROCESS_DETACHED);
      }
    6: ngx_spawn_process干了啥?
      ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
    char *name, ngx_int_t respawn);
      这个函数创建子进程,并让子进程执行proc函数,第五步传进来的是ngx_execute_proc
      
      ngx_spawn_process函数里有://创建子进程
        pid = fork();

        switch (pid) {

        case -1:
          ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "fork() failed while spawning \"%s\"", name);
          ngx_close_channel(ngx_processes[s].channel, cycle->log);
          return NGX_INVALID_PID;

        case 0:
          ngx_pid = ngx_getpid();
          //在子进程中执行传递进来的函数,即工作进程的具体工作
          proc(cycle, data);
          break;

        default:
          break;
      }
     7:ngx_execute_proc做了啥:
       static void
       ngx_execute_proc(ngx_cycle_t *cycle, void *data)
       {
         ngx_exec_ctx_t  *ctx = data;

         if (execve(ctx->path, ctx->argv, ctx->envp) == -1) {
          ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "execve() failed while executing %s \"%s\"",
                      ctx->name, ctx->path);
       }

       exit(1);
       }
       
       这个函数里调用的execve
       
       而文件描述符只要不设置close_on_exec就可以被继承下来。
       
       说到这里,我感觉原来的我已经懂了。
/* Copyright (c) 2025-2026 TP-LINK Technologies CO.LTD * All rights reserved * \file primeNumber.c * \brief The source file for primeNumber * \author zuoshicheng <zuoshicheng@tp-link.com> * \version 1.0.0 * \date 2025/07/30 * \history \arg 1.0.0 2025/07/30 zuoshicheng, Create file */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/stat.h> #include <fcntl.h> #define PORT 80 #define WEB_ROOT "./web" // 网站根目录 #define BUFFER_SIZE 8192 // 根据文件扩展名返回MIME类型 const char* get_mime_type(const char *ext) { if (strcmp(ext, "html") == 0) return "text/html"; if (strcmp(ext, "css") == 0) return "text/css"; if (strcmp(ext, "js") == 0) return "application/javascript"; if (strcmp(ext, "jpg") == 0) return "image/jpeg"; if (strcmp(ext, "png") == 0) return "image/png"; return "application/octet-stream"; } // 发送HTTP响应(含文件内容) void send_response(int client_fd, const char *path) { char buffer[BUFFER_SIZE]; FILE *file = fopen(path, "rb"); if (!file) { // 文件不存在时发送404 const char *not_found = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n<h1>404 Not Found</h1>"; send(client_fd, not_found, strlen(not_found), 0); return; } // 获取文件扩展名 const char *ext = strrchr(path, '.'); if (ext) ext++; else ext = ""; // 构造HTTP头部 struct stat st; stat(path, &st); snprintf(buffer, BUFFER_SIZE, "HTTP/1.1 200 OK\r\n" "Content-Type: %s\r\n" "Content-Length: %ld\r\n\r\n", get_mime_type(ext), st.st_size ); send(client_fd, buffer, strlen(buffer), 0); // 发送文件内容 size_t bytes_read; while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, file)) > 0) { send(client_fd, buffer, bytes_read, 0); } fclose(file); } // 主服务逻辑 int main() { int server_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t addr_len = sizeof(client_addr); // 创建Socket server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("Socket creation failed"); exit(EXIT_FAILURE); } // 绑定端口 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))) { perror("Bind failed"); close(server_fd); exit(EXIT_FAILURE); } // 监听请求 if (listen(server_fd, 10) < 0) { perror("Listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Server running on http://localhost:%d\n", PORT); // 主循环:接受请求并处理 while (1) { client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len); if (client_fd < 0) { perror("Accept error"); continue; } // 读取HTTP请求 char request[BUFFER_SIZE]; recv(client_fd, request, BUFFER_SIZE, 0); // 解析请求路径(简化版) char *method = strtok(request, " "); char *path = strtok(NULL, " "); if (!method || !path) { close(client_fd); continue; } // 映射路径到本地文件 char full_path[256]; if (strcmp(path, "/") == 0) path = "/Index.html"; // 默认首页 snprintf(full_path, sizeof(full_path), "%s%s", WEB_ROOT, path); // 发送响应 send_response(client_fd, full_path); close(client_fd); } close(server_fd); return 0; } 改用多进程方法实现以上代码
最新发布
08-06
<think>我们正在将现有的单进程HTTP服务器修改为多进程处理客户端请求的版本。在C语言中,我们可以使用fork()系统调用来创建子进程,每个子进程处理一个客户端连接。但是,需要注意的是,当多个进程同时监听同一个套接字时,我们需要使用某种机制来避免竞争条件(例如“惊群”效应)。在Linux中,我们可以使用SO_REUSEPORT选项来允许多个进程绑定到同一个端口,并由内核进行负载均衡。另一种传统方法是主进程接受连接,然后将连接分发给子进程处理(通过进程间通信传递文件描述符,但这种方法效率较低)。这里我们将采用第一种方法(SO_REUSEPORT)来简化设计。 步骤: 1. 创建监听套接字,并设置SO_REUSEPORT选项,这样多个进程可以绑定到同一个端口。 2. 使用fork()创建多个子进程(工作进程)。 3. 每个子进程都调用accept()等待客户端连接。内核会负责将连接分配给不同的子进程。 4. 每个子进程在接收到连接后,处理HTTP请求并返回响应。 注意:由于每个子进程都是独立的,它们之间不共享状态(除了最初共享的监听套接字,但通过SO_REUSEPORT每个进程都有自己的监听套接字,实际上在accept时内核会分配)。因此,我们需要小心处理全局资源,例如日志文件,可能需要使用互斥锁或单独的文件描述符。 但是,这里我们使用SO_REUSEPORT方法,每个进程都会有自己的监听套接字,绑定到同一个端口。因此,我们需要在fork之前创建监听套接字并绑定,然后每个子进程都会继承这个套接字,但是SO_REUSEPORT允许我们多个进程绑定到同一个端口,所以实际上我们可以在fork之前创建并绑定套接字,然后每个进程都去accept。 然而,更常见的做法是:主进程创建套接字并绑定、监听,然后fork多个子进程,每个子进程都循环accept。但是这样多个进程同时accept同一个套接字,在传统情况下会导致惊群。但现代Linux内核(3.9以上)支持SO_REUSEPORT,我们可以使用它来避免惊群,或者使用epoll等机制。但这里为了简单,我们使用SO_REUSEPORT。 另一种做法:主进程创建监听套接字,然后fork多个子进程,每个子进程都直接在这个套接字上accept。在Linux内核3.9以上,多个进程同时accept同一个套接字不会导致惊群,因为内核已经解决了这个问题(使用一个等待队列,只唤醒一个进程)。因此,我们可以不使用SO_REUSEPORT,而是让多个子进程共享同一个监听套接字(通过fork继承)。但要注意,在较老的内核上可能会有惊群问题。 这里我们假设运行在较新的Linux内核上(>3.9),所以我们可以采用共享监听套接字的方式,而不使用SO_REUSEPORT。这样,我们只需要在fork之前创建监听套接字并监听,然后每个子进程都循环accept即可。 步骤: 1. 创建监听套接字(socket, bind, listen)。 2. 设置套接字为SO_REUSEADDR(允许快速重启)。 3. 使用fork()创建多个子进程(工作进程)。 4. 每个子进程循环执行:接受连接(accept),处理请求,关闭连接。 5. 主进程等待子进程退出(但实际上子进程不会退出,所以主进程可以进入等待状态)。 但是,我们还需要考虑子进程崩溃的情况,以及如何优雅退出。为了简化,我们不考虑这些。 示例代码框架: 注意:在子进程中,accept返回的套接字是独立的,不会影响其他进程。但是,要注意关闭不需要的文件描述符(例如,监听套接字在子进程中不需要关闭?实际上,每个进程都需要监听套接字,所以不能关闭)。 另外,为了避免僵尸进程,主进程可以忽略SIGCHLD信号。 下面是一个简单的多进程HTTP服务器代码框架: ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <signal.h> #include <errno.h> #define PORT 8080 #define WORKER_NUM 4 void handle_client(int client_socket) { // 这里处理HTTP请求,读取请求,发送响应 char response[] = "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello world!"; send(client_socket, response, sizeof(response)-1, 0); close(client_socket); } int main() { int server_fd; struct sockaddr_in address; int opt = 1; pid_t pid; // 创建套接字 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 设置套接字选项,允许地址重用 if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { perror("setsockopt"); exit(EXIT_FAILURE); } address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); // 绑定 if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 监听 if (listen(server_fd, 10) < 0) { perror("listen"); exit(EXIT_FAILURE); } // 忽略SIGCHLD信号,避免僵尸进程 signal(SIGCHLD, SIG_IGN); // 创建多个子进程 for (int i = 0; i < WORKER_NUM; i++) { pid = fork(); if (pid < 0) { perror("fork"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程 while (1) { int client_socket; struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); // 接受连接 client_socket = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len); if (client_socket < 0) { perror("accept"); continue; } // 处理客户端请求 handle_client(client_socket); } // 子进程不会退出,所以这里不需要exit,但为了安全,可以在循环后加exit exit(0); // 实际上不会执行到这里 } } // 主进程等待(这里可以等待子进程,但由于我们忽略了SIGCHLD,所以不需要等待) while (1) { pause(); } return 0; } ``` 注意:上面的代码中,多个子进程共享同一个监听套接字(server_fd),它们都在同一个套接字上调用accept。在Linux内核3.9以上,多个进程同时accept同一个监听套接字不会导致惊群,因为内核会保证只有一个进程被唤醒(具体行为是:当有新连接时,内核会唤醒正在accept的进程中的一个,而不是全部)。 但是,如果内核较老(<3.9),则可能会出现惊群效应(多个进程被唤醒,但只有一个能成功accept,其他进程会返回EAGAIN或ECONNABORTED等错误)。因此,我们需要处理accept失败的情况(例如,在错误为EAGAIN、ECONNABORTED、EINTR时重试)。 修改子进程中的accept循环: ```c while (1) { client_socket = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len); if (client_socket < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR || errno == ECONNABORTED) { continue; // 重试 } else { perror("accept error"); exit(EXIT_FAILURE); } } break; } ``` 另外,上面的代码中,每个子进程处理一个请求后继续等待下一个请求。这可以同时处理多个请求(每个子进程处理一个请求,但可以同时有多个子进程在处理)。 但是,如果同时到达的请求数超过子进程数,那么新的请求将在内核的监听队列中等待(我们设置了listen的backlog为10)。 此外,我们还可以考虑在子进程中使用进程池,但这里我们使用固定数量的子进程。 最后,注意在handle_client函数中,我们只是简单发送了一个响应。实际应用中,需要解析HTTP请求,并根据请求内容返回响应。 以上代码是一个基本的多进程HTTP服务器框架,可以根据实际需求扩展。 注意:在真实环境中,我们还需要考虑信号处理、日志记录、资源清理等。 另外,由于每个进程都是独立处理请求,因此如果服务器需要维护状态(例如会话),则需要使用共享内存或其他进程间通信机制,或者使用无状态设计。在HTTP服务器中,通常设计为无状态。 如果服务器需要处理大量静态文件,还需要注意文件描述符的泄漏资源限制(例如,每个进程打开文件数的限制)。 最后,为了安全,可以在子进程中设置资源限制(例如setrlimit)信号处理。 由于用户要求修改现有的单进程服务器,所以需要将原有的请求处理函数(handle_client)替换为原来的逻辑。 以上代码在Linux环境下编译运行。 参考文献: [1] 关于SO_REUSEPORT惊群问题:https://lwn.net/Articles/542629/</think># 将单进程HTTP服务器修改为多进程处理客户端请求 在C语言中将单进程HTTP服务器修改为多进程处理客户端请求的核心是使用`fork()`系统调用创建子进程,每个子进程独立处理客户端连接。以下是详细步骤: ## 核心实现步骤 ### 1. 创建监听套接字 ```c int server_fd = socket(AF_INET, SOCK_STREAM, 0); setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)); struct sockaddr_in address = { .sin_family = AF_INET, .sin_addr.s_addr = INADDR_ANY, .sin_port = htons(PORT) }; bind(server_fd, (struct sockaddr*)&address, sizeof(address)); listen(server_fd, BACKLOG); ``` ### 2. 创建子进程池 ```c #define WORKER_COUNT 4 for (int i = 0; i < WORKER_COUNT; i++) { pid_t pid = fork(); if (pid == 0) { // 子进程 while (1) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); // 接受新连接 int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len); if (client_fd < 0) { perror("accept failed"); continue; } // 处理客户端请求 handle_request(client_fd); close(client_fd); } exit(0); // 子进程退出 } else if (pid < 0) { perror("fork failed"); } } ``` ### 3. 主进程等待 ```c // 主进程等待所有子进程 while (wait(NULL) > 0); close(server_fd); ``` ## 完整示例代码 ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/wait.h> #define PORT 8080 #define BACKLOG 10 #define WORKER_COUNT 4 void handle_request(int client_fd) { // 简化的HTTP响应 char response[] = "HTTP/1.1 200 OK\r\n" "Content-Type: text/plain\r\n" "Content-Length: 13\r\n\r\n" "Hello, World!"; send(client_fd, response, sizeof(response) - 1, 0); } int main() { int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 允许地址重用 int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { perror("setsockopt"); exit(EXIT_FAILURE); } struct sockaddr_in address = { .sin_family = AF_INET, .sin_addr.s_addr = INADDR_ANY, .sin_port = htons(PORT) }; if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } if (listen(server_fd, BACKLOG) < 0) { perror("listen"); exit(EXIT_FAILURE); } // 创建子进程池 for (int i = 0; i < WORKER_COUNT; i++) { pid_t pid = fork(); if (pid == 0) { // 子进程 printf("Worker %d (PID: %d) started\n", i+1, getpid()); while (1) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len); if (client_fd < 0) { perror("accept failed"); continue; } printf("Worker %d handling connection\n", getpid()); handle_request(client_fd); close(client_fd); } exit(0); } else if (pid < 0) { perror("fork failed"); } } // 主进程等待所有子进程 while (wait(NULL) > 0); close(server_fd); return 0; } ``` ## 关键注意事项 1. **文件描述符继承**: - 子进程继承父进程的监听套接字 - 内核自动负载均衡连接请求到不同进程 2. **避免僵尸进程**: - 使用`wait()`或`waitpid()`回收子进程资源 - 处理`SIGCHLD`信号更高效 3. **并发控制**: - 工作进程数量应匹配CPU核心数 - 避免创建过多进程导致资源耗尽 4. **共享资源处理**: - 避免在子进程中使用全局状态 - 使用进程间通信(IPC)处理共享数据 - 考虑使用无状态设计 5. **错误处理**: - 检查所有系统调用返回值 - 处理`EINTR`错误(系统调用被信号中断) ## 性能优化建议 1. **使用进程池**:避免频繁创建/销毁进程的开销 2. **非阻塞I/O**:结合`select()`或`poll()`提高单进程并发能力 3. **连接复用**:支持HTTP keep-alive减少连接建立开销 4. **负载均衡**:考虑使用反向代理如Nginx分配请求 这种方法允许服务器同时处理多个客户端请求,充分利用多核CPU资源,提高服务器吞吐量[^1][^2]。 --- ### 相关问题 1. **如何避免多进程服务器中的僵尸进程?** - 使用`wait()`或`waitpid()`系统调用回收子进程资源 - 设置`SIGCHLD`信号处理程序自动回收子进程 2. **多进程与多线程服务器模型有何区别?** - 进程有独立内存空间,线程共享内存 - 进程更健壮但资源开销大,线程更轻量但有同步问题 3. **如何处理多进程间的共享数据?** - 使用进程间通信(IPC)机制如管道、共享内存 - 使用无状态设计避免共享数据需求 - 通过数据库或外部存储共享状态 4. **如何确定最佳工作进程数量?** - 通常设置为CPU核心数 - 考虑I/O密集型或CPU密集型负载特点 - 动态调整基于系统负载 5. **多进程服务器如何优雅退出?** - 主进程发送信号通知子进程退出 - 子进程完成当前请求后退出 - 主进程回收所有子进程资源后退出
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值