面试题答案
一键面试事件驱动模型相较于传统阻塞式I/O模型的优势
- 资源利用效率
- 阻塞式I/O:在I/O操作(如读取网络数据)进行时,线程会被阻塞,无法处理其他任务,这意味着在等待I/O完成的这段时间内,线程资源被浪费,CPU利用率较低。例如,一个简单的服务器程序如果使用阻塞式I/O,当客户端连接较少时,可能还能正常工作,但当客户端数量增多,每个连接的I/O操作都可能阻塞线程,导致服务器无法高效处理所有连接。
- 事件驱动:事件驱动模型采用异步非阻塞I/O,线程不会被I/O操作阻塞。线程可以在等待I/O事件发生的同时,去处理其他任务,大大提高了CPU的利用率和资源利用效率。比如,在一个高并发的Web服务器中,事件驱动模型可以同时处理大量客户端的请求,而不会因为某个客户端的I/O操作未完成而影响其他客户端。
- 并发处理能力
- 阻塞式I/O:要处理多个并发连接,通常需要为每个连接创建一个新的线程或进程。但线程和进程的创建和销毁都有一定的开销,并且过多的线程或进程会消耗大量的系统资源,导致系统性能下降。例如,若有1000个客户端同时连接服务器,使用阻塞式I/O为每个客户端创建一个线程,这会带来很大的线程管理开销和资源消耗。
- 事件驱动:事件驱动模型可以使用单线程或少量线程来处理大量的并发连接。它通过事件循环不断监听I/O事件,当有事件发生时,才会调度相应的回调函数来处理,非常适合处理高并发场景。以Nginx为例,它基于事件驱动模型,能够高效地处理数以万计的并发连接。
- 响应及时性
- 阻塞式I/O:由于线程可能会被阻塞在I/O操作上,对于一些对响应时间要求较高的应用场景,如实时通信应用,可能无法及时响应新的请求或事件。
- 事件驱动:事件驱动模型能够及时响应各种I/O事件,因为线程不会被长时间阻塞,一旦有新的事件发生,就可以立即被处理,从而提高了系统的响应及时性。
使用select、poll或epoll实现简单事件驱动网络服务器
- select实现
- 步骤:
- 初始化套接字:创建一个TCP套接字,绑定到指定的IP地址和端口,并监听连接。
int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); listen(sockfd, BACKLOG);
- 初始化select相关参数:创建
fd_set
集合,将监听套接字添加到读集合中。fd_set read_fds; FD_ZERO(&read_fds); FD_SET(sockfd, &read_fds); int maxfd = sockfd;
- 进入事件循环:
while (1) { fd_set tmp_fds = read_fds; int activity = select(maxfd + 1, &tmp_fds, NULL, NULL, NULL); if (activity < 0) { perror("select error"); break; } else if (activity > 0) { if (FD_ISSET(sockfd, &tmp_fds)) { int connfd = accept(sockfd, NULL, NULL); FD_SET(connfd, &read_fds); if (connfd > maxfd) { maxfd = connfd; } } for (int i = sockfd + 1; i <= maxfd; i++) { if (FD_ISSET(i, &tmp_fds)) { char buf[BUFFER_SIZE]; int n = read(i, buf, sizeof(buf)); if (n <= 0) { close(i); FD_CLR(i, &read_fds); } else { // 处理读取到的数据 write(i, buf, n); } } } } }
- 初始化套接字:创建一个TCP套接字,绑定到指定的IP地址和端口,并监听连接。
- 步骤:
- poll实现
- 步骤:
- 初始化套接字:与select类似,创建TCP套接字,绑定并监听。
- 初始化pollfd数组:
struct pollfd fds[FD_SETSIZE]; fds[0].fd = sockfd; fds[0].events = POLLIN; int nfds = 1;
- 进入事件循环:
while (1) { int activity = poll(fds, nfds, -1); if (activity < 0) { perror("poll error"); break; } else if (activity > 0) { if (fds[0].revents & POLLIN) { int connfd = accept(sockfd, NULL, NULL); fds[nfds].fd = connfd; fds[nfds].events = POLLIN; nfds++; } for (int i = 1; i < nfds; i++) { if (fds[i].revents & POLLIN) { char buf[BUFFER_SIZE]; int n = read(fds[i].fd, buf, sizeof(buf)); if (n <= 0) { close(fds[i].fd); for (int j = i; j < nfds - 1; j++) { fds[j] = fds[j + 1]; } nfds--; i--; } else { // 处理读取到的数据 write(fds[i].fd, buf, n); } } } } }
- 步骤:
- epoll实现
- 步骤:
- 初始化套接字:同样创建TCP套接字,绑定并监听。
- 创建epoll实例:
int epfd = epoll_create1(0); struct epoll_event ev; ev.data.fd = sockfd; ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
- 进入事件循环:
struct epoll_event events[EPOLL_SIZE]; while (1) { int n = epoll_wait(epfd, events, EPOLL_SIZE, -1); if (n < 0) { perror("epoll_wait error"); break; } for (int i = 0; i < n; i++) { if (events[i].data.fd == sockfd) { int connfd = accept(sockfd, NULL, NULL); struct epoll_event ev; ev.data.fd = connfd; ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); } else { int connfd = events[i].data.fd; char buf[BUFFER_SIZE]; int n = read(connfd, buf, sizeof(buf)); if (n <= 0) { close(connfd); epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL); } else { // 处理读取到的数据 write(connfd, buf, n); } } } }
- 步骤:
以上代码示例为简单的C语言实现,实际应用中可能需要更多的错误处理、优化等操作。并且在使用select
时,FD_SETSIZE
需要根据实际情况设置合理的值,epoll
在大规模并发场景下通常性能更优,poll
在一定程度上改进了select
的一些缺点,但epoll
在现代Linux系统中是更推荐用于高并发网络编程的方式。