在单进程的网络编程模型中。所有的网络相关的动作都是在一个进程里完成的,如监听 socket 的创建, bind、listen。再比如 epoll 的创建、要监听事件的添加,以及 epoll_wait 等待时间发生。这些统统都是在一个进程里搞定。
一个客户端和使用了 epoll 的服务端的交互过程如下图所示。
以下是其大概的代码示例(没耐心看的同学可以先)。
int main(){
//监听
lfd = socket(AF_INET,SOCK_STREAM,0);
bind(lfd, ...)
listen(lfd, ...);
//创建epoll对象,并把 listen socket的事件管理起来
efd = epoll_create(...);
epoll_ctl(efd, EPOLL_CTL_ADD, lfd, ...);
//事件循环
for (;;)
{
size_t nready = epoll_wait(efd, ep, ...);
for (int i = 0; i < nready; ++i){
if(ep[i].data.fd == lfd){
//lfd上发生事件表示都连接到达,accept接收它
fd = accept(listenfd, ...);
epoll_ctl(efd, EPOLL_CTL_ADD, fd, ...);
}else{
//其它socket发生的事件都是读写请求、或者关闭连接
...
}
}
}
}
在单进程模型中,不管有多少的连接,是几万还是几十万,服务器都是通过 epoll 来监控这些连接 socket 上的可读和可写事件。当某个 socket 上有数据发生的时候,再以非阻塞的方式对 socket 进行读写操作。
事实上,Redis 5.0 及以前的版本中,它的网络部分去掉对 handler 的封装,去掉时间事件以后,代码基本和上述 demo 非常接近。而且因为 Redis 的业务特点只需要内存 IO,且 CPU 计算少,所以可以达到数万的 QPS。
但是单进程的问题也是显而易见的,没有办法充分发挥多核的优势。所以目前业界绝大部分的后端服务还都是需要基于多进程的方式来进行开发的。到了多进程的时候,更复杂的问题多进程之间的配合和协作问题就产生了。比如
-
哪个进程执行监听 listen ,以及 accept 接收新连接?
-
哪个进程负责发现用户连接上的读写事件?
-
当有用户请求到达的时候,如何均匀地将请求分散到不同的进程中?
-
需不需要单独搞一部分进程执行计算工作
-
...
事实上,以上这些问题并没有标准答案。各大应用或者网络框架都有自己不同的实现方式。为此业界还专门总结出了两类网络设计模式 - Reactor 和 Proactor。不过今天我不想讨论这种抽象模式,而是想带大家看一个具体的 Case - Nginx 是如何在多进程下使用 epoll 的。
一、 Nginx Master 进程初始化
在 Nginx 中,将进程分成了两类。一类是 Master 进程,一类是 Worker 进程。
在 Master 进程中,主要的任务是负责启动整个程序、读取配置文件、监听和处理各种信号,并对 Worker 进程进行统筹管理。
不过今天我们要查看的重点问题是看网络。在 Master 进程中,和网络相关的操作非常简单就是创建了 socket 并对其进行 bind 和 监听。
具体细节我们来看 Main 函数。
//file: src/core/nginx.c
int ngx_cdecl main(int argc, char *const *argv)
{
ngx_cycle_t *cycle, init_cycle;
//1.1 ngx_init_cycle 中开启监听
cycle = ngx_init_cycle(&init_cycle);
//1.2 启动主进程循环
ngx_master_process_cycle(cycle);
}
在 Nginx 中,ngx_cycle_t 是非常核心的一个结构体。这个结构体存储了很多东西,也贯穿了好多的函数。其中对端口的 bind 和 listen 就是在它执行时完成的。
ngx_master_process_cycle 是 Master 进程的主事件循环。它先是根据配置启动指定数量的 Worker 进程,然后就开始关注和处理重启、退出等信号。接下来我们分两个小节来更详细地看。
1.1 Nginx 的服务端口监听
我们看下 ngx_init_cycle 中是如何执行 bind 和 listen 的。
//file: src/core/ngx_cycle.c
ngx_cycle_t *ngx_init_cycle(ngx_cycle_t *old_cycle)
{
......
if (ngx_open_listening_sockets(cycle) != NGX_OK) {
goto failed;
}
}
真正的监听还是在 ngx_open_listening_sockets 函数中,继续看它的源码。
//file: src/core/ngx_connection.c
ngx_int_t ngx_open_listening_sockets(ngx_cycle_t *cycle)
{
......
//要监听的 socket 对象
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
//获取第i个socket
s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0);
//绑定
bind(s, ls[i].sockaddr, ls[i].socklen)
//监听
listen(s, ls[i].backlog)
ls[i].listen = 1;
ls[i].fd = s;
}
}
在这个函数中,遍历要监听的 socket。如果是启用了 REUSEPORT 配置,那先把 socket 设置上 SO_REUSEPORT 选项。然后接下来就是大家都熟悉的 bind 和 listen。所以,bind 和 listen 是在 Master 进程中完成的。