主线:

**
image.jpeg
把netty吃透,就可以说熟悉javaIO的了

需要的基础:
计算机组成原理

笔记

day01 系统中断

系统中断有哪几种?时钟中断、软中断、IO中断

在单核心cpu的电脑上,为什么能实现多进程并发运行?
cpu通过时钟中断切换进程轮流获取cpu时间片,使得在一个时间段内,cpu可以交替执行不同进程的指令,在肉眼看来,一段时间内多个进程被同时执行。

什么是 时钟中断 ?INT 0x30
计算机中的晶振将直流电转化成间歇中断的电流,这个中断的频率一般在100-1000/s. 当cpu收到间隔信号时,cpu会做执行程序的切换。此时cpu做三件事:
1. 停止从正在执行的进程A读取指令
2. 将cpu寄存器和缓存中的指令和数据写回到程序A内存空间,清空程序A对应的cpu cache
3. 调用中断向量表中的回调函数。 (中断向量表是在系统启动的时候注册的)
此时,程序A已经停止执行,cpu通过系统调用访问中断向量表中的回调函数,内核通过程序调度访问程序B,将B在内存中的指令和数据装载到寄存器和cpu cache,开始执行B的指令集。

为什么打开的应用程序越多,cpu的性能损耗会越大?
假定cpu通过时钟中断做程序切换花费的时间为0.1ms, 且1秒内同时执行的进程只有2个,则1秒内程序切换次数为2次花费时间为0.2ms,cpu的有效使用率为98%; 如果1秒内同时执行的进程有10个,则1秒内花费在进程切换上的时间为1ms,cpu的有效使用率下降为90%。

软中断 INT x80 应用程序要访问内核底层函数或者想和硬件交互,需要系统调用SystemCall来完成。应用程序从用户态到内核态转化,会触发软中断,比较损耗性能。

用户态和内核态切换

day02 网络通信

同步IO
无论是BIO/NIO,还是多路复用IO,只要是程序自己去读IO数据,那么都是同步IO.
目前只有windows的IOCP是异步IO. 内核线程做数据读取并把数据Copy到应用程序的内存空间。

  • select
  • poll
  • epoll

BIO

  1. @Test
  2. public void serverStart() throws IOException {
  3. ServerSocket ss = new ServerSocket(8090);
  4. while (true){
  5. Socket accept = ss.accept();
  6. new Thread(()->{
  7. try {
  8. InputStream inputStream = accept.getInputStream();
  9. String read = IoUtil.read(inputStream, Charset.defaultCharset());
  10. System.out.println(read);
  11. if (read.startsWith("a")){
  12. OutputStream outputStream = accept.getOutputStream();
  13. IoUtil.write(outputStream,Charset.defaultCharset(),true,"a你好啊,你是大哥");
  14. }
  15. } catch (IOException e) {
  16. e.printStackTrace();
  17. }
  18. }).start();
  19. }
  20. }
  21. @Test
  22. public void clientLink() throws IOException, InterruptedException {
  23. Socket socket=new Socket("localhost",8090);
  24. OutputStream outputStream = socket.getOutputStream();
  25. IoUtil.write(outputStream,Charset.defaultCharset(),true,"a:我是客户端a");
  26. Thread.sleep(10000);
  27. }

创建服务端连接 new ServerSocket(8090) ,客户端连接ServerSocket
1. socket( ?,socketStream, ip)=3 // 打开一个socket,输出到3这个文件描述符上
2. bind(3, …8090…) //把3这个FD绑定到8090端口
3. listen(3, 50) //监听3这个FD
4. accept(3, clientIp+port.. , ?) = 5 // 【阻塞】接受客户端连接,输出到5这个FD上
5. recv(5, ) //【阻塞】 当客户端连接请求到达时,服务端接收FD=5的数据传输,也就是从这个输入流通道上读取数据

不管你是什么网络通信,什么redis,dubbo,只要打开了一个ss,连接了一个s,必然会走linux内核的上述系统调用。

java线程的本质是 java应用进程通过系统调用clone得到一个在操作系统中的一个轻量级系统进程,该轻量级进程属于java应用进程,是进程组的一部分。

BIO的缺点:
一连接一线程 ,连接量过大时,线程上下文切换频繁,每个线程做的事太少,太多性能浪费在线程切换上。

  • 线程内存浪费
  • cpu调度消耗

NIO
java New IO 或者是说 Non-Blocking IO

  1. @Test
  2. public void serverStart() throws IOException, InterruptedException {
  3. LinkedList<SocketChannel> clients = new LinkedList<>();
  4. ServerSocketChannel ss = ServerSocketChannel.open();
  5. ss.bind(new InetSocketAddress(8090));
  6. ss.configureBlocking(false);// 重点 不阻塞
  7. while (true) {
  8. // 每2秒拉一次客户端请求
  9. Thread.sleep(1000);
  10. // 在NIO时代,如果ss.configureBlocking(false)上面这里不会阻塞,有连接返回连接,无连接返回-1
  11. SocketChannel accept = ss.accept();
  12. if (accept == null) {
  13. System.out.println("无客户端连接,accept方法没有阻塞=====");
  14. } else {
  15. accept.configureBlocking(false);
  16. clients.add(accept);
  17. }
  18. ByteBuffer bf = ByteBuffer.allocate(4096);
  19. // 遍历已经链接进来的客户端 读取数据
  20. for (SocketChannel client : clients) {
  21. int read = client.read(bf);
  22. if (read > 0) {
  23. bf.flip();
  24. byte[] bytes = new byte[bf.limit()];
  25. bf.get(bytes);
  26. String ret = new String(bytes);
  27. System.out.println(client.socket().getPort() + ":" + ret);
  28. bf.clear();
  29. }
  30. }
  31. }
  32. }
  33. @Test
  34. public void clientLink() throws IOException {
  35. SocketChannel open = SocketChannel.open();
  36. open.bind(new InetSocketAddress(8090));
  37. open.configureBlocking(false);
  38. }

从接客accpt到客户端读 都是非阻塞,
死循环走一次 非阻塞拉取一个客户端,如果没有请求的连接 则返回-1
读客户端传输非阻塞,每一次循环对所有clinet读一次。

NIO的优势:
规避了并发量很大时,多线程性能问题。

什么是 C10K问题 ?

弊端:
1. 当客户端数量过多时,单线程执行的任务量过大,一次循环耗时过长。(从客户端读,涉及系统调用,需要软中断)
2. 【重要】每次循环都会获取所有客户端发送过来的数据,即使客户端没有投递数据,服务端也会通过系统调用去获取。如果有1万个客户端连接,但平均每循环一次只有1个客户端发送数据且数据就绪,那么其余的9999次系统调用都是无效的。(这叫用户空间向内核空间的循环遍历,时间复杂度在对所有客户端循环遍历的系统调用上)

IO多路复用
核心思想是:先去内核查找1万个客户端连接中,有多少连接传输的数据已到达,然后再去读这些数据。
在NIO时代,一个IO要进行一次系统调用,但是现在1万个客户端连接 多路IO复用了一次系统调用。
【判断题】是否通过io多路复用器就可以快速读出IO数据?
不能。IO多路复用器只是select出the ready descriptors,并没有读数据。

select/poll

多路复用IO的优势:
1. 通过一次系统调用,把fds传递给内核,内核遍历所有并标记所有可用fd,减少了系统调用次数。
select/poll的弊端:
1. 每次都把1万个fds重复传递给内核; 解决方案:内核开辟空间保留fd, 把所有新的客户端维护在红黑树里
2. 全量遍历 每次select/poll的时候都要重新遍历全量的fd (中断、callback)

epoll
image.jpeg
epoll使用了【空间换时间】的思想
空间A用于应用进程所有已连接客户端fd, 使用红黑树
空间B 就绪的fd
两个cpu可以异步执行,1做io操作,2执行程序代码

  1. 解决重复投递:内核维护一个红黑树用于存放应用所有的fd,用户进程每次在systemCall只需要增量传递新accept到的客户端fd
    2. 解决全量遍历:基于事件通知,io读写就绪后直接copy到应用缓冲区
    以下系统调用:
  • socket() = fd3
  • fcntl(3,F_SETTFL,NONBLOCKING) =0 设置非阻塞
  • bind(4, 8090) 绑定端口
  • listen(3,50) 监听
  • epoll_create(size)=7 创建epoll句柄,返回一个文件描述符fd。这个fd描述的就是上图空间A的内存区域(只会create一次)
  • epoll_ctl(7,ADD,3,EPOLLIN) 控制某个epoll文件描述符上的事件。将fd3向空间A fd7添加accept的监听事件。
  • epoll_wait(7) 轮询IO事件的发生。超时阻塞。等1秒取不取得到都返回。
  • accept(3) =8 连接一个客户端fd8
  • epoll_ctl(7,ADD,8,epollin) 客户端连接fd8添加读监听事件到fd7

参考文献

IO模型及select、poll、epoll和kqueue的区别