nio,epoll,多路复用 学习笔记

本篇基本基于linux的系统API介绍的.

Blocking IO

起初BIO主线程负责accept(系统调用accept),然后单独启动一个线程对接收到的连接(fd文件描述符)进行阻塞获取数据(系统调用recvfrom).
然而这样会有很多问题:

  • 系统频繁调用clone(linux可通过clone创建线程)
  • 消耗资源,线程栈内存独占,JVM默认1M
  • 线程很多的时候,CPU线程调度浪费时间
  • blocking造成的时间片浪费

阻塞IO内核调用流程

Nonblocking IO

首先,因为是nio,所以我们不需要单独开一个线程读写每一个fd了.也解决了上述问题:

  • 没有了大量线程创建的需求,kernel的clone调用自然也减少了.
  • 我们可以用一个或多个线程一起做这些操作.线程池资源可设上限,能保护程序不会耗尽资源.
  • 线程池最大量配置也保证CPU调度控制在我们可预期的范围.
  • 更没有了blocking造成的CPU时间片浪费.

非阻塞IO内核调用流程

以前,我看了<Scalable IO in Java>,看了reactor及其变体等线程模型的引入带来的优点后.以为nio带来的好处就是能运用上这种线程模型.
后来,菜鸟面试官锤我的时候告诉我,相似的线程模型即便是blocking IO时代就有人在使用过了(通过超时中断方式).nio编程模型真正带来的本质好处还是对CPU时间片的节省,有过统计表明BIO中90%的时间片都浪费无效等待上.

Nonblocking IO 与 select(多路复用器)

非阻塞IO虽然解决了上述问题,但是接下来将面临C10K问题(上万客户端).
假设有上万客户端连接但其中只有极少数客户端发送了数据
如果是遍历所有文件描述符调用recvfrom来获取数据的话实际上很多系统调用都是无意义的.
每个系统调用都要走软终断浪费性能.
因此linux增加了一个系统调用叫做select(多路复用器)
系统调用API select可以做到:

  • select是阻塞的,会等待一个或多个文件描述符变成了ready状态后返回.
  • 参数能传入多个文件描述符(即监听多个连接)
  • 程序可接收到所有进入了ready状态的文件描述符们
  • 然后再单独对接收到的文件描述符做recvfrom

此时只有极少数发送了数据的客户端会调用recvfrom. 大大减少软中断次数.

select内核调用流程
但是这个模型也存在问题:

  1. 每次传入大量文件描述符(传输浪费,每次调用都会从用户态拷贝参数到内核态)
  2. 如果操作系统内核通过遍历所有文件描述符的方式去实现状态检查将会很浪费性能.

epoll

解决思路

对于问题1的解决办法:
开辟个内核空间记录想监听的文件描述符,通过api添加/删除/修改fd及想要监听的事件类型.
然后问题2的解决方法:
内核不要逐个遍历(如果是这么实现的),而基于事件驱动(event-driven)(比如通过网卡硬中断)的形式通知多路复用器ready的文件描述符事件信息.

epoll方案

epoll_create

创建一块内核空间,用于记录文件描述符.
epoll_create创建完成后返回的也是一个文件描述符. 此描述符用于操作此块空间.

epoll_ctl

epoll_ctl可以对epoll空间进行操作,可新增/修改/删除空间内的文件描述符信息(包括其监听的事件).
个人感觉使用体验类似java中的selector?


附录-视频中用到的linux命令

监听进程的系统调用 strace

strace -ff -o ./file java TestSocket
监听名叫TestSocket的java进程对系统的调用记录输出到当前目录下的file前缀一批文件里.

可能会生成一批记录了此进程所有的线程对系统调用的日志文件. 比如:
file.2878
file.2879
...
每个文件记录了一个线程对操作系统API的调用.

查看进程系统信息: /proc/{进程号} 文件夹

linux文件夹 /proc/{进程号}/ 下有进程的各种(?)信息

比如 /proc/2878 下有
attr cmdline environ task fd .......等等文件/文件夹

其中task文件夹里有此进程下的所有线程号
如以下文件(具体干啥的没说,但是以线程号作为文件名):

2878 2879 2880

其中fd文件夹里存有文件描述符,如下: ls -l样例

lrwx------ 1 root root 64 Mar 21 20:27 0 -> /dev/pts/0
lrwx------ 1 root root 64 Mar 21 20:27 1 -> /dev/pts/0
lrwx------ 1 root root 64 Mar 21 20:27 2 -> /dev/pts/0
lrwx------ 1 root root 64 Mar 21 20:27 3 -> /usr/java/jdk1.8.0_181-amd64/jre/lib/rt.jar
lrwx------ 1 root root 64 Mar 21 20:27 4 -> socket:[19542]
lrwx------ 1 root root 64 Mar 21 20:27 5 -> socket:[19544]

标准输入0, 标准输出1 ,错误输出2
4,5有两个socket的原因是因为一个ipv4和ipv6

网络状态命令 netstat 查看端口占用

netstat -natp(a:显示所有,t:显示tcp协议,p:显示进程号)
如下:

Active Internet connections (servers and established)
Proto Recv-Q Send-Q   Local Address            Foreign Address              State                  PID/Program name
tcp                 0         0    0.0.0.0:22                 0.0.0.0:*                 ..............................
tcp            0         0    127.0.0.1:25              ............................................
tcp            0         0    0.0.0.0:3306               ...................
tcp            0       52    192.168.150.11:22           ................

表格形式展示就是

Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 991/sshd
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1238/master
tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN 1146/mysqld
tcp 0 0 192.168.150.11:22 192.168.150.1:52071 ESTABLISHED 2932/sshd
tcp 0 0 192.168.150.11:22 192.168.150.1:52043 ESTABLISHED 2890/sshd
tcp 0 0 192.168.150.11:22 192.168.150.1:51157 ESTABLISHED 2822/sshd
tcp 0 0 :::22 ::😗 LISTEN 991/sshd
tcp 0 0 ::1:25 ::😗 LISTEN 1238/master
tcp 0 0 :::8090 ::😗 LISTEN 2878/java
tcp 0 0 ::1:8090 ::1:\58181 ESTABLISHED 2878/java
tcp 0 0 ::1:58181 ::1:8090 ESTABLISHED 2947/nc

附录-linux 几个API简介( linux C API )

linux C API

recv

doc

receive a message from a socket
从socket中接收消息

  • NAME
    recv, recvfrom, recvmsg
  • SYNOPSIS
       #include <sys/types.h>
       #include <sys/socket.h>
       
       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

       ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

accept

doc

accept a connection on a socket
从socket中接收一个连接

  • NAME
    accept, accept4
  • SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <sys/socket.h>

       int accept4(int sockfd, struct sockaddr *addr,
                   socklen_t *addrlen, int flags);

select

doc

synchronous I/O multiplexing
同步IO多路复用

  • NAME
    select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO
  • SYNOPSIS
       #include <sys/select.h>

       int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

       void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);

       int pselect(int nfds, fd_set *readfds, fd_set *writefds,
                   fd_set *exceptfds, const struct timespec *timeout,
                   const sigset_t *sigmask);

epoll

doc

I/O event notification facility
I/O事件通知工具

epoll_create

doc

open an epoll file descriptor
打开一个epoll文件描述符

  • NAME
    epoll_create, epoll_create1
  • SYNOPSIS
       #include <sys/epoll.h>
       int epoll_create(int size);
       int epoll_create1(int flags);

epoll_ctl

doc

control interface for an epoll file descriptor
控制用于epoll文件描述符的接口

  • NAME
    epoll_ctl
  • SYNOPSIS
       #include <sys/epoll.h>

       int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参考

周志垒讲解视频:清华大牛权威讲解nio,epoll,多路复用,更好的理解redis-netty-Kafka等热门技术
后来又看了一篇介绍不错的推荐,占小狼介绍内核io演进史彻底搞懂 select/poll/epoll,这篇就够了

posted @ 2020-07-27 03:45  江借时www  阅读(713)  评论(0编辑  收藏  举报