nio,epoll,多路復用 學習筆記


周志磊講bio,nio,select,epoll

周志壘講解視頻:清華大牛權威講解nio,epoll,多路復用,更好的理解redis-netty-Kafka等熱門技術
本篇基本基於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);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM