Nginx是多进程的方式来完成并发工作的。nginx在启动后,会有一个master进程和多个worker进程。每个worker进程负责处理若干个来自客户端的请求。worker进程之间的地位是相等的,推荐设置worker的个数为cpu的核数。一个请求只能由一个worker来负责,不能做重复的工作。
master进程主要用来管理worker进程,包含:接收来自外界(管理员)的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。
问题一:多个worker进程如何处理请求?
每个worker进程都是从master进程fork过来的子进程,在master进程里面,先建立好需要listen的socket之后,然后再fork出多个worker进程,这样每个worker进程都可以去accept这socket(即每个进程的这个socket会监控在同一个ip地址与端口)。如果不做特殊处理的话,当来了一个请求之后,只会有一个worker会accept成功,其他所有worker会accept失败(惊群现象)。Nginx为避免这样,做出的处理是提供了一个accept_mutex,给每个worker的accept加上一把共享锁。
现在貌似大多数内核已经解决了惊群的问题,我在mac osx10.9上测试,有两个进程监听同一个套接字,当来到请求时,只有一个进程被唤醒去处理这个请求。
#include<iostream>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
using namespace std;
int main()
{
int socket_listen, socket_data, c, read_size;
struct sockaddr_in server, client;
char client_message[2000];
socket_listen = socket(AF_INET, SOCK_STREAM, 0);
if(socket_listen == -1)
cout<<"create socket fail!"<<endl;
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(8888);
if(bind(socket_listen, (struct sockaddr*)&server, sizeof(server)) < 0)
cout<<"bind fail!"<<endl;
listen(socket_listen, 5);
c = sizeof(struct sockaddr_in);
if(fork() != 0)
{
cout<<"father will accept."<<endl;
socket_data = accept(socket_listen, (struct sockaddr*)&client, (socklen_t*)&c);
cout<<"this is father."<<endl;
}
else
{
cout<<"son will accept."<<endl;
socket_data = accept(socket_listen, (struct sockaddr*)&client, (socklen_t*)&c);
cout<<"this is son."<<endl;
}
return 0;
}
程序运行起来之后,换一个终端用lsof -i:8888 可以看出有两个进程监听的本地的8888端口。
telnet localhost 8888
访问第一次,父进程被激发;访问第二次,子进程被激发。可见内核不存在这个问题了。
问题二:为何nginx的可以高并发?
nginx采用了异步非阻塞的方式来处理请求。什么阻塞?阻塞是进程等待读写事件,这等待期间进程啥也不做,cpu让给别的进程。什么是非阻塞?事件没有准备好,马上返回EAGAIN(一会儿再来),然后需要不断再过来看,这样会造成切换上下文的代价。现在采用的机制是:当事件没准备好时,放到epoll里面,事件准备好了,我们就去读写;当返回EAGAIN时,我们将它再次加入到epoll里面。这样的方式可以做到一个线程能不断切换同时处理多个请求。