epoll概念
epoll对文件描述符的操作方式有两种工作模式:LT模式(Level Trigger,水平触发) 和ET模式(Edge Trigger,边缘触发)。
- LT模式:当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件,这样,当应用程序下一次调用epoll_wait时,epoll_wait还会向应用程序通告此事件,直到该事件被处理。
- ET模式:当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不在向应用程序通告此事件。
1. 水平触发
对于读操作
- 只要缓冲内容不为空,LT模式返回读就绪。
对于写操作
- 只要缓冲区还不满,LT模式会返回写就绪。
2. 边缘触发
对于读操作
- 当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。
- 当有新数据到达时,即缓冲区中的待读数据变多的时候。
- 当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。
对于写操作
- 当缓冲区由不可写变为可写时。
- 当有旧数据被发送走,即缓冲区中的内容变少的时候。
- 当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。
实验一
1. 测试代码:
1 #include <unistd.h> 2 #include <stdio.h> 3 #include <sys/epoll.h> 4 5 int main() 6 { 7 int epfd, nfds; 8 struct epoll_event event, events[5]; 9 epfd = epoll_create(1); 10 event.data.fd = STDIN_FILENO; 11 event.events = EPOLLIN | EPOLLET; 12 epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event); 13 while (1) 14 { 15 nfds = epoll_wait(epfd, events, 5, -1); 16 int i; 17 for (i = 0; i < nfds; ++i) 18 { 19 if (events[i].data.fd == STDIN_FILENO) 20 printf("hello world "); 21 } 22 } 23 }
输出结果:
分析:当用户输入一组字符,这组字符被送入缓冲区,因为缓冲区由空变成不空,所以ET返回读就绪,输出”hello world”。之后再次执行epoll_wait,但ET模式下只会通知应用进程一次,故导致epoll_wait阻塞。 如果用户再次输入一组字符,导致缓冲区内容增多,ET会再返回就绪,应用进程再次输出”hello world”。 如果将上面的代码中的event.events = EPOLLIN | EPOLLET;改成event.events = EPOLLIN;,即使用LT模式,则运行程序后,会一直输出hello world。
2. 测试代码:
1 #include <unistd.h> 2 #include <stdio.h> 3 #include <sys/epoll.h> 4 5 int main() 6 { 7 int epfd, nfds; 8 char buf[256]; 9 struct epoll_event event, events[5]; 10 epfd = epoll_create(1); 11 event.data.fd = STDIN_FILENO; 12 event.events = EPOLLIN; // LT是默认模式 13 epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event); 14 while (1) { 15 nfds = epoll_wait(epfd, events, 5, -1); 16 int i; 17 for (i = 0; i < nfds; ++i) 18 { 19 if (events[i].data.fd == STDIN_FILENO) 20 { 21 read(STDIN_FILENO, buf, sizeof(buf)); 22 printf("hello world "); 23 } 24 } 25 } 26 }
输出:
分析:
实验2中使用的是LT模式,则每次epoll_wait
返回时我们都将缓冲区的数据读完,下次再调用epoll_wait
时就会阻塞,直到下次再输入字符。
如果将上面的程序改为每次只读一个字符,那么每次输入多少个字符,则会在屏幕中输出多少个“hello world”。
3. 测试代码:
1 #include <unistd.h> 2 #include <stdio.h> 3 #include <sys/epoll.h> 4 5 int main() 6 { 7 int epfd, nfds; 8 struct epoll_event event, events[5]; 9 epfd = epoll_create(1); 10 event.data.fd = STDIN_FILENO; 11 event.events = EPOLLIN | EPOLLET; 12 epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event); 13 while (1) 14 { 15 nfds = epoll_wait(epfd, events, 5, -1); 16 int i; 17 for (i = 0; i < nfds; ++i) 18 { 19 if (events[i].data.fd == STDIN_FILENO) 20 { 21 printf("hello world "); 22 event.data.fd = STDIN_FILENO; 23 event.events = EPOLLIN | EPOLLET; 24 epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &event); 25 } 26 } 27 } 28 }
输出结果:
对标准输入文件描述符使用ET模式进行监听。当我们输入任何输入并接下回车时,屏幕中会死循环输出”hello world”。
分析:
实验3使用ET模式,但是每次读就绪后都主动对描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件,由上面的描述我们可以知道,会再次触发读就绪,这样就导致程序出现死循环,不断地在屏幕中输出”hello world”。但是,如果我们将EPOLL_CTL_MOD 改为EPOLL_CTL_ADD,则程序的运行将不会出现死循环的情况。
实验二
问题:
1.阻塞读数据(不用epoll),你说读到一半有新消息又来了怎么办?
2.非阻塞读数据(不用epoll),你说读到一半有新消息又来了怎么办?
3.epoll的ET模式时,如果数据只读了一半,也就是缓冲区的数据只读了一点,然后又来新事件了怎么办?
答案:
1:来了就来了呗,读就是了啊。可能我们一次读到两次发过来的消息。
2:来了就来了呗,读就是了啊。可能我们一次读到两次发过来的消息。
3:单线程/进程不会有任何问题,多进程/多线程我们只需要设置EPOLLONESHOT这个参数就好了
客户端代码:(下面四个示例都是同一个客户端)
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 #include <sys/socket.h> 5 #include <strings.h> 6 #include <string.h> 7 #include <ctype.h> 8 #include <arpa/inet.h> 9 10 int main() 11 { 12 int sock, n; 13 sock= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 14 if(sock < 0) 15 return 0; 16 17 struct sockaddr_in servaddr; 18 memset(&servaddr,0,sizeof(servaddr)); 19 servaddr.sin_family = AF_INET; 20 servaddr.sin_port = htons(8888); 21 servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); 22 23 if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) 24 return 0; 25 char *buf1 = "hello "; 26 n = write(sock, buf1, strlen(buf1) + 1); 27 printf("%d : buf = %s ", n, buf1); 28 29 sleep(1); 30 31 char *buf2 = "world "; 32 n = write(sock,buf2,strlen(buf2) + 1); 33 printf("%d : buf = %s ", n, buf2); 34 35 sleep(2); 36 37 char *buf3 = "陈明东"; 38 n = write(sock,buf3,strlen(buf3) + 1); 39 printf("%d : buf = %s ", n, buf3); 40 41 sleep(10); 42 close(sock); 43 }
输出:
1. 服务端阻塞读:
1 nclude <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 #include <sys/socket.h> 5 #include <strings.h> 6 #include <string.h> 7 #include <ctype.h> 8 #include <arpa/inet.h> 9 10 #define SERV_PORT 8888 11 12 int main(void) 13 { 14 int sfd, cfd; 15 int len, i; 16 char buf[BUFSIZ], clie_IP[BUFSIZ]; 17 18 struct sockaddr_in serv_addr, clie_addr; 19 socklen_t clie_addr_len; 20 21 sfd = socket(AF_INET, SOCK_STREAM, 0); 22 bzero(&serv_addr, sizeof(serv_addr)); 23 serv_addr.sin_family = AF_INET; 24 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); 25 serv_addr.sin_port = htons(SERV_PORT); 26 27 bind(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); 28 29 listen(sfd, 64); 30 printf("wait for client connect ... "); 31 32 clie_addr_len = sizeof(clie_addr_len); 33 cfd = accept(sfd, (struct sockaddr *)&clie_addr, &clie_addr_len); 34 printf("client IP:%s port:%d ", 35 inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)), 36 ntohs(clie_addr.sin_port)); 37 38 while (1) 39 { 40 printf("sleep "); 41 sleep(2); 42 int len = read(cfd, buf, 1024); 43 if (0 == len) 44 { 45 printf("客户端退出 "); 46 close(cfd); 47 break; 48 } 49 /*把读到的数据打印出来*/ 50 printf("%d: ", len); 51 for (int i = 0; i < len; ++i) 52 printf("%c", buf[i]); 53 printf(" "); 54 } 55 close(sfd); 56 return 0; 57 }
输出:
- 客户端
- 服务端