• Linux C : select简介和epoll 实现


    目录

    一、基础知识

    二、select 模型服务流程

    二、select 模式的缺点。

    三、poll 概要

    四、epoll 服务端实现流程

    1.epoll_create:

    2.epoll_ctl

    3.epoll_wait

    五、epoll示例代码实现

    1.epoll实现服务端

    2.客户端采用tcp进行访问


    一、基础知识

          首先要知道,服务器与客户端的网络通信。服务器首先要创建一个socket用于指定需要监听的ip地址和端口。其中主机ip对应了一台主机,而ip+端口对应了一台主机上的某一个应用程序。当服务器将需要监听socket的文件描述符与监听的ip+端口绑定起来时,服务器就可以开启监听。此时客户端便可以连接上服务器这个端口。服务器利用accept查看监听端口中是否有输入事件,如果有就说明有新的客户端连接请求,并会开启一个新的文件描述符用于服务器和客户端的读写操作了。

            selelct 模型,poll模型,epoll模型 为网络通信中的IO多路复用方法。是为了提高网络通信并发量和通信效率而生的。除了IO多路复用方法,还可以利用多线程,多进程等方式,但是效果都不如IO多路复用。

            select模型,poll模型,epoll模型 首先也要经历创建监听socket,监听socket绑定主机和端口,开启监听,accept客户端连接这些阶段。它们和一般网络通信区别在于如何管理这些多个网络通信连接,使通信过程高效。

            在主机上的ip和端口用的是主机字节序,而网络上的通信用的是网络字节序。网络字节序采用大端的方式,即高位字节排放在内存的低地址端(即该值的起始地址),低位字节排放在内存的高地址端。而主机字节序根据CPU种类可能有小端模式或者大端模式。

    二、select 模型服务流程

           1.select模型在accpet一堆客户端连接后,便把这些与客户端通信的文件描述符放进fd_set进行管理。fd_set是一个bitmap,哪些文件描述符被加入到fd_set中,该文件描述符在fd_set对应的bit位就会置为1.

           2.select 函数首先会把fd_set从用户态拷贝进内核态。由内核负责遍历哪一个文件描述符中有数据到达。没有数据就进行阻塞。其中第一个参数 maxfd,用于限定遍历范围,指定从0开始到maxfd之前的文件描述符进行遍历。

           3.如果没数据,进程就会进入阻塞状态,并将改进程放入socket的等待队列中。等待客户端把数据发送过来。

            4.网卡接受到了一个或多个客户端发来的数据。此时会产生一个中断,并且调用DMA拷贝,将网卡收到的数据,拷贝到内核环形缓冲区,而后再根据文件描述符信息放入对应的数据接收队列中。并唤醒进程。

            5.如果遍历中发现了多个客户端通信的文件描述符有接收到数据。那么select 函数就会修改fd_set,将有数据的文件描述符对应的bit位置为1,其他bit位置0. 。而后将修改后的fd_set拷贝会用户态,并return有数据的文件描述符的个数。

            6.应用程序可以遍历修改后的fd_set中的bit位,或者遍历文件描述符,判断哪些描述符中有对应的事件发生。而后可以开始与对应的客户端读写数据。

    二、select 模式的缺点。

    1.由于fd_set是bitmap,通常的最大容量为1024个bit位,意味着最多遍历1024个文件描述符。有最大连接限制。

    2.由于fd_set传进select函数中为引用传递,每次循环都要重新赋值fd_set,无法重用。

    3。频繁进行用户态和内核态的,当连接多时开销大。

    4.  应用程序的轮询就绪的文件描述符的时间复杂度为O(n).

    三、poll 概要

            poll流程和select模型流程大多都一样。都要建立监听,accept客户端连接,把客户端连接放在一个集合中,再将集合放到模型中管理,最后遍历有事件的文件描述符,进行通信。

            poll流程和select模型不一样的地方。也就是将集合fd_set 替换成pollfd。也就是poll模型不用bitmap了,而是用一个pollfd的结构体进行操作。再pollfd中设置需要注册的事件events,而,poll函数中不修改events属性,而是修改revents属性来表示发生的事件。 在客户端响应后只需要重置revents就可以达到重用效果。

    1.由于pollfd不再是bitmap,而是一个结构体,再poll模型中,内核态采用链表的形式对accept进来的socket进行管理,意味着再内存允许的范围内不再限制最大连接数。

    2.pollfd传进poll函数中为引用传递,但是poll不会修改注册的监听事件属性,可以达到重用的效果。

    3。依旧频繁进行用户态和内核态的,当连接多时开销大。

    4.  时间复杂度依旧为O(n).

    四、epoll 服务端实现流程

            参考 深入了解epoll模型(特别详细) - 知乎

            和正常网络服务端程序一样,服务端首先要创建一个socket用于监听,其中socket地址指定需要监听的主机和程序对应的端口。并与socket对应的文件描述符绑定起来。而后利用epoll函数对网络通信的文件描述符管理起来。

            首先要了解epoll的数据结构,对epoll数据结构的管理和具体操作都是在内核态进行,操作系统对epoll管理都已经封装好了,程序员只需要负责调用就行。epoll的数据结构主要包括:

    1.存放已就绪事件的就绪队列,它是双向链表构成的。

    2.用于管理socket的红黑树。

    3.以及双向链表表示的阻塞进程的队列。

            程序员可对epoll的操作主要是3种:

    1.epoll_create:

    在epoll文件系统建立了个file节点(B+树),并开辟epoll自己的内核高速cache区,建立红黑树,建立双向链表用于存储准备就绪的事件。其中

    首先创建一个struct eventpoll对象;然后分配一个未使用的文件描述符;然后创建一个struct file对象,将file中的struct file_operations *f_op设置为全局变量eventpoll_fops,将void *private指向刚创建的eventpoll对象ep;然后设置eventpoll中的file指针;最后将文件描述符添加到当前进程的文件描述符表中,并返回给用户。因为内核初始化时(操作系统启动)注册了一个新的文件系统,叫"eventpollfs"专门由于服务epoll。返回的fd就是该文件系统新建的文件描述符

    2.epoll_ctl

            epoll_ctl()首先判断op是不是删除操作,如果不是则将event参数从用户空间拷贝到内核中;获得struct eventpoll对象,接下来会从epoll实例的红黑树里寻找和被监控文件对应的epollitem对象,如果不存在,也就是之前没有添加过该文件;如果是添加操作,那么就会修改已有的或者创建新的epitem加入到红黑树中。

            这个加入过程将epollitem对象添加到被监视文件的等待队列上去。等待队列实际上就是一个回调函数链表,定义在/include/linux/wait.h文件中。通过struct file的poll操作,以回调的方式返回对象的等待队列,这里设置的回调函数是ep_ptable_queue_proc。

            在回调函数ep_ptable_queue_proc中,内核会创建一个struct eppoll_entry对象,然后将等待队列中的回调函数设置为ep_poll_callback()。也就是说,当被监控文件有事件到来时,比如socket收到数据时,ep_poll_callback()会被回调.ep_poll_callback函数主要的功能是将被监视文件的等待事件就绪时,将文件对应的epitem实例添加到就绪队列中

    3.epoll_wait

            观察就绪列表里面有没有数据,并进行提取和清空就绪列表.没数据就sleep直到超时。传入epoll_events,用于获取已就绪的事件。而epoll_wait返回已就绪的个数。换而言之epoll_events避免了像select模型中通过遍历所有监听的socket获取已就绪文件的问题。epoll获取已就绪的算法复杂度达到了O(1).

    五、epoll示例代码实现

    1.epoll实现服务端

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #define MAX 1024
    11. #define SERVER_IP "127.0.0.1"
    12. #define SERVER_HOST "localhost"
    13. // #define SERVER_PORT 1234
    14. int server_init(int port){
    15. printf("=============server init========\n");
    16. printf("1. create a TCP socket\n");
    17. //sock的文件描述符
    18. int mysock = socket (AF_INET,SOCK_STREAM,0);
    19. if(mysock < 0){
    20. printf("socket call failed\n"); return -1;
    21. }
    22. printf("2. fill server_addr with host_ip and port number \n");
    23. int opt =1; unsigned int len = sizeof(opt);
    24. setsockopt(mysock,SOL_SOCKET,SO_REUSEADDR,&opt,len);
    25. setsockopt(mysock,SOL_SOCKET,SO_KEEPALIVE,&opt,len);
    26. struct sockaddr_in server_addr ;
    27. server_addr .sin_family = AF_INET;
    28. server_addr .sin_port = htons(port);
    29. server_addr .sin_addr.s_addr = htonl(INADDR_ANY);
    30. printf("3. bind socket to server address \n");
    31. int r =bind (mysock ,(struct sockaddr *) & server_addr ,sizeof (server_addr ));
    32. if( r<0){
    33. printf("bind call failed\n"); exit(3);
    34. }
    35. printf(" hostname =%s, port = %d \n", SERVER_HOST ,port);
    36. listen(mysock , 5 );
    37. printf("============init done ===========\n");
    38. return mysock;
    39. }
    40. int main (int argc, char *argv[]){
    41. if(argc !=2){
    42. printf("usage:./tcpepoll port\n");
    43. return 1;
    44. }
    45. //char line [MAX];
    46. int listensocket=server_init(atoi(argv[1]));
    47. printf("listensocket=%d\n",listensocket);
    48. if(listensocket < 0){
    49. printf("listensocket failed \n");
    50. }
    51. char buffer[1024];
    52. memset(buffer,0,sizeof(buffer));
    53. //create a fd
    54. int epollfd=epoll_create(1);
    55. //add a listen event
    56. struct epoll_event ev;
    57. ev.data.fd=listensocket;
    58. ev.events=EPOLLIN;
    59. epoll_ctl(epollfd,EPOLL_CTL_ADD,listensocket,&ev);
    60. while (1){
    61. struct epoll_event events[MAX];
    62. // waiting event occurt
    63. int infds=epoll_wait(epollfd,events,MAX,-1);
    64. if(infds <0 ){
    65. printf("epoll wait failed \n");break;
    66. }
    67. if(infds ==0){
    68. printf("epoll wait timeout \n");continue;
    69. }
    70. for(int ii=0; ii
    71. if( (events[ii].data.fd == listensocket) &&
    72. (events[ii].events & EPOLLIN)){
    73. //如果监听的socket有事件,那么说明有客户端程序连接上来
    74. struct sockaddr_in client;
    75. socklen_t len = sizeof(client);
    76. printf("accepting \n");
    77. int clientsock = accept(listensocket,(struct sockaddr*)&client,&len);
    78. if(clientsock < 0){
    79. printf("accept() fail\n"); continue;
    80. }
    81. //把监听到的socket加入到epoll中去管理
    82. memset(&ev,0,sizeof(struct epoll_event));
    83. ev.data.fd=clientsock;
    84. ev.events=EPOLLIN;
    85. epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
    86. printf("clientsock=%d success \n",clientsock);
    87. continue;
    88. }else if(events[ii].events & EPOLLIN){
    89. //如果是客户端socket有事件
    90. char buffer[1024];
    91. memset(buffer,0,sizeof(buffer));
    92. //读取客户端数据
    93. ssize_t isize = read(events[ii].data.fd,buffer,sizeof(buffer));
    94. if(isize <= 0){
    95. //已断开的连接从epoll中删除
    96. memset(&ev,0,sizeof(struct epoll_event));
    97. ev.events = EPOLLIN;
    98. ev.data.fd = events[ii].data.fd;
    99. epoll_ctl(epollfd,EPOLL_CTL_DEL,events[ii].data.fd,&ev);
    100. close(events[ii].data.fd);
    101. continue;
    102. }
    103. printf("recv(eventfd=%d,size=%ld): %s \n",events[ii].data.fd,isize,buffer);
    104. //把收到的保温发回客户端
    105. write(events[ii].data.fd,buffer,strlen(buffer));
    106. }
    107. }
    108. }
    109. close(epollfd);
    110. return 0 ;
    111. }

    2.客户端采用tcp进行访问

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #define MAX 256
    9. #define SERVER_HOST "localhost"
    10. #define SERVER_PORT 1234
    11. struct sockaddr_in server_addr ;
    12. int sock,r;
    13. int client_init(){
    14. printf("=============client init========\n");
    15. printf("1. create a TCP socket\n");
    16. sock = socket (AF_INET,SOCK_STREAM,0);
    17. if(sock < 0){
    18. printf("socket call failed\n"); exit(1);
    19. }
    20. printf("2. fill server_addr with host_ip and port number \n");
    21. server_addr .sin_family = AF_INET;
    22. server_addr .sin_port = htons(SERVER_PORT);
    23. server_addr .sin_addr.s_addr = htonl(INADDR_ANY);
    24. printf("3. connecting to server \n");
    25. r = connect (sock ,(struct sockaddr *) & server_addr ,sizeof (server_addr ));
    26. if( r<0){
    27. printf("bind call failed\n"); exit(3);
    28. }
    29. printf(" hostname =%s, port = %d \n", SERVER_HOST ,SERVER_PORT);
    30. printf("============client init done ===========\n");
    31. return 0;
    32. }
    33. int main (){
    34. char line [MAX] , ans[MAX];
    35. int n;
    36. client_init();
    37. printf(" ********processing loop *******************");
    38. while (1){
    39. printf("put a line... \n");
    40. bzero(line ,MAX);
    41. fgets(line,MAX,stdin);
    42. line[strlen(line) - 1] = 0;
    43. if(line[0] ==0) exit(0);
    44. n=write(sock,line,MAX);
    45. printf("client : wrote n =%d bytes ; line %s :\n", n , line);
    46. n =read(sock ,ans,MAX);
    47. printf("client : read n =%d bytes ; line %s :\n", n , line);
    48. }
    49. }

  • 相关阅读:
    【Jetson 设备】window10主机下使用VNC可视化控制Jetson Orin NX
    页面中满屏水印
    找出重复成员
    META在2022年提出的最新创新
    第二届邯郸钢铁展会,图扑软件荣获“2022钢铁行业智造之星奖”
    农业大数据概论-按章节复习
    vue-cli4+vue2兼容安卓7(h5嵌入app),一步步排查发现问题并且解决(vue白屏)
    MySQL 1、初识数据库
    基于若依ruoyi-nbcio增加flowable流程待办消息的提醒,并提供右上角的红字数字提醒(六)
    Linux下systemd深入指南:如何优化Java服务管理与开机自启配置
  • 原文地址:https://blog.csdn.net/superSmart_Dong/article/details/132769271