模型图

1.png

单线程处理

  • 我们自己会怎样写单线程处理多个客户端连接呢?我们知道在linux里面中每个网络连接在内核中都是文件描述符(Fd)的形式存在,为了使大家看得明白,我们使用一段伪代码来编写一个单线程网络服务器,以下伪代码中我们需要用程序判断当前Fdx是否有数据,这个其实过程还是有些慢的,下面让我们看一下select/poll/epoll的原代码是怎么写的。
while (1){
    for(Fdx in (Fd1~Fd5)) { # Fdx 为当前遍历到的文件;Fd1~Fd5为这5个网络连接在内核中的文件描述符
        if (Fdx) { # 是否有数据
            # 读取Fdx并处理数据
        }
    }
}

select 函数源码

# 准备文件描述符的数组 Fds

# 创建socket服务端
sockfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&addr, 0, sizeof (addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd,(struct sockaddr*)&addr ,sizeof(addr));
listen (sockfd, 5); 

# 创建了5个文件描述符
for (i=0;i<5;i++) 
  {
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    fds[i] = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    # 计算出一个最大的文件描述符
    if(fds[i] > max)
        max = fds[i]; 
  }

  # 通过以上程序我们准备好了一个文件描述符的集合假如是1 3 6 7 9 和 一个最大的文件描述符 9

  while(1){
    FD_ZERO(&rset);
      for (i = 0; i< 5; i++ ) {
          FD_SET(fds[i],&rset);
      }

       puts("round again");
  # select参数依次含义:最大的文件描述符 + 1 (用于内核截取文件描述符); 读文件描述符集合; 写文件描述符集合;异常文件描述符集合; 超时时间;  
                 
  # rset参数接受的并不是Fd集合,而是接受了FD_SET,FD_SET的类型是一个bitmap
  # 假如我们文件描述符集合是 1 3 6 7 9,那么我们bitmap里面存的值就是0101001101000...  (1024位01的位置坑位)。
  
  # 无数据时select阻塞
    select(max+1, &rset, NULL, NULL, NULL);
  # 有数据时select会把Fd置位并返回向下执行。
    
    for(i=0;i<5;i++) {
                # 循环判断哪个Fd被置位了,然后读取数据并且处理
        if (FD_ISSET(fds[i], &rset)){
            memset(buffer,0,MAXBUF);
            read(fds[i], buffer, MAXBUF);
            puts(buffer);
        }
    }    
  }
  • select 函数执行过程过程中会把用户态空间rset的bitmap类型的数据复制到内核态一份,由内核判断Fd是否有数据。我们知道用户态程序判断效率一定是低于内核去判断的,因为用户态判断也是在询问内核,它是有一个用户态到内核态的切换的,判断每一个Fd都需要用户态到内核态的切换。如果没数据的话,select函数是一个阻塞函数,会一直阻塞在select所在的行。如果有数据的话,内核会把有数据的Fd置位(可以理解为标识成有数据)并且返回,不在阻塞。

2.png

select 缺点

  • rset 的 bitmap 类型有大小限制1024位,即FD_SET的大小限制为1024位
  • FD_SET是不可重用,rset 的数据会被内核置位修改,每次while循环的时候都需要重新赋值
  • 虽然是比我们在用户态判断的消耗好些,但是 rset 数据从用户态复制到内核态仍然是有很大的开销的
  • select 函数返回的时候说明至少有一个Fd有数据(被置位了),但是我们并不知道是哪一个Fd或者那些Fd被置位了,需要我们再去遍历一边哪个被置位了,需要o(n)的复杂度的。

poll 函数源码

# 声明结构图pollfd

struct pollfd {
      int fd;
      short events; 
      short revents;
};
  # 构造pollfd数据
  for (i=0;i<5;i++) 
  {
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    pollfds[i].fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    # POLLIN 读事件
    pollfds[i].events = POLLIN;
  }
  sleep(1);

  while(1){
      puts("round again");
        # 传入pollfds 结构体
        # 5 传入的数量
        # 50000 超时时间
        # 也是阻塞函数
    poll(pollfds, 5, 50000);
         
    for(i=0;i<5;i++) {
        if (pollfds[i].revents & POLLIN){
            pollfds[i].revents = 0;
            memset(buffer,0,MAXBUF);
            read(pollfds[i].fd, buffer, MAXBUF);
            puts(buffer);
        }
    }
  }
  • 同select一样,poll函数也阻塞函数,只不过poll传入的是一个数组类型的结构体,poll函数也会把用户态数据复制到内核态置位,但是select置位的是bitmap(导致不可从重用),而poll置位的是结构体中的revents(初始化为0,有数据置位为POLLIN)字段。同理,无数据时poll堵塞,有数据时执行读操作并处理数据,并把revents设置为0,所以pollfd数据并没有发生改变可以复用。

poll函数解决了select的哪些问题

  • pollfd这种结构体,解决了bitmap 1024位的限制
  • 结构体的revents字段在有数据是被置位(POLLIN),我们遍历读取的时候重新设置为0,所以pollfds可以重用,解决了select中 FD_SET 不可重用的弊端。

epoll 函数源码

  # 创建一个epfd,在执行epoll_wait时需要,epfd 就相当于一种空的数据格式
  struct epoll_event events[5];
  int epfd = epoll_create(10);
  ...
  ...
  for (i=0;i<5;i++) 
  {
    static struct epoll_event ev;
    memset(&client, 0, sizeof (client));
    addrlen = sizeof(client);
    ev.data.fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);
    ev.events = EPOLLIN;
    # 对 epfd配置,相当于poll声明的结构体,定义一种数据格式,epoll_ctl相当于构造epfd这种数据格式,大概是一个文件描述符对应的一个events事件 (fd-events)
    epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev); 
  }
  # 准备好epfd执行下面程序
  while(1){
      puts("round again");
      nfds = epoll_wait(epfd, events, 5, 10000);
    
    for(i=0;i<nfds;i++) {
            memset(buffer,0,MAXBUF);
            read(events[i].data.fd, buffer, MAXBUF);
            puts(buffer);
    }
  }
  • epoll 也是阻塞函数,无数据时阻塞;有数据时,它也是置位,不过它的置位其实是一种变相的位,会把有数据的fd重拍到最前面位置,之前的select和poll都没有返回值,而epoll是有返回值的,它的返回值是有数据的个数。这样的话我们后续的遍历读取处理也简单了,遍历复杂度由 o(n)次简化为 o(1)次了。

5.png

select/poll/epoll 源码参考文章

Last modification:February 24, 2023
如果觉得我的文章对你有用,请随意赞赏