并发式IO、IO多路复用

阻塞式IO与非阻塞式IO

可以简单理解为需要做一件事能不能立即得到返回应答,如果不能立即获得返回,需要等待,那就阻塞了(进程或线程就阻塞在那了,不能做其它事情),否则就可以理解为非阻塞(在等待的过程中可以做其它事情)。

阻塞式IO带来的问题

比如当读取键盘和操作鼠标两个事件都是以阻塞方式操作的话,那么两个设备的使用顺序必须严格按照程序的流程走,否则会阻塞住无法走下去。

非阻塞式IO

对于未打开文件在open时使用O_NONBLOCK标志,对于已打开文件使用fcntl添加O_NONBLOCK标志。
该方法可以实现功能,但是由于相当于采用轮询的方式来操作各路io,因此存在占用资源的问题,影响cpu的性能。

fcntl()非阻塞属性

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ )

fd:文件描述符

cmd:操作命令

        1)复制文件描述符(F_DUPFD 或 F_DUPFD_CLOEXEC) 

        2)获取 / 设置文件描述符标志(F_GETFD 或 F_SETFD)

        3)获取 / 设置文件状态标志(F_GETFL 或 F_SETFL)

        4)获取 / 设置异步IO所有权(F_GETOWN 或 F_SETOWN)

        5)获取 / 设置记录锁(F_GETLK 或 F_SETLK)

…:fcntl 函数是一个可变参函数,第三个参数需要根据不同的 cmd 来传入对应的实参,配合 cmd 来使 用。

返回值: 执行失败情况下,返回 -1 ,并且会设置 errno ;执行成功的情况下,其返回值与 cmd (操作命 令)有关,譬如 cmd=F_DUPFD (复制文件描述符)将返回一个新的文件描述符、 cmd=F_GETFD (获取文 件描述符标志)将返回文件描述符标志、cmd=F_GETFL (获取文件状态标志)将返回文件状态标志等。

在调用 open 函数时传入的 flags 标志,可以指定一个或多个(通过位或 | 运算符组合),但是文件权限标志(O_RDONLY 、 O_WRONLY 、 O_RDWR )以及文件创建标志( O_CREAT 、 O_EXCL、 O_NOCTTY 、 O_TRUNC )不能被设置、会被忽略;

在 Linux 系统中,只有 O_APPEND 、 O_ASYNC、O_DIRECT 、 O_NOATIME 以及 O_NONBLOCK 这些标志可以被修改。
对于一个已经打开的文件描述符,可以通过这种方式添加或移除标志。

Linux系统中,可修改的标志

O_APPEND:写入数据时添加到文件末尾

O_ASYNC:使用异步IO模式

O_DIRECT:对该文件进行直接 I/O,写入磁盘中,不使用 VFS Cache

O_NOATIME:读取文件时,不更新文件最后的访问时间

O_NONBLOCK:将该文件描述符设置为非阻塞的(默认都是阻塞的)

文件权限标志

O_RDONLY:以只读方式打开文件

O_WRONLY:以只写方式打开文件

O_RDWR:以读写方式打开文件

文件创建标志

O_CREATE:如果文件不存在则产生该文件

O_EXCL:该标志用于确保是此次调用创建的文件,需要与 O_CREAT 同时使用; 当文件已经存在时,open 函数会返回失败。

O_TRUNC:在打开文件的时候,将文件长度截断为0,需要与O_RDWR或O_WRONLY同时使用。在写文件时,如果是作为新文件重新写入,一定要使用O_TRUNC标志,否则可能会造成旧内容依然存在于文件中的错误,如生成配置文件、pid文件等。

O_NOCTTY:如果打开的是一个终端设备,这个程序不会成为对应这个端口的控制终端,如果没有该标志,任何一个输入,例如键盘中止信号等,都将影响进程。

/*未打开的文件*/
fd = open("/dev/input/mousel", O_RDONLY | O_NONBLOCK);


/*已经打开的文件*/
int flag = -1;        

flag = fcntl(0, F_GETFL);        //获取原先的flag

flag |= O_NONBLOCK;        //添加非阻塞属性

fcntl(0, F_SETFL, flag);        //更新flag

并发式IO

什么是并发式IO?

比如要同时读取几个文件的数据,但是这些文件什么时候可以读取是不确定的,要实现当某个文件可以读取的时候就立马去读取,这就是并发式。

并发式IO分类

1、非阻塞式IO

2、多路复用IO

3、异步IO

多路复用IO(select、poll、epoll)

原理:外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO。
解读:select/poll本身是阻塞函数,当进程调用该函数时会产生阻塞; select/poll函数内部是非阻塞的,它以自动轮询的方式扫描所管理的各路IO;被管理的各路IO均是阻塞方式; select/poll函数检测到某路IO有信号时,立即上报。

select函数

1、select的IO复用计数速度慢的原因:

        1)调用select函数后针对所有文件描述符的循环语句

        2)每次调用select函数都需要向该函数传递监视对象信息(耗时严重)

2、select兼容性好

3、select适用于服务器端接入少和程序应具有兼容性

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

nfds:要轮询的文件描述符的范围,即最大文件描述符的值 + 1

readfds:监听是否有可读取数据的IO

writefds:监听要被写入的IO是否阻塞

exceptfds:监听异常的IO

timeout:设置select阻塞的时间,如果超时则返回

其中,fd_set结构体用来存储复用的IO,以及相应IO的状态

int  FD_ISSET(int fd, fd_set *set);	//用来查看io接口的状态
void FD_SET(int fd, fd_set *set);	//用来添加一个io接口对应的文件描述符
void FD_CLR(int fd, fd_set *set);	//用来去除一个io接口对应的文件描述符
void FD_ZERO(fd_set *set);		//用来清除fd_set结构体的所有内容

int fd=-1,ret=-1; 
fd_set myset;
struct timeval tm;

fd = open(...);//open一路io
FD_ZERO(&myset);	//先清除
FD_SET(fd, &myset);//后设置,这里可以多次调用设置多路io
tm.tv_sec = 10;	//设置超时时间
tm.tv_usec = 0;

ret = select(fd+1, &myset, NULL, NULL, &tm);
if(ret<0){}	//返回错误
else if(ret == 0) {} //select超时
else
{
	if( FD_ISSET(fd, &myset) )	//判断是否为该路IO事件
	{
		//处理此io
	}
}

poll函数

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

 fds:需要被管理的IO

nfds:轮询的范围,即最大文件描述符的值 + 1

timeout:设置poll阻塞的时间,如果超时则返回

其中,fds里包含了所有需要被管理的io,以及该io的配置信息和状态信息。

struct pollfd {
	int   fd;         /* 文件描述符 */
	short events;     /* 有效的状态 */
	short revents;    /* 读取到的状态 */
};

events和revents值为POLLIN(可读) POLLOUT(可写) POLLERR(错误)等。设置poll时,需要指定fd和events。当两者的值相同时,即该io的条件满足。

int ret = -1,fd=-1;
struct pollfd myfds[N]={0};	//定义一个pollfd结构体数组,假设共有N路io,则有N个结构体
fd = open(...);//open一路io
myfds[0].fd = fd;//设置第0项pollfd
myfds[0].events = POLLIN;
ret = poll(myfds, fd+1, 1000);	//调用poll函数,等待1000ms即1s

if(ret<0){}	//返回错误
else if(ret == 0) {} //select超时
else
{
	for(i=0 ; i<N ; i++)
	{
		if( myfds[i].events == myfds[i].revents)	//判断是否为该路IO事件
		{
			//处理此io
		}
	}
}

epoll函数

优于select的epoll

优点:

1、支持打开大数目的socket描述符(相对于select)

2、I\O效率不随fd数目的增加而线性下降(因为每次内核返回的是已就绪的文件描述符)

可显著提高在大量并发连接中只有少量活跃连接下CPU利用率原因:

- 复用文件描述符集合传递结果
- 只遍历被内核IO事件唤醒而加入到队列中的fd集合 

触发机制

* 条件触发(水平触发)——epoll默认工作模式。当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。如果不处理,下一次调用epoll_wait时还会再次向应用程序通告此事件。

* 边沿触发——当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,否则后续的epoll_wait调用将不再向应用程序通知这一事件。

通常边沿触发和非阻塞IO配合使用

边缘触发的优势:可以分离接收数据和处理数据的时间点!

三个重要的API

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* events);
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

应用步骤

1、创建epoll_create() ---- 创建保存epoll文件描述符的空间

头文件:#include <sys/epoll.h>
函数功能:创建保存epoll文件描述符的空间
返回值:成功时返回epoll文件描述符,失败时返回-1
函数原型:int epoll_create(int size);
//size 建议监听数量,返回文件描述符(描述符集合的根节点描述符)

2、控制epoll监听的fd事件:注册、修改、删除

头文件:#include <sys/epoll.h>
函数功能:注册监视对象文件描述符
返回值:成功时返回0,失败时返回-1
函数原型:int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
参数:
epfd——用于注册监视对象的epoll例称的文件描述符
op——EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL分别表示对fd进行的操作:注册、修改、删除
fd——需要注册的监视对象文件描述符
event——监视对象的事件类型

  
struct epoll_event{
      	uint32_t events;    //epoll事件:EPOLLIN, EPOLLOUT, EPOLLERR
	    epoll_data_t data;  //用户数据
  };
 
typedef union epoll_data{
   	void* ptr; //指定与fd相关的用户数据
   	int fd;    //指定满足事件的fd
   	……
   }epoll_data_t;
注意:fd和ptr不能同时使用(联合体),建议在ptr指定的数据中包含fd

epoll_event成员events中可以保存的常量及所指的事件类型:
EPOLLIN:需要读取数据的情况
EPOLLOUT:输出缓冲为空,可以立即发送数据的情况
EPOLLPRI:收到OOB数据的情况
EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用
EPOLLERR:发生错误的情况
EPOLLET:以边缘触发的方式得到事件通知
EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向 epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件。

3、监听 ---- 等待文件描述符发生变化

头文件:#include <sys/epoll.h>
函数功能:等待文件描述符发生变化
返回值:返回有多少个fd就绪,时间:0, 出错:-1
函数原型:int epoll_wait(int epfd, struct epoll_event* event, int maxevent, int timeout);
参数:
epfd——表示事件发生监视范围的epoll例程的文件描述符
events——事件集合,是结构体epoll_event数组(需要动态分配内存)
maxevents——第二个参数中可以保存的最大事件数
timeout    -1: 阻塞等待,0:非阻塞等待,>0:等待时间(ms)

调用方式如下:

Linux epoll机制是通过红黑树和双向链表实现:

1)首先通过epoll_create()系统调用在内核中创建一个eventpoll类型的句柄,其中包括红黑树根节点和双向链表头节点。

2)然后通过epoll_ctl()系统调用,向epoll对象的红黑树结构中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。

3)最后通过epoll_wait()系统调用判断双向链表是否为空,如果为空则阻塞。当文件描述符状态改变,fd上的回调函数被调用,该函数将fd加入到双向链表中,此时epoll_wait函数被唤醒,返回就绪好的事件。

int event_cnt;
struct epoll_event* ep_events;
……
ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);    //EPOLL_SIZE是宏常量
……
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
……

4、程序流程

epoll_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>


#define BUF_SIZE 100
#define EPOLL_SIZE 50

void errorHandling(const char* buf);


int main(int argc, char **argv)
{
  int servSock, clientSock;
  struct sockaddr_in servAddr, clientAddr;
  socklen_t clientAddrSize;
  int strLen, i;
  char buf[BUF_SIZE];

  struct epoll_event* epollEvents;
  struct epoll_event event;
  int epfd, eventCount;

  if(2 != argc){
    printf("Usage: %s <port>\n", argv[0]);
    exit(1);
  }

  //创建套接字
  servSock = socket(PF_INET, SOCK_STREAM, 0);
  if(-1 == servSock){
    errorHandling("socket error");
  }

  memset(&servAddr, 0, sizeof(servAddr));
  servAddr.sin_family = AF_INET;
  servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servAddr.sin_port = htons(atoi(argv[1]));

  //绑定IP地址和port端口号
  if(-1 == bind(servSock, (struct sockaddr *)&servAddr, sizeof(servAddr))){
    errorHandling("bind error");
  }

  //监听套接字
  if(-1 == listen(servSock, 5)){
    errorHandling("listen error");
  }

  //创建epoll句柄
  epfd = epoll_create(EPOLL_SIZE);
  epollEvents = (struct epoll_event*)malloc(sizeof(struct epoll_event)* EPOLL_SIZE);
  
  //添加epoll监听事件
  event.events = EPOLLIN;
  event.data.fd = servSock;
  epoll_ctl(epfd, EPOLL_CTL_ADD, servSock, &event);

  while(1){
    eventCount = epoll_wait(epfd, epollEvents, EPOLL_SIZE, -1);

    if(-1 == eventCount){
      puts("epoll_wait error");
      break;
    }

    for(i=0; i<eventCount; i++){
      if(epollEvents[i].data.fd == servSock){
        clientAddrSize = sizeof(clientAddr);
        clientSock = accept(servSock, (struct sockaddr*)&clientAddr, &clientAddrSize);
        if(-1 == clientSock){
          errorHandling("clientSock error");
        }

        event.events = EPOLLIN;
        event.data.fd = clientSock;
        epoll_ctl(epfd, EPOLL_CTL_ADD, clientSock, &event);
        printf("connected client: %d\n", clientSock);
      }
      else{
        strLen = read(epollEvents[i].data.fd, buf, BUF_SIZE);
        if(0 == strLen) //close request!
        {
            epoll_ctl(epfd, EPOLL_CTL_DEL, epollEvents[i].data.fd, NULL);
            close(epollEvents[i].data.fd);
            printf("closed client:%d\n", epollEvents[i].data.fd);
        }
        else
        {
            write(epollEvents[i].data.fd, buf, strLen); //echo!
        }
      }
    }
  }
  close(servSock);
  close(epfd);
 
  return 0;

}

void errorHandling(const char *buf)
{
  fputs(buf, stderr);
  fputc('\n', stderr);
  exit(1);
}

epoll_client.c 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUF_SIZE 1024
void errorHandling(const char* message);
 
int main(int argc, char* argv[])
{
    int sock = -1;
    char message[BUF_SIZE] = {0};
    int clntDataLen = -1;
    struct sockaddr_in servAddr;
 
    if(argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    //创建套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if(-1 == sock)
        errorHandling("socket() error");
 
    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = inet_addr(argv[1]);
    servAddr.sin_port = htons(atoi(argv[2]));

    //连接IP地址和Port端口号
    if(connect(sock, (struct sockaddr*)&servAddr, sizeof(servAddr)))
        errorHandling("connect() error");
    else
        puts("Connected............");
 
 
    while(1)
    {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
 
        if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;
 
        write(sock, message, strlen(message));
        clntDataLen = read(sock, message, BUF_SIZE);
        message[clntDataLen] = 0;
        printf("Message from server: %s\n", message);
    }
 
    close(sock);
 
    return 0;
}
 
void errorHandling(const char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

异步IO

所谓同步,类似于我知道你什么时候会来,到时间我去接你。而异步,类似于我不知道你什么时候来,当你到的时候告诉我让我去接你。
使用异步io的前提是
底层驱动要有发送SIGIO信号的功能;应用层要有相应的信号处理功能。

 应用层实现的步骤:

1、使用signal函数设置SIGIO信号的捕获函数

signal(SIGIO, func);

2、使用fcntl的F_SETOWN命令,将接收SIGIO信号的进程设置为当前进程。这样,底层驱动就知道应该把SIGIO信号发送给哪个进程。

fcntl(fd, F_SETOWN, getpid());	//fd对应发送SIGIO信号的io的文件描述符
								//getpid()对应当前进程

3、使用fcntl函数,对文件描述符添加O_ASYNC的状态标志,让其支持异步

fcntl(fd, F_SETFL, oldFlag|O_ASYNC);

void func(int sig)	//此函数绑定到sigio信号,处理异步io事件
{
	if(sig != SIGIO) return;
	....//处理事件
}

int main(void)
{
	int fd = -1;
	fd = open(...);//open一路io	
	
	signal(SIGIO, func);//设置SIGIO的捕获函数
	fcntl(fd, F_SETOWN, getpid());	//告诉该io,一有SIGIO信号就发送给当前进程
	
	flag = fcntl(fd, F_GETFL);	//设置该io,让其支持异步IO,即可以发送SIGIO信号
	flag |= O_ASYNC;
	fcntl(fd, F_SETFL, flag);

	while(1) {...}//处理主要的工作
}

select、poll、epoll相关问题

1、利用select函数实现服务器端时,代码层存在的两个缺点是?

1)调用select函数后,针对所有文件描述符的循环语句

2)每次调用select函数时都要传递监视对象信息

 2、无论是select还是epoll,都需要将监视对象文件描述信息通过函数调用传递给操作系统

文件描述符是由操作系统管理,所以必须要借助操作系统才能完成

 3、select和epoll的最大差异在于监视对象文件描述符传递给操作系统的方式

select函数每次调用都要传递所有的监视对象信息,而epoll函数仅向操作系统传递1次监视对象,监视范围或内容发生变化时,只通知发生变化的事项。

select采用这种方法是为了保持兼容性

4、epoll是以条件触发和边沿触发方式工作的。二者有何区别?

条件触发:只要输入缓冲区有数据,就会一直通知该事件。边沿触发中输入缓冲区收到数据时仅注册1次该事件,即使输入缓冲区中还保留数据,也不会再进行注册。

边沿触发:可以分离接收数据和处理数据的时间点,给服务端的实现带来了很大的灵活性。

5、select、poll、epoll的区别

select、poll和epoll都是多路IO复用的机制。 多路IO 复用就通过一种机制,可以监视多个描述符, 一旦某个描述符就绪( 一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

但select 、poll 和epoll 本质上都是同步IO ,因为它们都需要在读写事件就绪后自己负责进行读写,即是阻塞的,而异步IO 则无须自己负责进行读写,异步I/O 的实现会负责把数据从内核拷贝到用户空间。

<>

一般认为poll() 比 select() 要高级一些,原因如下:

1)poll() 不要求开发者在计算最大文件描述符时进行+1 的操作。

2)poll() 在应付大数目的文件描述符的时候速度更快,因为对于 select() 来说内核需要检查大量描述符对应的“ set 中的每一个比特位,比较费时。

3)select() 可以监控的文件描述符数目是固定的,相对来说也较少( 1024 或2048 ) 。如果需要监控数值比较大的文件描述符,或是分布得很稀疏的较少的描述符,效率也会很低。而对于 poll() 函数来说,就可以创建特定大小的数组来保存监控的描述符,而不受文件描述符值大小的影响,而且 poll() 可以监控的文件数目远大于select() 。

4)对于 select() 来说,所监控的fd_set 在 select() 返回之后会发生变化,所以在下一次进入 select () 之前都需要重新初始化需要监控的fd_set, poll() 函数将监控的输入和输出事件分开,允许被监控的文件数组被复用而不需要重新初始化。

5)select () 函数的超时参数在返回时也是未定义的,考虑到可移植性,每次在超时之后在下一次进入到 select () 之前都需要重新设置超时参数。

6、epoll() 的优点

1、支持一个进程打开大数目的socket 描述符( FD ) 

select() 均不能忍受的是一个进程所打开的FD 是有一定限制的,由FD_SETSIZE 的默认值是1024/2048 。对于那些需要支持上万连接数目的IM服务器来说显然太少了。这时候可以选择修改这个宏然后重新编译内核。不过 epoll 则没有这个限制,它所支持的FD 上限是最大可以打开文件的数目,这个数字一般远大于2048 。举个例子,在1 GB 内存的空间中这个数字一般是10 万左右,具体数目可以使用 cat /proc/sys/fs/file-max 查看, 一般来说这个数目和系统内存关系很大。

2、IO 效率不随FD 数目增加而线性下降
传统的select、poll 另一个致命弱点就是当你拥有一个很大的socket 集合,不过由于网络延迟,任一时间只有部分的socket 是“活跃” 的,但是select/poll 每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll 不存在这个问题,它只会对“活跃”的 socket 进行操作一这是因为在内核中实现 epoll 是根据每个 fd 上面的 callback 函数实现的。那么,只有“活跃”的socket 才会主动去调用 callback 函数,其他 idle 状态socket 则不会,在这点上, epoll 实现了一个“伪” AIO ,因为这时候推动力由Linux 内核提供。
 
3、使用mmap 加速内核与用户空间的消息传递
这点实际上涉及epoll 的具体实现。无论是select 、poll 还是epoll 都需要内核把“消息通知给用户空间,如何避免不必要的内存拷贝就显得尤为重要。在这点上, epoll 是通过内核与用户空间mmap 处于同一块内存实现的
对于poll 来说需要将用户传入的 pollfd 数组拷贝到内核空间,因为拷贝操作和数组长度相关,时间上来看,这是一个O(n)操作,当事件发生后, poll 将获得的数据传送到用户空间,并执行释放内存和剥离等待队列等工作,向用户空间拷贝数据与剥离等待队列等操作的时间复杂度同样是O(n) 。

原理
select

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1、单个进程可监视的fd数量被限制

2、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

3、对socket进行扫描时,是线性扫描

poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪,则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它基于链表存储的,但是同样有一个缺点:

》大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义

poll 还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll

前面说的复制耗时问题,epoll使用mmap减少复制开销

还有一个特点:epoll使用 “事件” 的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

FD剧增后带来的IO效率问题
select        因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的 “线性下降” 性能问题。
poll同上
epoll因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

消息传递方式
select内核需要将消息传递到用户空间,都需要内核拷贝动作
poll同上
epollepoll通过内核和用户空间共享一块内存来实现的

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值