韩国保姆2_少女伦理电影_HD中文字幕在线观看,玩偶姐姐在线观看高清,8090新视觉电影免费播放在线观看,98蜜桃

沃趣科技技術社區
行業前沿信息一網打盡
技術社區 > 數據庫技術a|技術干貨 | 漫游Linux塊IO

數據庫技術a|技術干貨 | 漫游Linux塊IO

2023年06月13日

前言

在計算機的世界里,我們可以將業務進行抽象簡化為兩種場景——計算密集型IO密集型。這兩種場景下的表現,決定這一個計算機系統的能力。數據庫作為一個典型的基礎軟件,它的所有業務邏輯同樣可以抽象為這兩種場景的混合。因此,一個數據庫系統性能的強悍與否,往往跟操作系統和硬件提供的計算能力、IO能力緊密相關。



除了硬件本身的物理極限,操作系統在軟件層面的處理以及提供的相關機制也尤為重要。因此,想要數據庫發揮更加極限的性能,對操作系統內部相關機制和流程的理解就很重要。


本篇文章,我們就一起看下Linux中一個IO請求的生命周期。Linux發展到今天,其內部的IO子系統已經相當復雜。每個點展開都能自成一篇,所以本次僅是對塊設備的寫IO做一個快速的漫游,后續再對相關專題進行詳細分解。

image-20220921214446637.png




從用戶態程序出發

首先需要明確的是,什么是塊設備?我們知道IO設備可以分為字符設備和塊設備,字符設備以字節流的方式訪問數據,比如我們的鍵盤鼠標。而塊設備則是以塊為單位訪問數據,并且支持隨機訪問,典型的塊設備就是我們常見的機械硬盤和固態硬盤。


一個應用程序想將數據寫入磁盤,需要通過系統調用來完成:open打開文件 ---> write寫入文件 ---> close關閉文件。


下面是write系統調用的定義,我們可以看到,應用程序只需要指定三個參數:

1. 想要寫入的文件

2. 寫入數據所在的內存地址

3. 寫入數據的長度

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
    size_t, count)
{
  struct fd f = fdget_pos(fd);
  ssize_t ret = -EBADF;
 
  if (f.file) {
    loff_t pos = file_pos_read(f.file);
    ret = vfs_write(f.file, buf, count, &pos);
    if (ret >= 0)
      file_pos_write(f.file, pos);
    fdput_pos(f);
  }
 
  return ret;
}


而剩下的工作就進入到內核中的虛擬文件系統(VFS)中進行處理。



虛擬文件系統(VFS)

在Linux中一切皆文件,它提供了虛擬文件系統VFS的機制,用來抽象各種資源,使應用程序無需關心底層細節,只需通過open、read/write、close這幾個通用接口便可以管理各種不同的資源。不同的文件系統通過實現各自的通用接口來滿足不同的功能。


devtmpfs

掛載在/dev目錄,devtmpfs中的文件代表各種設備。因此,對devtmpfs文件的讀寫操作,就是直接對相應設備的操作。


如果應用程序打開的是一個塊設備文件,則說明它直接對一個塊設備進行讀寫,調用塊設備的write函數:

const struct file_operations def_blk_fops = {
  .open    = blkdev_open,
    ... ...
  .read    = do_sync_read,
  .write    = do_sync_write,
    ... ...
};



磁盤文件系統(ext4等)

這是我們最為熟悉的文件系統類型,它的文件就是我們一般理解的文件,對應實際磁盤中按照特定格式組織并管理的區域。對這類文件的讀寫操作,都會按照固定規則轉化為對應磁盤的讀寫操作。


應用程序如果打開的是一個ext4文件系統的文件,則會調用ext4的write函數:

const struct file_operations_extend  ext4_file_operations = {
  .kabi_fops = {
    ... ...
    .read    = do_sync_read,
    .write    = do_sync_write,
    ... ...
    .open    = ext4_file_open,
    ... ...
};


buffer/cache


Linux提供了緩存來提高IO的性能,無論打開的是設備文件還是磁盤文件,一般情況IO會先寫入到系統緩存中并直接返回,IO生命周期結束。后續系統刷新緩存或者主動調用sync,數據才會被真正寫入到塊設備中。有意思的是,針對塊設備的稱為buffer,針對磁盤文件的稱為cache。

ssize_t __generic_file_aio_write(struct kiocb *iocb, const struct iovec *iov,
         unsigned long nr_segs, loff_t *ppos)
    ... ...
  if (io_is_direct(file)) {
    ... ...
    written = generic_file_direct_write(iocb, iov, &nr_segs, pos,
              ppos, count, ocount);
    ... ...
  } else {
    written = generic_file_buffered_write(iocb, iov, nr_segs,
        pos, ppos, count, written);
  }
    ... ...



Direct IO

當打開文件時候指定了O_DIRECT標志,則指定文件的IO為direct IO,它會繞過系統緩存直接發送給塊設備。在發送給塊設備之前,虛擬文件系統會將write函數參數表示的IO轉化為dio,在其中封裝了一個個bio結構,接著調用submit_bio將這些bio提交到通用塊層進行處理。

 do_blockdev_direct_IO 
    -> dio_bio_submit 
      -> submit_bio



通用塊層

核心結構

1.bio/request

  • bio是Linux通用塊層和底層驅動的IO基本單位,可以看到它的最重要的幾個屬性,一個bio就可以表示一個完整的IO操作:

struct bio {
  sector_t    bi_sector; //io的起始扇區
... ...
  struct block_device  *bi_bdev;  //對應的塊設備
... ...
  bio_end_io_t    *bi_end_io;  //io結束的回調函數
... ...
  struct bio_vec    *bi_io_vec;  //內存page列表
... ...
};


  • request代表一個獨立的IO請求,是通用塊層和驅動層進行IO傳遞的結構,它容納了一組連續的bio。通用塊層提供了很多IO調度策略,將多個bio合并生成一個request,以提高IO的效率。


2.gendisk

每個塊設備都對應一個gendisk結構,它定義了設備名、主次設備號、請求隊列,和設備的相關操作函數。通過add_disk,我們就真正在系統中定義一個塊設備。


3.request_queue

這個即是日常所說的IO請求隊列,通用塊層將IO轉化為request并插入到request_queue中,隨后底層驅動從中取出完成后續IO處理。

struct request_queue {
 ... ...
 struct elevator_queue  *elevator;  //調度器

 request_fn_proc    *request_fn;  //請求處理函數
 make_request_fn    *make_request_fn;  //請求入隊函數
 ... ...
 softirq_done_fn    *softirq_done_fn;  //軟中斷處理

 struct device    *dev;
 unsigned long    nr_requests;
 ... ...
};



處理流程

在收到上層文件系統提交的bio后,通用塊層最主要的功能就是根據bio創建request,并插入到request_queue中。


在這個過程中會對bio進行一系列處理:當bio長度超過限制會被分割,當bio訪問地址相鄰則會被合并。


request創建后,根據request_queue配置的不同elevator調度器,request插入到對應調度器隊列中。在底層設備驅動程序從request_queue取出request處理時,不同elevator調度器返回request策略不同,從而實現對request的調度。

void blk_queue_bio(struct request_queue *q, struct bio *bio)
{
   ... ...
 el_ret = elv_merge(q, &req, bio);    //嘗試將bio合并到已有的request中
 ... ...
 req = get_request(q, rw_flags, bio, 0);  //無法合并,申請新的request
   ... ...
 init_request_from_bio(req, bio);
}

void blk_flush_plug_list(struct blk_plug *plug, bool from_schedule)
{
   ... ...
     __elv_add_request(q, rq, ELEVATOR_INSERT_SORT_MERGE);  //將request插入request_queue的elevator調度器
   ... ...
}



請求隊列


Linux中提供了不同類型的request_queue,一個是本文主要涉及的single-queue,另外一個是multi-queue。single-queue是在早期的硬件設備(例如機械硬盤)只能串行處理IO的背景下創建的,而隨著更快速的SSD設備的普及,single-queue已經無法發揮底層存儲的性能了,進而誕生了multi-queue,它優化了很多機制使IOPS達到了百萬級別以上。至于multi-queue和single-queue的詳細區別,本篇不做討論。


每個隊列都可以配置不同的調度器,常見的有noop、deadline、cfq等。不同的調度器會根據IO類型、進程優先級、deadline等因素,對request請求進一步進行合并和排序。我們可以通過sysfs進行配置,來滿足業務場景的需求:

#/sys/block/sdx/queue
scheduler      #調度器配置
nr_requests      #隊列深度
max_sectors_kb    #最大IO大小


設備驅動

在IO經過通用塊層的處理和調度后,就進入到了設備驅動層,就開始需要和存儲硬件進行交互。


以scsi驅動為例:在scsi的request處理函數scsi_request_fn中,循環從request_queue中取request,并創建scsi_cmd下發給注冊到scsi子系統的設備驅動。需要注意的是,scsi_cmd中會注冊一個scsi_done的回調函數。


static void scsi_request_fn(struct request_queue *q)
{
 for (;;) {
   ... ...
   req = blk_peek_request(q);    //從request_queue中取出request
   ... ...
       cmd->scsi_done = scsi_done;    //指定cmd完成后回調
   rtn = scsi_dispatch_cmd(cmd);  //下發將request對應的scsi_cmd
   ... ...
 }
}

int scsi_dispatch_cmd(struct scsi_cmnd *cmd)
{
 ... ...
 rtn = host->hostt->queuecommand(host, cmd);
   ... ...
}



IO完成

軟中斷

每個request_queue都會注冊軟中斷號,用來進行IO完成后的下半部處理,scsi驅動中注冊的為:scsi_softirq_done

struct request_queue *scsi_alloc_queue(struct scsi_device *sdev)
{
    ... ...
  q = __scsi_alloc_queue(sdev->host, scsi_request_fn);
    ... ...
  blk_queue_softirq_done(q, scsi_softirq_done);
  ... ...
}


硬中斷

當存儲設備完成IO后,會通過硬件中斷通知設備驅動,此時設備驅動程序會調用scsi_done回調函數完成scsi_cmd,并最終觸發BLOCK_SOFTIRQ軟中斷。

void __blk_complete_request(struct request *req)
{
      ... ...
      raise_softirq_irqoff(BLOCK_SOFTIRQ);
      ... ...
}


而BLOCK_SOFTIRQ軟中斷的處理函數就是之前注冊的scsi_softirq_done,通過自下而上層層回調,到達bio_end_io,完成整個IO的生命周期。

  -> scsi_finish_command
      -> scsi_io_completion
        -> scsi_end_request
          -> blk_update_request
            -> req_bio_endio
              -> bio_endio



總結

以上,我們很粗略地漫游了Linux中一個塊設備IO的生命周期,這是一個很復雜的過程,其中很多機制和細節只是點到為止,但是我們有了對整個IO路徑的整體的認識。當我們再遇到IO相關問題的時候,可以更加快速地找到關鍵部分,并深入研究解決。



讓數據庫基礎設施更簡單
加速企業數字化轉型建設及落地
立即咨詢

沃趣科技

中立的企業級數據庫云
十年磨一劍十年來始終如一的專注數據庫生態領域
夯實技術底蘊打造最適合時代的數據庫基礎設施
業績持續領先目前已累計服務超3000家企業客戶

留言咨詢

完善信息,我們第一時間跟您聯系
姓名
手機
公司
所在地區
咨詢問題