• IO多路转接--select--poll--epoll


    目录

    一,五种IO模型

    1,阻塞IO

    2,非阻塞IO

    3, 信号驱动IO

     4,IO多路转接

     5,异步IO

    二,同步通信和异步通信

    三,IO模型的设置

    1,阻塞IO

    2,非阻塞IO

    3,I/O多路转接之select

    1,系统提供select函数来实现多路复用输入/输出模型.

    2,函数原型

    3,select特点

    4,select服务端举例

    5,select缺点

    4,poll多路转接

    5,epoll多路转接

            1,int epoll_create(int size)

            2,int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

            3.epoll_wait

    6.epoll工作原理

    epoll的优点(和 select 的缺点对应)

    7,epoll代码示例


    一,五种IO模型

    1,阻塞IO

            在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式。

            

    2,非阻塞IO

            在内核将数据未准备好之前,系统调用仍然会返回,并且返回EWOULDBLOCK错误码,非阻塞IO一般需要程序员以循环的方式反复读写文件描述符,这个过程称为轮询,这对CPU是极大的浪费,一般特定的场景使用。

    3, 信号驱动IO

            在内核将数据准备好以后,通过SIGIO信号通知应用程序进行IO操作

     4,IO多路转接

            虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件 描述符的就绪状态。

     5,异步IO

            在内核将数据拷贝完成后,通知应用程序(而信号驱动是告诉应用程序何时开始拷贝数据).

    小结:

            任何IO过程中都包含两个步骤,第一是等待,第二是拷贝,而在实际应用的过程中,等待时间远远高于拷贝的过程。让IO更高效,最核心的办法就是让等待的时间尽可能的长。

    二,同步通信和异步通信

    同步和异步关心的是消息通信机制

    1,同步,在系统调用时,在没有的得到结果之前,该调用不返回,直到调用返回结果。

    2,异步,调用在发出以后,这个调用就直接返回了,没有返回结果,调用者通过信号,状态,来通知调用者。

    //此同步非互斥和同步里面的同步

    1,进程/线程同步也是进程/线程之间直接的制约关系

    2,是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、 传递信息所产生的制约关系. 尤其是在访问临界资源的时候。

    三,IO模型的设置

    1,阻塞IO

            一个文件描述符,默认都是阻塞IO。

    2,非阻塞IO

            通过fcntl设置文件描述符,函数原型如下

    1. #include
    2. #include
    3. int fcntl(int fd, int cmd, ... /* arg */ );

    传入的cmd的值不同, 后面追加的参数也不相同. fcntl函数有5种功能:

            复制一个现有的描述符(cmd=F_DUPFD).

            获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).

            获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).

            获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).

            获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

    我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞

     实现函数setnoblock()

    1. void setnoblock(int fd)
    2. {
    3. int f1=fctl(fd,F_GETFL);
    4. if(f1<0)
    5. {
    6. cerr<<"fcntl"<
    7. return ;
    8. }
    9. fcntl(fd,f_SETFL,f1|O_NONBLOCK);
    10. }

    a,使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).

    b,然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数

    1. 1 #include
    2. 2 #include
    3. 3 #include
    4. 4
    5. 5 void setnoblock(int fd)
    6. 6 {
    7. 7 int f1=fcntl(fd,F_GETFD);
    8. 8 if(f1<0)
    9. 9 {
    10. 10 std::cerr<<"fcntl"<
    11. 11 return ;
    12. 12 }
    13. 13 fcntl(fd,F_SETFD,f1|O_NONBLOCK);
    14. 14
    15. 15 }
    16. 16 int main()
    17. 17 {
    18. 18 setnoblock(0);
    19. 19 while (1) {
    20. 20 char buf[1024] = {0};
    21. 21 ssize_t read_size = read(0, buf, sizeof(buf) - 1);
    22. 22 if (read_size < 0) {
    23. 23 std::cout<<"read"<
    24. 24 sleep(1);
    25. 25 continue;
    26. 26 }
    27. 27 std::cout<
    28. 28 }
    29. 29 return 0;
    30. 30 }

    3,I/O多路转接之select

    1,系统提供select函数来实现多路复用输入/输出模型.

            select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;

            程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

             select时间复杂度---》O(n)

    2,函数原型

    1. #include
    2. #include
    3. #include
    4. int select(int nfds, fd_set *readfds, fd_set *writefds,
    5. fd_set *exceptfds, struct timeval *timeout);

            1,nfds,代表最大文件描述符+1。

            2,rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描 述符的集合;

            3,参数timeout为结构timeval,用来设置select()的等待时间

                    a,参数timeout取值

                    NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件; 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

                    b,fd_set结构,其准确的来说是一个位图,是采用对应的位监视某一个文件描述符,

    听过一组函数来操作位图。

    1. void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
    2. int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
    3. void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
    4. void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

    3,select特点

            可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=512,每bit表示一个文件 描述符,则我服务器上支持的最大文件描述符是512*8=4096

            将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd, 一,是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。

    二,是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数

    4,select服务端举例

    1. #include"sock.hpp"
    2. #define NUM 1024
    3. #define DFL_FD -1
    4. namespace so_sever
    5. {
    6. class Select_Sever
    7. {
    8. private:
    9. int listen_sock;
    10. unsigned short port;
    11. public:
    12. Select_Sever(unsigned short _port):port(_port)
    13. {}
    14. //先初始化fd,起任务,填充select变量
    15. void InitSelectSever()
    16. {
    17. listen_sock= so_Sock::so_sock::Socket();
    18. so_Sock::so_sock::Bind(listen_sock,port);
    19. so_Sock::so_sock::Listen(listen_sock);
    20. }
    21. void Run()
    22. {
    23. fd_set rfds;
    24. int fd_array[NUM]={0};
    25. //让数组中所有数据都变成-1,然后填充对应的监听套接字
    26. clearArrar(fd_array,NUM,DFL_FD);
    27. fd_array[0]=listen_sock;
    28. for( ; ;)
    29. {
    30. //select时间也需要设置,输入输出型参数
    31. struct timeval timeout={5,0};
    32. //对所有的合法fd重新设置
    33. int maxfd=DFL_FD;
    34. FD_ZERO(&rfds);//对select中读描述符进行重新设置
    35. //对文件描述符数组进行判断
    36. for(int i=0;i
    37. {
    38. if(fd_array[i]==DFL_FD)
    39. {
    40. continue;
    41. }
    42. //合法的文件描述符
    43. FD_SET(fd_array[i],&rfds);
    44. if(fd_array[i]>maxfd)
    45. {
    46. maxfd=fd_array[i];
    47. }
    48. }
    49. //select 阻塞等待
    50. switch (select(maxfd+1,&rfds,nullptr,nullptr,/*&timeout*/ nullptr))
    51. {
    52. case 0:
    53. std::cout<<"timeout....... "<
    54. break;
    55. case -1:
    56. std::cout<<"select error "<
    57. break;
    58. default:
    59. // std::cout<<"select wait success"<
    60. Hander(rfds,fd_array,NUM);
    61. break;
    62. }//end switch
    63. }//end for
    64. }
    65. void Hander(const fd_set &rfds,int fd_array[],int num)
    66. {
    67. //读取套接字
    68. //如何判断套接字已经等待成功 在fd数组里&&rfds里面这个已经存在
    69. for(int i=0;i
    70. {
    71. if(fd_array[i]==DFL_FD)
    72. {
    73. continue;
    74. }
    75. //说明这个文件描述符已存在
    76. if(FD_ISSET(fd_array[i],&rfds) && fd_array[i]==listen_sock)
    77. {
    78. //说明等待成功
    79. //接受套接字等待成功,读事件还没有就绪
    80. struct sockaddr_in peer;
    81. socklen_t len=sizeof(peer);
    82. //这里会不会阻塞,不会,已经有套接字加入到数组里面,
    83. int sock=accept(fd_array[i],(struct sockaddr*)&peer,&len);
    84. if(sock<0)
    85. {
    86. std::cout<<"accept error"<
    87. continue;
    88. }
    89. //端口转换
    90. uint16_t peer_port=htons(peer.sin_port);
    91. //ip转换
    92. std::string peer_ip=inet_ntoa(peer.sin_addr);
    93. std::cout<<"get a new link "<<" port "<" ip "<
    94. //走到这里 能否读取数据?? 不能 recv是IO,select只是等
    95. //要将文件描述符添加到fd——fd_array
    96. if(!AddFdTorray(fd_array,num,sock))
    97. {
    98. //说明没添加成功
    99. close(sock);
    100. std::cout << "select server is full, close fd : " << sock << std::endl;
    101. }
    102. }//end if
    103. else
    104. {
    105. //说明可以读取数据了
    106. if(FD_ISSET(fd_array[i],&rfds))
    107. {
    108. //是一个合法的fd,并且可以读取了
    109. //是一个合法的fd,并且已经就绪了,是读数据事件就绪
    110. //实现读写,会阻塞吗??绝对不会
    111. char buffer[1024];
    112. //能确定你读完了请求吗???
    113. //如果我一条链接给你发了多个请求数据,但是每个都只有10字节, 粘包?
    114. //如果没有读到一个完整的报文,数据可能丢失
    115. //这里我们怎么保证自己能拿到完整的数据呢??
    116. //1. 定制协议
    117. //2. 还要给每一个sock定义对应的缓冲区
    118. //ssize_t s=read(fd_array[i],buffer,sizeof(buffer)-1);
    119. ssize_t s=recv(fd_array[i],buffer,sizeof(buffer)-1, 0);
    120. if(s>0)
    121. {
    122. buffer[s]=0;
    123. std::cout<
    124. }
    125. else if(s == 0)
    126. {
    127. std::cout<<" client close "<
    128. //对端关闭
    129. close(fd_array[i]);
    130. fd_array[i]=DFL_FD;//清除文件描述符
    131. }
    132. else
    133. {
    134. std::cout<<" recv error "<
    135. close(fd_array[i]);
    136. fd_array[i]=DFL_FD;
    137. }
    138. }
    139. else
    140. {
    141. //todo
    142. }
    143. }//end if
    144. }//end for
    145. }
    146. ~Select_Sever()
    147. {}
    148. private:
    149. void clearArrar(int fd_array[],int num,int default_fd)
    150. {
    151. for(int i=0;i
    152. {
    153. fd_array[i]=default_fd;
    154. }
    155. }
    156. bool AddFdTorray(int fd_array[],int num,int sock)
    157. {
    158. for(int i=0;i
    159. {
    160. if(fd_array[i]==DFL_FD)
    161. {
    162. fd_array[i]=sock;
    163. return true;
    164. }
    165. }
    166. return false;
    167. }
    168. };
    169. }

    5,select缺点

            每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.

            每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

            select支持的文件描述符数量太小

    4,poll多路转接

            poll==>时间复杂度O(n),

            poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

            poll与select函数基本一致,只是把事件集合封装了一下,使得提前处理参数,后续也不用每次轮询时查找。

    1. #include"sock.hpp"
    2. #include
    3. namespace Poll_etta
    4. {
    5. class poll_sever
    6. {
    7. private:
    8. int listen_sock;
    9. unsigned short port;
    10. public:
    11. poll_sever(int _port):port(_port)
    12. {}
    13. //初始化
    14. void InitSever()
    15. {
    16. listen_sock=so_Sock::so_sock::Socket();
    17. so_Sock::so_sock::Bind(listen_sock,port);
    18. so_Sock::so_sock::Listen(listen_sock);
    19. }
    20. //RUN 任务
    21. void Run()
    22. {
    23. struct pollfd rfds[64];
    24. //初始化参数
    25. for(int i=0;i<64;i++)
    26. {
    27. rfds[i].fd=-1;
    28. rfds[i].events=0; //我所关心的事件
    29. rfds[i].revents=0; //操作系统对我关心的事件 做出回应
    30. }
    31. //填充我所关心的事件
    32. rfds[0].fd=listen_sock;
    33. rfds[0].events=POLLIN;//关心读事件
    34. rfds[0].revents=0;//内核填充
    35. for(; ;)
    36. {
    37. switch(poll(rfds,64,-1))
    38. {
    39. case 0:
    40. std::cout<<"time out"<
    41. break;
    42. case -1:
    43. std::cerr<<"poll error"<
    44. break;
    45. default:
    46. //处理逻辑,有事件到来
    47. for(int i=0;i<64;i++)
    48. {
    49. if(rfds[i].fd==-1)
    50. {
    51. continue;
    52. }
    53. if(rfds[i].revents&POLLIN)
    54. {
    55. //能accept吗 不能 要填充就绪事件
    56. if(listen_sock==rfds[i].fd)
    57. {
    58. std::cout<<" get a new link"<
    59. }
    60. else
    61. {
    62. //recv数据
    63. }
    64. }
    65. }
    66. break;
    67. }
    68. }
    69. }
    70. ~poll_sever()
    71. {}
    72. };
    73. }

    5,epoll多路转接

            手册上说:为处理大批量句柄而作改进的POLL,

            其主要有三个函数接口。

            1,int epoll_create(int size)

                    创建出一个epoll的句柄,size一般被忽略。

                    调用完成以后,必须close掉。

            2,int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

                    epoll的事件注册函数.

                           它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注                           册要监听的事件类型.

                          第一个参数是epoll_create()的返回值(epoll的句柄).

                          第二个参数表示动作,用三个宏来表示.

                          第三个参数是需要监听的fd.

                          第四个参数是告诉内核需要监听什么事.

                   第二个参数的取值:

                            EPOLL_CTL_ADD :注册新的fd到epfd中;

                            EPOLL_CTL_MOD :修改已经注册的fd的监听事件;

                            EPOLL_CTL_DEL :从epfd中删除一个fd;

                    struct epoll_event结构如下:

    events可以是以下几个宏的集合:

    EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);

    EPOLLOUT : 表示对应的文件描述符可以写;

    EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来); EPOLLERR : 表示对应的文件描述符发生错误;

    EPOLLHUP : 表示对应的文件描述符被挂断;

    EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.

    EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里. 

            3.epoll_wait

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

            收集在epoll监控的事件中已经发送的事件.

            参数events是分配好的epoll_event结构体数组. epoll将会把发生的事件赋值到events数组中           (events不可以是空指针,内核只负责把数据复制到这个

            events数组中,不会去帮助我们在用户态中分配内存). maxevents告之内核这个events有多           大,这个 maxevents的值不能大于创建epoll_create()时的size.

            参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).

           如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小        于0表示函数失败

    6.epoll工作原理

           当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有         两个成员与epoll的使用方式密切相关。

            每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象添            加进来的事件

            这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来            (红黑树的插入时间效率是lgn,其中n为树的高度).

            而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的            事件发生时会调用这个回调方法.

            这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.

            在epoll中,对于每一个事件,都会建立一个epitem结构体

    1. struct epitem{
    2. struct rb_node rbn;//红黑树节点
    3. struct list_head rdllink;//双向链表节点
    4. struct epoll_filefd ffd; //事件句柄信息
    5. struct eventpoll *ep; //指向其所属的eventpoll对象
    6. struct epoll_event event; //期待发生的事件类型
    7. }

    当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.

    如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度 是O(1)

    总结一下, epoll的使用过程就是三部曲:

            调用epoll_create创建一个epoll句柄;

            调用epoll_ctl, 将要监控的文件描述符进行注册;

            调用epoll_wait, 等待文件描述符就绪

    epoll的优点(和 select 的缺点对应)

    1,接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文 件描述符, 也做到了输入输出参数分离开 数据拷贝轻量: 只在合适的时候调用 2,EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频 繁(而select/poll都是每次循环都要进行拷贝)

    3,事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述 符数目很多, 效率也不会受到影响.

    4,没有数量限制: 文件描述符数目无上限

    7,epoll代码示例

            

    1. #include"sock.hpp"
    2. #include
    3. #define MAX_NUM 64
    4. namespace ns_epoll
    5. {
    6. class EpollSever
    7. {
    8. private:
    9. int epfd; //epoll_ctl 的第一个参数,表示当前有几个文件描述符存在epoll里面
    10. int listen_sock;
    11. uint16_t port;
    12. public:
    13. //构造
    14. EpollSever(uint16_t _port):port(_port)
    15. {}
    16. //初始化套接字
    17. void InitSever()
    18. {
    19. listen_sock=so_Sock::so_sock::Socket();
    20. so_Sock::so_sock::Bind(listen_sock,port);
    21. so_Sock::so_sock::Listen(listen_sock);
    22. //监听完毕,打印出来kankan
    23. std::cout<<"deBug test "<<" Listen sock "<
    24. //初始化epoll函数的第一个参数
    25. if((epfd=epoll_create(256))<0)
    26. {
    27. //创建失败
    28. std::cout<<"epoll create fail"<
    29. exit(4);
    30. }
    31. }
    32. void AddEvent(int sock,uint16_t event)
    33. {
    34. struct epoll_event ev;
    35. ev.events=0;//初始化
    36. ev.events|=event;
    37. ev.data.fd=sock;
    38. //添加等待队列失败,继续等待
    39. if(epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&ev)<0)
    40. {
    41. std::cout<<" epoll_ctl add fail "<<" sock "<
    42. }
    43. }
    44. void DeleteEvent(int sock)
    45. {
    46. if(epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr)<0)
    47. {
    48. std::cout<<" delete events fail "<
    49. }
    50. }
    51. //事件跑起来
    52. void Run()
    53. {
    54. //走到这里,至少有一个套接字,把套接字加到等待队列中,让epoll_wait “等”个文件描述符就绪
    55. AddEvent(listen_sock,EPOLLIN);
    56. int timeout=-1;
    57. struct epoll_event revs[MAX_NUM];//定义最大等待数
    58. //循环等待
    59. for(;;)
    60. {
    61. //返回值代表有几个事件准备好
    62. int num=epoll_wait(epfd,revs,MAX_NUM,timeout);
    63. if(num>0)
    64. {
    65. //说明等待成功,但是那个文件描述符就绪 不知道 遍历
    66. for(int i=0;i
    67. {
    68. int sock = revs[i].data.fd;
    69. if(revs[i].events & EPOLLIN)//读事件就绪
    70. {
    71. if(sock==listen_sock)
    72. {
    73. //连接事件就绪
    74. struct sockaddr_in peer;
    75. socklen_t len=sizeof(peer);
    76. int sk=accept(sock,(struct sockaddr*)&peer,&len);
    77. if(sk<0)
    78. {
    79. std::cout<<"accept fail "<
    80. continue;
    81. }
    82. //得到一个新连接
    83. std::cout<<"get a new link "<
    84. AddEvent(sk,EPOLLIN);
    85. }
    86. //彻底就绪,直接读取文件
    87. else
    88. {
    89. char buffer[1024];
    90. ssize_t s=recv(sock,buffer,sizeof(buffer)-1,0);
    91. if(s>0)
    92. {
    93. buffer[s]=0;
    94. std::cout<
    95. }
    96. else
    97. {
    98. std::cout<<"Client close "<
    99. close(sock);
    100. //移除这个套接字在epfd中
    101. DeleteEvent(sock);
    102. }
    103. }
    104. }
    105. else if(revs[i].events&EPOLLOUT)
    106. {
    107. // todo
    108. }
    109. else
    110. {
    111. //do some thing
    112. }
    113. }
    114. }
    115. else if(num==0)
    116. {
    117. std::cout<<" time out"<
    118. }
    119. else
    120. {
    121. std::cout<<" epoll error "<
    122. }
    123. }
    124. }
    125. ~EpollSever()
    126. {
    127. if(listen_sock>=0)close(listen_sock);
    128. if(epfd>=0)close(epfd);
    129. }
    130. };
    131. }

  • 相关阅读:
    HDFS High Availability(HA)高可用配置
    八股文之git
    DFT specification file & string
    域名信息收集
    【浅学Java】Bean的作用域和生命周期
    https 原理与实践
    C#——字符串
    【687. 最长同值路径】
    拉格朗日多项式
    PS(Photoshop)去水印的4个方法
  • 原文地址:https://blog.csdn.net/m0_63111921/article/details/126832424