0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

网络IO套路分享

Linux爱好者 ? 来源:Linux爱好者 ? 作者:Linux爱好者 ? 2020-10-13 14:52 ? 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

转自:我是程序员小贱-L的存在

1 阻塞与非阻塞--开胃菜

阻塞

我们知道在调用某个函数的时候无非就是两种情况,要么马上返回,然后根据返回值进行接下来的业务处理。当在使用阻塞IO的时候,应用程序会被无情的挂起,等待内核完成操作,因为此时的内核可能将CPU时间切换到了其他需要的进程中,在我们的应用程序看来感觉被卡主(阻塞)了。

阻塞IO

传统阻塞IO模型

传统阻塞IO模型

特点:

通过阻塞式IO获取输入的数据

其中每个连接都采用独立的线程完成数据输入,业务处理以及数据返回的操作

这种方案有什么问题?

首先当并发较大的时候,需要创建大量的线程来处理连接,需要占用大量的系统资源。

连接建立完成以后,如果当前线程没有数据可读,将会阻塞在read操作上造成线程资源的浪费

鉴于上面的两个问题,通常是解决方案是啥呢?

第一种是采用IO复用的模型,所谓IO复用模型即多个连接共享一个阻塞对象,应用程序只会在一个阻塞对象上等待。当某个连接有新的数据处理,操作系统直接通知应用程序,线程从阻塞状态返回并开始业务处理

第二种方案即采用线程池复用的方式。将连接完成后的业务处理任务分配给线程,一个线程处理多个连接的业务。IO复用结合线程池的方案即Reactor模式。

Reactor

从上图我们可以发现,通过一个或者多个请求传递给服务器,通过统一的事件管理机制进行请求分发,这种模式即事件驱动处理模式。

通常一个服务端处理网络请求的过程是啥样的?

服务端处理请求的常规过程

服务端将这些请求分别同步分派给多个处理线程,即IO多路复用统一监听事件,收到事件再进行分发。那么图中重要的两个关键字是啥意思?

Reactor

在一个单独的线程运行,主要负责监听和分发事件。就仿佛我们手机设置的转接,将来自前任的电话转接给适当的联系人

Handlers

主要负责处理执行IO实际的事情。

根据Reactor的数量和处理的资源大小通常又分为单Reactor线程,单Reactor多线程,主从Reactor多线程。这部分将在文章后面进行详细阐述,先和大家一起复习几个基本概念

非阻塞

当使用非阻塞函数的时候,和阻塞IO类比,内核会立即返回,返回后获得足够的CPU时间继续做其他的事情。

非阻塞

这样说可能有点不太好理解,试试一个例子来

小蓝经常去楼下的小卖部买烟,因为那小姐姐确实好看,即使不买去看看也饱了那种。有一天去买烟,让我等下,他去仓库看看,就一直在那里等着小姐姐回复,就仿佛阻塞在了小姐姐的店。

那么阻塞IO是个啥样子嘞?

小姐姐,今天有黄鹤楼烟没,小姐姐看看了柜台,没有,到处找也没有了,然后告诉我这周没有了,下周应该会有货,好嘛,我寂寞的小手颤抖了,其实我就是想去小姐姐家买东西,于是下周我又去问小姐姐,小姐姐果然有心,就知道我回去她家店买,直接给我留了两包黄鹤楼,就这样反反复复,和小姐姐的感情越来越好,这样就是阻塞IO的轮询,我没有被阻塞而是不断地咨询小姐姐(轮询)。

抽烟的人,经常一句话就是“这一根抽了就不抽了”,怎么能忍住一周?看来轮询效率太低,直接给小姐姐打电话:“小姐姐,烟到了麻烦通知一声,我来你家拿”,这就是IO多路复用。

感情嘛,最激烈的时期不外乎是最开始的那么两个月,不,渣男,怎么可能就两个月,感情真是越来越好,然后我就给小姐姐说:“小姐姐,我给你个地址,还有微信,到时候到货了麻烦给我寄过来”,这尼玛,不仅加了微信,还给我送到了家,这就是异步IO,至于后续的故事是怎么样的想知道?

好勒,就是写IO模型,配上线程/进程所向披靡(网络编程的核心)

非阻塞IO之读(继续查阅资料)

咱们知道套接字有个缓冲区,如果缓冲区没有数据可读,那么在非阻塞的情况下调用read就会立即返回,返回自然会有个状态,不然我们一脸懵逼,无法进行下一步。返回可能是EWOULDBLOCK或者EAGAIN出错信息。

非阻塞IO之写

刚才我们说了,有个叫做缓冲区的概念,当然也有发送缓冲区,如果发送缓冲区满了,不能容纳更多的字节,这个时候操作系统内核就会尽全力从应用程序拷贝数据到发送缓冲区并立即从write调用返回。在拷贝的过程中,可能全部拷贝了,也可能一字节也没拷贝,所以使用返回值来告诉应用程序到底有多少数据拷贝到了发送发送缓冲区,方便再次调用write,输出未完成的字节。

总结下两种方式:

阻塞IO是:拷贝-知道所有数据拷贝到发送缓冲区。

非阻塞IO是拷贝-返回-再拷贝-再返回。ok,read和write的骚操作如下图

读写阻塞不同点

说了这么多,当面试官问你的时候,能不能对答如流嘞,总结如下:

read总是在接受缓存区有数据的时候直接返回,而不是等到应用程序哥顶的数据充满才返回。如果此时缓冲区是空的,那么阻塞模式会等待,非阻塞则会返回-1并有EWOULDBLOCK或EAGAIN错误

和read不太一样的是,在阻塞模式下,write只有在发送缓冲区足矣容纳应用程序的输出字节时才会返回。在非阻塞的模式下,能写入多少则写入多少,并返回实际写入的字节数

当使用fgets等待标准输入的时候,如果此时套接字有数据但不能读出。IO多路复用意味着可以将标准输入、套接字等都当做IO的一路,任何一路IO有事件发生,都将通知相应的应用程序去处理相应的IO事件,在我们看来就反复同时可以处理多个事情。这就是IO复用。

IO复用

2 select

当使用select函数的时候,先通知内核挂起进程,一旦一个或者多个IO事情发生,控制权将返回给应用程序,然后由应用程序进行IO处理。

那么IO事件都包含哪些

标准输入文件描述符可以读

已连接套接字准备好可以写

如果一个IO事件等待超过10秒,发生超时

select使用方法

intselect(intmaxfdp,fd_set*readset,fd_set*writeset,fd_set*exceptset,structtimeval*timeout);

maxfdp 表示待测试描述符基数,它的值为待测试最大描述符加1.假设当前的select的测试描述符集合为{0,1,3},那么这个时候maxfd为4,

随后为三个描述符集合,分别为读集合readset,写集合writeset和异常集合exceptset。它们会通知内核,当有可读可写发生的时候记得通知它们

如何设置这些描述符

intFD_ZERO(intfd,fd_set*fdset);//一个fd_set类型变量的所有位都设为0 intFD_CLR(intfd,fd_set*fdset);//清除某个位时可以使用 intFD_SET(intfd,fd_set*fd_set);//设置变量的某个位置位 intFD_ISSET(intfd,fd_set*fdset);//测试某个位是否被置位

最后一个参数是时间

structtimeval { longtv_sec;/*秒*/ longtv_usec;/*微秒*/ };

设置为NULL,select会一直等待下去

设置非零值,等待固定时间后返回

将tv_sec和tv_usec均设置为0,表示不等待,检测完毕就返回

程序案例

#include #include #include #include #include #include conststaticintMAXLINE=1024; conststaticintSERV_PORT=10001; intmain() { inti,maxi,maxfd,listenfd,connfd,sockfd; /*nready描述字的数量*/ intnready,client[FD_SETSIZE]; intn; /*创建描述字集合,由于select函数会把未有事件发生的描述字清零,所以我们设置两个集合*/ fd_setrset,allset; charbuf[MAXLINE]; socklen_tclilen; structsockaddr_incliaddr,servaddr; /*创建socket*/ listenfd=socket(AF_INET,SOCK_STREAM,0); /*定义sockaddr_in*/ memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_port=htons(SERV_PORT); servaddr.sin_addr.s_addr=htonl(INADDR_ANY); bind(listenfd,(structsockaddr*)&servaddr,sizeof(servaddr)); listen(listenfd,100); /*listenfd是第一个描述字*/ /*最大的描述字,用于select函数的第一个参数*/ maxfd=listenfd; /*client的数量,用于轮询*/ maxi=-1; /*init*/ for(i=0;imaxfd)maxfd=connfd; if(i>maxi)maxi=i; if(nready<=1)?continue; ????????????else?nready?--; ????????} ????????for(i=0?;?i<=maxi?;?i++) ????????{ ????????????if?(client[i]<0)?continue; ????????????sockfd?=?client[i]; ????????????if(FD_ISSET(sockfd,&rset)) ????????????{ ????????????????n?=?read(sockfd?,?buf?,?MAXLINE); ????????????????if?(n==0) ????????????????{ ????????????????????/*当对方关闭的时候,server关闭描述字,并将set的sockfd清空*/ ????????????????????close(sockfd); ????????????????????FD_CLR(sockfd,&allset); ????????????????????client[i]?=?-1; ????????????????} ????????????????else ????????????????{ ????????????????????buf[n]=''; ????????????????????printf("Socket?%d?said?:?%s ",sockfd,buf); ????????????????????write(sockfd,buf,n);?//Write?back?to?client ????????????????} ????????????????nready?--; ????????????????if(nready<=0)?break; ????????????} ????????} ????} ????return?0; }

但是他有个比较明显的特点就是所支持文件描述符有限,默认为1024个,随机出现poll

3 poll

鉴于select所支持的描述符有限,随后提出poll解决这个问题

还是先看声明

intpoll(structpollfd*fds,nfds_tnfds,inttimeout);

再看pollfd结构

structpollfd{ intfd;/*文件描述符*/ shortevents;/*描述符待检测的事件*/ shortrevents;/*returnedevents*/ };

注意下这个结构体,分别包含了文件描述符和描述符对应的事件,这事件和文件描述符紧密相结合,其中事件使用二进制掩码表示,如POLLIN代表读事件,POLLOUT代表写事件。

#definePOLLIN0x0001/*anyreadabledataavailable*/ #definePOLLPRI0x0002/*OOB/Urgentreadabledata*/ #definePOLLOUT0x0004/*filedescriptoriswriteable*/

从结构中可以看见还有个参数是revents,从字面意思可知事件备份。对的,相当于将poll每次检测的结果保留在revents,这样就不需要每次都重置描述符和事件。那到底有哪些事件

可读事件---系统内核会通知应用程序数据可读

#definePOLLIN0x0001/*anyreadabledataavailable*/ #definePOLLPRI0x0002/*OOB/Urgentreadabledata*/ #definePOLLRDNORM0x0040/*non-OOB/URGdataavailable*/ #definePOLLRDBAND0x0080/*OOB/Urgentreadabledata*/

可写事件---系统内核会通知套接字缓冲区已经可以安排,随后使用write函数不会被堵塞

#definePOLLOUT0x0004/*filedescriptoriswriteable*/ #definePOLLWRNORMPOLLOUT/*nowritetypedifferentiation*/ #definePOLLWRBAND0x0100/*OOB/Urgentdatacanbewritten*/

了解函数返回值

小于0----表示事件发生前永远等待

-1---发生错误

0--在指定的而时间没有任何事件发生

poll和select不同之处在于,在select中,文件描述符个数随着fd_set的实现而固定,而在poll函数中,我们可以通过控制pollfd数组的大小来改变描述符的个数

案例

#include #include #include #include #include #include #defineINFTIM-1 #definePOLLRDNORM0x040/*Normaldatamayberead.*/ #definePOLLRDBAND0x080/*Prioritydatamayberead.*/ #definePOLLWRNORM0x100/*Writingnowwillnotblock.*/ #definePOLLWRBAND0x200/*Prioritydatamaybewritten.*/ #defineMAXLINE1024 #defineOPEN_MAX16//一些系统会定义这些宏 #defineSERV_PORT10001 intmain() { inti,maxi,listenfd,connfd,sockfd; intnready; intn; charbuf[MAXLINE]; socklen_tclilen; structpollfdclient[OPEN_MAX]; structsockaddr_incliaddr,servaddr; listenfd=socket(AF_INET,SOCK_STREAM,0); memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_port=htons(SERV_PORT); servaddr.sin_addr.s_addr=htonl(INADDR_ANY); bind(listenfd,(structsockaddr*)&servaddr,sizeof(servaddr)); listen(listenfd,10); client[0].fd=listenfd; client[0].events=POLLRDNORM; for(i=1;imaxi)maxi=i; nready--; if(nready<=0)?continue; ????????} ????????for(i=1;i<=maxi;i++) ????????{ ????????????if(client[i].fd<0)?continue; ????????????sockfd?=?client[i].fd; ????????????if(client[i].revents?&?(POLLRDNORM|POLLERR)) ????????????{ ????????????????n?=?read(client[i].fd,buf,MAXLINE); ????????????????if(n<=0) ????????????????{ ????????????????????close(client[i].fd); ????????????????????client[i].fd?=?-1; ????????????????} ????????????????else ????????????????{ ????????????????????buf[n]=''; ????????????????????printf("Socket?%d?said?:?%s ",sockfd,buf); ????????????????????write(sockfd,buf,n);?//Write?back?to?client ????????????????} ????????????????nready--; ????????????????if(nready<=0)?break;?//no?more?readable?descriptors ????????????} ????????} ????} ????return?0; }

5 epoll

epoll 通过监控注册的多个描述字,来进行 I/O 事件的分发处理。不同于 poll 的是,epoll 不仅提供了默认的 level-triggered(条件触发)机制,还提供了性能更为强劲的edge triggered(边缘触发)机制

在The Linux Programming Interface有张图展示三种IO复用技术在面对不同文件描述符时的差异

The Linux Programming Interface

从上图咱们知道即使10000个描述符的时候,常规的select和poll性能下降明显,而epoll变化不大

那么epoll是什么操作这么6?

epoll通过监控注册多个描述字进行IO事件的分发。不同poll的是,epoll不仅提供默认的level-trigger机制还提供了边缘触发机制,这里可以先思考下两者有什么区别

编程三步骤

intepoll_create(intsize);

就是通过它来创造实例,这个返回的值将用于后续的技能解锁,如果不需要了这个实例,则需要使用close方法来释放示例,不要占着坑不拉屎,这样很不道德。

那这个size是干撒子的嘞?它告诉内核期望监控多少个描述符,然后使用这部分的信息来初始化内核底层的数据结构,不过时代在变化,在现在高版本的实现中,不在需要这个参数,自动动态化了,高级了吧。

intepoll_ctl(intepfd,intop,intfd,structepoll_event*event);

创建了实例,并有了返回值来标识这个示例,是时候添加事件了,此时就是使用epoll_ctl。第一个参数epfd即是上面的返回值。第二个参数表示是准备删除事件还是监控事件,哪都有哪些选项呢

三个事件

第三个参数为注册事件的文件描述符比如一个监听字

第四个参数表示注册的事件类型,可以在这个结构体中自定义数据。

intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout); //返回值:成功返回的是一个大于0的数,表示事件的个数;返回0表示的是超时时间到;若出错返回-1

epoll_wait函数和之前的select等类似,等待内核IO事件的分发。第一个参数为create返回值句柄。第二个参数回给用户空间需要处理的 I/O 事件。第三个参数为一个大于0的整数,表示epoll_wait可以返回的最大事件值。第四个参数是epoll_wait阻塞时的超时值,如果设置为-1表示不超时,如果设置为0则立即返回。

6 epoll的底层实现

这部分内容,我在之前的文章中提过且给大家分享过一篇源码笔记,大家需要的话也可以微信找我拿。

当我们使用epoll_fd增加一个fd的时候,内核会为我们创建一个epitem实例,讲这个实例作为红黑树的节点,此时你就可以BB一些红黑树的性质,当然你如果遇到让你手撕红黑树的大哥,在最后的提问环节就让他写写吧

随后查找的每一个fd是否有事件发生就是通过红黑树的epitem来操作

epoll维护一个链表来记录就绪事件,内核会当每个文件有事件发生的时候将自己登记到这个就绪列表,然后通过内核自身的文件file-eventpoll之间的回调和唤醒机制,减少对内核描述字的遍历,大俗事件通知和检测的效率

7 C10K问题

这里的C代表并发,10K=10000。虽然现在通过现成的框架libevent/Netty可以轻松完成这个目标,但是在二十多年前,突破这个并发量还是非常困难的。那么要同时支撑这么多用户,需要从哪些方面考虑呢

文件句柄

我们知道每个连接代表一个文件描述符,如果不够用,新的链接将会被丢弃并产生错误

连接数过多

在Linux中默认为1024,但是如果你是root,你可以通过/etc/sysctl.cong来修改,使得支持1w个描述符

文件句柄

系统内存

每个连接不是只占用链接套接字那么简单,每个链接都需要占用发送缓冲区和接收缓冲区,不信我们看看

系统内存

上面三个值分别代表的是最小分配值、默认分配至和最大分配至,这样如果1w个连接需要消耗

发送接收缓冲区

此时假设一个连接需要128K缓冲区,那么 1w 个连接大约需要 1.2G 应用层缓冲,所以支持w的连接不是内存的问题。

网络带宽

假设当前10个连接,其中每个链接传输大约1KB的数据,那么带宽需要10 * 10000 * 1KB/s * 8=80MBPS。在现在看来也是很一般了

那么如何解决C10K问题?

一方面需要考虑到IO,也就是上面说的阻塞IO和非阻塞IO

如何分配进程,线程的资源服务上w的连接

阻塞IO与进程

这个好理解,来个连接我就分配个进程(fork)去处理,这个进程处理此链接的所有IO,不管是阻塞还是非阻塞IO,多个连接也不会产生影响,毕竟进程之间有着各自的进程空间

进程是程序执行的最小单位,一个进程有着完整的地址空间,程序计数器,想要创建一个进程,使用fork即可,fork后会在父子进程中各返回一次,如果返回值为0则是子进程,随后父子进程处理各自的逻辑

fork

创建完进程执行了任务,当要离开的时候需要清理干净资源,如果退出了还将进程的相关信息留下,不回收就会变为僵尸进程。这些僵尸进程不是没人管了,会交给一个叫做init的进程,如果僵尸进程太多,势必会占用太多的内存空间甚至耗尽我们的系统资源。

那如果想主动的回收这些资源,怎么办呢?

处理子进程退出一般是注册一个信号处理函数,然后捕捉信号SIGCHILD信号,在信号处理函数中调用waitpid函数完成资源的回收即可。

假设此时服务端开始监听,两个客户端AB分别连接服务端,客户端A发起请求后,连接成立返回新的套接字叫做连接套接字,此时父进程派生子进程,在子进程中使用连接套接字和客户端通信,所以这个时候子进程不关心监听套接字。父进程则相反,服务交给子进程后,不再关心连接套接字,而是关心监听套接字,如下图所示

客户端A发起连接

缺点:效率不高,扩展性较差且资源占用率高

此时客户端B发来新的请求,accept返回新的已连接套接字,父进程又派生子进程

客户端B发起请求

部分实现

while(1) { if((conn=accept(listenfd,(structsockaddr*)&peeraddr,&peerlen))

缺点:效率不高,扩展性较差且资源占用率高,注意事项

对套接字的关闭

子进程的回收

阻塞IO+线程

刚才不是说进程占用资源多么,那么就是用线程呗。单进程中可以有多个线程,每个线程都有自己的上下文,包括唯一标识的线程ID 程序计数器等,同一个进程的多个线程共享整个虚拟地址空间,其中包含了代码、数据、堆。

为什么线程的上下文的开销比进程少呢

我们的代码是交给CPU执行的,程序计数器会告诉CPU代码执行到哪儿了,寄存器呢会存储当前计算的中间值,内存存放当前使用的变量,当切换到另外的计算场景的时候,需要重新载入新的值,这个时候就出现了上下文的切换

现在每个连接由一个线程处理

do{ acceptconnections pthread_createforconnecedconnectionfd thread_run(fd) }while(true)

主线程负责监听连接请求

while(1) { //服务一直在运行,直到被某个操作或命令终止该进程 intrecvbytes; socklen_tlength=sizeof(cliaddr); //accept()函数让服务器接收客户的连接请求 intclientfd=accept(servfd,(structsockaddr*)&cliaddr,&length); if(clientfd

每个客户连接的线程函数

void*recv_msg_from_client(void*arg) { //分离线程,使主线程不必等待此线程 pthread_detach(pthread_self()); intclientfd=*(int*)arg; intrecvBytes=0; char*recvBuf=newchar[BUFFER_SIZE]; memset(recvBuf,0,BUFFER_SIZE); while(1) { if((recvBytes=recv(clientfd,recvBuf,BUFFER_SIZE,0))<=?0)? ????????{ ????????????perror("recv出错! "); ????????????return?NULL; ????????} ????????recvBuf[recvBytes]=''; ????????printf("recvBuf:%s ",?recvBuf); ????} ????close(clientfd); ????return?NULL; }

可是频繁的创建线程也还是比较耗资源,这样子是不是可以使用线程池提前创建一波线程,多个连接复用它即可

线程池

上面程序虽然可以较好地处理连接,但是如果并发较多,就会引起线程的频繁切换和销毁,怎们优化?

我们在服务端启动的时候,预先分配固定大小的多个线程,当新连接建立的时候,从连接队列中取出这个连接描述字进程处理

主线程与工作线程

细心地同学可能发现,既要从队列取数据,也会从队列写数据,会不会有混乱。是的,所以通常还会使用mutex和条件变量进行加持。

但是不是每个链接都是需要时刻服务,每次创建线程还是比较消耗资源,那就提前创建一批线程,所谓线程池,复用线程池来获取某种效率的提升

线程池

非阻塞 I/O + readiness notification + 单线程

我们的程序可以通过轮询的方式对套接字进行挨个访问,从而找出进行IO处理的套接字。

在这里插入图片描述

描述符少还行,如果太多,每次的循环将消耗大量的CPU时间,而且可能循环完了都没发现一个套接字可以读写。既然这样,我们直接交给操作系统,让它告诉我们哪些套接字可以读写。程序就变为这样

poll

我们每次dispatch就相当于对所有的套接字进行排查,这样显然效率不是很高。如果dispatch之后只提供有IO事件或者IO变化的套接字就好了,这就是epoll的设计

epoll

非阻塞 I/O + readiness notification + 多线程

上述几种方案都是在一个线程分发,显然没有利用当今的多核技术,我们完全可以让每个核作为一个IO分发器进行事件的分发,这其实就是reactor模式,也是后续将谈到的事件驱动。

单Reactor多线程

8 事件驱动

事件驱动也叫做反应堆模型或者Event loop模型,重要的是两点

通过poll、epoll等IO分发技术实现一个无限循环的事件分发线程

将所有的IO事件抽象为事件,每个事件设置回调函数

在处理大部分网络程序的时候,无外乎处理一下几个事儿

从套接字读取数据

对收到的数据进行解析

根据解析的内容进行计算处理

处理后的结果按照约定的格式编码

通过套接字发送出去

那么之前我们说了使用fork子进程的方式实现通信,随着客户端的增多,处理效率不高,因为fork的开销太大

fork

为什么说事件驱动是一种高性能,高并发的模式?

既然这么牛皮,当然有它的特点。举个例子,来成都一个月,印象特别深刻却是到处都是咖啡馆,点一杯咖啡坐着一边喝一边看妹子,服务员小姐姐也不会找我,只有当我去续杯的时候,再找小姐姐勾搭(触发事件了),小姐姐满足了我的需求,我就接着边喝咖啡边看其他小姐姐,这就是事件驱动。看个图

事件驱动

为了模式整体的效率,不能因为处理业务逻辑导致IO事件处理的效率下降,所以我们决定将注入XML文件的解析,数据库的查找等工作放在其他线程中,所谓将这些工作和反应堆线程解耦。让这个反应堆只处理IO相关任务,业务逻辑这些操作分成小任务放在线程池中让其他空闲的线程处理。处理完后再交给反应堆,然后发送出去

拆分业务逻辑

主从Reactor

ok,咱们已经知道使用Reactor反应堆的方式同时分发Acceptor上的连接建立事件,但是我们还是没有完全实现解耦,这个Reactor线程既要分发连接建立,还要分发已经建立连接的IO,如果太多的客户请求是不是会处理不过来,那么能不能让其分离嘞

主从Reactor

我们看看主从reactor的方式,思路很清晰,主reactor主负责分发Acceptor连接建立,然后已经联机的IO事件交给sub-reactor,这个sub-reactor的数量可以根据CPU的核数来定。

假设咱们是一个四核的CPU,设置sub-reactor为4.这个时候4个反应堆同时干活,是不是增强了IO分发处理的效率,因为多核操作,也大大的减少并发处理的锁开销。

多Reactor

epoll

上面说了poll的reactor反应堆模式,和poll相比,epoll可谓更加高效的事件机制。

如何切换到epoll呢

在lib/event_loop.c中的event_loop_init_with_name中,可以发现通过宏EPOLL_ENABEL来决定使用哪一个

EPOLL_ENABEL

然后我们在根目录中查看CMakeLists.txt文件,如果系统中有epoll_create就会自动开启EPOLL_ENALE,如果没有则采用默认的poll作为事件分发机制。

EPOLL_ENALE

这还没完,我们需要让编译器知道这个宏,所以需要让CMake往config.h文件写入这个宏的最终值。

配置config.h

那么,为啥epoll的性能就比poll更好呢

首先poll和select,在使用之前需要一个感兴趣的事件集合,系统内核通过它构建相应的数据结构并注册,epoll却不是,它会维护一个全局的事件集合,通过epoll的句柄操作这个集合,操作系统内核不需要每次重新扫描整个集合

使用poll或者select的时候,应用程序需要扫描整个感兴趣的事件集合并找出活动的事件,如果请求量过大,扫描一次花费的时间就太长。而epoll不是,epoll直接返回活动的事件,减少大量的扫描时间。

那么边缘触发与条件触发到底是啥意思

如果某个套接字有1000个字节需要读,两个方案都会产生read ready notification,如果应用程序只读了500字节,就会陷入等待,对于条件触发就不一样,它会因为还有500字节而不断地产生read ready notification

异步IO

用程序告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到应用程序的缓冲区)完成后通知应用程序。那么和信号驱动有啥不一样?

异步IO

信号驱动IO试内核通知应用程序什么时候启动一个IO操作。而异步IO模型是由内核通知应用程序啥时候完成。

异步IO的主要优点是充分的利用DMA特性。缺点是,如果要完成真正的异步IO,对于操作系统的压力较大,需要做大量的工作。

现在我们已经知道了阻塞IO 非阻塞IO,以及通过select epoll poll等IO多路复用并结合线程池的方案实现高性能的网络框架。但是还有个与之相对应叫做proactor的网络驱动模式,两者有什么区别?

在windows中这一套完整的支持套接字的异步编程接口叫做IOCP,和Reactor模式一样之处在于,也存在一个无限循环的event loop线程的,但是不同于Reactor模式,这个线程不负责处理IO调用,只是负责在对应的read,write操作完成的情况下,分发完成事件道不同的处理函数。简单的一句话总结即Reactor模式基于待完成的IO事件,而Proactor模式基于已完成的IO事件。

责任编辑:xj

原文标题:「网络IO套路」当时就靠它追到女友

文章出处:【微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 网络
    +关注

    关注

    14

    文章

    7838

    浏览量

    91146
  • 编程
    +关注

    关注

    88

    文章

    3690

    浏览量

    95394
  • 异步
    +关注

    关注

    0

    文章

    62

    浏览量

    18332
  • 阻塞
    +关注

    关注

    0

    文章

    24

    浏览量

    8305

原文标题:「网络IO套路」当时就靠它追到女友

文章出处:【微信号:LinuxHub,微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    Analog Devices / Maxim Integrated MAXREFDES177 IO-Link通用模拟IO特性/框图

    Analog Devices MAXREFDES177 IO-Link通用模拟IO是一款完备的IO-Link?通用模拟输入-输出 (IO) 参考设计,采用内置集成保护功能的MAX225
    的头像 发表于 06-30 09:30 ?176次阅读
    Analog Devices / Maxim Integrated MAXREFDES177 <b class='flag-5'>IO</b>-Link通用模拟<b class='flag-5'>IO</b>特性/框图

    宜科电子全新IO-Link信号集线器助力工业自动化升级

    工业4.0浪潮下,设备之间的“高效对话”成为智能工厂的核心竞争力。IO-Link作为一种可实现从控制器到自动化最底层级之间的IO通信技术,通过IO-Link主站将传感器及执行器等信息经由现场总线
    的头像 发表于 04-11 15:44 ?521次阅读

    算力魔方IO扩展模块介绍 网络篇1

    不同的总线接口功能。在不同场景中,有采用串口、网络或者是CAN总线通信,算力魔方?都可以通过模块的选型提供支持;并且可以根据场景需要,提供微定制服务。 以上3款为新推出IO扩展模块,不支持单独出货,需搭配Z系列算力魔方使用,可实现不同
    的头像 发表于 04-09 14:33 ?365次阅读
    算力魔方<b class='flag-5'>IO</b>扩展模块介绍 <b class='flag-5'>网络</b>篇1

    GPU显卡维修避坑指南:手把手教你识别行业套路

    你的显卡维修被“套路”过吗?“一块H100显卡维修报价5万?修完3个月又坏!”你是否也遇到过——高价采购的显卡突然故障,返厂维修耗时数月,第三方服务商张口就是“核心损坏,必须换新”?在算力需求激增
    的头像 发表于 04-02 20:31 ?1143次阅读
    GPU显卡维修避坑指南:手把手教你识别行业<b class='flag-5'>套路</b>!

    探索分布式 IO 模块网络适配器

    在自动化控制领域,分布式 IO 模块网络总线适配器,也就是耦合器模块,发挥着极为关键的作用。但对于很多非专业人士来说,这个名字听起来既陌生又晦涩。别担心,接下来就让我们一起深入了解它。
    的头像 发表于 02-21 17:05 ?375次阅读
    探索分布式 <b class='flag-5'>IO</b> 模块<b class='flag-5'>网络</b>适配器

    虹科直播回放 | IO-Link技术概述与虹科IO-Link OEM

    虹科「一站式通讯解决方案」系列直播第1期圆满落幕!本期主题为「IO-Link技术概述与虹科IO-LinkOEM」感谢各位朋友的热情参与!本期直播中虹科专业讲师瞿工带大家走进工业4.0深度解读了IO-Link技术及其应用并重磅推出
    的头像 发表于 02-19 17:34 ?706次阅读
    虹科直播回放 | <b class='flag-5'>IO</b>-Link技术概述与虹科<b class='flag-5'>IO</b>-Link OEM

    借助IO-Link收发器简化微控制器设计

    IO-Link是24 V、3线工业通信标准,支持工业从站和IO-Link主站之间的点对点通信,进而与更高级别的过程控制网络进行通信。
    的头像 发表于 01-03 11:02 ?1742次阅读
    借助<b class='flag-5'>IO</b>-Link收发器简化微控制器设计

    一体式远程IO的Profinet通信协议模组-三格电子

    ,采用实时(RT)通讯功能,符合:GB/T 25105-2014《工业通信网络现场总线规范类型 10: PROFINET IO 规范》,IEC 61158-5-10:2007,IDT。 产品外观 产品选型 设备参数 审核编
    的头像 发表于 12-26 14:12 ?614次阅读
    一体式远程<b class='flag-5'>IO</b>的Profinet通信协议模组-三格电子

    λ-IO:存储计算下的IO栈设计

    动机和背景? ? 存储计算存储资源的充分利用。IO栈是管理存储器的的基本组件,包括设备驱动、块接口层、文件系统,目前一些用户空间IO库(如SPDK)有效降低了延迟,但是io栈仍然不可或缺。这是因为1
    的头像 发表于 12-02 10:35 ?677次阅读
    λ-<b class='flag-5'>IO</b>:存储计算下的<b class='flag-5'>IO</b>栈设计

    一文解读Linux 5种IO模型

    Linux里有五种IO模型:阻塞IO、非阻塞IO、多路复用IO、信号驱动式IO和异步IO,我发现
    的头像 发表于 11-09 11:12 ?899次阅读
    一文解读Linux 5种<b class='flag-5'>IO</b>模型

    MR20远程IOIO-Link的差异化应用

    在工业自动化领域,远程IO)和IO-Link作为两种重要的通信协议,各自扮演着不可或缺的角色。尽管它们都是为了实现设备之间的数据传输与控制,但在应用场景、系统架构和功能特点上却存在着显著的差异。本文
    的头像 发表于 10-21 17:29 ?649次阅读

    本地IO与远程IO:揭秘工业自动化中的两大关键角色

    在工业自动化领域,IO(Input/Output,输入/输出)模块扮演着至关重要的角色。它们作为连接控制系统与现场设备的桥梁,负责数据的采集与指令的执行。然而,随着技术的不断进步,IO模块也分为本地IO和远程
    的头像 发表于 10-08 18:06 ?1205次阅读

    解析一体式IO与分布式IO:从架构到应用

    在工业自动化领域,IO(输入/输出)系统扮演着举足轻重的角色。它们不仅负责数据的采集和控制指令的发送,还直接影响到系统的灵活性、可靠性和成本效益。明达技术将为您介绍一体式IO和分布式IO在架构及应用层的主要区别,帮助您更好地理解
    的头像 发表于 10-08 10:02 ?901次阅读
    解析一体式<b class='flag-5'>IO</b>与分布式<b class='flag-5'>IO</b>:从架构到应用

    MCU IO口的作用和特点

    MCU(微控制器)的IO口(Input/Output Port,输入输出端口)是单片机与外界进行信息交互的关键接口。这些IO口在微控制器的功能实现中扮演着至关重要的角色,它们不仅负责数据的输入和输出,还承载着电平转换、中断处理、功能复用等多种功能。以下是对MCU
    的头像 发表于 09-30 11:52 ?3000次阅读

    FPGA-5G通信算法的基本套路

    5G通信的风口虽然经过近3年的洗礼,热度稍减,但不可否认的是,全球5G网络的部署正在持续快速推进,而我国更是部署了占据全球70% 左右的5G基站。 随着工业互联网的推进,“5G+工业互联网”的企业
    发表于 08-15 17:34