Linux学习——多路复用

Linux学习——多路复用这样做用户线程再发起 IO 请求后可以立即返回 如果该次读操作并未读取到任何数据 用户线程需要不断的发起 IO 请求 直到数据到达后 才真正读取到数据 继续执行 整个过程中 虽然用户线程每次发起 IO 请

大家好,欢迎来到IT知识分享网。

目录

一、五种网络I/O模型

二、select()多路复用

三、poll()多路复用

四、epoll()多路复用 


一、五种网络I/O模型

 1.常见的IO模型:

        (1)同步阻塞IO:即传统的IO模型,在linux中默认情况下所有的socket都是阻塞模式。

Linux学习——多路复用

        (2)非同步阻塞IO:默认创建的socket都是阻塞的,同步非阻塞IO是在同步阻塞IO的基础上,将socket设置为NONBLOCK,这个可以使用ioctl()来设置。这样做用户线程再发起IO请求后可以立即返回,如果该次读操作并未读取到任何数据,用户线程需要不断的发起IO请求,直到数据到达后,才真正读取到数据,继续执行,整个过程中,虽然用户线程每次发起IO请求都可以立即返回,但是为了等到数据仍然需要不断轮询、重复请求,消耗大部分CPU资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。

Linux学习——多路复用

        (3)IO多路复用:IO多路复用是建立在select()函数上,使用select函数可以避免同步非阻塞IO模型中的轮询问题,此外poll、epoll都是这种模型。在这种模型下,用户首先将需要进行IO操作的socket添加到select中去,然后阻塞等待select函数返回。当数据到达时,socket被激活,select函数返回,然后系统内核开始read数据并进一步执行操作。使用select最大的优势是用户可以在同一个线程内同时处理多个socket的请求,用户可以注册多个socket,然后不断的调用select读取被激活的socket,即可以达到同一个线程内处理多个IO请求的目的。在同步阻塞模型中,必须调用很多的线程才能达到同样的效果。

Linux学习——多路复用

        (4)信号驱动IO:调用sigaltion系统调用,当内核中IO数据就绪时以SIGIO信号通知请求进程,请求进程再把数据从内核读入到用户空间,这一步是阻塞的。

         (5)异步IO:即经典的Proactor设计模式,也称异步非阻塞IO。“真正” 的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程的指定缓冲区内,内核在IO完成后通知用户线程直接使用即可。

相比于IO多路复用模型,信号驱动IO和异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。

二、select()多路复用

        select()函数允许进程指示内核等待多个事件(文件描述符)中的任何一个发生,并只有在一个或多个事件发生或经历一段指定时间后唤醒她,然后判断究竟是哪个文件描述符发生了事件并进行相应的处理。

#include<sys/select.h>

#include<sys/time.h>

struct timeval

{

        long tv_sec;//seconds

        long tv_usec;//microseconds

};

FD_ZERO(fd_set*           fds)                //清空集合

FD_SET(int fd,fd_set*     fds)                //将给定的描述符加入集合

FD_ISSET(int fd,fd_set*  fds)                //判断指定描述符是否在集合中

FD_CLR(int fd,fd_set*     fds)                //将给定的描述符从文件中删除

int select(int max_fd,fd_set *readset,fd_set *exceptset,struct timeval *timeout);

说明:select监视并等待多个文件描述符属性发生变化,它监视的属性分为三类:分别是readfds(文件描述符有数据到来可读)、writefds(文件描述符可写)、和exceptfds(文件描述符异常)。调用select之后函数会阻塞,直到有描述符就绪(有数据可读、有数据可写、或者错误异常)或者超时(timeout指定等待时间)发生函数才返回。当select()函数返回后,可以通过遍历 fdset,来找到究竟是哪些文件描述符就绪。

1.select函数的返回值是就绪文件描述符的数目,超时返回0,出错返回-1。

2.第一个参数max_fd指待测试的fd的总个数,它的值是待测试的最大文件描述符加1。Linux内核从0开始到max_fd-1扫描文件描述符,如果有数据出现事件(读、写、异常)返回;假设需要监测的文件描述符是8、9、10,那么Linux内核也要检测0~7,此时真正待测试的文件描述符为0~10,共11个;

3.中间三个参数readset、writset和exceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试可以设为NULL;

4.最后一个参数是设置select的超时时间,如果设为NULL,则代表永不超时;

下面是使用select()实现socket客户端的示意图:

Linux学习——多路复用

                                                               

使用select实现服务器端代码:

        progname = basename(argv[0]);

        /*Parser the command line parameters*/

                                /*Get help information*/

        printf(“%s server start to listen on port %d\n”,argv[0],serv_port);

        }

        fds_array[0] = listenfd;

                /*program will blocked here*/

                rv = select(maxfd+1,&rdset,NULL,NULL,NULL);

                /*listen socket get event means new client start connect now*/

                /*data arrive from already connected client*/

                                        printf(“socket[%d] read get %d bytes data\n”,fds_array[i],rv);

                                        /*convert letter from lowercase to uppercase*/

                                        for(j = 0;j < rv;j++)

                                                buf[j] = toupper(buf[j]);

                }

        }

        printf(“%s is a socket server program,which used to verify client and echo back string from it\n”,progname);

        printf(” -b[daemon ] set program running on background\n”);

        printf(” -p[port  ] Socket server port address\n”);

        printf(” -h[help ] Display this help information\n”);

        /*Set socket port reuseable ,fix ‘Address already in use’ bug when socket server restart*/

        setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));

        /*Listen all the local IP address*/

        /*listen the specified IP address*/

 

三、poll()多路复用

        select()与poll()系统调用的本质一样,前者在BSD UNIX中引入,后者在System Ⅴ中引入。poll()机制与select()类似,管理多个描述符也是进行轮询,根据描述符状态进行处理,但是poll()没有最大文件描述符的限制(select默认最多1024个)。poll()与select()同样的缺点是包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就位,她的开销随着文件描述符数量的增加而线性增大。

#include      <poll.h>

struct pollfd

{

        int         fd;                /* 文件描述符 */

        short     events;        /* 等待的事件 */

        short     revents;       /* 实际发生了的事件 */

};

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

参数说明:

        第一个参数用来指向struct pollfd类型的数组,每一个pollfd结构体指定了一个被监视的文件描述符,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,内核在调用返回时设置这个域,events域中请求任何事件都可能在revents域中返回。下图给出指定的events标志以及测试revents标志的一些常值:

Linux学习——多路复用

POLLIN | POLLPRI等价于select可读事件,POLLOUT | POLLWRBAND等价于select写事件。POLLIN等价于POLLRDNORM | POLLRDBAND,而POLLOUT等价于POLLWRNORM。例如:要同时监视一个文件描述符是否可读和可写,我们可以设置events为POLLIN | POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都正常返回而不被阻塞。

        第二个参数nfds指定数组中监听元素的个数;

        第三个参数timeout指等待的毫秒数。无论I/O是否准备好,poll都会返回。timeout若为负数则表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好的I/O文件描述符,但并不等待其它事件,在这种情况下,poll()就像他的名字一样,一旦选举出来,立即返回;

poll()函数成功调用时 ,poll返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下值之一:

                EBADF                        一个或多个结构体中指定的文件描述符无效

                EFAULTfds                   指针指向的地址超出进程的地址空间

                EINTR                           请求的事件之前产生一个信号,调用可以重新发起

                EINVALnfds                   参数超出PLIMIT_NOFILE值

                ENOMEM                       可用内存不足、无法完成请求

下面是使用poll()实现socket服务器端的代码:

#define ARRAY_SIZE(x)   (sizeof(x)/sizeof(x[0]))

int socket_server_init(char *listen_ip,int listen_port);

        progname = basename(argv[0]);

        printf(“%s server start to listen on port %d\n”,argv[0],serv_port);

        max = 0;

                /*Program will blockd here*/

                rv = poll(fds_array,max+1,-1);

                /*listen socket get event means new client start connect now*/

        printf(“\nMandatory arguments to long options are mandatory for short options too:\n”);

        printf(” -b[daemon] set program running on background\n”);

        /*set socket port resusable,fix ‘Address already in use’ bug when socket server restart*/

        setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));

        memset(&servaddr,0,sizeof(servaddr));

                }

 

四、epoll()多路复用 

        epoll()的实现与select()实现完全不同。epoll通过在Linux内核中申请一个简易的文件系统,把原来的select/poll分成了下面三个部分:

                (1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)

                (2)调用epoll_ctl向epoll对象中添加100万个套接字

                (3)调用epoll_wait收集发生事件的连接

        如此,要实现与select/poll相同的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有将这100万个连接句柄全部复制给操作系统,内核也不需要遍历全部连接。

1.创建epoll实例:epoll_create()

        #include<sys/epoll.h>

        int epoll_create(int size);

        系统调用epoll_create()创建了一个新的epoll()实例,其对应的兴趣列表初始化为空。成功返回文件描述符,是=失败返回-1;参数size指定了我们想要通过实例来检查的文件描述符的个数。现在一般忽略不用。

2.修改epoll()兴趣列表:epoll_ctl()

#include <sys/epoll.h>

int epoll_ctl(int epfd,int op,int fd,struct epoll_event *ev);

参数说明:

        第一个参数epfd是epoll_ctl的返回值;

        第二个参数op用来指定需要执行的操作,他可以是如下几种值:

                EPOLL_CTL_ADD (添加事件):

                将文件描述符添加到 epoll 实例的监听集合中,以便监听该文件描述符上的事件。

                EPOLL_CTL_MOD (修改事件):

                修改 epoll 实例中已经存在的文件描述符的监听事件,可以用于修改感兴趣的事件类型或者修改文件描述符关联的数据。

                EPOLL_CTL_DEL (删除事件):

                从 epoll 实例的监听集合中删除一个文件描述符,不再监听它上面的事件。

        第三个参数fd指明了要修改兴趣列表中哪一个文件描述符的设定。该参数可以代表管道、FIFO、套接字、POSIX消息队列、inotify实例、终端、设备,甚至是另一个epoll实例的文件描述符。但是,这里fd、不能作为普通文件或目录的文件描述符;

        第四个参数ev是指向结构体epoll_event的指针,结构体定义如下:

        typedef union epoll_data        

        {

                void                *ptr;                                /* Pointer to user-defind data */

                int                   fd;                                  /* File descripter */

                uint32_t          u32;                               /* 32-bit interger */

                uint64             u64;                               /* 64 bit interger */

        }epoll_data_t;

        struct epoll_event

        {

                uint32                                events;        /* epoll events(bit mask) */

                epoll_data_t data               data;           /* User data */

        };

        ·events字段是一个位掩码,他指定了我们为待检查的描述符fd上的所感兴趣的集合

        ·data字段是一个联合体,当描述符fd稍后称为就绪态时,联合的成员可用来指定传回给调用进程的信息

系统调用epoll_ctl()能够修改由文件描述符epfd所代表的epoll实例中的兴趣列表 。成功返回0.出错返回-1;

3.等待事件:epoll_wait()

#include <sys/epoll.h>

int epoll_wait(int epfd,struct epoll_event *evlist,int maxevents,int timeout);

参数说明:

        第一个参数epfd是epoll_create的返回值;

        第二个参数evlist所指向的结构体数组中返回的是有关就绪态文件描述符的信息,数组evlist的空间由调用者负责申请;

        第三个参数maxevents所制定evlist数组中包含的元素个数;

        第四个参数timeout用来确定epoll_wait的阻塞行为,有以下几种:

                timeout等于-1,一直阻塞,直到兴趣列表中的文件描述符有事件产生或者直到捕捉到一个信号为止;

                timeout等于0,执行一次非阻塞式的检查,看兴趣列表中的文件描述符上发生了哪个事件;

                timeout大于0,调用将阻塞至多timeout毫秒,直到文件描述符上有事件发生,或者直到捕捉到一个信号为止。

        数组evlist中,每个元素返回的都是单个就绪态文件描述符的信息。events字段返回在该文件描述符上已经发生事件的事件掩码。data字段返回的是我们在描述符上使用epoll_ctl()注册感兴趣的事件时在ev.data中所指定的值。

        当我们调用epoll_ctl()时可以在ev.events职工指定的位掩码以及由epoll_wait()返回的evlist[].events中的值如下所示:

Linux学习——多路复用

下面是通过epoll实现客户端的代码:

        progname = basename(argv[0]);

        /*Parser the command line parameters*/

                        /*get help information*/

        /*set max open socket count*/

        set_socket_rlimit();

        printf(“%s server start to listen on port %d\n”,argv[0],serv_port);

        /*set program running on background*/

                /*program will blocked here*/

                events = epoll_wait(epollfd,event_array,MAX_EVENTS,-1);

                /*rv > 0 is the active events count*/

                        /* listen socket get event means new client start connect now */

                        /*already connected client socket get data incoming */

                                        /*convert letter from lowercase to uppercase*/

                                        for(j = 0;j < rv;j++)

                                                buf[j] = toupper(buf[j]);

Cleanup:

        printf(“\nMandatory arguments to long options are mandatory for short options too :\n”);

        /*Set socket port reuseable ,fix ‘Address already in use ‘bug when socket server restart */

        setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));

        memset(&servaddr,0,sizeof(servaddr));

 

 

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/126817.html

(0)
上一篇 2025-09-15 19:20
下一篇 2025-09-15 19:26

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信