【itsrohan技术随笔1】阻塞recv,select,epoll用法及实现详解

从事服务端编程,epoll是绕不开的话题,很多著名的开源软件如redis,nginx等都使用了epoll,公司内部的很多产品也用到了epoll。我个人在工作中也用过epoll做开发,但会用只是第一步,完整地理解它的实现,明白它为什么如此高效,才是我的目标,今天写文记录一下,如果哪里写的不准确,希望大家不吝赐教,帮忙指出来。

最开始,先说一下,最普通的场景---阻塞recv的用法,要先明白epoll为什么如此高效,需要先明白最简单的用法有什么问题。

首先,每个socketfd在内核中,都有以下三个成员:发送缓冲区、接收缓冲区、等待队列

代码运行到recv的时候,进程陷入内核,如果当前该socketfd对应的接收缓冲区没有数据的话,内核将进程从运行态切换为阻塞态,并将当前进程加入到该socketfd的等待队列中。如果该连接有数据到达网卡,会触发网卡的中断,进入网卡设备的中断服务程序。

网卡设备的中断服务程序中,首先将数据拷贝到对应socketfd的接收缓冲区,然后把socketfd的等待队列中的进程重新放入就绪列表中,进程重新得到执行。

进程重新执行后,当前socketfd的接收缓冲区已经有数据,直接将数据拷贝到用户传入的buff指针中。

这里有2次拷贝。

 

这种场景的问题:只能同时监听同一个fd。

解决这个问题的方法就是多路复用,历史上先出现的解决方案是select,它的大致原理是:

将fd1,fd2,fd3,一共三个fd,通过select接口传入内核,内核将当前进程同时加入到三个fd的等待队列中。

执行select的时候,陷入内核阻塞,之后的过程和上面的第一种用法类似,数据到达时,内核会拷贝数据到对应fd的接收缓冲区,然后将当前进程放入就绪列表执行。

select通过一次性传入多个fd的方式,实现多路复用,这种方案的问题是:

1. 每次调用select都需要把进程添加进所有fd的等待队列,每次唤起又需要从等待队列中移除,2次遍历,开销比较大;

2. 每次调用select都需要把完整的fd列表传给内核,也有一定的开销;

3. select返回后,需要用户自己遍历所有的fd,看哪个fd有事件发生,这是第三次遍历。

 

epoll是为了解决select的这三个问题而出现的,epoll的使用如下:


int s = socket(AF_INET, SOCK_STREAM, 0);    

bind(s, ...) 

listen(s, ...) 

 

int epfd = epoll_create(...); 

epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中 

 

while(1){ 

    int n = epoll_wait(...) 

    for(接收到数据的socket){ 

        //处理 

    } 

} 

在梳理epoll的实现之前,需要了解一下背景,epoll的出现,是为了解决select和poll的问题:

1. 每次调用select或poll,都需要传输完整的监听fds信息到内核;

2. select或poll返回时,只是通知应用层有fd可读,但却没有告诉应用层哪个fd可读,因此还需要应用层主动进行O(n)的遍历,才能拿到具体的fd进行读取操作;

为了解决第一个问题,内核需要在内核中存储需要监听的fd信息,存储的数据结构需要支持快速增删查改,因为用户可能对这些数据进行修改,这部分内核使用了红黑树来作为数据结构。

为了解决第二个问题,内核需要维护就绪状态fd列表,来返回给应用层,让应用层知道哪个fd有事件发生,这部分内核使用了双向链表作为数据结构,目前链表可以理解,但为什么使用双向链表还不是太理解。

下面说下我对epoll实现的理解:epoll由3个系统调用组成,epoll_create,epoll_ctl,epoll_wait。

epoll_create的时候,创建内核中的epollevent对象。

epoll_ctl的时候,内核对需要监听的fd信息(红黑树上的信息)进行操作,有add,del,mod三种命令。

epoll_wait的时候,内核检测epollevent对象中的就绪fd列表中是否有数据,若没有,则阻塞知道超时;若有,则直接返回该fd信息。

最后,这里还有一个疑点,涉及到内核epoll的实现原理:有网络数据到达的时候,内核如何操作,让有数据到来的fd,出现在就绪列表中的呢?

用户通过epoll_ctl添加fd进epollevent时,内核将该epollevent添加到fd的等待队列中。

如果网卡上有数据到来,内核发现fd等待队列中的是一个epollevent,则会把该fd添加到epollevent对象的就绪列表中,然后把网卡数据拷贝到fd的接收缓冲区。

用户调用epoll_wait时,此时epollevent对象的就绪列表中已经有fd了,则不会阻塞,直接返回,此时应用层已经知道了可读的fd,则直接调用recv即可拿到数据了。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个基于 epoll 技术的 C++ 服务端代码示例,可以实现多个客户端的连接和消息交互: ```cpp #include <iostream> #include <cstring> #include <cstdlib> #include <cstdio> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/epoll.h> using namespace std; const int MAX_EVENTS = 100; const int MAX_CLIENTS = 10; const int BUF_SIZE = 1024; int main(int argc, char *argv[]) { int listen_fd, conn_fd, epoll_fd, n_events; struct sockaddr_in serv_addr, cli_addr; struct epoll_event event, events[MAX_EVENTS]; char buf[BUF_SIZE]; int n_ready; // 初始化服务端地址 memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(5555); // 创建监听套接字 if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket error"); exit(EXIT_FAILURE); } // 绑定监听套接字 if (bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { perror("bind error"); exit(EXIT_FAILURE); } // 开始监听 if (listen(listen_fd, MAX_CLIENTS) < 0) { perror("listen error"); exit(EXIT_FAILURE); } // 创建 epoll 实例 if ((epoll_fd = epoll_create(MAX_CLIENTS)) < 0) { perror("epoll_create error"); exit(EXIT_FAILURE); } // 将监听套接字加入 epoll 实例中 event.events = EPOLLIN; event.data.fd = listen_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event) < 0) { perror("epoll_ctl error"); exit(EXIT_FAILURE); } // 循环等待事件 while (true) { n_ready = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (n_ready < 0) { perror("epoll_wait error"); exit(EXIT_FAILURE); } for (int i = 0; i < n_ready; i++) { if (events[i].data.fd == listen_fd) { // 有新连接 socklen_t cli_len = sizeof(cli_addr); conn_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, &cli_len); if (conn_fd < 0) { perror("accept error"); exit(EXIT_FAILURE); } // 将新连接加入 epoll 实例中 event.events = EPOLLIN; event.data.fd = conn_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &event) < 0) { perror("epoll_ctl error"); exit(EXIT_FAILURE); } cout << "new client connected: " << inet_ntoa(cli_addr.sin_addr) << endl; } else { // 有数据可读 conn_fd = events[i].data.fd; int len = read(conn_fd, buf, BUF_SIZE); if (len < 0) { perror("read error"); exit(EXIT_FAILURE); } else if (len == 0) { // 客户端关闭连接 cout << "client disconnected" << endl; close(conn_fd); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, conn_fd, &events[i]); } else { // 处理客户端发送的消息 buf[len] = '\0'; cout << "recv: " << buf << endl; write(conn_fd, buf, len); } } } } return 0; } ``` 这个示例中,我们创建了一个监听套接字,然后使用 epoll 实例来监听该套接字和所有客户端套接字的事件。当有新客户端连接时,我们将其加入 epoll 实例中,并使用 `read` 函数读取客户端发送的消息;当客户端关闭连接时,我们将其从 epoll 实例中删除。同时,我们使用 `write` 函数向客户端发送消息。 需要注意的是,这个示例仅供参考,具体实现还需要根据自己的需求进行修改和完善。同时,这个示例中没有考虑多线程、多进程等并发情况,如果需要支持更高的并发量,还需要进行相关优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值