77百科网
当前位置: 首页 生活百科

aso书籍在线阅读(讲的很明白的文章)

时间:2023-07-02 作者: 小编 阅读量: 4 栏目名: 生活百科

基于这个基本的设定,在讨论IO时,一定要严格区分网络IO和磁盘文件IO。epoll文件描述符在进程被fork时,子进程是可以继承的。这可以给对多进程共享一份epoll数据,实现并行监听网络请求带来便利。但这超过了本文的讨论范围,就此打住。第二个参数op表示如何对文件名进行操作,共有3种。管理fd事件注册第三步,使用epoll_wait来等待事件的发生。只有当注册的事件至少有一个发生,或者timeout达到时,该调用才会返回。这与select和poll几乎一致。

到底什么是“IO Block”

很多人说BIO不好,会“block”,但到底什么是IO的Block呢?考虑下面两种情况:

  • 用系统调用read从socket里读取一段数据
  • 用系统调用read从一个磁盘文件读取一段数据到内存

如果你的直觉告诉你,这两种都算“Block”,那么很遗憾,你的理解与Linux不同。Linux认为:

  • 对于第一种情况,算作block,因为Linux无法知道网络上对方是否会发数据。如果没数据发过来,对于调用read的程序来说,就只能“等”。
  • 对于第二种情况,不算做block

是的,对于磁盘文件IO,Linux总是不视作Block。

你可能会说,这不科学啊,磁盘读写偶尔也会因为硬件而卡壳啊,怎么能不算Block呢?但实际就是不算。

一个解释是,所谓“Block”是指操作系统可以预见这个Block会发生才会主动Block。例如当读取TCP连接的数据时,如果发现Socket buffer里没有数据就可以确定定对方还没有发过来,于是Block;而对于普通磁盘文件的读写,也许磁盘运作期间会抖动,会短暂暂停,但是操作系统无法预见这种情况,只能视作不会Block,照样执行。

基于这个基本的设定,在讨论IO时,一定要严格区分网络IO和磁盘文件IO。NIO和后文讲到的IO多路复用只对网络IO有意义。

严格的说,O_NONBLOCK和IO多路复用,对标准输入输出描述符、管道和FIFO也都是有效的。但本文侧重于讨论高性能网络服务器下各种IO的含义和关系,所以本文做了简化,只提及网络IO和磁盘文件IO两种情况。

本文先着重讲一下网络IO。

BIO

有了Block的定义,就可以讨论BIO和NIO了。BIO是Blocking IO的意思。在类似于网络中进行read, write, connect一类的系统调用时会被卡住。

epoll创建

为什么epoll要创建一个用文件描述符来指向的表呢?这里有两个好处:

  • epoll是有状态的,不像select和poll那样每次都要重新传入所有要监听的fd,这避免了很多无谓的数据复制。epoll的数据是用接口epoll_ctl来管理的(增、删、改)。
  • epoll文件描述符在进程被fork时,子进程是可以继承的。这可以给对多进程共享一份epoll数据,实现并行监听网络请求带来便利。但这超过了本文的讨论范围,就此打住。

epoll创建后,第二步是使用epoll_ctl接口来注册要监听的事件。

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

其中第一个参数就是上面创建的epfd。第二个参数op表示如何对文件名进行操作,共有3种。

  • EPOLL_CTL_ADD - 注册一个事件
  • EPOLL_CTL_DEL - 取消一个事件的注册
  • EPOLL_CTL_MOD - 修改一个事件的注册

第三个参数是要操作的fd,这里必须是支持NIO的fd(比如socket)。

第四个参数是一个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 */};

比方说,想关注一个fd1的读取事件事件,并采用边缘触发(下文会解释什么是边缘触发),大概要这么写:

struct epoll_data ev;ev.events = EPOLLIN | EPOLLET; // EPOLLIN表示读事件;EPOLLET表示边缘触发ev.data.fd = fd1;

通过epoll_ctl就可以灵活的注册/取消注册/修改注册某个fd的某些事件。

管理fd事件注册

第三步,使用epoll_wait来等待事件的发生。

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

特别留意,这一步是"block"的。只有当注册的事件至少有一个发生,或者timeout达到时,该调用才会返回。这与select和poll几乎一致。但不一样的地方是evlist,它是epoll_wait的返回数组,里面只包含那些被触发的事件对应的fd,而不是像select和poll那样返回所有注册的fd。

监听fd事件

综合起来,一段比较完整的epoll代码大概是这样的。

#define MAX_EVENTS 10struct epoll_event ev, events[MAX_EVENTS];int nfds, epfd, fd1, fd2; // 假设这里有两个socket,fd1和fd2,被初始化好。// 设置为non blockingsetnonblocking(fd1);setnonblocking(fd2); // 创建epollepfd = epoll_create(MAX_EVENTS);if (epollfd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE);} //注册事件ev.events = EPOLLIN | EPOLLET;ev.data.fd = fd1;if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd1, &ev) == -1) { perror("epoll_ctl: error register fd1"); exit(EXIT_FAILURE);}if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd2, &ev) == -1) { perror("epoll_ctl: error register fd2"); exit(EXIT_FAILURE);} // 监听事件for (;;) { nfds = epoll_wait(epdf, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_wait"); exit(EXIT_FAILURE); }for (n = 0; n < nfds;n) { // 处理所有发生IO事件的fd process_event(events[n].data.fd); // 如果有必要,可以利用epoll_ctl继续对本fd注册下一次监听,然后重新epoll_wait }}

此外,epoll的手册 中也有一个简单的例子。

所有的基于IO多路复用的代码都会遵循这样的写法:注册——监听事件——处理——再注册,无限循环下去。

epoll的优势

为什么epoll的性能比select和poll要强呢? select和poll每次都需要把完成的fd列表传入到内核,迫使内核每次必须从头扫描到尾。而epoll完全是反过来的。epoll在内核的数据被建立好了之后,每次某个被监听的fd一旦有事件发生,内核就直接标记之。epoll_wait调用时,会尝试直接读取到当时已经标记好的fd列表,如果没有就会进入等待状态。

同时,epoll_wait直接只返回了被触发的fd列表,这样上层应用写起来也轻松愉快,再也不用从大量注册的fd中筛选出有事件的fd了。

简单说就是select和poll的代价是"O(所有注册事件fd的数量)",而epoll的代价是"O(发生事件fd的数量)"。于是,高性能网络服务器的场景特别适合用epoll来实现——因为大多数网络服务器都有这样的模式:同时要监听大量(几千,几万,几十万甚至更多)的网络连接,但是短时间内发生的事件非常少。

但是,假设发生事件的fd的数量接近所有注册事件fd的数量,那么epoll的优势就没有了,其性能表现会和poll和select差不多。

epoll除了性能优势,还有一个优点——同时支持水平触发(Level Trigger)和边沿触发(Edge Trigger)。

水平触发和边沿触发

默认情况下,epoll使用水平触发,这与select和poll的行为完全一致。在水平触发下,epoll顶多算是一个“跑得更快的poll”。

而一旦在注册事件时使用了EPOLLET标记(如上文中的例子),那么将其视为边沿触发(或者有地方叫边缘触发,一个意思)。那么到底什么水平触发和边沿触发呢?

考虑下图中的例子。有两个socket的fd——fd1和fd2。我们设定监听f1的“水平触发读事件“,监听fd2的”边沿触发读事件“。我们使用在时刻t1,使用epoll_wait监听他们的事件。在时刻t2时,两个fd都到了100bytes数据,于是在时刻t3, epoll_wait返回了两个fd进行处理。在t4,我们故意不读取所有的数据出来,只各自读50bytes。然后在t5重新注册两个事件并监听。在t6时,只有fd1会返回,因为fd1里的数据没有读完,仍然处于“被触发”状态;而fd2不会被返回,因为没有新数据到达。

水平触发和边沿触发

这个例子很明确的显示了水平触发和边沿触发的区别。

  • 水平触发只关心文件描述符中是否还有没完成处理的数据,如果有,不管怎样epoll_wait,总是会被返回。简单说——水平触发代表了一种“状态”。
  • 边沿触发只关心文件描述符是否有的事件产生,如果有,则返回;如果返回过一次,不管程序是否处理了,只要没有新的事件产生,epoll_wait不会再认为这个fd被“触发”了。简单说——边沿触发代表了一个“事件”。

那么边沿触发怎么才能迫使新事件产生呢?一般需要反复调用read/write这样的IO接口,直到得到了EAGAIN错误码,再去尝试epoll_wait才有可能得到下次事件。

那么为什么需要边沿触发呢?

边沿触发把如何处理数据的控制权完全交给了开发者,提供了巨大的灵活性。比如,读取一个http的请求,开发者可以决定只读取http中的headers数据就停下来,然后根据业务逻辑判断是否要继续读(比如需要调用另外一个服务来决定是否继续读)。而不是次次被socket尚有数据的状态烦扰;写入数据时也是如此。比如希望将一个资源A写入到socket。当socket的buffer充足时,epoll_wait会返回这个fd是准备好的。但是资源A此时不一定准备好。如果使用水平触发,每次经过epoll_wait也总会被打扰。在边沿触发下,开发者有机会更精细的定制这里的控制逻辑。

但不好的一面时,边沿触发也大大的提高了编程的难度。一不留神,可能就会miss掉处理部分socket数据的机会。如果没有很好的根据EAGAIN来“重置”一个fd,就会造成此fd永远没有新事件产生,进而导致饿死相关的处理代码。

再来思考一下什么是“Block”

上面的所有介绍都在围绕如何让网络IO不会被Block。但是网络IO处理仅仅是整个数据处理中的一部分。如果你留意到上文例子中的“处理事件”代码,就会发现这里可能是有问题的。

  • 处理代码有可能需要读写文件,可能会很慢,从而干扰整个程序的效率;
  • 处理代码有可能是一段复杂的数据计算,计算量很大的话,就会卡住整个执行流程;
  • 处理代码有bug,可能直接进入了一段死循环……

这时你会发现,这里的Block和本文之初讲的O_NONBLOCK是不同的事情。在一个网络服务中,如果处理程序的延迟远远小于网络IO,那么这完全不成问题。但是如果处理程序的延迟已经大到无法忽略了,就会对整个程序产生很大的影响。这时IO多路复用已经不是问题的关键。

试分析和比较下面两个场景:

  • web proxy。程序通过IO多路复用接收到了请求之后,直接转发给另外一个网络服务。
  • web server。程序通过IO多路复用接收到了请求之后,需要读取一个文件,并返回其内容。

它们有什么不同?它们的瓶颈可能出在哪里?

总结

小结一下本文:

  • 对于socket的文件描述符才有所谓BIO和NIO。
  • 多线程 BIO模式会带来大量的资源浪费,而NIO IO多路复用可以解决这个问题。
  • 在Linux下,基于epoll的IO多路复用是解决这个问题的最佳方案;epoll相比select和poll有很大的性能优势和功能优势,适合实现高性能网络服务。

但是IO多路复用仅仅是解决了一部分问题,另外一部分问题如何解决呢?且听下回分解。

,
    推荐阅读
  • 手机最耐用是哪款(手机耐用排行榜)

    华为Mate40配备4200mAh电池,支持40W快充,搭载麒麟9000E,后置5000万+1600万+800万,其中1300万,配备6.5英寸OLED屏,支持90Hz刷新率。小米11第三款推荐小米11,这是全球首款搭载骁龙888的手机,而且就连雷军也调侃,如今依然有很多小米6钉子户,由此来看,小米数字旗舰机还是很耐用的。当然最关键的是,小米11价格便宜,搭载三星供应的2K曲面屏,支持120Hz刷新率,支持全场景快充,后置108MP三摄,以3999元这个价位来评价小米11,几乎看不到短板。

  • 消逝的光芒2阔剑怎么获得 消逝的光芒2阔剑怎么获得的

    游戏中有一把名字叫阔剑的双手斧,许多小伙伴不知道如何获取这把武器,下面小编就带来消逝的光芒2阔剑获取方法分享,一起来看看吧。消逝的光芒2阔剑获取方法分享这3个点水下都有箱子,包括空投物资,会刷新,装备等级是自己等级,随机出金。这个地点车后面会刷新武器阔剑。

  • 美国一父亲剃掉胡子(美国一父亲剃掉胡子)

    来源:环球网来自美国印第安纳州30岁的亚伦·布鲁克斯蓄着络腮胡子,在他1岁女儿康布里眼中,父亲的形象就是这样的。亚伦近日剃掉了胡子想看看女儿的反应,他先用毛巾盖住脸,然后抱着女儿。当他露出现在的形象之后,女儿懵了,似乎很吃惊,马上就要哭出来,并伸手要妈妈。妈妈凯特琳说康布里很快就意识到这是爸爸,然后就没事了。夫妇俩在他们的大女儿艾比小时候也做过这样的事,超级可爱,所以也想看看小女儿的反应。

  • 祭祖扫墓是什么意思(清明将至返乡祭祖)

    “故”指的是过世、离世的含义。而“先”与“显”的差异,对于“先”来说,指的是去世的父母,饱含缅怀与哀思之情,更多用于普通身份的人群中。而“显”有显赫、显贵的含义,体现了对逝者的歌功颂德,多用于显贵人群,彰显其身份地位!当然,在农村习俗丧葬礼仪中,对于先或显的使用也是有要求的,对于逝者倘若身后还有在世的长辈,是不能用“显”字的!只有身后子孙繁盛,无在世长辈才能用“显”字来彰显其功德!

  • 我们结婚吧最般配的三对(综艺我们结婚了你最喜欢哪对夫妇)

    《我们结婚了》是韩国MBC电视台制作的一档真人秀节目,韩国当红明星们组成假想夫妻,进行假想夫妻生活。目前进行到第四季,共播出300多期,已有超过30对假想夫妻参与节目。亚当夫妇红薯夫妇维尼夫妇蚂蚁夫妇鲸鱼夫妇幸运夫妇酒窝夫妇闪光夫妇初恋夫妇鬼泽夫妇彩虹夫妇兔绒夫妇格斗夫妇执着夫妇珍惜夫妇有很多夫妇,我不一一列举了,那你最喜欢哪对夫妇呢?

  • 历年考研英语单词汇总(考研英语核心500词day-2)

    考研英语学起来困难?首先词汇量是一个基础的标准!核心500词为重中之重懵逼人,懵逼魂,懵逼树下学英文搞快点,搞快点!

  • 五四青年节以青春之名致敬岁月(五四青年节特别节目)

    2022年将召开党的第二十次全国代表大会,今年也是中国共产主义青年团成立100周年。1920年,俞秀松等8人在渔阳里6号发起成立上海社会主义青年团,这是中国第一个社会主义青年团。节目主创之一、“中国节日”系列执行策划徐娜表示,这一次特别节目,通过文艺形式展现党领导中国青年运动一百年的光辉历程,进而让当代青年从百年团史中汲取精神力量,从先辈嘱托中明确历史责任,自觉为复兴大业努力奋斗。

  • 用烤箱怎样烤鸡翅(如何用烤箱烤鸡翅)

    下面内容希望能帮助到你,我们来一起看看吧!用烤箱怎样烤鸡翅原料:鸡翅,生抽,老抽,蚝油,料酒,花椒油,十三香,盐,生姜蓉。首先鸡翅中清洗干净,并沥干水分。用刀将鸡翅的两面划几道口子,方便入味,将准备好的调料加入鸡翅里,用手抓匀后密封放入冰箱冷藏。鸡翅腌制时间越长越入味,最好腌制24小时,等鸡翅腌制好后,预热烤箱200度。将鸡翅的烤盘放入烤箱先烤15分钟,然后在鸡翅的表面刷一层蜂蜜,再继续烤五分钟即可。

  • 炎症是什么引起的 女人炎症是什么引起的

    没错炎症就是我们平时俗称的发炎,而且这是我们人体自然发生的防御反应之一,此外与细菌,病毒等感染,甚至是化学性因子等等都是密切相关的。具有血管系统的活体组织对损伤因子的防御性反应称为炎症。因此可以说炎症是损伤和抗损伤的统一过程。由生物病原体引起的炎症又称感染。(六)变态反应:当机体免疫反应状态异常时,可引起不适当或过度的免疫反应,造成组织和细胞损伤而导致炎症。