本文参考如下,可直接看原文,这里只做了一些摘要
http://blog.chinaunix.net/uid-28458801-id-4464639.html
同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?这个问题其实不同的人给出的答案都可能不同,比如wiki,就认为asynchronous IO和non-blocking IO是一个东西。这其实是因为不同的人的知识背景不同,并且在讨论这个问题的时候上下文(context)也不相同。所以,为了更好的回答这个问题,我先限定一下本文的上下文。
对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:
1)等待数据准备 (Waiting for the data to be ready)
2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
记住这两点很重要,因为这些IO模型的区别就是在两个阶段上各有不同的情况。
四种常用IO
blocking IO
non-blocking IO
IO multiplexing
asynchronous IO
1:blocking IO, 阻塞IO
在linux中,默认情况下所有的socket都是blocking
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。
2:non-blocking IO, 非阻塞IO
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
3:IO multiplexing, 多路复用IO
它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。因此select()与非阻塞IO类似。
4:asynchronnous IO,异步IO
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
synchronous IO和asynchronous IO两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。有人可能会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个系统调用。non-blocking IO在执行recvfrom这个系统调用的时候,如果kernel的数据没有准备好,这时候不会block进程。但是当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内进程是被block的。而asynchronous IO则不一样,当进程发起IO操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
5:epoll
https://www.cnblogs.com/hepingqingfeng/p/6656073.html
先给结论
内核缓冲--数据好了(完整)--产生中断--调用中断函数表中的回调函数--将此fd放到list链表中--epoll_wait一直在遍历这个list链表--有就直接处理
一、epoll 触发方式
epoll支持 ET 和 LT 两种触发方式
ET(边缘触发):Nginx 就是采用 ET 触发方式,只支持 no-block 方式,当一个 fd 缓冲区就绪的时候,只会发送一次事件触发, 而不会管缓冲区的数据是否已经被读取,都不会再发送第二次
LT(边缘触发):支持no-block 和 block 两种方式,当一个 fd 缓冲区就绪时,只要缓冲区有数据,就会不停的发送就绪通知
二、epoll 相关函数:
2.1、int epoll_create(int size);
用于创建一个 epoll 句柄,创建一个 epoll 句柄之后,会占用一个 fd 描述符,对于一个进程来说,它相关的 fd 描述符可以查看/proc/进程id/fd/, 在使用完epoll 之后,需要对他进行 close ,否则会导致 fd 太多被耗尽
2.2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
事件注册函数,将 fd 添加、修改、删除到 epfd 中,通过 op 参数修改
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
2.3、int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
可以理解为收集epoll 监控的所有事件中,已经发生的那部分 fd 的数量
三、epoll 工作原理
3.1、在调用 epoll_create 之后,内核就已经创建了一个 eventpoll 红黑树结构体,一个 list 双向链表,在内核态准备接受存储需要监控的 fd。
3.2、在调用 epoll_ctr 之后,直接向内核态的 eventpoll 进行 add/mod/del 对应的 fd,对于新添加进来的 fd,重复的 fd 可以通过 eventpoll 红黑树识别出来,而不需要再次从用户态拷贝到内核态这个过程
3.3、同时 epoll 还维护了一个双向的 list 链表, 在epoll_ctr执行的时候,除了会向eventpoll 红黑树添加修改外,还会在内核中断函数处理程序中注册一个回调函数,告诉内核,当这个 fd 就绪之后,将他放到 list 里面去。
3.4、在 epoll_wait 调用的时候,就是观察这个双向 list 是否有数据,有就直接处理即可
6:系统IO
https://www.cnblogs.com/losing-1216/p/5073051.html
一个简单实例
{
char *buf = malloc(MAX_BUF_SIZE);
strncpy(buf, src, , MAX_BUF_SIZE);
fwrite(buf, MAX_BUF_SIZE, 1, fp);
fclose(fp);
}
这里malloc的buf对于图层中的application buffer,即应用程序的buffer;调用fwrite后,把数据从application buffer 拷贝到了 CLib buffer,即C库标准IObuffer。fwrite返回后,数据还在CLib buffer,如果这时候进程core掉。这些数据会丢失。没有写到磁盘介质上。当调用fclose的时候,fclose调用会把这些数据刷新到磁盘介质上。除了fclose方法外,还有一个主动刷新操作fflush函数,不过fflush函数只是把数据从CLib buffer 拷贝到page cache 中,并没有刷新到磁盘上,从page cache刷新到磁盘上可以通过调用fsync函数完成。
从上面类子看到,一个常用的fwrite函数过程,基本上历经千辛万苦,数据经过多次copy,才到达目的地。有人心生疑问,这样会提高性能吗,反而会降低性能吧。这个问题先放一放。
有人说,我不想通过fwrite+fflush这样组合,我想直接写到page cache。这就是我们常见的文件IO调用read/write函数。这些函数基本上是一个函数对应着一个系统调用,如sys_read/sys_write. 调用write函数,是直接通过系统调用把数据从应用层拷贝到内核层,从application buffer 拷贝到 page cache 中。
系统调用,write会触发用户态/内核态切换?是的。那有没有办法避免这些消耗。这时候该mmap出场了,mmap把page cache 地址空间映射到用户空间,应用程序像操作应用层内存一样,写文件。省去了系统调用开销。
那如果继续刨根问底,如果想绕过page cache,直接把数据送到磁盘设备上怎么办。通过open文件带上O_DIRECT参数,这是write该文件。就是直接写到设备上。
如果继续较劲,直接写扇区有没有办法。这就是所谓的RAW设备写,绕开了文件系统,直接写扇区,想fdsik,dd,cpio之类的工具就是这一类操作。