笔者之前写过关于epoll的两篇博文:浅谈epoll的水平触发与边沿触发_码农诗人的博客-CSDN博客和Nginx事件模块学习之epoll_码农诗人的博客-CSDN博客仅仅只是对epoll模型的基本用法做一记录学习。本篇主要对epoll模型的代码实现及epoll的惊群效应做以验证加深理解。
概念
如上图,服务器端程序往往为了服务端的并发量及处理数据强度增多而选择多进程或多线程。从而出现上述的实现结构:主进程负责创建监听socket并绑定、监听程序,然后创建多个子进程去accept监听socket上的连接事件。这样在有一个客户端连接到来时,同时会惊醒所有在accept监听的子进程,这种现象被称为惊群现象。
案例
案例代码如下:
/**************************************** author : lijd date : 2022-09-09 ****************************************/ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <fcntl.h> #include <stdlib.h> #include <sys/epoll.h> #include <pthread.h> #define true 1 #define false 0 #define MAX_EVENT_NUMBER 1024 #define BUFFER_SIZE 10 #define PROCESS_NUM 3 /*将文件描述符设置为非阻塞*/ int setnonblocking(int fd) { int old_option = fcntl(fd, F_GETFL); int new_option = old_option | O_NONBLOCK; fcntl(fd, F_SETFL, new_option); return old_option; } /*将文件描述符fd上的EPOLLIN注册到epollfd指示的epoll内核事件表中。 参数enable_et 指定是否对fd采用ET模式*/ void addfd(int epollfd, int fd, int enable_et) { struct epoll_event event; event.data.fd = fd; event.events = EPOLLIN; if(enable_et){ event.events |= EPOLLET; } epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event ); setnonblocking(fd); } /*LT 模式的工作原理*/ void lt(struct epoll_event *events, int number, int epollfd, int listenfd) { char buf[BUFFER_SIZE]; int i = 0; for(i= 0;i<number;i++){ int sockfd = events[i].data.fd; if(sockfd == listenfd){ struct sockaddr_in client_address; socklen_t client_addrlength = sizeof(client_address); int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength); addfd(epollfd, connfd, false); }else if(events[i].events & EPOLLIN){ /*只要socket读缓存中的还有未读的数据,此代码就能触发*/ printf("event trigger once\n"); memset(buf, 0, BUFFER_SIZE); int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0); if(ret <= 0){ close(sockfd); continue; } printf("get %d bytes of content: %s\n", ret, buf); }else{ printf("Something else happen !!!\n"); } } } /*ET 模式的工作流程*/ void et(struct epoll_event* events, int number, int epollfd, int listenfd) { char buf[BUFFER_SIZE]; int i=0; for(i =0; i<number; i++){ int sockfd = events[i].data.fd; if(sockfd == listenfd){ struct sockaddr_in client_address; socklen_t client_addrlength = sizeof(client_address); int connfd = accept(listenfd, (struct sockaddr* )&client_address, &client_addrlength); addfd(epollfd, connfd, true);/*对connfd开启ET模式*/ }else if(events[i].events & EPOLLIN){ /*这段代码不会被重复触发,所以我们循环读取数据,以确保把socket缓冲区的数据全部读取*/ printf("Event trigger once !!!\n"); while(1){ memset(buf, 0, BUFFER_SIZE); int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0); if(ret < 0){ /*对于非阻塞IO,下面的事件成立标识数据已经全部读取完毕。此后,epoll就能再次触发sockfd上的sockfd的EPOLLIN事件,以驱动下一次读操作*/ if((errno == EAGAIN) || (errno == EWOULDBLOCK)){ printf("read later\n"); break; } close(sockfd); break; }else if(ret == 0){ close(sockfd); }else{ printf("-----pid:%d------get %d bytes of content: %s\n", getpid(), ret, buf); } } }else{ printf("something else happen\n"); } } } int main(int argc, char *argv[]) { if(argc <= 2){ printf("Usage: %s ip_address port_number\n", argv[0]); return 1; } const char *ip = argv[1]; int port = atoi(argv[2]); int ret = 0; struct sockaddr_in address; bzero(&address, sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET, ip, &address.sin_addr); address.sin_port = htons(port); int listenfd = socket(AF_INET, SOCK_STREAM, 0); assert(listenfd >= 0); ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address)); assert(ret != -1); int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); ret = listen(listenfd, 5); assert(ret != -1); printf("-----main process------pid:%d, ret:%d\n", getpid(), ret); pid_t pid; int i = 0; for(i = 0; i < PROCESS_NUM; i++) { pid = fork(); if( 0 == pid ) //子进程正在运行 { printf("-----fork sucess------pid:%d-----------\n", getpid()); break; } } if(pid == 0) { struct epoll_event events[MAX_EVENT_NUMBER]; int epollfd = epoll_create(5); assert(epollfd != -1); addfd(epollfd, listenfd, true); while(1){ int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1); if(ret<0){ printf("epoll failure\n"); break; } //lt(events, ret, epollfd, listenfd); et(events, ret, epollfd, listenfd); } }else{ while(1){ sleep(1000); } } close(listenfd); return 0; }
编译成功后,利用strace命令执行进程记录进程的系统调用日志如下:
再查看一下追踪日志如下:
由上述追踪日志可得:主进程fork出的三个子进程在一个连接到来时同事被唤醒去accept连接,只有一个进程的accept成功,另外两个进程返回EAGAIN错误码即没有争取到连接,这种现象为惊群现象。