高并发服务器架构笔记(1)——poll 和 epoll

来源:互联网 发布:淘宝微淘怎么做 编辑:程序博客网 时间:2024/06/10 21:18
  • signal(SIGPIPE, SIG_IGN); //忽略 SIGPIPE 信号

    • 如果客户端关闭套接字 close(sockfd);
      而服务器调用了一次write()
      这时候服务器会产生一个 SIGPIPE 信号。
  • TIME_WAIT 状态对高并发服务器有不利影响。

    • 应尽可能避免在服务器端出现 TIME_WAIT 状态,因为会耗费大量比必要的资源。
    • 协议设计上,应尽可能让客户端先 close,这样就能把TIME_WAIT 状态分散到大量的客户端。
    • 如果有个别不活跃的客户端,但又不 close,服务端也要有个机制来踢掉不活跃的客户端。
  • listenfd = socket(PF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_TCP);

    • SOCK_NONBLOCK 非阻塞套接字
    • SOCK_CLOEXEC 在 fork 之后 exec 的时候,关掉这个描述符,http://blog.csdn.net/chrisniu1984/article/details/7050663
  • 取 vector 元素的首地址,&(*v.begin());

poll

  • poll 第三个参数为负数,表示 永远等待,不设超时
  • 假如有一个std::vector v, 值为{1, 2, 3, 4, 5, 6},现在有一个 迭代器 it 指向 4 这个值,v.erase(it) 后, 值为{1, 2, 3, 5, 6},it 指向 4 原来的位置(现在的5),所以如果在循环中遍历要先 –it, 因为 for 的最后一步是 ++it,直接指向 6,而忽略 5 了。

  • 如果 read 没有把 connfd 所对应的接收缓冲区的数据都读完,那么connfd 仍然是活跃的。

  • 当我们 read 一个 socket 时(poll 情况下),如果如果一次没读完数据,那么这个数据可能是无效的,我们需要把数据存在缓冲区里,直至读完才使用数据。(poll 的电平触发机制不用while(1)read()可以直接read(); 因为下次会继续触发 EPOLLIN 事件,与 epoll 的 ET 不同。)

  • POLLOUT 事件的触发条件: connfd 的发送缓冲区不满(可以容纳数据)。

  • 使用 poll 时,不能一接收到 connfd 就马上关注POLLOUT 事件,如果我们每有一个新连接就马上关注 POLLOUT 事件,这个 connfd 的缓冲区一直可写,但我们暂时不需要发送数据,那么 poll 会一直触发 POLLOUT 事件,造成busy-loop。
    正确做法应该是添加一个应用层缓冲区。
    以 echo server 为例
connfd POLLIN 事件到来read(connfd, ....);ret = write(connfd, buf, 10000);if(ret < 10000){    // 将未发完的数据添加到应用层缓冲区 OutBuffer    // 继续关注connfd 的 POLLOUT 事件}// 到这里已经可以发送了//取出应用层缓冲区中的数据发送write(connfd, ...);如果应用层缓冲区中的数据发送完毕,取消关注 POLLOUT事件

总结:POLLOUT 需要写时才关注,数据全发出去才取消关注 POLLOUT

  • poll 和 epoll 的 LT 使用几乎一样。
  • accept(2) 返回 EMFILE 的处理。
    • 表示描述符用完了
    • 解决方法
      • 调高进程文件描述符数目(治标不治本)
      • 死等。当描述符到达上限时,新的需求要等到旧的释放套接字后再用被释放的套接字(效率低)。
      • 退出程序。(小题大做,且不满足 7 * 24 小时不间断服务)
      • 关闭监听套接字 listenfd。(不现实)
      • 如果是 epoll,可改用 ET 模式。(后面会说)
      • (推荐)一开始就准备一个空闲的文件描述符,再 accept( ), 拿到 socket 连接的文件描述符, 随后马上 close( ),这样就优雅的拒绝了与客户端的连接。最后再次打开这个空闲文件
int idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);// ... while (1){    nready = poll(&*pollfds.begin(), pollfds.size(), -1)    // ...    if (pollfds[0].revens & POLLIN)    {        if (connfd == -1)        {            if (errno == EMFILE)            {                close(idlefd);                idlefd = accept(listenfd, NULL, NULL);                // 先接收再拒绝                close(idlefd);                idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);                continue;            }            else            {                //.....            }        }    }}
  • 每次调用 poll 函数的时候,都需要把监听套接字与已连接的感兴趣的描述符数组拷贝到内核,效率比较低。

epoll

  • 两个结构体
typedef union epoll_data{    void *ptr;    int fd;    uint32_t u32;    uint64_t u64;}epoll_data_t;struct epoll_event{    uint32 events;      /*需要监听的事件,例如 EPOLLIN */    epoll_data_t data;  /*某个套接字,例如 listenfd */}
  • 两种模式:LT 和 ET(默认为 LT)
    • LT 和 poll 几乎一样,唯一不同的是:epoll 的 LT 返回结果时,不需要再所有套接字中遍历,因为epoll_wait()返回的都是活跃的套接字。
  • epoll 能够管理的描述符的个数取决于系统资源。

  • epollfd = epoll_create();

    • 这个函数返回一个epoll 描述符。
  • epoll_ctl = (epollfd, EPOLL_CTL_ADD, listenfd, &event);
    • EPOLL_CTL_ADD 表示添加关注。
    • 把 listenfd 添加到 epollfd 所关注的事件。
  • epoll_wait()
    • 第一个参数就是上面创建的 epoll 描述符。
    • 第二个参数是一个数组的首地址,用于存放返回的活跃事件()存放结果。
    • 第三个参数是数组大小。
    • 第四个是等待时间。
  • 调用 epoll_wait( ) 的时候不需要从用户空间把感兴趣的描述符数组传递到内核空间,高效。(poll 需要)。
  • 使用 ET 模式时如果 出现 EMFILE 错误,比较难处理。而在 LT 模式下,可以用先创建空描述符的方式处理,因为该描述符一直可读/写,那么下一次 epoll_wait() 时,LT 模式下还会触发。而 ET 不会再触发了。
  • 使用 epoll 的一个要点就是掌握触发条件。
  • 每一个套接字都有两个缓冲区,接收缓冲区和发送缓冲区。

  • LT 电平触发(在高电平时持续触发

    • EPOLIN (有数据来,可读)
      • 内核中的 socket 接收缓冲区为空 低电平 不触发
      • 内核中的 socket 接收缓冲区非空 高电平 触发
    • EPOLLOUT(可往套接字写数据)
      • 内核中的 socket 发送缓冲区为非满 高电平 触发
      • 内核中的 socket 发送缓冲区为满 低电平 不触发
  • ET 边沿触发(从低电平转化为高电平时触发,非持续

    • 低电平 -> 高电平 触发 (略,高低电平条件同上)
  • LT 新建连接后不能马上关注 EPOLLOUT 事件,否则 busy-loop。

  • ET 可以马上关注 EPOLLOUT 事件。

  • LT 一次没 read 完没关系,下次继续触发。

  • ET 必须一次接接收完, 否则下次不再触发,遗漏数据。

  • 已连接套接字数量不大,并且这些套接字非常活跃, 如果使用 epoll 的话,那么系统的开销主要用于不停地调用 callback 函数。可能比 poll select 效率更低。(数量不多,遍历开销不高)。在处理高并发时 epoll 才有优势。

阅读全文
0 0