cpu运行原理:
假设现在只有一个cpu,说一下在这里的产生的两种中断:
1.时钟中断:一个cpu需要执行所有的程序,那么就需要晶振产生时钟中断cpu,cpu再进行进程调度执行下一个程序。
由于同一时间只能执行一个程序,所以需要将上一个程序的现场保护起来,把将要执行的程序的现场恢复。这里的保护现场和恢复现场涉及到IO,假设有1w个进程,那么cpu势必有大量的时间浪费在进程的调度上。
2.系统调用中断:程序自身也有可能会需要中断来做其他的事情,由于中断程序是在内核里面的,所以程序会调用中断。
3.外部中断:比如鼠标的移动,cpu需要计算鼠标位置的更新,也会产生中断(自己的理解,不知道对不对)
之所以说上面的知识是为我们理解下面的IO打下基础。
BIO(阻塞式IO)
先看代码:
写过Socket的话应该知道,这里有两个方法是阻塞的。一个是serverSocket的accept方法,一个是从客户端读取数据的方法。
这个时候,为了能一直接收客户端请求,在接收到客户端请求以后就会抛出一个线程(一个轻量级的进程,但是显示依然是主进程的id号)
在linux使用strace命令追踪进程的执行过程,我们得到上面的过程。首先会创建一个标识符为3的socket,然后绑定到8090端口,执行监听方法。接着调动accpet方法等待客户端连接,如果有客户点连接上来,就抛出一个线程,即把内存复制一份到其他地方去。
但是,如果线程过多不仅消耗大量内存,同时cpu资源调度的消耗也很严重。所以我们需要一种非阻塞式IO来解决这个问题。
NIO(非阻塞式IO)
NIO的理解:
对操作系统:nonbloking IO 非阻塞式IO
对Java:new IO 新的IO体系
先看代码:
可以看见,ServerSocket的创建与上面类似,但是可以配置block为false即非阻塞式IO。并且配置了客户端链表来存放新的连接。
接下来是一个死循环,调用accept方法,但是这个方法不会阻塞了。如果有客户端连接过来返回一个client,如果没有就返回null。接下来判断client是否为空,不为空则把client加入到客户端链表中。
对所有的client遍历,如果有新的发送内容则接收。
在linux使用strace命令追踪进程的执行过程,我们得到上面的过程。前面创建socket,绑定端口号,开启监听,与BIO不同的是这里设置了nonbloking。接下来进入到死循环,不停的查看是否有新的连接过来,然后遍历所有客户端是否有新的数据发送。
这里只有一个主线程,解决了BIO的内存和cpu消耗的问题。但是新的问题是,如果连接数很多,但是发送数据的又很少,这样遍历一次所有的客户端是很浪费的。所以出现了多路复用器来解决上面的问题。
多路复用器
对于select、poll、epoll来说它们都是同步的,就是需要程序自己来读取IO。
select、poll
对于select、poll来说,只需要将所有的client文件描述符(fds)传递给它们,就会返回一个状态告知程序有几个可读,接下来只需要遍历可读的即可。
这样的方式把所有fds传递给内核,内核遍历减少了系统调用的次数,也就减小了IO的开销。但是这种模型依然存在弊端:fds每次需要重复传递、只返回状态还需要自己来遍历。
epoll
先看代码:
1.这里selector调用open方法(底层是create方法),即在内存里面开辟一块空间,然后调用register方法(底层是ctl方法)把上面创建的server放进这个空间。
2.接下来是一个死循环,调用select方法(底层是wait方法,阻塞,但是一旦有新的连接或者已连接有IO的话就会执行下一步),然后得到所有状态有更新的fd(文件描述符)进行遍历。
3.对取得的fd进行判断。
如果是可读的,那么肯定就是客户端有消息发送过来,调用方法接收信息。
如果是可接收的,那么肯定是客户端的连接来了,需要把这个新的客户端放入这个空间(即调用1里面的register方法)。看下图
在linux使用strace命令追踪进程的执行过程,我们得到上面的过程。前面创建server,绑定端口号,开启监听,设置了nonbloking。然后epoll开辟内存空间,把server放进去,然后阻塞等待新的客户端连接或者接收客户端信息。当有一个新的客户端连接上来,得到fd为8,把这个客户端也放进内存空间,然后继续阻塞。
即使是向上面那样,我们依然可以看见存在的问题,如果这里的客户端很多,那么接收信息肯定需要大量的时间,而新的连接将很久不能连接上,因为需要等待所有客户端读写完毕。这个时候,可以创建多个selector,一个负责接收客户端,另外几个负责把客户端注册到他们各自的空间里面去,这样负责接收的就可以快速响应客户端。
比如nginx就是一个master,多个worker在工作。