标签:而且 lib ted 如何 get 进程 添加 reset from
首先要明白,Nginx 采用的是多进程(单线程) & 多路IO复用模型。使用了 I/O 多路复用技术的 Nginx,就成了”并发事件驱动“的服务器。
异步非阻塞(AIO)的详解http://www.ibm.com/developerworks/cn/linux/l-async/
注意 worker 进程数,一般会设置成机器 cpu 核数。因为更多的worker 数,只会导致进程相互竞争 cpu,从而带来不必要的上下文切换。
使用多进程模式,不仅能提高并发率,而且进程之间相互独立,一个 worker 进程挂了不会影响到其他 worker 进程。
主进程(master 进程)首先通过 socket() 来创建一个 sock 文件描述符用来监听,然后fork生成子进程(workers 进程),子进程将继承父进程的 sockfd(socket 文件描述符),之后子进程 accept() 后将创建已连接描述符(connected descriptor)),然后通过已连接描述符来与客户端通信。
那么,由于所有子进程都继承了父进程的 sockfd,那么当连接进来时,所有子进程都将收到通知并“争着”与它建立连接,这就叫“惊群现象”。大量的进程被激活又挂起,只有一个进程可以accept() 到这个连接,这当然会消耗系统资源。
Nginx 提供了一个 accept_mutex 这个东西,这是一个加在accept上的一把共享锁。即每个 worker 进程在执行 accept 之前都需要先获取锁,获取不到就放弃执行 accept()。有了这把锁之后,同一时刻,就只会有一个进程去 accpet(),这样就不会有惊群问题了。accept_mutex 是一个可控选项,我们可以显示地关掉,默认是打开的。
Nginx在启动后,会有一个master进程和多个worker进程。
主要用来管理worker进程,包含:接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。
master进程充当整个进程组与用户的交互接口,同时对进程进行监护。它不需要处理网络事件,不负责业务的执行,只会通过管理worker进程来实现重启服务、平滑升级、更换日志文件、配置文件实时生效等功能。
我们要控制nginx,只需要通过kill向master进程发送信号就行了。比如kill -HUP pid,则是告诉nginx,从容地重启nginx,我们一般用这个信号来重启nginx,或重新加载配置,因为是从容地重启,因此服务是不中断的。master进程在接收到HUP信号后是怎么做的呢?首先master进程在接到信号后,会先重新加载配置文件,然后再启动新的worker进程,并向所有老的worker进程发送信号,告诉他们可以光荣退休了。新的worker在启动后,就开始接收新的请求,而老的worker在收到来自master的信号后,就不再接收新的请求,并且在当前进程中的所有未处理完的请求处理完成后,再退出。当然,直接给master进程发送信号,这是比较老的操作方式,nginx在0.8版本之后,引入了一系列命令行参数,来方便我们管理。比如,./nginx -s reload,就是来重启nginx,./nginx -s stop,就是来停止nginx的运行。如何做到的呢?我们还是拿reload来说,我们看到,执行命令时,我们是启动一个新的nginx进程,而新的nginx进程在解析到reload参数后,就知道我们的目的是控制nginx来重新加载配置文件了,它会向master进程发送信号,然后接下来的动作,就和我们直接向master进程发送信号一样了。
而基本的网络事件,则是放在worker进程中来处理了。多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致,这里面的原因与nginx的进程模型以及事件处理模型是分不开的。
worker进程之间是平等的,每个进程,处理请求的机会也是一样的。当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接,怎么做到的呢?首先,每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程。所有worker进程的listenfd会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。worker进程之间是平等的,每个进程,处理请求的机会也是一样的。当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接,怎么做到的呢?首先,每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程。所有worker进程的listenfd会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。
当一个 worker 进程在 accept() 这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,一个完整的请求。一个请求,完全由 worker 进程来处理,而且只能在一个 worker 进程中处理。
这样做带来的好处:
1、节省锁带来的开销。每个 worker 进程都是独立的进程,不共享资源,不需要加锁。同时在编程以及问题查上时,也会方便很多。
2、独立进程,减少风险。采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master 进程则很快重新启动新的 worker 进程。当然,worker 进程的也能发生意外退出。
多进程模型每个进程/线程只能处理一路IO,那么 Nginx是如何处理多路IO呢?
如果不使用 IO 多路复用,那么在一个进程中,同时只能处理一个请求,比如执行 accept(),如果没有连接过来,那么程序会阻塞在这里,直到有一个连接过来,才能继续向下执行。
而多路复用,允许我们只在事件发生时才将控制返回给程序,而其他时候内核都挂起进程,随时待命。
epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树),其工作流程分为三部分:
这样,注册好事件之后,只要有 fd 上事件发生,epoll_wait() 就能检测到并返回给用户,用户就能”非阻塞“地进行 I/O 了。
epoll() 中内核则维护一个链表,epoll_wait 直接检查链表是不是空就知道是否有文件描述符准备好了。(epoll 与 select 相比最大的优点是不会随着 sockfd 数目增长而降低效率,使用 select() 时,内核采用轮训的方法来查看是否有fd 准备好,其中的保存 sockfd 的是类似数组的数据结构 fd_set,key 为 fd,value 为 0 或者 1。)
能达到这种效果,是因为在内核实现中 epoll 是根据每个 sockfd 上面的与设备驱动程序建立起来的回调函数实现的。那么,某个 sockfd 上的事件发生时,与它对应的回调函数就会被调用,来把这个 sockfd 加入链表,其他处于“空闲的”状态的则不会。在这点上,epoll 实现了一个”伪”AIO。但是如果绝大部分的 I/O 都是“活跃的”,每个 socket 使用率很高的话,epoll效率不一定比 select 高(可能是要维护队列复杂)。
可以看出,因为一个进程里只有一个线程,所以一个进程同时只能做一件事,但是可以通过不断地切换来“同时”处理多个请求。
例子:Nginx 会注册一个事件:“如果来自一个新客户端的连接请求到来了,再通知我”,此后只有连接请求到来,服务器才会执行 accept() 来接收请求。又比如向上游服务器(比如 PHP-FPM)转发请求,并等待请求返回时,这个处理的 worker 不会在这阻塞,它会在发送完请求后,注册一个事件:“如果缓冲区接收到数据了,告诉我一声,我再将它读进来”,于是进程就空闲下来等待事件发生。
这样,基于 多进程+epoll, Nginx 便能实现高并发。
使用 epoll 处理事件的一个框架,代码转自:http://www.cnblogs.com/fnlingnzb-learner/p/5835573.html
1 for( ; ; ) 2 { 3 nfds = epoll_wait(epfd,events,20,500); 4 for(i=0;i<nfds;++i) 5 { 6 if(events[i].data.fd==listenfd) //有新的连接 7 { 8 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接 9 ev.data.fd=connfd; 10 ev.events=EPOLLIN|EPOLLET; 11 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中 12 } 13 else if( events[i].events&EPOLLIN ) //接收到数据,读socket 14 { 15 n = read(sockfd, line, MAXLINE)) < 0 //读 16 ev.data.ptr = md; //md为自定义类型,添加数据 17 ev.events=EPOLLOUT|EPOLLET; 18 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓 19 } 20 else if(events[i].events&EPOLLOUT) //有数据待发送,写socket 21 { 22 struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据 23 sockfd = md->fd; 24 send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据 25 ev.data.fd=sockfd; 26 ev.events=EPOLLIN|EPOLLET; 27 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据 28 } 29 else 30 { 31 //其他的处理 32 } 33 } 34 }
下面给出一个完整的服务器端例子:
1 #include <iostream> 2 #include <sys/socket.h> 3 #include <sys/epoll.h> 4 #include <netinet/in.h> 5 #include <arpa/inet.h> 6 #include <fcntl.h> 7 #include <unistd.h> 8 #include <stdio.h> 9 #include <errno.h> 10 11 using namespace std; 12 13 #define MAXLINE 5 14 #define OPEN_MAX 100 15 #define LISTENQ 20 16 #define SERV_PORT 5000 17 #define INFTIM 1000 18 19 void setnonblocking(int sock) 20 { 21 int opts; 22 opts=fcntl(sock,F_GETFL); 23 if(opts<0) 24 { 25 perror("fcntl(sock,GETFL)"); 26 exit(1); 27 } 28 opts = opts|O_NONBLOCK; 29 if(fcntl(sock,F_SETFL,opts)<0) 30 { 31 perror("fcntl(sock,SETFL,opts)"); 32 exit(1); 33 } 34 } 35 36 int main(int argc, char* argv[]) 37 { 38 int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber; 39 ssize_t n; 40 char line[MAXLINE]; 41 socklen_t clilen; 42 43 44 if ( 2 == argc ) 45 { 46 if( (portnumber = atoi(argv[1])) < 0 ) 47 { 48 fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]); 49 return 1; 50 } 51 } 52 else 53 { 54 fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]); 55 return 1; 56 } 57 58 59 60 //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件 61 62 struct epoll_event ev,events[20]; 63 //生成用于处理accept的epoll专用的文件描述符 64 65 epfd=epoll_create(256); 66 struct sockaddr_in clientaddr; 67 struct sockaddr_in serveraddr; 68 listenfd = socket(AF_INET, SOCK_STREAM, 0); 69 //把socket设置为非阻塞方式 70 71 //setnonblocking(listenfd); 72 73 //设置与要处理的事件相关的文件描述符 74 75 ev.data.fd=listenfd; 76 //设置要处理的事件类型 77 78 ev.events=EPOLLIN|EPOLLET; 79 //ev.events=EPOLLIN; 80 81 //注册epoll事件 82 83 epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); 84 bzero(&serveraddr, sizeof(serveraddr)); 85 serveraddr.sin_family = AF_INET; 86 char *local_addr="127.0.0.1"; 87 inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber); 88 89 serveraddr.sin_port=htons(portnumber); 90 bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr)); 91 listen(listenfd, LISTENQ); 92 maxi = 0; 93 for ( ; ; ) { 94 //等待epoll事件的发生 95 96 nfds=epoll_wait(epfd,events,20,500); 97 //处理所发生的所有事件 98 99 for(i=0;i<nfds;++i) 100 { 101 if(events[i].data.fd==listenfd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。 102 103 { 104 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); 105 if(connfd<0){ 106 perror("connfd<0"); 107 exit(1); 108 } 109 //setnonblocking(connfd); 110 111 char *str = inet_ntoa(clientaddr.sin_addr); 112 cout << "accapt a connection from " << str << endl; 113 //设置用于读操作的文件描述符 114 115 ev.data.fd=connfd; 116 //设置用于注测的读操作事件 117 118 ev.events=EPOLLIN|EPOLLET; 119 //ev.events=EPOLLIN; 120 121 //注册ev 122 123 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); 124 } 125 else if(events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。 126 127 { 128 cout << "EPOLLIN" << endl; 129 if ( (sockfd = events[i].data.fd) < 0) 130 continue; 131 if ( (n = read(sockfd, line, MAXLINE)) < 0) { 132 if (errno == ECONNRESET) { 133 close(sockfd); 134 events[i].data.fd = -1; 135 } else 136 std::cout<<"readline error"<<std::endl; 137 } else if (n == 0) { 138 close(sockfd); 139 events[i].data.fd = -1; 140 } 141 line[n] = ‘/0‘; 142 cout << "read " << line << endl; 143 //设置用于写操作的文件描述符 144 145 ev.data.fd=sockfd; 146 //设置用于注测的写操作事件 147 148 ev.events=EPOLLOUT|EPOLLET; 149 //修改sockfd上要处理的事件为EPOLLOUT 150 151 //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); 152 153 } 154 else if(events[i].events&EPOLLOUT) // 如果有数据发送 155 156 { 157 sockfd = events[i].data.fd; 158 write(sockfd, line, n); 159 //设置用于读操作的文件描述符 160 161 ev.data.fd=sockfd; 162 //设置用于注测的读操作事件 163 164 ev.events=EPOLLIN|EPOLLET; 165 //修改sockfd上要处理的事件为EPOLIN 166 167 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); 168 } 169 } 170 } 171 return 0; 172 }
事件驱动适合于I/O密集型服务,多进程或线程适合于CPU密集型服务:
1、Nginx 更主要是作为反向代理,而非Web服务器使用。其模式是事件驱动。
2、事件驱动服务器,最适合做的就是这种 I/O 密集型工作,如反向代理,它在客户端与WEB服务器之间起一个数据中转作用,纯粹是 I/O 操作,自身并不涉及到复杂计算。因为进程在一个地方进行计算时,那么这个进程就不能处理其他事件了。
3、Nginx 只需要少量进程配合事件驱动,几个进程跑 libevent,不像 Apache 多进程模型那样动辄数百的进程数。
5、Nginx 处理静态文件效果也很好,那是因为读写文件和网络通信其实都是 I/O操作,处理过程一样。
参考文章:https://blog.csdn.net/m0_38110132/article/details/75126316
标签:而且 lib ted 如何 get 进程 添加 reset from
原文地址:https://www.cnblogs.com/atai/p/11809910.html