epoll分为LT水平触发模型和ET边沿触发模型
LT模型类似于原来的select/poll操作,只要事件还没有处理完就会一直通知直到处理为止.
ET模型当套接字“状态发生变化”时候触发一次通知.
epoll使用epoll_create1函数相当于创建了一棵红黑树,epoll_ctl函数把要监听的fd放到监听集合(红黑树)中去,并给每个fd命名好了回调函数,当集合中任何fd有事件发生时,就通过相应的回调函数把对应的fd放到fd_list中去;epoll_wait就是收集fd_list中的fd的并根据其对应的event的某个位去进入到用户的处理函数中;
【1】
http://haoningabc.iteye.com/blog/1432958
JAVA理解epoll两种模式
linux异步IO浅析
http://hi.baidu.com/_kouu/blog/item/e225f67b337841f42f73b341.html
epoll有两种模式,Edge Triggered(简称ET) 和 Level Triggered(简称LT).在采用这两种模式时要注意的是,
如果采用ET模式,那么仅当【状态发生变化时】才会通知,而采用LT模式类似于原来的select/poll操作,只要还有没有处理的事件就会一直通知.
阳光梦:对于ET边沿触发模式,重点理解什么是所谓的“状态发生变化”
以代码来说明问题:
首先给出server的代码,需要说明的是每次accept的连接,加入可读集的时候采用的都是ET模式,而且接收缓冲区是5字节的,也就是每次只接收5字节的数据:
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()
37. {
38. int i, maxi, listenfd, connfd, sockfd,epfd,nfds;
39. ssize_t n;
40. char line[MAXLINE];
41. socklen_t clilen;
42. //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
43. struct epoll_event ev,events[20];
44. //生成用于处理accept的epoll专用的文件描述符
45. epfd=epoll_create(256);
46. struct sockaddr_in clientaddr;
47. struct sockaddr_in serveraddr;
48. listenfd = socket(AF_INET, SOCK_STREAM, 0);
49. //把socket设置为非阻塞方式
50. //setnonblocking(listenfd);
51. //设置与要处理的事件相关的文件描述符
52. ev.data.fd=listenfd;
53. //设置要处理的事件类型
54. ev.events=EPOLLIN|EPOLLET;
55. //ev.events=EPOLLIN;
56. //注册epoll事件
57. epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
58. bzero(&serveraddr, sizeof(serveraddr));
59. serveraddr.sin_family = AF_INET;
60. char *local_addr="127.0.0.1";
61. inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT);
62. serveraddr.sin_port=htons(SERV_PORT);
63. bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
64. listen(listenfd, LISTENQ);
65. maxi = 0;
66. for ( ; ; ) {
67. //等待epoll事件的发生
68. nfds=epoll_wait(epfd,events,20,500);
69. //处理所发生的所有事件
70. for(i=0;i<nfds;++i)
71. {
72. if(events[i].data.fd==listenfd)
73. {
74. connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
75. if(connfd<0){
76. perror("connfd<0");
77. exit(1);
78. }
79. //setnonblocking(connfd);
80. char *str = inet_ntoa(clientaddr.sin_addr);
81. cout << "accapt a connection from " << str << endl;
82. //设置用于读操作的文件描述符
83. ev.data.fd=connfd;
84. //设置用于注测的读操作事件
85. ev.events=EPOLLIN|EPOLLET;
86. //ev.events=EPOLLIN;
87. //注册ev
88. epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
89. }
90. else if(events[i].events&EPOLLIN)
91. {
92. cout << "EPOLLIN" << endl;
93. if ( (sockfd = events[i].data.fd) < 0)
94. continue;
95. if ( (n = read(sockfd, line, MAXLINE)) < 0) {
96. if (errno == ECONNRESET) {
97. close(sockfd);
98. events[i].data.fd = -1;
99. } else
100. std::cout<<"readline error"<<std::endl;
101. } else if (n == 0) {
102. close(sockfd);
103. events[i].data.fd = -1;
104. }
105. line[n] = '\0';
106. cout << "read " << line << endl;
107. //设置用于写操作的文件描述符
108. ev.data.fd=sockfd;
109. //设置用于注测的写操作事件
110. ev.events=EPOLLOUT|EPOLLET;
111. //修改sockfd上要处理的事件为EPOLLOUT
112. //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
113. }
114. else if(events[i].events&EPOLLOUT)
115. {
116. sockfd = events[i].data.fd;
117. write(sockfd, line, n);
118. //设置用于读操作的文件描述符
119. ev.data.fd=sockfd;
120. //设置用于注测的读操作事件
121. ev.events=EPOLLIN|EPOLLET;
122. //修改sockfd上要处理的事件为EPOLIN
123. epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
124. }
125. }
126. }
127. return 0;
128. }
下面给出测试所用的Perl写的client端,在client中发送10字节的数据,同时让client在发送完数据之后进入死循环, 也就是在发送完之后连接的状态不发生改变--既不再发送数据, 也不关闭连接,这样才能观察出server的状态:
1. #!/usr/bin/perl
2.
3. use IO::Socket;
4.
5. my $host = "127.0.0.1";
6. my $port = 5000;
7.
8. my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";
9. my $msg_out = "1234567890";
10. print $socket $msg_out;
11. print "now send over, go to sleep \n";
12.
13. while (1)
14. {
15. sleep(1);
16. }
运行server和client发现,server仅仅读取了5字节的数据,而client其实发送了10字节的数据,也就是说,server仅当第一次监听到了EPOLLIN事件,由于没有读取完数据,而且采用的是ET模式,状态在此之后不发生变化,因此server再也接收不到EPOLLIN事件了.
(友情提示:上面的这个测试客户端,当你关闭它的时候会再次出发IO可读事件给server,此时server就会去读取剩下的5字节数据了,但是这一事件与前面描述的ET性质并不矛盾.)
如果我们把client改为这样:
1. #!/usr/bin/perl
2.
3. use IO::Socket;
4.
5. my $host = "127.0.0.1";
6. my $port = 5000;
7.
8. my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";
9. my $msg_out = "1234567890";
10. print $socket $msg_out;
11. print "now send over, go to sleep \n";
12. sleep(5);
13. print "5 second gone send another line\n";
14. print $socket $msg_out;
15.
16. while (1)
17. {
18. sleep(1);
19. }
可以发现,在server接收完5字节的数据之后一直监听不到client的事件,而当client休眠5秒之后重新发送数据,server再次监听到了变化,只不过因为只是读取了5个字节,仍然有10个字节的数据(client第二次发送的数据)没有接收完.
如果上面的实验中,对accept的socket都采用的是LT模式,那么只要还有数据留在buffer中,server就会继续得到通知,读者可以自行改动代码进行实验.
基于这两个实验,可以得出这样的结论:ET模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的.
阳光梦:现在说明一直强调的"状态变化"究竟指的是什么:
1)对于监听可读事件时,如果是socket是监听socket,那么当有新的主动连接到来为状态发生变化;
但是,如果在一个时间同时接收了N个连接(N>1),但是监听socket只accept了一个连接,那么其它未 accept的连接将不会在ET模式下给监听socket发出通知,此时状态不发生变化(当accept同时得到多个客户的已连接套接字时,防止丢了几个,利用只要accept返回值>0则继续调用accept);
对一般的socket而言,协议栈中相应的缓冲区有新的数据为状态发生变化.当发送者发送数据非常大时,接受者要多次读取,因此判断缓冲区空了认为读完了。
对于一般的socket,就如例子中而言,如果对应的缓冲区本身已经有了N字节的数据,而只取出了小于N字节的数据,那么残存的数据不会造成状态发生变化.
发送者调用一次write可能发送1M的数据,经过网络传输可能会将1M数据分片的,这样就会多次发给接收者(非阻塞),因此接收者利用epoll模型的ET模式时需要多次读取并往一个大的buff中存放。
2)对于监听可写事件时,同理可推,不再详述.
而不论是监听可读还是可写,对方关闭socket连接都将造成状态发生变化,比如在例子中,如果强行中断client脚本,也就是主动中断了socket连接,那么都将造成server端发生状态的变化,从而server得到通知,将已经在本方缓冲区中的数据读出.
把前面的描述可以总结如下:仅当对方的动作(发出数据,关闭连接等)造成的事件才能导致状态发生变化,而本方协议栈中已经处理的事件(包括接收了对方的数据,接收了对方的主动连接请求)并不是造成状态发生变化的必要条件,状态变化一定是对方造成的.所以在ET模式下的,必须一直处理到出错或者完全处理完毕,才能进行下一个动作,否则可能会发生错误.
另外,从这个例子中,也可以阐述一些基本的网络编程概念.
【2】
epoll为什么这么快
epoll是多路复用IO(I/O Multiplexing)中的一种方式,但是仅用于linux2.6以上内核,在开始讨论这个问题之前,先来解释一下为什么需要多路复用IO.
以一个生活中的例子来解释.
假设你在大学中读书,要等待一个朋友来访,而这个朋友只知道你在A号楼,但是不知道你具体住在哪里,于是你们约好了在A号楼门口见面.
如果你使用的阻塞IO模型来处理这个问题,那么你就只能一直守候在A号楼门口等待朋友的到来,在这段时间里你不能做别的事情,不难知道,这种方式的效率是低下的.
现在时代变化了,开始使用多路复用IO模型来处理这个问题.你告诉你的朋友来了A号楼找楼管大妈,让她告诉你该怎么走.这里的楼管大妈扮演的就是多路复用IO的角色.
进一步解释select和epoll模型的差异.
select版大妈做的是如下的事情:比如同学甲的朋友来了,select版大妈比较笨,她带着朋友挨个房间进行查询谁是同学甲,你等的朋友来了,于是在实际的代码中,select版大妈做的是以下的事情:
1. int n = select(&readset,NULL,NULL,100);
2. for (int i = 0; n > 0; ++i)
3. {
4. if (FD_ISSET(fdarray[i], &readset))
5. {
6. do_something(fdarray[i]);
7. --n;
8. }
9. }
epoll版大妈就比较先进了,她记下了同学甲的信息,比如说他的房间号,那么等同学甲的朋友到来时,只需要告诉该朋友同学甲在哪个房间即可,不用自己亲自带着人满大楼的找人了.于是epoll版大妈做的事情可以用如下的代码表示:
1. n=epoll_wait(epfd,events,20,500);
2. for(i=0;i<n;++i)
3. {
4. do_something(events[n]);
5. }
6. 在epoll中,关键的数据结构epoll_event定义如下:
7. typedef union epoll_data {
8. void *ptr;
9. int fd;
10. __uint32_t u32;
11. __uint64_t u64;
12. } epoll_data_t;
13. struct epoll_event {
14. __uint32_t events; /* Epoll events */
15. epoll_data_t data; /* User data variable */
16. };
可以看到,epoll_data是一个union结构体,它就是epoll版大妈用于保存同学信息的结构体,它可以保存很多类型的信息:fd,指针,等等.有了这个结构体,epoll大妈可以不用吹灰之力就可以定位到同学甲.
别小看了这些效率的提高,在一个大规模并发的服务器中,轮询IO是最耗时间的操作之一.再回到那个例子中,如果每到来一个朋友楼管大妈都要全楼的查询同学,那么处理的效率必然就低下了,过不久楼底就有不少的人了.
对比最早给出的阻塞IO的处理模型, 可以看到采用了多路复用IO之后,程序可以自由的进行自己除了IO操作之外的工作,只有到IO状态发生变化的时候由多路复用IO进行通知, 然后再采取相应的操作, 而不用一直阻塞等待IO状态发生变化了.
从上面的分析也可以看出,epoll比select的提高实际上是一个用空间换时间思想的具体应用.
多进程服务器中,epoll的创建应该在创建子进程之后
看我的测试代码,似乎应该是在创建子进程之后创建epoll的fd,否则程序将会有问题,试将代码中两个CreateWorker函数的调用位置分别调用,一个在创建epoll fd之前,一个在之后,在调用在创建之前的代码会出问题,在我的机器上(linux内核2.6.26)表现的症状就是所有进程的epoll_wait函数返回0, 而客户端似乎被阻塞了:
服务器端:
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. #include <sys/types.h>
11. #include <sys/wait.h>
12.
13. using namespace std;
14.
15. #define MAXLINE 5
16. #define OPEN_MAX 100
17. #define LISTENQ 20
18. #define SERV_PORT 5000
19. #define INFTIM 1000
20.
21. typedef struct task_t
22. {
23. int fd;
24. char buffer[100];
25. int n;
26. }task_t;
27.
28. int CreateWorker(int nWorker)
29. {
30. if (0 < nWorker)
31. {
32. bool bIsChild;
33. pid_t nPid;
34.
35. while (!bIsChild)
36. {
37. if (0 < nWorker)
38. {
39. nPid = ::fork();
40. if (nPid > 0)
41. {
42. bIsChild = false;
43. --nWorker;
44. }
45. else if (0 == nPid)
46. {
47. bIsChild = true;
48. printf("create worker %d success!\n", ::getpid());
49. }
50. else
51. {
52. printf("fork error: %s\n", ::strerror(errno));
53. return -1;
54. }
55. }
56. else
57. {
58. int nStatus;
59. if (-1 == ::wait(&nStatus))
60. {
61. ++nWorker;
62. }
63. }
64. }
65. }
66.
67. return 0;
68. }
69.
70. void setnonblocking(int sock)
71. {
72. int opts;
73. opts=fcntl(sock,F_GETFL);
74. if(opts<0)
75. {
76. perror("fcntl(sock,GETFL)");
77. exit(1);
78. }
79. opts = opts|O_NONBLOCK;
80. if(fcntl(sock,F_SETFL,opts)<0)
81. {
82. perror("fcntl(sock,SETFL,opts)");
83. exit(1);
84. }
85. }
86.
87. int main()
88. {
89. int i, maxi, listenfd, connfd, sockfd,epfd,nfds;
90. ssize_t n;
91. char line[MAXLINE];
92. socklen_t clilen;
93. struct epoll_event ev,events[20];
94.
95. struct sockaddr_in clientaddr;
96. struct sockaddr_in serveraddr;
97. listenfd = socket(AF_INET, SOCK_STREAM, 0);
98. bzero(&serveraddr, sizeof(serveraddr));
99. serveraddr.sin_family = AF_INET;
100. char *local_addr="127.0.0.1";
101. inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT);
102. serveraddr.sin_port=htons(SERV_PORT);
103. // 地址重用
104. int nOptVal = 1;
105. socklen_t nOptLen = sizeof(int);
106. if (-1 == ::setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &nOptVal, nOptLen))
107. {
108. return -1;
109. }
110. setnonblocking(listenfd);
111. bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
112. listen(listenfd, LISTENQ);
113.
114. CreateWorker(5);
115.
116. //把socket设置为非阻塞方式
117.
118. //生成用于处理accept的epoll专用的文件描述符
119. epfd=epoll_create(256);
120. //设置与要处理的事件相关的文件描述符
121. ev.data.fd=listenfd;
122. //设置要处理的事件类型
123. ev.events=EPOLLIN|EPOLLET;
124. //ev.events=EPOLLIN;
125. //注册epoll事件
126. epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
127.
128. //CreateWorker(5);
129.
130. maxi = 0;
131.
132. task_t task;
133. task_t *ptask;
134. while(true)
135. {
136. //等待epoll事件的发生
137. nfds=epoll_wait(epfd,events,20,500);
138. //处理所发生的所有事件
139. for(i=0;i<nfds;++i)
140. {
141. if(events[i].data.fd==listenfd)
142. {
143. connfd = accept(listenfd,NULL, NULL);
144. if(connfd<0){
145. printf("connfd<0, listenfd = %d\n", listenfd);
146. printf("error = %s\n", strerror(errno));
147. exit(1);
148. }
149. setnonblocking(connfd);
150.
151. //设置用于读操作的文件描述符
152. memset(&task, 0, sizeof(task));
153. task.fd = connfd;
154. ev.data.ptr = &task;
155. //设置用于注册的读操作事件
156. ev.events=EPOLLIN|EPOLLET;
157. //ev.events=EPOLLIN;
158. //注册ev
159. epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
160. }
161. else if(events[i].events&EPOLLIN)
162. {
163. cout << "EPOLLIN" << endl;
164. ptask = (task_t*)events[i].data.ptr;
165. sockfd = ptask->fd;
166.
167. if ( (ptask->n = read(sockfd, ptask->buffer, 100)) < 0) {
168. if (errno == ECONNRESET) {
169. close(sockfd);
170. events[i].data.ptr = NULL;
171. } else
172. std::cout<<"readline error"<<std::endl;
173. } else if (ptask->n == 0) {
174. close(sockfd);
175. events[i].data.ptr = NULL;
176. }
177. ptask->buffer[ptask->n] = '\0';
178. cout << "read " << ptask->buffer << endl;
179.
180. //设置用于写操作的文件描述符
181. ev.data.ptr = ptask;
182. //设置用于注测的写操作事件
183. ev.events=EPOLLOUT|EPOLLET;
184.
185. //修改sockfd上要处理的事件为EPOLLOUT
186. epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
187. }
188. else if(events[i].events&EPOLLOUT)
189. {
190. cout << "EPOLLOUT" << endl;
191. ptask = (task_t*)events[i].data.ptr;
192. sockfd = ptask->fd;
193.
194. write(sockfd, ptask->buffer, ptask->n);
195.
196. //设置用于读操作的文件描述符
197. ev.data.ptr = ptask;
198.
199. //修改sockfd上要处理的事件为EPOLIN
200. epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,&ev);
201. cout << "write " << ptask->buffer;
202. memset(ptask, 0, sizeof(*ptask));
203. close(sockfd);
204. }
205. }
206. }
207. return 0;
208. }
测试客户端:
#!/usr/bin/perl
use strict;
use Socket;
use IO::Handle;
sub echoclient
{
my $host = "127.0.0.1";
my $port = 5000;
my $protocol = getprotobyname("TCP");
$host = inet_aton($host);
socket(SOCK, AF_INET, SOCK_STREAM, $protocol) or die"socket() failed: $!";
my $dest_addr = sockaddr_in($port, $host);
connect(SOCK, $dest_addr) or die "connect() failed:$!";
SOCK->autoflush(1);
my $msg_out = "hello world\n";
print "out = ", $msg_out;
print SOCK $msg_out;
my $msg_in = <SOCK>;
print "in = ", $msg_in;
close SOCK;
}
#&echoclient;
#exit(0);
for (my $i = 0; $i < 9999; $i++)
{
echoclient;
}
我查看了lighttpd的实现,也是在创建完子进程之后才创建的epoll的fd.
请问谁知道哪里有讲解这个的文档?
假如fd1是由A进程加入epfd的,而且用的是ET模式,那么加入通知的是进程B,显然B进程不会对fd1进行处理,所以以后fd1的事件再不会通知,所以经过几次循环之后,所有的fd都没有事件通知了,所以epoll_wait在timeout之后就返回0了。而在客户端的结果可想而知,只能是被阻塞。
也就是说, 这是一种发生在epoll fd上面的类似于"惊群"的现象.
对于linux socket与epoll配合相关的一些心得记录
没有多少高深的东西,全当记录,虽然简单,但是没有做过测试还是挺容易让人糊涂的
int nRecvBuf=32*1024;//设置为32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(constchar*)&nRecvBuf,sizeof(int));
1、通过上面语句可以简单设置缓冲区大小,测试证明:跟epoll结合的时候只有当单次发送的数据全被从缓冲区读完毕之后才会再次被触发,多次发送数据如果没有读取完毕当缓冲区未满的时候数据不会丢失,会累加到后面。
2、如果缓冲区未满,同一连接多次发送数据会多次收到EPOLLIN事件。
单次发送数据>socket缓冲区大小的数据数据会被阻塞分次发送,所以循环接收可以用ENLIGE错误判断。
3、如果缓冲区满,新发送的数据不会触发epoll事件(也无异常),每次recv都会为缓冲区腾出空间,只有当缓冲区空闲大小能够再次接收数据epollIN事件可以再次被触发
接收时接收大小为0表示客户端断开(不可能有0数据包触发EPOLLIN),-1表示异常,针对errorno进行判断可以确定是合理异常还是需要终止的异常,>0而不等于缓冲区大小表示单次发送结束。
4、如果中途临时调整接收缓存区大小,并且在上一次中数据没有完全接收到用户空间,数据不会丢失,会累加在一起
所以总结起来,系统对于数据的完整性还是做了相当的保正,至于稳定性没有作更深一步的测试
新增加:
5、如果主accept监听的soctet fd也设置为非阻塞,那么单纯靠epoll事件来驱动的服务器模型会存在问题,并发压力下发现,每次accept只从系统中取得第一个,所以如果恰冯多个连接同时触发server fd的EPOLLIN事件,在返回的event数组中体现不出来,会出现丢失事件的现象,所以当用ab等工具简单的压载就会发现每次都会有最后几条信息得不到处理,原因就在于此,我现在的解决办法是将server fd的监听去掉,用一个线程阻塞监听,accept成功就处理检测client fd,然后在主线程循环监听client事件,这样epoll在边缘模式下出错的概率就小,测试表明效果明显
我的办法是:while( connfd = accept() > 0 )
{
加入connfd 到epoll中
}6 、对于 SIG 部分信号还是要做屏蔽处理,不然对方 socket 中断等正常事件都会引起整个服务的退出
7 、 sendfile(fd, f->SL->sendBuffer.inFd, (off_t*)&f->SL->sendBuffer.offset, size_need); 注意 sendfile 函数的地三个变量是传送地址,偏移量会自动增加,不需要手动再次增加,否则就会出现文件传送丢失现象
8 、 单线程epoll驱动模型误解 :以前我一直认为单线程是无法处理 web 服务器这样的有严重网络延迟的服务,但 nginx 等优秀服务器都是机遇事件驱动模型,开始我在些的时候也是担心这些问题,后来测试发现,当 client socket设为非阻塞模式的时候,从读取数据到解析http协议,到发送数据均在epoll的驱动下速度非常快, 没有必要采用多线程,我的单核 cpu (奔三)就可以达到 10000page / second ,这在公网上是远远无法达到的一个数字(网络延迟更为严重),所以单线程的数据处理能力已经很高了,就不需要多线程了,所不同的是你在架构服务器的时候需要将所有阻塞的部分拆分开来,当 epoll 通知你可以读取的时候,实际上部分数据已经到了 socket 缓冲区,你所读取用的事件是将数据从内核空间拷贝到用户空间,同理,写也是一样的,所以 epoll 重要的地方就是将这两个延时的部分做了类似的异步处理,如果不需要处理更为复杂的业务,那单线程足以满足 1000M 网卡的最高要求,这才是单线程的意义。
我以前构建的 web 服务器就没有理解 epoll ,采用 epoll 的边缘触发之后怕事件丢失,或者单线程处理阻塞,所以自己用多线程构建了一个任务调度器,所有收到的事件统统压进任无调度器中,然后多任务处理,我还将 read 和 write 分别用两个调度器处理,并打算如果中间需要特殊的耗时的处理就增加一套调度器,用少量线程+ epoll 的方法来题高性能,后来发现 read 和 write 部分调度器是多余的, epoll 本来就是一个事件调度器,在后面再次缓存事件分部处理还不如将 epoll 设为水平模式,所以多此一举,但是这个调度起还是有用处的
上面讲到如果中间有耗时的工作,比如数据库读写,外部资源请求(文件, socket )等这些操作就不能阻塞在主线程里面,所以我设计的这个任务调度器就有用了,在 epoll 能处理的事件驱动部分就借用 epoll 的,中间部分采用模块化的设计,用函数指针达到面相对象语言中的 “ 委托 ” 的作用,就可以满足不同的需要将任务( fd 标识)加入调度器,让多线程循环执行,如果中间再次遇到阻塞就会再次加入自定义的阻塞器,检测完成就加入再次存入调度器,这样就可以将多种复杂的任务划分开来,相当于在处理的中间环节在自己购置一个类似于 epoll 的事件驱动器
9 、多系统兼容:我现在倒是觉得与其构建一个多操作系统都支持的服务器不如构建特定系统的,如果想迁移再次改动,因为一旦兼顾到多个系统的化会大大增加系统的复杂度,并且不能最优性能,每个系统都有自己的独有的优化选项,所以我觉得迁移的工作量远远小于兼顾的工作量
10 模块化编程,虽然用 c 还是要讲求一些模块化的设计的,我现在才发现几乎面相对想的语言所能实现的所有高级特性在 c 里面几乎都有对应的解决办法(暂时发现除了操作符重载),所有学过高级面相对象的语言的朋友不放把模式用 c 来实现,也是一种乐趣,便于维护和自己阅读
11、养成注释的好习惯