同步和异步
发出一个调用时,在没有得到结果之前,该调用就不返回,返回时,就携带了结果。
调用在发出之后,这个调用就直接返回了,所以没有返回结果,通过状态,通知,回调来得到结果。
阻塞和非阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
举个栗子
-
普通水壶,水开没有特征
小明烧水,等水开了,再去吃饭
小明烧水,立马去吃饭,但在吃饭过程中,没隔一段就去看水的状态
-
响水壶,水开壶会响
小明烧水,等水壶响了,再去吃饭
小明烧水,立马去吃饭,等水壶响了,关火,继续吃饭
IO发展史
相关技术
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不一定是最佳实践,要结合具体业务场景
用户态与内核态
内核态:控制计算机的硬件资源,并提供上层应用程序运行的环境。
用户态:上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源。
系统调用:为了使上层应用能够访问到这些资源,内核为上层应用提供访问的接口。
-
系统调用与上层应用程序的关系:
- 如果将系统调用比作是一个“比画”,那么上层应用就是一个“汉字”。如果完成一个“汉字”,就需要通过多个系统调用。
-
系统调用与公用函数库的关系:
- 公用函数库实现对系统调用的封装,将简单的业务逻辑接口呈现给用户,方便用户调用,从这个角度上看,库函数就像是组成汉字的“偏旁”。
在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。所以,CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令。
-
用户态到内核态
- 系统调用
- 异常事件,缺页中断
- 外设中断,Ctrl+C
系统调用的本质其实也是中断,相对于外围设备的硬中断,这种中断称为软中断。从触发方式和效果上来看,这三种切换方式是完全一样的,都相当于是执行了一个中断响应的过程。但是从触发的对象来看,系统调用是进程主动请求切换的,而异常和硬中断则是被动的。