完全搞懂零拷貝

  爲了完全搞懂零拷貝,咱們趁熱打鐵,接着上一節來繼續講解零拷貝的底層原理。
  
  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

相關文章
相關標籤/搜索