linux下的IO复用epoll详解

25 篇文章 0 订阅

前言

      I/O多路复用有很多种实现。在linux上,2.4内核前主要是select和poll,自Linux 2.6内核正式引入epoll以来,epoll已经成为了目前实现高性能网络服务器的必备技术。尽管他们的使用方法不尽相同,但是本质上却没有什么区别。本文将重点探讨将放在EPOLL的实现与使用详解。

为什么会是EPOLL

select的缺陷

      高并发的核心解决方案是1个线程处理所有连接的“等待消息准备好”,这一点上epoll和select是无争议的。但select预估错误了一件事,当数十万并发连接存在时,可能每一毫秒只有数百个活跃的连接,同时其余数十万连接在这一毫秒是非活跃的。select的使用方法是这样的:

      返回的活跃连接 =select(全部待监控的连接)。

      什么时候会调用select方法呢?在你认为需要找出有报文到达的活跃连接时,就应该调用。所以,调用select在高并发时是会被频繁调用的。这样,这个频繁调用的方法就很有必要看看它是否有效率,因为,它的轻微效率损失都会被“频繁”二字所放大。它有效率损失吗?显而易见,全部待监控连接是数以十万计的,返回的只是数百个活跃连接,这本身就是无效率的表现。被放大后就会发现,处理并发上万个连接时,select就完全力不从心了。

      此外,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数。


1 /linux/posix_types.h:
2 
3 #define __FD_SETSIZE         1024

 其次,内核中实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即 select要检测的句柄数越多就会越费时。看到这里,您可能要要问了,你为什么不提poll?笔者认为select与poll在内部机制方面并没有太大的差异。相比于select机制,poll只是取消了最大监控文件描述符数限制,并没有从根本上解决select存在的问题。

       

      接下来我们看张图,当并发连接为较小时,select与epoll似乎并无多少差距。可是当并发连接上来以后,select就显得力不从心了。
图 1.主流I/O复用机制的benchmark
 

 

一个例子:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>
#include <fcntl.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#define BACKLOG 100  
 
int main()  
{
    int iListenSock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    inet_aton("0.0.0.0", &addr.sin_addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
 
    int iOpt = 1;
    setsockopt(iListenSock, SOL_SOCKET, SO_REUSEADDR, &iOpt, sizeof(iOpt)); // 标配
    bind(iListenSock, (sockaddr*)&addr, sizeof(addr));
    listen(iListenSock, BACKLOG);
  
    epoll_event ev;
	ev.data.fd = iListenSock;
	ev.events = EPOLLIN;
	
    epoll_event events[BACKLOG + 1];
 
    // 告诉内核监测的数目, 返回的epollFD为epoll管理句柄
	int epollFD = epoll_create(BACKLOG + 1); 
    // 将ev和对应的iListenSock添加到epoll句柄,用于被epollFD管理  
    epoll_ctl(epollFD, EPOLL_CTL_ADD, iListenSock, &ev);  
    while(1) 
	{
		int timeoutMS = -1; // 永不超时
        // events和nfds是一对输出值
        int nfds = epoll_wait(epollFD, events, BACKLOG + 1, timeoutMS);   
		printf("nfds is %d\n", nfds);
		
        for(int i = 0; i < nfds; i++) 
        {  
            // 用于监听客户端连接的socket
            if(events[i].data.fd == iListenSock)         
            {  
                int iConnSock = accept(iListenSock, NULL, NULL);
                if (iConnSock < 0)
            	{
                    continue;
            	}
 
                ev.data.fd = iConnSock;
                ev.events = EPOLLIN;
                // 将ev和对应的iConnSock添加到epoll句柄,用于被epollFD管理
                epoll_ctl(epollFD, EPOLL_CTL_ADD, iConnSock, &ev); 
 
                printf("new sock came, fd is %d\n", iConnSock);
            }else 
            {
                int iConnSock = events[i].data.fd;      // 用于通信的socket
				char szBuf[1024] = {0};
                int recvLen = recv(iConnSock, szBuf, sizeof(szBuf) - 1, 0);
                if (recvLen > 0) 
                {  
                    printf("recv data [%s] from fd [%d]\n", szBuf, iConnSock);
                }else if(0 == recvLen)
                {  
                    ev.data.fd = iConnSock;
                    epoll_ctl(epollFD, EPOLL_CTL_DEL, iConnSock, &ev);
                    close(iConnSock);
                    printf("connection closed, local fd is [%d]\n", iConnSock);
                }else
                {
                    ev.data.fd = iConnSock;
                    epoll_ctl(epollFD, EPOLL_CTL_DEL, iConnSock, &ev);
                    close(iConnSock);
                    printf("recv error, local fd is [%d]\n", iConnSock);
				}
            }  
        }  
    }  
	
    close(epollFD);
    close(iListenSock);
    return 0;
}  
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>
#include <fcntl.h>
 
int main()
{
    int sockClient = socket(AF_INET, SOCK_STREAM, 0);
 
    struct sockaddr_in addrSrv;
    addrSrv.sin_addr.s_addr = inet_addr("127.0.0.1");
    addrSrv.sin_family = AF_INET;
    addrSrv.sin_port = htons(8888);
    connect(sockClient, ( const struct sockaddr *)&addrSrv, sizeof(struct sockaddr_in));
 
	char szSendBuf[100] = "this is me";	
	while(1)
	{
		send(sockClient, szSendBuf, strlen(szSendBuf) + 1, 0);   
		scanf("%s", szSendBuf);
	}
 
    close(sockClient);
	return 0;
}

  编译链接后, 先启动服务端, 然后在同一机器上启动三个不同的客户端, 此时,在服务端界面, 结果如下:
xxxxxx:~/network> ./server
nfds is 1
new sock came, fd is 5
nfds is 1
recv data [this is me] from fd [5]
nfds is 1
new sock came, fd is 6
nfds is 1
recv data [this is me] from fd [6]
nfds is 1
new sock came, fd is 7
nfds is 1
recv data [this is me] from fd [7]

下面来介绍epoll相关函数

#include<sys/epoll.h>
 
int epoll_create(int size);
int epoll_create1(int flags);

       epoll_create创建一个epoll实例,返回epoll实例的文件描述符,后面epoll_ctl,epoll_wait会引用该文件描述符来指示该epoll。最初的epoll_create实现中,size参数告诉内核,调用者期望添加到epoll实例中的文件描述符数量。 内核使用该信息作为初始分配内部数据结构空间大小的“提示”(如果调用者的使用超过了size,则内核会在必要的情况下分配更多的空间)。目前,这种“提示”已经不再需要了,内核动态的改变数据结构的大小,但是为了保证向后兼容性(新的epoll应用运行于旧的内核上),size参数还是要大于0。
         epoll_create1,如果flags参数为0,则除了省略了size参数之外,它与epoll_create是相同的。如果flags参数不为0,则目前它只能是EPOLL_CLOEXEC,用于设置该描述符的close-on-exec(FD_CLOEXEC)标志。
         这两个系统调用,成功时返回非负的文件描述符,失败时返回-1.

#include<sys/epoll.h>
 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);


        该系统调用控制在epoll实例epfd(epoll_create创建)上的执行动作。在epoll实例上注册、注销、更改文件描述符。
        该调用会在描述符fd上执行op操作,op的值可以有:
        EPOLL_CTL_ADD,在epoll实例epfd上注册文件描述符fd,并将事件event关联到fd。
        EPOLL_CTL_MOD,更改fd上关联的事件为event
        EPOLL_CTL_DEL,在epoll实例epfd上,删除(注销)描述符fd。event参数被忽略,可以为NULL。

        event参数,描述了关联到fd上的事件对象。struct epoll_event的定义如下:

typedef union epoll_data 
{
   void        *ptr;
   int          fd;
   uint32_t     u32;
   uint64_t     u64;
} epoll_data_t;
 
struct epoll_event 
{
   uint32_t     events;      /* Epoll events */
   epoll_data_t data;        /* User data variable */
};

        成员events是由下列值组成的位数组:

        EPOLLIN,文件可读;
        EPOLLOUT,文件可写;
        EPOLLRDHUP对端关闭了连接,或者半关闭了写操作。该标志,在边沿触发模式下检测对端关闭时,是很有用的;                    EPOLLPRI,读操作上有紧急数据;
        EPOLLERR,文件描述符上发生了错误。epoll_wait始终监听该事件,无需再events上设置;
        EPOLLHUP,文件描述符上发生了Hang up。epoll_wait始终监听该事件,无需再events上设置;
        EPOLLET,置文件描述符为边沿触发模式,epoll上默认的行为模式是水平触发模式;
        EPOLLONESHOT (since Linux 2.6.2),置文件描述符为一次性的(one-shot)。这意味着,当该文件上通过epoll_wait触发了                         一个事件之后,该文件描述符就被内部disable了,在epoll实例上不再报告该文件描述符上的事件了。用户必须用                          EPOLL_CTL_MOD调用epoll_ctl,以新的事件掩码再次注册该描述符。

        epoll_ctl成功时返回0,失败返回-1.

#include <sys/epoll.h>
 
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
 
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);

      epoll_wait系统调用在epoll实例epfd上等待事件的发生。成功返回时,events指向的内存空间,会记录触发的事件。maxevents告知内核events的大小。epoll_wait的返回值最大为maxevents, maxevents参数必须大于0.

        当调用返回时,如果检测到事件,就将所有就绪的事件从内核事件表中复制到events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪事件(数组大小为epoll_wait函数的返回值),而不像select和poll的数组参数那样,既用于传入用户注册的事件,又用于输出检测到的事件。

timeout参数指定epoll_wait的阻塞时间,单位为毫秒。epoll_wait会一直阻塞,直到:
        某个文件描述符上触发了一个事件;
        epoll_wait调用被信号中断;
        超时时间到;
        注意,timeout在内部会向上取整到系统时钟粒度,而且由于内核调度的原因,阻塞时间会有少量的延长。如果timeout为-1,意味着epoll_wait会一直阻塞;如果timeout为0,则即使没有事件发生,epoll_wait也会立即返回。

         结构体epoll_event的定义见上,epoll_wait返回时,该结构中的data成员,与调用epoll_ctl(EPOLL_CTL_ADD, EPOLL_CTL_MOD)时的data一样。events成员包含触发的事件掩码。

epoll_wait和epoll_pwait之间的关系,类似于select和pselect之间的关系:epoll_pwait可以使应用程序安全的等待信号的发生。
        下面的语句:

        ready = epoll_pwait(epfd, &events, maxevents, timeout, &sigmask);
       等价于下列语句的原子执行:

sigset_t origmask;
 
sigprocmask(SIG_SETMASK, &sigmask, &origmask);
ready = epoll_wait(epfd, &events, maxevents, timeout);
sigprocmask(SIG_SETMASK, &origmask, NULL);

       如果将sigmask置为NULL,则epoll_pwait等价于epoll_wait。
       epoll_wait成功时返回准备好的文件描述符数,如果超时时间到了,还没有准备好的文件描述符,则返回0.出错时返回-1.
      注意,如果某个线程阻塞于epoll_wait时,另一个线程向相同的epoll实例上添加了新的文件描述符,而且该描述符上的事件触发,则会导致原来阻塞于epoll_wait上的线程停止阻塞。

        在select系统调用中,如果select上监听的描述符,在其他线程上被关闭了,则这种行为是未定义的。某些UNIX系统上,select会停止阻塞直接返回,并且将该描述符视为准备好的(接下来的IO操作会发生错误)。在Linux(以及其他系统)上,在其他线程上关闭该描述符对select无影响,epoll上的处理方式也一样。

三:示例

        1:对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。也就是说只要还有没有处理的事件就会一直通知。

        而对于采用 ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。边沿触发需要一个不同的方式来写程序,通常利用非阻塞IO。并需要仔细检查EAGAIN。

        下面的代码体现了LT和ET在工作方式上的差异。
 

#define MAX_EVENT_NUMBER    1024
#define BUFFER_SIZE         10
 
int setnonblocking( int fd )
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}
 
void addfd(int epollfd,  int fd,  bool enable_et)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;
    if( enable_et )
    {
        event.events |= EPOLLET;
    }
    epoll_ctl( epollfd,  EPOLL_CTL_ADD,  fd,  &event );
    setnonblocking(fd);
}
 
void lt(struct epoll_event* events,  int number,  int epollfd, int listenfd )
{
    char buf[ BUFFER_SIZE ];
    for ( int i = 0; i < number; i++ )
    {
        int sockfd = events[i].data.fd;
        if ( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof( client_address );
            int connfd = accept(listenfd, (struct sockaddr* )&client_address, &client_addrlength );
            addfd( epollfd, connfd, false );
        }
        else if ( events[i].events & EPOLLIN )
        {
            printf( "event trigger once\n" );
            memset( buf, '\0', BUFFER_SIZE );
            int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
            if( ret <= 0 )
            {
                close( sockfd );
                continue;
            }
            printf( "get %d bytes of content: %s\n", ret, buf );
        }
        else
        {
            printf( "something else happened \n" );
        }
    }
}
 
void et( epoll_event* events, int number, int epollfd, int listenfd )
{
    char buf[ BUFFER_SIZE ];
    for ( int i = 0; i < number; i++ )
    {
        int sockfd = events[i].data.fd;
        if ( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof( client_address );
            int connfd = accept(listenfd, (struct sockaddr* )&client_address, &client_addrlength );
            addfd( epollfd, connfd, true );
        }
        else if (events[i].events & EPOLLIN)
        {
            printf( "event trigger once\n" );
            while( 1 )  //循环读取数据,以确保将缓存中的数据全部读出
            {
                memset( buf, '\0', BUFFER_SIZE );
                int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
                if( ret < 0 )
                {
                    //对于非阻塞IO,下面的条件成立表示数据已经全部读取完毕。
                    //此后,epoll就能再次触发sockfd上的EPOLLIN事件。
                    if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) )
                    {
                        printf( "read later\n" );
                        break;
                    }
                    close( sockfd );
                    break;
                }
                else if( ret == 0 )
                {
                    close( sockfd );
                }
                else
                {
                    printf( "get %d bytes of content: %s\n", ret, buf );
                }
            }
        }
        else
        {
            printf( "something else happened \n" );
        }
    }
}
 
int main( int argc, char* argv[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );
 
    int ret = 0;
    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );
 
    int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( listenfd >= 0 );
 
    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );
 
    ret = listen( listenfd, 5 );
    assert( ret != -1 );
 
    struct epoll_event events[ MAX_EVENT_NUMBER ];
    int epollfd = epoll_create( 5 );
    assert( epollfd != -1 );
    addfd( epollfd, listenfd, false);
 
    while( 1 )
    {
        int ret = epoll_wait( epollfd,  events,  MAX_EVENT_NUMBER,  -1);
        if ( ret < 0 )
        {
            printf( "epoll failure\n" );
            break;
        }
    
        lt( events, ret, epollfd, listenfd);
        //et( events, ret, epollfd, listenfd);
    }
 
    close( listenfd );
    return 0;
}

2:即使使用ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题。比如一个线程(或进程,下同)在读取完某个socket的数据后开始处理这此数据,而在数据的处理过程中该socket上又有新数据可读,此时另外一个线程被唤醒来读取这些新的数据(参见下面的代码)。于是就出现了两个线程同时操作一个socket的局面。这当然不是找们期望的。我们期望的是一个socket连接在任一时刻都只被一个线程处理。这一点可以使用epoll的EPOLLONESHOT事件实现。

        对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理某个socket时.其他线程是不可能有机会操作该socket的。但反过来思考,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket。

        下面的代码展示EPOLLONESHOT事件的使用:
 

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024
struct fds
{
   int epollfd;
   int sockfd;
};
 
int setnonblocking( int fd )
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}
 
void addfd( int epollfd, int fd, bool oneshot )
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;
    if( oneshot )
    {
        event.events |= EPOLLONESHOT;
    }
    epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
    setnonblocking( fd );
}
 
void reset_oneshot( int epollfd, int fd )
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
    epoll_ctl( epollfd, EPOLL_CTL_MOD, fd, &event );
}
 
void* worker( void* arg )
{
    int sockfd = ( (fds*)arg )->sockfd;
    int epollfd = ( (fds*)arg )->epollfd;
    printf( "start new thread to receive data on fd: %d\n", sockfd );
    char buf[ BUFFER_SIZE ];
    memset( buf, '\0', BUFFER_SIZE );
    while( 1 )
    {
        int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
        if( ret == 0 )
        {
            close( sockfd );
            printf( "foreiner closed the connection\n" );
            break;
        }
        else if( ret < 0 )
        {
            if( errno == EAGAIN )
            {
                reset_oneshot( epollfd, sockfd );
                printf( "read later\n" );
                break;
            }
        }
        else
        {
            printf( "get content: %s\n", buf );
            sleep( 5 );//休眠5s,模拟数据处理过程
        }
    }
    printf( "end thread receiving data on fd: %d\n", sockfd );
}
 
int main( int argc, char* argv[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );
 
    int ret = 0;
    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );
 
    int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( listenfd >= 0 );
 
    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );
 
    ret = listen( listenfd, 5 );
    assert( ret != -1 );
 
    epoll_event events[ MAX_EVENT_NUMBER ];
    int epollfd = epoll_create( 5 );
    assert( epollfd != -1 );
    addfd( epollfd, listenfd, false );//listenfd不能注册EPOLLONESHOT事件,否则应用程序只能处理一个客户连接,后续的客户连接请求将不再出发listenfd上的EPOLLIN事件。
 
    while( 1 )
    {
        int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
        if ( ret < 0 )
        {
            printf( "epoll failure\n" );
            break;
        }
    
        for ( int i = 0; i < ret; i++ )
        {
            int sockfd = events[i].data.fd;
            if ( sockfd == listenfd )
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof( client_address );
                while((connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength )) > 0)
                {
                    addfd( epollfd, connfd, true );
                }
            }
            else if ( events[i].events & EPOLLIN )
            {
                pthread_t thread;
                fds fds_for_new_worker;
                fds_for_new_worker.epollfd = epollfd;
                fds_for_new_worker.sockfd = sockfd;
                pthread_create( &thread, NULL, worker, ( void* )&fds_for_new_worker );
            }
            else
            {
                printf( "something else happened \n" );
            }
        }
    }
 
    close( listenfd );
    return 0;
}

从工作线程函数worker来看,如果一个工作线程处理完某个socket上的一次请求(用休眠5s来模拟这个过程)之后,又接收到该socket上新的客户请求,则该线程将继续为这个socket服务。并且因为该socket上注册了EPOLLONESHOT事件,其他线程没有机会接触这个socket,如果工作线程等待5s后仍然没收到该socket上的下一批客户数据,则它将放弃为该socket服务。同时,它调用reset_oneshot函数来重置该socket的注册事件,这将使epoll有机会再次检测到该socket的EPOLLIN事件,进而使得其他线程有机会为该socket服务。

        由此看来,尽管一个socket在不同时间能被不同的线程处理,但同一时刻肯定只有一个线程在为它服务。这就保证了连接的完整性,从而避免了很多可能的竟态条件。

参考:

https://blog.csdn.net/gqtcgq/article/details/48767691

https://www.cnblogs.com/lojunren/p/3856290.html

man手册

 

 

 

 


 

 


 

 

 

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux IO复用是指在处理多个I/O事件时,通过一种机制使得一个进程可以同时监听多个I/O操作,从而提高程序的效率和性能。 在Linux系统中,常用的IO复用机制有三种:select、poll和epoll。 1. select:select函数是最早引入的IO复用机制之一,它通过传入一组文件描述符集合,来监听这些文件描述符上是否有事件发生。当其中任意一个文件描述符上有事件发生时,select函数就会返回,然后程序可以通过遍历文件描述符集合来判断哪些文件描述符上有事件发生。 2. poll:poll函数是对select的改进,其使用方式和select类似。不同的是,poll函数使用一个pollfd结构数组来存储待监听的文件描述符及其对应的感兴趣事件,通过调用poll函数时传入这个数组来实现IO复用。相对于select,poll没有最大文件描述符数量的限制,并且效率更高。 3. epollepollLinux下最新的IO复用机制,它提供了更加高效的IO事件通知机制。epoll使用一个文件描述符来管理被监听的其他文件描述符,通过调用epoll_ctl函数向这个文件描述符中注册或者删除需要监听的文件描述符。当某个文件描述符上有事件发生时,epoll_wait函数会返回该文件描述符的相关信息给程序处理。相对于select和poll,epoll在处理大量连接时具有更好的性能。 总结来说,Linux IO复用机制可以让一个进程同时监听多个I/O事件,避免了使用阻塞IO时的等待时间,提高了程序的效率和性能。而select、poll和epoll是常用的IO复用机制。其中,epoll是效率最高的一种机制。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值