IO发展史

同步和异步

  • 发出一个调用时,在没有得到结果之前,该调用就不返回,返回时,就携带了结果。

  • 调用在发出之后,这个调用就直接返回了,所以没有返回结果,通过状态,通知,回调来得到结果。

阻塞和非阻塞

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

举个栗子

  • 普通水壶,水开没有特征

    • 小明烧水,等水开了,再去吃饭

    • 小明烧水,立马去吃饭,但在吃饭过程中,没隔一段就去看水的状态

  • 响水壶,水开壶会响

    • 小明烧水,等水壶响了,再去吃饭

    • 小明烧水,立马去吃饭,等水壶响了,关火,继续吃饭

IO发展史

阻塞IO.png
非阻塞IO.png
信号驱动IO.png
IO多路复用.png
AIO.png
IO对比.png

相关技术

  • select

  • poll

  • epoll

  • kqueue

  • IOCP

epoll

//用户数据载体
typedef union epoll_data {
   void    *ptr;
   int      fd;
   uint32_t u32;
   uint64_t u64;
} epoll_data_t;
//fd装载入内核的载体
 struct epoll_event {
     uint32_t     events;    /* Epoll events */
     epoll_data_t data;      /* User data variable */
 };
 //三板斧api
int epoll_create(int size); 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  
int epoll_wait(int epfd, struct epoll_event *events,
                 int maxevents, int timeout);
  • poll_create是在内核区创建一个epoll相关的一些列结构,并且将一个句柄fd返回给用户态,后续的操作都是基于此fd的,参数size是告诉内核这个结构的元素的大小。

  • epoll_ctl是将fd添加/删除于epoll_create返回的epfd中,其中epoll_event是用户态和内核态交互的结构,定义了用户态关心的事件类型和触发时数据的载体epoll_data。

  • epoll_wait是阻塞等待内核返回的可读写事件,epfd还是epoll_create的返回值,events是个结构体数组指针存储epoll_event,也就是将内核返回的待处理epoll_event结构都存储下来,maxevents告诉内核本次返回的最大fd数量,这个和events指向的数组是相关的。

  • epoll_event是用户态需监控fd的代言人,后续用户程序对fd的操作都是基于此结构的。

  • epoll_create场景:

    • 我们刚进入公司,需要设备,确定一个学习委员,学习委员帮大家申请设备,学习委员告诉IT部门,我是校招负责人,他们给一个凭证,以后都会用到
  • epoll_ctl场景:

    • 学习委员开始收集大家的设备需求,比如AA需要一个11 max pro,BB需要一个2019年16.1寸 macbook pro,学习委员告诉IT部门
  • epoll_wait场景:

    • 学习委员就在等通知,这时候蓝信收到通知,BB的macbook pro批下来了,AA的还没有批下来,学习委员就继续等;

epoll高效的原因

  • 在Linux内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。

  • epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。

  • 由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件.

  • 当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。

  • 当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已.

执行过程

  • 执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。

补充说明

  • epoll不一定是最佳实践,要结合具体业务场景

用户态与内核态

用户态.png
  • 内核态:控制计算机的硬件资源,并提供上层应用程序运行的环境。

  • 用户态:上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源。

  • 系统调用:为了使上层应用能够访问到这些资源,内核为上层应用提供访问的接口。

  • 系统调用与上层应用程序的关系:

    • 如果将系统调用比作是一个“比画”,那么上层应用就是一个“汉字”。如果完成一个“汉字”,就需要通过多个系统调用。
  • 系统调用与公用函数库的关系:

    • 公用函数库实现对系统调用的封装,将简单的业务逻辑接口呈现给用户,方便用户调用,从这个角度上看,库函数就像是组成汉字的“偏旁”。
  • 在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。所以,CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令。

  • 用户态到内核态

    • 系统调用
    • 异常事件,缺页中断
    • 外设中断,Ctrl+C
  • 系统调用的本质其实也是中断,相对于外围设备的硬中断,这种中断称为软中断。从触发方式和效果上来看,这三种切换方式是完全一样的,都相当于是执行了一个中断响应的过程。但是从触发的对象来看,系统调用是进程主动请求切换的,而异常和硬中断则是被动的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。