成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

BIO、NIO 到多路復用的演進路徑,你明白了嗎?

系統(tǒng) 其他OS
IO 從開始的瓶頸就是在操作系統(tǒng)的 read 數(shù)據(jù)讀取方法,由于這個阻塞的方法導致了 BIO 的產(chǎn)生,為了解決阻塞 IO 的問題,同時提高效率,就產(chǎn)生了使用多線程技術(shù)操作 IO 來提升性能,但是 IO 的瓶頸問題并沒有解決。

從 NIO 到 Netty

IO 是編程中一個重要的概念,不論是數(shù)據(jù)存儲和網(wǎng)絡(luò)通信,底層都是會用到,理解 IO 對面試和工作都有很大的幫助,也能從基礎(chǔ)理論層面扎實基礎(chǔ),理解其上層應(yīng)用就簡單的多了。在常用的軟件中,例如 Nginx、Redis、Dubbo、Kafka 都涉及到了 NIO 的一些基礎(chǔ)知識,本文就從簡單的 IO 開始剖析,從 BIO 到 NIO 再到 Netty,從理論到實踐進行深入的理解。

計算機組成

計算機的組成包括 CPU、內(nèi)存存儲、網(wǎng)卡、磁盤存儲和其它外部設(shè)備。在 Linux 操作系統(tǒng)中,一切皆文件(即文件描述符 fd,file descriptor),在服務(wù)啟動時,會加載內(nèi)核程序到 CPU 中運行。為了保證服務(wù)的正常運行,內(nèi)核程序具有較高的優(yōu)先級,所占用的空間為內(nèi)核空間,其它應(yīng)用程序所占用的空間為用戶空間。以 Java 程序為例來講,其也是一個程序并且占用一定的內(nèi)存空間,在應(yīng)用運行過程中,如果有 IO 操作或者計算需求,則需要將其轉(zhuǎn)交給內(nèi)核程序來完成。因為內(nèi)核(kernel)保護模式的存在,應(yīng)用程序是沒有權(quán)限調(diào)用 CPU 的,一切的操作都需要通過內(nèi)核程序來完成,只有這樣才能保證一旦應(yīng)用程序錯誤,內(nèi)核程序不會受影響,整個系統(tǒng)就沒有宕機的風險。

內(nèi)核程序和應(yīng)用程序之間通過中斷(通常的有 80 中斷)來完成操作的切換,應(yīng)用系統(tǒng)通過內(nèi)核程序提供的系統(tǒng)調(diào)用(System Call,是一系列的系統(tǒng)操作函數(shù),是內(nèi)核系統(tǒng)暴露出來的 API)來實現(xiàn)對 CPU 或 IO 的操作,CPU 通過 FCFS(非搶占式的先來先服務(wù)算法)分配各個任務(wù)的時間片,來實現(xiàn)各個任務(wù)并發(fā)運算。在 Java 的多線程應(yīng)用中,有個上下文切換的概念,這就是應(yīng)用線程將任務(wù)切換到內(nèi)核線程,在 CPU 的時間片內(nèi)繼續(xù)進行操作,完成操作后將內(nèi)核線程切換到應(yīng)用線程。

進程是系統(tǒng)分配資源的基本單位,線程是 cpu 執(zhí)行調(diào)度的基本單位,線程也稱之為輕量級的進程(LWP)。java 的線程就是通過內(nèi)核的系統(tǒng)調(diào)用,在操作系統(tǒng)中獲取到的輕量級進程。

阻塞與非阻塞/同步與異步

這里線說一下小編理解的阻塞與非阻塞以及同步和異步的概念:

阻塞和非阻塞描述的是用戶線程調(diào)用內(nèi)核 IO 操作的方式,阻塞是發(fā)起調(diào)用后需要等待直至內(nèi)核給出結(jié)果數(shù)據(jù)是否可讀可寫,非阻塞是發(fā)起用調(diào)用后無需等待結(jié)果,給出狀態(tài)值-1 表示正在處理。

同步和異步描述的是用戶線程和內(nèi)核的交互數(shù)據(jù)的方式,同步是需要用戶線程自己獲取數(shù)據(jù),即使是多路復用器也是解決了阻塞的問題,還需要用戶線程自己獲取數(shù)據(jù),依舊是同步 IO 模型。而異步是用戶線程發(fā)起調(diào)用后不需要主動獲取數(shù)據(jù),而是內(nèi)核處理完畢后將數(shù)據(jù)放入用戶空間中再通知用戶線程繼續(xù)業(yè)務(wù)處理。

在常見 socket 編程中,如下所示:

//把Socket服務(wù)端啟動
ServerSocket server = new ServerSocket(8986);
while (true) {
// 阻塞方法,等待客戶端的接入
Socket client = server.accept();
// 得到輸入流
InputStream input = client.getInputStream();
// 建立緩沖區(qū)
byte[] buff = new byte[4096];
int len = input.read(buff);
// 只要一直有數(shù)據(jù)寫入,len就會一直大于0
if (len > 0) {
String msg = new String(buff, 0, len);
System.out.println("收到" + msg);
}
}

在操作系統(tǒng)中運行使,如何監(jiān)聽其操作系統(tǒng)級別的指令呢?首先需要將創(chuàng)建 java 文件

# 創(chuàng)建 java 文件
Bio001Test.java
# 然后使用 javac 命令編譯成 Bio001Test.class
javac Bio001Test.java
# 執(zhí)行java 代碼
java Bio001Test
# 使用 strace 命令進行監(jiān)聽系統(tǒng)調(diào)用的情況,其底層是使用內(nèi)核的ptrace 特性來實現(xiàn)的
strace -ff -o out java Bio001Test

下圖是 java 代碼打印出的信息,顯示了 http 的請求記錄:

相比 BIO 的代碼, NIO 的代碼就比較復雜了,BIO 是阻塞的,NIO 是非阻塞的, BIO 是面向流的,只能單向讀寫,NIO 是面向緩沖的, 可以雙向讀寫。

# bio 的阻塞方法
server.accept()
# nio 的非阻塞方法,提出 channel selector buffer 的概念來解決io,利用事件注冊狀態(tài)來處理請求信息
selecter.select()

使用 man socket 來查看操作系統(tǒng)中 socket 傳入的參數(shù),如下所示:

# 操作系統(tǒng)的函數(shù)都是 C 語言編寫的,java 也是類C 的語言
socket()
# 創(chuàng)建一個用于通信的文件描述符
creates an endpoint for communication and returns a descriptor.
...
# 設(shè)置非阻塞參數(shù)項
SOCK_NONBLOCK
Set the O_NONBLOCK file status flag on the new open file description. Using this flag saves extra calls to fcntl(2) to achieve the same result.

socket 稱之為套接字、或者插座,屬于網(wǎng)絡(luò)應(yīng)用程序接口。即是應(yīng)用層到傳輸層的接口,也是用戶進程與系統(tǒng)內(nèi)核交互的接口。一個 TCP 連接的標記為四元組,即源 ip:源 port + 目標 ip:目標 port, 我們都知道計算機的端口范圍為 0-65535,也就是說一個客戶端最多可以向目標服務(wù)器發(fā)起 65535 個連接。

BIO 的模型

當應(yīng)用發(fā)起調(diào)用后,在 kernel 沒有準備好數(shù)據(jù)之前,應(yīng)用進程一直會阻塞 block 進入等待階段,當 kernel 準備好數(shù)據(jù)之后,才會返回數(shù)據(jù),此時應(yīng)用進程阻塞解除。

NIO 的模型

因為 kernel 是阻塞的,在引入了 nio 之后,在應(yīng)用發(fā)起調(diào)用后會立即返回結(jié)果-1,代表內(nèi)核尚未準備好數(shù)據(jù),應(yīng)用進程無需等待,可以輪詢查看結(jié)果,直到數(shù)據(jù)準備好為止,此時應(yīng)用進程阻塞獲取數(shù)據(jù)。

多路復用器

即便是 nio 解決了阻塞的問題,但是無效的輪詢會造成 cpu 空轉(zhuǎn),浪費資源,使用 IO 多路復用技術(shù),當內(nèi)核將數(shù)據(jù)準備好之后,通知應(yīng)用進程來獲取數(shù)據(jù),就解決了這個問題,根據(jù)其操作的方式不同,分為 select/poll/epoll 三種多路復用器。 由內(nèi)核 kernel 監(jiān)控所有的 socket 當數(shù)據(jù)準備好之后,發(fā)起系統(tǒng)調(diào)用,即 system call 將數(shù)據(jù)從內(nèi)核拷貝到用戶進程。

所以,I/O 多路復用的特點是通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態(tài),select()函數(shù)就可以返回。

I/O 多路復用的優(yōu)勢是:同時處理多個連接請求。

select 是操作系統(tǒng)提供的系統(tǒng)調(diào)用函數(shù),通過它,可以把一個文件描述符的數(shù)組發(fā)給操作系統(tǒng), 讓操作系統(tǒng)去遍歷,確定哪個文件描述符可以讀寫, 然后告訴我們?nèi)ヌ幚恚?/p>

select 是操作系統(tǒng)提供的調(diào)用函數(shù),通過這個函數(shù)可以把一組 fd 傳給操作系統(tǒng),操作系統(tǒng)遍歷 fd,將完成準備的文件描述符個數(shù)返回給用戶線程,用戶線程再去逐個遍歷 fd 查看哪個 fd 已經(jīng)處于就緒的狀態(tài),然后再去處理。

select 的特點如下:

  • 用戶需要將監(jiān)聽的 fd list 傳入到操作系統(tǒng)內(nèi)核中,內(nèi)核來完成遍歷操作并將解決返回,這樣在高并發(fā)場景數(shù)組的復制操作下會過多的消耗資源。select 的這一操作僅解決了系統(tǒng)的上下文切換的開銷,遍歷數(shù)組是依舊存在的。select 返回結(jié)果是就緒的 fd 個數(shù),用戶線程還需要判斷哪個 fd 處于就緒狀態(tài)。
  • select 可以傳入一組 socket 然后等待內(nèi)核的處理結(jié)果,但是其 list 大小只有 1024 個,每次調(diào)用 select 都需要將 fd 數(shù)組從用戶態(tài)復制到內(nèi)核態(tài),其開銷比較大。調(diào)用 select 后返回的是就緒 fd 數(shù)量,還需要用戶再次遍歷。

針對 select 的缺點,poll 為了增加單次監(jiān)聽 socket 的個數(shù),采用了鏈表的結(jié)構(gòu),放棄了數(shù)組的結(jié)構(gòu),但是其核心需要遍歷的缺點依然沒有解決。

針對 select 和 poll 的缺點,epoll 應(yīng)運而生,其核心主要包括三個方法:

# 在內(nèi)核開辟一個區(qū)域用來存放需要監(jiān)聽的fd
epoll_create
# 向內(nèi)核中添加、修改、刪除需要監(jiān)控的fd
epoll_ctl
# 返回已經(jīng)就緒的fd
epoll_wait

核心如下:

  • 內(nèi)核中存儲了一份文件描述符 fd 的集合,無需用戶每次都從用戶態(tài)傳入,只需要告訴內(nèi)核修改的部分就可以。
  • 內(nèi)核中不再通過輪詢的方式找到就緒的文件描述符 fd,而是通過異步 IO 事件進行喚醒。
  • 內(nèi)核會將有 IO 事件發(fā)生的文件描述符 fd 返回給用戶,用戶不需要自己進行遍歷。

epoll 的數(shù)據(jù)操作有兩種模式:水平模式 LT(level trigger)和邊緣模式 ET(edge trigger)。LT 是 epoll 的默認操作模式

  • LT 模式: epollwait 函數(shù)檢測到有事件發(fā)生時需要通知應(yīng)用程序,但是應(yīng)用程序不一定及時進行處理,當 epollwait 函數(shù)再次檢測到該事件的時還會通知應(yīng)用程序,直到事件被處理。可以理解為 mq 發(fā)送消息的 at least once 模型。
  • ET 模式:epollwait 函數(shù)檢測到事件發(fā)生只會通知應(yīng)用程序一次,后續(xù) epollwait 函數(shù)將不再監(jiān)控該事件。因此 ET 模式降低了同一個事件被 epoll 觸發(fā)的次數(shù),效率比 LT 模式高。可以理解為 mq 發(fā)送消息的 exactly once 模型。

IO 多路復用方式有 select,poll 以及 epoll,該函數(shù)都是內(nèi)核層面的,從 BIO 的代碼中可以看到 accept 函數(shù),從之前的分析可以知道該方法是阻塞的,

Netty 實戰(zhàn)

大家都可能注意到了,在實際的操作中 NIO 的代碼是比較復雜的,Netty 就是對 NIO 做了包裝,保證在實際操作中方便使用。 針對 Server 端的代碼如下:

//Netty的Reactor線程池,初始化了一個NioEventLoop數(shù)組,用來處理I/O操作,如接受新的連接和讀/寫數(shù)據(jù)
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup work = new NioEventLoopGroup(8);

try {
//用于啟動NIO服務(wù)
ServerBootstrap serverBoot = new ServerBootstrap();
serverBoot.group(boss, work)
//通過工廠方法設(shè)計模式實例化一個channel
.channel(NioServerSocketChannel.class)
//設(shè)置監(jiān)聽端口
.localAddress(new InetSocketAddress(port))
// 設(shè)置 server 端的一些參數(shù)項
.childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS,30000)
.childOption(ChannelOption.MAX_MESSAGES_PER_READ,16)
.childOption(ChannelOption.WRITE_SPIN_COUNT,16)
// 設(shè)置監(jiān)聽的處理 channel initializer
.childHandler(new AppServerChannelInitializer());

//綁定服務(wù)器,該實例將提供有關(guān)IO操作的結(jié)果或狀態(tài)的信息
ChannelFuture channelFuture = serverBoot.bind().sync();
System.out.println("在" + channelFuture.channel().localAddress() + "上開啟監(jiān)聽");

//阻塞操作,closeFuture()開啟了一個channel的監(jiān)聽器(這期間channel在進行各項工作),直到鏈路斷開
channelFuture.channel().closeFuture().sync();

} catch (Exception e) {
log.error("encounter exception and detail is {}", e.getMessage());

} finally {
boss.shutdownGracefully().sync();//關(guān)閉EventLoopGroup并釋放所有資源,包括所有創(chuàng)建的線程
work.shutdownGracefully().sync();//關(guān)閉EventLoopGroup并釋放所有資源,包括所有創(chuàng)建的線程
}

一般情況下 IO 的壓力都是在服務(wù)端,默認情況下客戶端也是采用的 BIO,除非是在客戶端也是需要提供服務(wù)。

 //  配置相應(yīng)的參數(shù),提供連接到遠端的方法
// I/O線程池
EventLoopGroup group = new NioEventLoopGroup();
try {
//客戶端輔助啟動類
Bootstrap bs = new Bootstrap();
bs.group(group)
//實例化一個Channel
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
//通道初始化配置
.handler(new AppClientChannelInitializer());

//連接到遠程節(jié)點;等待連接完成
ChannelFuture future = bs.connect().sync();

//發(fā)送消息到服務(wù)器端,編碼格式是utf-8
future.channel().writeAndFlush(Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8));

//阻塞操作,closeFuture()開啟了一個channel的監(jiān)聽器(這期間channel在進行各項工作),直到鏈路斷開
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}

總結(jié)

IO 從開始的瓶頸就是在操作系統(tǒng)的 read 數(shù)據(jù)讀取方法,由于這個阻塞的方法導致了 BIO 的產(chǎn)生,為了解決阻塞 IO 的問題,同時提高效率,就產(chǎn)生了使用多線程技術(shù)操作 IO 來提升性能,但是 IO 的瓶頸問題并沒有解決。后來操作系統(tǒng)做出了改變,提供了非阻塞的 read 函數(shù),這樣應(yīng)用程序在發(fā)起調(diào)用后不需要等待解決,而是采用輪詢的方式查詢數(shù)據(jù)有沒有準備好,這樣相比 BIO 在同一時間內(nèi)就可以完成更多的 fd 操作,這就是 NIO。但是在高并發(fā)的場景下,對文件描述符的遍歷和讀取帶來了更多的輪詢操作,額外增加的系統(tǒng)調(diào)用增加了 cpu 的負擔,并沒有帶來期望的性能提升。

后來操作系統(tǒng)做出了改進,將遍歷文描述符的操作放進了內(nèi)核來實現(xiàn),這就是 IO 多路復用技術(shù)。多路復用的技術(shù)分為三個函數(shù), select、poll 和 epoll。 poll 解決了 select 單次傳入文件描述符的限制,但是沒有解決客戶端遍歷查詢文件描述符的問題,epoll 的產(chǎn)生解決了這個問題,只是將數(shù)據(jù)準備好的 fd 返回給客戶端,減少了客戶端的遍歷操作。IO 模型的演進也是根據(jù)應(yīng)用的需求而升級,倒逼操作系統(tǒng)的內(nèi)核增加更多的提升性能的操作。

責任編輯:武曉燕 來源: 今日頭條
相關(guān)推薦

2011-12-08 10:51:25

JavaNIO

2023-11-07 08:19:35

IO多路復用磁盤、

2020-08-31 07:16:04

BIONIO多路復用器

2021-03-04 08:34:55

同步阻塞非阻塞

2023-03-01 14:32:31

redisIOEpoll

2022-08-26 00:21:44

IO模型線程

2021-03-24 08:03:38

NettyJava NIO網(wǎng)絡(luò)技術(shù)

2024-09-26 16:01:52

2023-01-09 10:04:47

IO多路復用模型

2019-10-18 08:22:43

BIONIOAIO

2023-12-06 07:16:31

Go語言語句

2021-06-09 19:25:13

IODubbo

2023-08-07 08:52:03

Java多路復用機制

2021-05-31 06:50:47

SelectPoll系統(tǒng)

2020-10-14 09:11:44

IO 多路復用實現(xiàn)機

2009-06-29 18:09:12

多路復用Oracle

2022-12-28 14:14:04

Redis網(wǎng)絡(luò)

2022-09-12 06:33:15

Select多路復用

2023-05-08 00:06:45

Go語言機制

2024-08-08 14:57:32

點贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 久久久女| 欧美日韩精品在线一区 | 一区二区三区高清 | 中文字幕av在线播放 | jizz视频| av网站免费| 亚洲精品成人在线 | 国产成年人小视频 | 精品视频一区二区三区四区 | 国产精品成人国产乱一区 | 亚洲免费一区二区 | 国产精品成人在线 | 久草网址| 五月婷婷丁香婷婷 | 日韩欧美国产一区二区三区 | 国产精品69毛片高清亚洲 | 国产免费播放视频 | 天天干天天爽 | 欧美日韩久久精品 | 天堂精品 | 黄色网址免费看 | 久久久久久久电影 | 亚洲成人日韩 | 久久久久久www | 亚洲久久一区 | 中文字幕在线观看一区二区 | 99精品在线观看 | 国产乱码精品一区二区三区中文 | 亚洲 91| 视频一区在线观看 | 中文字幕一区二区三区在线观看 | 日日碰狠狠躁久久躁96avv | 一区二区三区观看视频 | 91精品国产乱码久久久久久久 | 日本精品视频一区二区 | 欧美日韩亚洲一区二区 | 国产精品一区二区三区在线 | 欧美激情网站 | 99精品视频网 | 日韩国产精品一区二区三区 | 国产高清自拍视频在线观看 |