爲了完全搞懂零拷貝,咱們趁熱打鐵,接着上一節來繼續講解零拷貝的底層原理。
2|0感覺一下NIO的速度
以前的章節中咱們說過,Nio並不能解決網絡傳輸的速度。可是爲何不少人卻說Nio的速度比傳統IO快呢?
沒錯,zero copy。咱們先拋出一個案例,而後根據案例來說解底層原理。
首先,咱們實現一個IO的服務端接受數據,而後分別用傳統IO傳輸方式和NIO傳輸方式來直觀對比傳輸相同大小的文件所耗費的時間。
服務端代碼以下:
public class OldIOServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8899);
while (true) {
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
try {
byte[] byteArray = new byte[4096];
while (true) {
int readCount = dataInputStream.read(byteArray, 0, byteArray.length);
if (-1 == readCount) {
break;
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
這個是最普通的socket編程的服務端,沒什麼好多說的。就是綁定本地的8899端口,死循環不斷接受數據。
2|1傳統IO傳輸
public class OldIOClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 8899);
String fileName = "C:\\Users\\Administrator\\Desktop\\test.zip"; //大小兩百M的文件
InputStream inputStream = new FileInputStream(fileName);
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
byte[] buffer = new byte[4096];
long readCount;
long total = 0;
long startTime = System.currentTimeMillis();
while ((readCount = inputStream.read(buffer)) >= 0) {
total += readCount;
dataOutputStream.write(buffer);
}
System.out.println("發送總字節數: " + total + ", 耗時: " + (System.currentTimeMillis() - startTime));
dataOutputStream.close();
socket.close();
inputStream.close();
}
}
客戶端向服務端發送一個119M大小的文件。計算一下耗時用了多久
因爲個人筆記本性能太渣,大概平均每次消耗的時間大概是 500ms左右。值得注意的是,咱們客戶端和服務端分配的緩存大小都是4096個字節。若是將這個字節分配的更小一點,那麼所耗時間將會更多。由於上述傳統的IO實際表現並非咱們想象的那樣直接將文件讀到內存,而後發送。
實際狀況是什麼樣的呢?咱們在後續分析。
2|2NIO傳輸
public class NewIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8899));
socketChannel.configureBlocking(true);
String fileName = "C:\\Users\\Administrator\\Desktop\\test.zip"; //大小200M的文件
FileChannel fileChannel = new FileInputStream(fileName).getChannel();
long startTime = System.currentTimeMillis();
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel); //1
System.out.println("發送總字節數:" + transferCount + ",耗時: " + (System.currentTimeMillis() - startTime));
fileChannel.close();
}
}
NIO編程不熟的同窗不要緊,後面會有一篇專門的章節來說。
這裏咱們來關注一下注釋1關於FileChannel的transferTo方法。(方法的doc文檔很長。我刪除了不少,只看重點)
/**
* Transfers bytes from this channel's file to the given writable byte
* channel.
*
* <p> This method is potentially much more efficient than a simple loop
* that reads from this channel and writes to the target channel. Many
* operating systems can transfer bytes directly from the filesystem cache
* to the target channel without actually copying them. </p>
*/
public abstract long transferTo(long position, long count,
WritableByteChannel target)
throws IOException;
翻譯一下:
將文件channel的數據寫到指定的channel
這個方法可能比簡單的將數據從一個channel循環讀到另外一個channel更有效,
許多操做系統能夠直接從文件系統緩存傳輸字節到目標通道,**而不實際複製它們**。
意思是咱們調用FileChannel的transferTo方法就實現了零拷貝(想實現零拷貝並不止這一種方法,有更優雅的方法,這裏只是做爲一個演示)。固然也要看你操做系統支不支持底層zero copy。由於這部分工做實際上是操做系統來完成的。
個人電腦平均執行下來大概在200ms左右。比傳統IO快了300ms。
3|0底層原理
num_rows, num_cols = img_mat.shape[:2]
tx=5
ty=0
translation_matrix =www.meiwanyule.cn np.float32([ [1,0,tx], [0,1,ty] ])
img_translation = cv2.warpAffine(img_mat, translation_matrix, (num_cols, num_rows),borderValue=(0,0,0))
效果以下
圖像旋轉
圖像的旋轉其實和平移的原理是相似的,opencv裏提供了一個api幫咱們去獲取旋轉矩陣.咱們只須要給出旋轉中心和旋轉角度便可.
cv::Mat src = cv::imread(www.yongshenyuL.com "lenna.jpg"www.yuxinyulept.com );
cv::Mat dst;
//旋轉角度
double angle = 45;
cv::Size src_sz = src.size();
cv::Size dst_sz(src_sz.height, src_sz.width);
int len = std::max(www.yacuangpt.com src.cols, src.rows);
/http://blog.sina.com.cn/u/5125835408
https://www.douban.com/people/187906486/
cv::Point2f center(www.hnawesm.com / 2., len / 2.);
//獲取旋轉矩陣(2x3矩陣)
cv::Mat rot_mat = cv::getRotationMatrix2D(center, angle, 1.0);
//根據旋轉矩陣進行仿射變換
cv::warpAffine(src,www.51kunlunyule.com dst, rot_mat, dst_sz);
//顯示旋轉效果
cv::imshow("image", src);
cv::imshow("result", dst);
cv::waitKey(0);
return 0;
你們也能夠用本身的電腦運行一下上述代碼,看看NIO傳輸一個文件比IO傳輸一個文件快多少。
在上訴代碼中,樓主這裏指定的緩存只有4096個字節,而傳送的文件大小有125581592個字節。
在前面咱們分析過,對於傳統的IO而言,讀取的緩存滿了之後會有兩次零拷貝過程。那麼換算下來傳輸這個文件大概在內存中進行了6w屢次無心義的內存拷貝,這6w屢次拷貝在個人筆記本上大概所耗費的時間就是300ms左右。這就是致使NIO比傳統IO快的更本緣由。
3|1傳統IO底層時序圖
由上圖咱們能夠看到。當咱們想將磁盤中的數據經過網絡發送的時候,
底層調用的了sendfile()方法,而後切換用戶態(User space)->內核態(Kemel space)。
從本地磁盤獲取數據。獲取的數據存儲在內核態的內存空間內。
將數據複製到用戶態內存空間裏。
切換內核態->用戶態。
用戶操做數據,這裏就是咱們編寫的java代碼的具體操做。
調用操做系統的write()方法,將數據複製到內核態的socket buffer中。
切換用戶態->內核態。
發送數據。
發送完畢之後,切換內核態->用戶態。繼續執行咱們編寫的java代碼。
由上圖能夠看出。傳統的IO發送一次數據,進行了兩次「無心義」的內存拷貝。雖然內存拷貝對於整個IO來講耗時是能夠忽略不計的。可是操做達到必定次數之後,就像咱們上面案例的代碼。就會由量變引發質變。致使速率大大下降。
3|2linux2.4版本前的NIO時序圖
轉存失敗
從新上傳
取消
底層調用的了sendfile()方法,而後切換用戶態(User space)->內核態(Kemel space)。
從本地磁盤獲取數據。獲取的數據存儲在內核態的內存空間內。
將內核緩存中的數據拷貝到socket緩衝中。
將socket緩存的數據發送。
發送完畢之後,切換內核態->用戶態。繼續執行咱們編寫的java代碼。
能夠看出,即使咱們使用了NIO,其實在咱們的緩存中依舊會有一次內存拷貝。拷貝到socket buffer(也就是發送緩存區)中。
到這裏咱們能夠看到,用戶態已經不須要再緩存數據了。也就是少了用戶態和系統態之間的數據拷貝過程。也少了兩次用戶態與內核態上下文切換的過程。可是仍是不夠完美。由於在底層仍是執行了一次拷貝。
要想實現真真意義上的零拷貝,仍是須要操做系統的支持,操做系統支持那就支持。不支持你代碼寫出花了也不會支持。因此在linux2.4版本之後,零拷貝進化爲如下模式。
3|3linux2.4版本後的NIO時序圖
轉存失敗
從新上傳
取消
這裏的步驟與上面的步驟是相似的。看圖能夠看出,到這裏內存中才真正意義上實現了零拷貝。
不少人就會發問了。爲何少了一次內核緩存的數據拷貝到socket緩存的操做?
不急,聽我慢慢道來~
咱們再來看另外一張NIO的流程圖:
轉存失敗
從新上傳
取消
上面這個圖稍稍有點複雜,都看到這裏了,別半途而廢。多看幾遍是能看懂的!
首先第一條黑線咱們能夠看出,在NIO只切換了兩次用戶態與內核態之間的上下文切換。
咱們重點看這張圖下面的部分。
首先咱們將硬盤(hard drive)上的數據複製到內核態緩存中(kemel buffer)。而後發生了一次拷貝(CPU copy)到socket緩存中(socket buffer)。最後再經過協議引擎將數據發送出去。
在linux2.4版本前的的確是這樣。可是!!!!
在linux2.4版本之後,上圖中的從內核態緩存中(kemel buffer)的拷貝到socket緩存中(socket buffer)的就再也不是數據了。而是對內核態緩存中數據的描述符(也就是指針)。協議引擎發送數據的時候實際上是經過socket緩存中的描述符。找到了內核態緩存中的數據。再將數據發送出去。這樣就實現了真正的零拷貝。java