自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

5分鐘搞懂Linux中直接I/O原理

新聞 Linux
直接IO方式確實能夠減少CPU的使用率以及內(nèi)存帶寬的占用,但是有時候也會造成性能的影響。

在介紹直接 I/O 之前,先來介紹下直接I/O這種機制產(chǎn)生的原因。畢竟已經(jīng)有了緩存I/O(Buffered I/O),那肯定能夠像到緩存I/O有缺陷吧,就按照這個思路來。

[[268000]]

什么是緩存 I/O (Buffered I/O)

緩存 I/O 又被稱作標(biāo)準(zhǔn) I/O,大多數(shù)文件系統(tǒng)的默認(rèn) I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操作系統(tǒng)會將 I/O 的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存( page cache )中,也就是說,數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。寫的過程就是數(shù)據(jù)流反方向。緩存 I/O 有以下這些優(yōu)點:

  1. 緩存 I/O 使用了操作系統(tǒng)內(nèi)核緩沖區(qū),在一定程度上分離了應(yīng)用程序空間和實際的物理設(shè)備。
  2. 緩存 I/O 可以減少讀盤的次數(shù),從而提高性能。

對于讀操作:當(dāng)應(yīng)用程序要去讀取某塊數(shù)據(jù)的時候,如果這塊數(shù)據(jù)已經(jīng)在頁緩存中,那就返回之。而不需要經(jīng)過硬盤的讀取操作了。如果這塊數(shù)據(jù)不在頁緩存中,就需要從硬盤中讀取數(shù)據(jù)到頁緩存。

對于寫操作:應(yīng)用程序會將數(shù)據(jù)先寫到頁緩存中,數(shù)據(jù)是否會被立即寫到磁盤,這取決于所采用的寫操作機制:

  • 同步機制,數(shù)據(jù)會立即被寫到磁盤中,直到數(shù)據(jù)寫完,寫接口才返回;
  • 延遲機制:寫接口立即返回,操作系統(tǒng)會定期地將頁緩存中的數(shù)據(jù)刷到硬盤。所以這個機制會存在丟失數(shù)據(jù)的風(fēng)險。想象下寫接口返回的時候,頁緩存的數(shù)據(jù)還沒刷到硬盤,正好斷電。對于應(yīng)用程序來說,認(rèn)為數(shù)據(jù)已經(jīng)在硬盤中。

5分鐘搞懂Linux中直接I/O原理

緩存I/O的寫操作

緩存 I/O 的缺點

在緩存I/O的機制中,以寫操作為例,數(shù)據(jù)先從用戶態(tài)拷貝到內(nèi)核態(tài)中的頁緩存中,然后又會從頁緩存中寫到磁盤中,這些拷貝操作帶來的CPU以及內(nèi)存的開銷是非常大的。

對于某些特殊的應(yīng)用程序來說,能夠繞開內(nèi)核緩沖區(qū)能夠獲取更好的性能,這就是直接I/O出現(xiàn)的意義。

5分鐘搞懂Linux中直接I/O原理

直接I/O寫操作

直接I/O 介紹

凡是通過直接I/O方式進行數(shù)據(jù)傳輸,數(shù)據(jù)直接從用戶態(tài)地址空間寫入到磁盤中,直接跳過內(nèi)核緩沖區(qū)。對于一些應(yīng)用程序,例如:數(shù)據(jù)庫。他們更傾向于自己的緩存機制,這樣可以提供更好的緩沖機制提高數(shù)據(jù)庫的讀寫性能。直接I/O寫操作如上圖所示。

直接I/O 設(shè)計與實現(xiàn)

要在塊設(shè)備中執(zhí)行直接 I/O,進程必須在打開文件的時候設(shè)置對文件的訪問模式為 O_DIRECT,這樣就等于告訴操作系統(tǒng)進程在接下來使用 read() 或者 write() 系統(tǒng)調(diào)用去讀寫文件的時候使用的是直接 I/O 方式,所傳輸?shù)臄?shù)據(jù)均不經(jīng)過操作系統(tǒng)內(nèi)核緩存空間。使用直接 I/O 讀寫數(shù)據(jù)必須要注意緩沖區(qū)對齊( buffer alignment )以及緩沖區(qū)的大小的問題,即對應(yīng) read() 以及 write() 系統(tǒng)調(diào)用的第二個和第三個參數(shù)。這里邊說的對齊指的是文件系統(tǒng)塊大小的對齊,緩沖區(qū)的大小也必須是該塊大小的整數(shù)倍。

下面主要介紹三個函數(shù):open(),read() 以及 write()。Linux 中訪問文件具有多樣性,所以這三個函數(shù)對于處理不同的文件訪問方式定義了不同的處理方法,本文主要介紹其與直接 I/O 方式相關(guān)的函數(shù)與功能.首先,先來看 open() 系統(tǒng)調(diào)用,其函數(shù)原型如下所示:

  1. int open(const char *pathname, int oflag, … /*, mode_t mode * / ) ; 

當(dāng)應(yīng)用程序需要直接訪問文件而不經(jīng)過操作系統(tǒng)頁高速緩沖存儲器的時候,它打開文件的時候需要指定 O_DIRECT 標(biāo)識符。

操作系統(tǒng)內(nèi)核中處理 open() 系統(tǒng)調(diào)用的內(nèi)核函數(shù)是 sys_open(),sys_open() 會調(diào)用 do_sys_open() 去處理主要的打開操作。它主要做了三件事情:

  1. 調(diào)用 getname() 從進程地址空間中讀取文件的路徑名;
  2. do_sys_open() 調(diào)用 get_unused_fd() 從進程的文件表中找到一個空閑的文件表指針,相應(yīng)的新文件描述符就存放在本地變量 fd 中;
  3. 函數(shù) do_filp_open() 會根據(jù)傳入的參數(shù)去執(zhí)行相應(yīng)的打開操作。

下面列出了操作系統(tǒng)內(nèi)核中處理 open() 系統(tǒng)調(diào)用的一個主要函數(shù)關(guān)系圖。

  1. sys_open()  
  2.  |-----do_sys_open()  
  3.  |---------getname()  
  4.  |---------get_unused_fd()  
  5.  |---------do_filp_open()  
  6.  |--------nameidata_to_filp()  
  7.  |----------__dentry_open() 

函數(shù) do_flip_open() 在執(zhí)行的過程中會調(diào)用函數(shù) nameidata_to_filp(),而 nameidata_to_filp() 最終會調(diào)用 __dentry_open() 函數(shù),若進程指定了 O_DIRECT 標(biāo)識符,則該函數(shù)會檢查直接 I./O 操作是否可以作用于該文件。下面列出了 __dentry_open() 函數(shù)中與直接 I/O 操作相關(guān)的代碼。

  1. if (f->f_flags & O_DIRECT) {  
  2.  if (!f->f_mapping->a_ops ||  
  3.  ((!f->f_mapping->a_ops->direct_IO) &&  
  4.  (!f->f_mapping->a_ops->get_xip_page))) {  
  5.  fput(f);  
  6.  f = ERR_PTR(-EINVAL);  
  7.  }  

當(dāng)文件打開時指定了 O_DIRECT 標(biāo)識符,那么操作系統(tǒng)就會知道接下來對文件的讀或者寫操作都是要使用直接 I/O 方式的。

下邊我們來看一下當(dāng)進程通過 read() 系統(tǒng)調(diào)用讀取一個已經(jīng)設(shè)置了 O_DIRECT 標(biāo)識符的文件的時候,系統(tǒng)都做了哪些處理。 函數(shù) read() 的原型如下所示:

  1. ssize_t read(int feledes, void *buff, size_t nbytes) ; 

操作系統(tǒng)中處理 read() 函數(shù)的入口函數(shù)是 sys_read(),其主要的調(diào)用函數(shù)關(guān)系圖如下:

  1. sys_read()  
  2.  |-----vfs_read()  
  3.  |----generic_file_read()  
  4.  |----generic_file_aio_read()  
  5.  |--------- generic_file_direct_IO() 
  6. ​ 

函數(shù) sys_read() 從進程中獲取文件描述符以及文件當(dāng)前的操作位置后會調(diào)用 vfs_read() 函數(shù)去執(zhí)行具體的操作過程,而 vfs_read() 函數(shù)最終是調(diào)用了 file 結(jié)構(gòu)中的相關(guān)操作去完成文件的讀操作,即調(diào)用了 generic_file_read() 函數(shù),其代碼如下所示:

  1. ssize_t  
  2. generic_file_read(struct file *filp,  
  3. char __user *buf, size_t count, loff_t *ppos)  
  4. {  
  5.  struct iovec local_iov = { .iov_base = buf, .iov_len = count };  
  6.  struct kiocb kiocb;  
  7.  ssize_t ret;  
  8.   
  9.  init_sync_kiocb(&kiocb, filp);  
  10.  ret = __generic_file_aio_read(&kiocb, &local_iov, 1, ppos);  
  11.  if (-EIOCBQUEUED == ret)  
  12.  ret = wait_on_sync_kiocb(&kiocb);  
  13.  return ret;  

函數(shù) generic_file_read() 初始化了 iovec 以及 kiocb 描述符。描述符 iovec 主要是用于存放兩個內(nèi)容:用來接收所讀取數(shù)據(jù)的用戶地址空間緩沖區(qū)的地址和緩沖區(qū)的大?。幻枋龇?kiocb 用來跟蹤 I/O 操作的完成狀態(tài)。之后,函數(shù) generic_file_read() 凋用函數(shù) __generic_file_aio_read()。該函數(shù)檢查 iovec 中描述的用戶地址空間緩沖區(qū)是否可用,接著檢查訪問模式,若訪問模式描述符設(shè)置了 O_DIRECT,則執(zhí)行與直接 I/O 相關(guān)的代碼。函數(shù) __generic_file_aio_read() 中與直接 I/O 有關(guān)的代碼如下所示:

  1. if (filp->f_flags & O_DIRECT) {  
  2.  loff_t pos = *ppos, size;  
  3.  struct address_space *mapping;  
  4.  struct inode *inode;  
  5.   
  6.  mapping = filp->f_mapping;  
  7.  inode = mapping->host;  
  8.  retval = 0;  
  9.  if (!count)  
  10.  goto out;  
  11.  size = i_size_read(inode);  
  12.  if (pos < size) {  
  13.  retval = generic_file_direct_IO(READ, iocb,  
  14.  iov, pos, nr_segs);  
  15.  if (retval > 0 && !is_sync_kiocb(iocb))  
  16.  retval = -EIOCBQUEUED;  
  17.  if (retval > 0)  
  18.  *ppos = pos + retval;  
  19.  }  
  20.  file_accessed(filp);  
  21.  goto out;  

上邊的代碼段主要是檢查了文件指針的值,文件的大小以及所請求讀取的字節(jié)數(shù)目等,之后,該函數(shù)調(diào)用 generic_file_direct_io(),并將操作類型 READ,描述符 iocb,描述符 iovec,當(dāng)前文件指針的值以及在描述符 io_vec 中指定的用戶地址空間緩沖區(qū)的個數(shù)等值作為參數(shù)傳給它。當(dāng) generic_file_direct_io() 函數(shù)執(zhí)行完成,函數(shù) __generic_file_aio_read()會繼續(xù)執(zhí)行去完成后續(xù)操作:更新文件指針,設(shè)置訪問文件 i 節(jié)點的時間戳;這些操作全部執(zhí)行完成以后,函數(shù)返回。 函數(shù) generic_file_direct_IO() 會用到五個參數(shù),各參數(shù)的含義如下所示:

  1. rw:操作類型,可以是 READ 或者 WRITE
  2. iocb:指針,指向 kiocb 描述符 
  3. iov:指針,指向 iovec 描述符數(shù)組
  4. offset:file 結(jié)構(gòu)偏移量
  5. nr_segs:iov 數(shù)組中 iovec 的個數(shù)

函數(shù) generic_file_direct_IO() 代碼如下所示:

  1. static ssize_t  
  2. generic_file_direct_IO(int rw, struct kiocb *iocb, const struct iovec *iov,  
  3.  loff_t offset, unsigned long nr_segs)  
  4. {  
  5.  struct file *file = iocb->ki_filp;  
  6.  struct address_space *mapping = file->f_mapping;  
  7.  ssize_t retval;  
  8.  size_t write_len = 0;  
  9.   
  10.  if (rw == WRITE) {  
  11.  write_len = iov_length(iov, nr_segs);  
  12.  if (mapping_mapped(mapping))  
  13.  unmap_mapping_range(mapping, offset, write_len, 0);  
  14.  }  
  15.   
  16.  retval = filemap_write_and_wait(mapping);  
  17.  if (retval == 0) {  
  18.  retval = mapping->a_ops->direct_IO(rw, iocb, iov,  
  19.  offset, nr_segs);  
  20.  if (rw == WRITE && mapping->nrpages) {  
  21.  pgoff_t end = (offset + write_len - 1)  
  22.  >> PAGE_CACHE_SHIFT;  
  23.  int err = invalidate_inode_pages2_range(mapping,  
  24.  offset >> PAGE_CACHE_SHIFT, end);  
  25.  if (err)  
  26.  retval = err;  
  27.  }  
  28.  }  
  29.  return retval;  

函數(shù) generic_file_direct_IO() 對 WRITE 操作類型進行了一些特殊處理。除此之外,它主要是調(diào)用了 direct_IO 方法去執(zhí)行直接 I/O 的讀或者寫操作。在進行直接 I/O 讀操作之前,先將頁緩存中的相關(guān)臟數(shù)據(jù)刷回到磁盤上去,這樣做可以確保從磁盤上讀到的是***的數(shù)據(jù)。這里的 direct_IO 方法最終會對應(yīng)到 __blockdev_direct_IO() 函數(shù)上去。__blockdev_direct_IO() 函數(shù)的代碼如下所示:

  1. ssize_t  
  2. __blockdev_direct_IO(int rw, struct kiocb *iocb, struct inode *inode,  
  3.  struct block_device *bdev, const struct iovec *iov, loff_t offset,  
  4.  unsigned long nr_segs, get_block_t get_block, dio_iodone_t end_io,  
  5.  int dio_lock_type)  
  6. {  
  7.  int seg;  
  8.  size_t size;  
  9.  unsigned long addr;  
  10.  unsigned blkbits = inode->i_blkbits;  
  11.  unsigned bdev_blkbits = 0;  
  12.  unsigned blocksize_mask = (1 << blkbits) - 1;  
  13.  ssize_t retval = -EINVAL;  
  14.  loff_t end = offset;  
  15.  struct dio *dio;  
  16.  int release_i_mutex = 0;  
  17.  int acquire_i_mutex = 0;  
  18.   
  19.  if (rw & WRITE)  
  20.  rw = WRITE_SYNC;  
  21.   
  22.  if (bdev)  
  23.  bdev_blkbits = blksize_bits(bdev_hardsect_size(bdev));  
  24.   
  25.  if (offset & blocksize_mask) {  
  26.  if (bdev)  
  27.  blkbits = bdev_blkbits;  
  28.  blocksize_mask = (1 << blkbits) - 1;  
  29.  if (offset & blocksize_mask)  
  30.  goto out;  
  31.  }  
  32.   
  33.  for (seg = 0; seg < nr_segs; seg++) {  
  34.  addr = (unsigned long)iov[seg].iov_base;  
  35.  size = iov[seg].iov_len;  
  36.  end += size;  
  37.  if ((addr & blocksize_mask) || (size & blocksize_mask)) {  
  38.  if (bdev)  
  39.  blkbits = bdev_blkbits;  
  40.  blocksize_mask = (1 << blkbits) - 1;  
  41.  if ((addr & blocksize_mask) || (size & blocksize_mask))  
  42.  goto out;  
  43.  }  
  44.  }  
  45.   
  46.  dio = kmalloc(sizeof(*dio), GFP_KERNEL);  
  47.  retval = -ENOMEM;  
  48.  if (!dio)  
  49.  goto out;  
  50.  dio->lock_type = dio_lock_type;  
  51.  if (dio_lock_type != DIO_NO_LOCKING) {  
  52.  if (rw == READ && end > offset) {  
  53.  struct address_space *mapping;  
  54.   
  55.  mapping = iocb->ki_filp->f_mapping;  
  56.  if (dio_lock_type != DIO_OWN_LOCKING) {  
  57.  mutex_lock(&inode->i_mutex);  
  58.  release_i_mutex = 1;  
  59.  }  
  60.   
  61.  retval = filemap_write_and_wait_range(mapping, offset,  
  62.  end - 1);  
  63.  if (retval) {  
  64.  kfree(dio);  
  65.  goto out;  
  66.  }  
  67.   
  68.  if (dio_lock_type == DIO_OWN_LOCKING) {  
  69.  mutex_unlock(&inode->i_mutex);  
  70.  acquire_i_mutex = 1;  
  71.  }  
  72.  }  
  73.   
  74.  if (dio_lock_type == DIO_LOCKING)  
  75.  down_read_non_owner(&inode->i_alloc_sem);  
  76.  }  
  77.   
  78.  dio->is_async = !is_sync_kiocb(iocb) && !((rw & WRITE) &&  
  79.  (end > i_size_read(inode)));  
  80.   
  81.  retval = direct_io_worker(rw, iocb, inode, iov, offset,  
  82.  nr_segs, blkbits, get_block, end_io, dio);  
  83.   
  84.  if (rw == READ && dio_lock_type == DIO_LOCKING)  
  85.  release_i_mutex = 0;  
  86.   
  87. out:  
  88.  if (release_i_mutex)  
  89.  mutex_unlock(&inode->i_mutex);  
  90.  else if (acquire_i_mutex)  
  91.  mutex_lock(&inode->i_mutex);  
  92.  return retval;  

該函數(shù)將要讀或者要寫的數(shù)據(jù)進行拆分,并檢查緩沖區(qū)對齊的情況。本文在前邊介紹 open() 函數(shù)的時候指出,使用直接 I/O 讀寫數(shù)據(jù)的時候必須要注意緩沖區(qū)對齊的問題,從上邊的代碼可以看出,緩沖區(qū)對齊的檢查是在 __blockdev_direct_IO() 函數(shù)里邊進行的。用戶地址空間的緩沖區(qū)可以通過 iov 數(shù)組中的 iovec 描述符確定。直接 I/O 的讀操作或者寫操作都是同步進行的,也就是說,函數(shù) __blockdev_direct_IO() 會一直等到所有的 I/O 操作都結(jié)束才會返回,因此,一旦應(yīng)用程序 read() 系統(tǒng)調(diào)用返回,應(yīng)用程序就可以訪問用戶地址空間中含有相應(yīng)數(shù)據(jù)的緩沖區(qū)。但是,這種方法在應(yīng)用程序讀操作完成之前不能關(guān)閉應(yīng)用程序,這將會導(dǎo)致關(guān)閉應(yīng)用程序緩慢。

直接I/O 優(yōu)點

***的優(yōu)點就是減少操作系統(tǒng)緩沖區(qū)和用戶地址空間的拷貝次數(shù)。降低了CPU的開銷,和內(nèi)存帶寬。對于某些應(yīng)用程序來說簡直是福音,將會大大提高性能。

直接I/O 缺點

直接IO并不總能讓人如意。直接IO的開銷也很大,應(yīng)用程序沒有控制好讀寫,將會導(dǎo)致磁盤讀寫的效率低下。磁盤的讀寫是通過磁頭的切換到不同的磁道上讀取和寫入數(shù)據(jù),如果需要寫入數(shù)據(jù)在磁盤位置相隔比較遠,就會導(dǎo)致尋道的時間大大增加,寫入讀取的效率大大降低。

總結(jié)

直接IO方式確實能夠減少CPU的使用率以及內(nèi)存帶寬的占用,但是有時候也會造成性能的影響。所以在使用直接IO之前一定要清楚它的原理,只有在各項都清晰的情況下,才考慮使用。本人只是介紹了原理,如想深入,建議參考內(nèi)核相關(guān)文檔。

責(zé)任編輯:張燕妮 來源: 底層軟件架構(gòu)
相關(guān)推薦

2017-05-18 11:11:20

Google谷歌開發(fā)者大會

2024-12-11 07:00:00

面向?qū)ο?/a>代碼

2025-03-13 06:22:59

2024-01-12 07:38:38

AQS原理JUC

2020-03-03 15:40:51

開發(fā)技能代碼

2011-01-14 09:25:28

LinuxIO機制

2019-08-09 10:33:36

開發(fā)技能代碼

2025-01-20 08:50:00

2021-06-18 07:34:12

Kafka中間件微服務(wù)

2017-03-30 19:28:26

HBase分布式數(shù)據(jù)

2025-01-21 07:39:04

Linux堆內(nèi)存Golang

2021-01-27 18:15:01

Docker底層宿主機

2018-09-27 13:56:14

內(nèi)網(wǎng)外網(wǎng)通信

2021-05-28 07:38:20

內(nèi)存溢出場景

2009-10-10 15:50:25

2012-06-28 10:26:51

Silverlight

2024-01-16 07:46:14

FutureTask接口用法

2023-09-18 15:49:40

Ingress云原生Kubernetes

2023-12-06 08:48:36

Kubernetes組件

2021-12-01 06:50:50

Docker底層原理
點贊
收藏

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