在計(jì)算機(jī)的世界里,我們可以將業(yè)務(wù)進(jìn)行抽象簡化為兩種場景——計(jì)算密集型和IO密集型。這兩種場景下的表現(xiàn),決定這一個(gè)計(jì)算機(jī)系統(tǒng)的能力。數(shù)據(jù)庫作為一個(gè)典型的基礎(chǔ)軟件,它的所有業(yè)務(wù)邏輯同樣可以抽象為這兩種場景的混合。因此,一個(gè)數(shù)據(jù)庫系統(tǒng)性能的強(qiáng)悍與否,往往跟操作系統(tǒng)和硬件提供的計(jì)算能力、IO能力緊密相關(guān)。
除了硬件本身的物理極限,操作系統(tǒng)在軟件層面的處理以及提供的相關(guān)機(jī)制也尤為重要。因此,想要數(shù)據(jù)庫發(fā)揮更加極限的性能,對操作系統(tǒng)內(nèi)部相關(guān)機(jī)制和流程的理解就很重要。
本篇文章,我們就一起看下Linux中一個(gè)IO請求的生命周期。Linux發(fā)展到今天,其內(nèi)部的IO子系統(tǒng)已經(jīng)相當(dāng)復(fù)雜。每個(gè)點(diǎn)展開都能自成一篇,所以本次僅是對塊設(shè)備的寫IO做一個(gè)快速的漫游,后續(xù)再對相關(guān)專題進(jìn)行詳細(xì)分解。
首先需要明確的是,什么是塊設(shè)備?我們知道IO設(shè)備可以分為字符設(shè)備和塊設(shè)備,字符設(shè)備以字節(jié)流的方式訪問數(shù)據(jù),比如我們的鍵盤鼠標(biāo)。而塊設(shè)備則是以塊為單位訪問數(shù)據(jù),并且支持隨機(jī)訪問,典型的塊設(shè)備就是我們常見的機(jī)械硬盤和固態(tài)硬盤。
一個(gè)應(yīng)用程序想將數(shù)據(jù)寫入磁盤,需要通過系統(tǒng)調(diào)用來完成:open打開文件 ---> write寫入文件 ---> close關(guān)閉文件。
下面是write系統(tǒng)調(diào)用的定義,我們可以看到,應(yīng)用程序只需要指定三個(gè)參數(shù):
1. 想要寫入的文件
2. 寫入數(shù)據(jù)所在的內(nèi)存地址
3. 寫入數(shù)據(jù)的長度
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; }
而剩下的工作就進(jìn)入到內(nèi)核中的虛擬文件系統(tǒng)(VFS)中進(jìn)行處理。
在Linux中一切皆文件,它提供了虛擬文件系統(tǒng)VFS的機(jī)制,用來抽象各種資源,使應(yīng)用程序無需關(guān)心底層細(xì)節(jié),只需通過open、read/write、close這幾個(gè)通用接口便可以管理各種不同的資源。不同的文件系統(tǒng)通過實(shí)現(xiàn)各自的通用接口來滿足不同的功能。
掛載在/dev目錄,devtmpfs中的文件代表各種設(shè)備。因此,對devtmpfs文件的讀寫操作,就是直接對相應(yīng)設(shè)備的操作。
如果應(yīng)用程序打開的是一個(gè)塊設(shè)備文件,則說明它直接對一個(gè)塊設(shè)備進(jìn)行讀寫,調(diào)用塊設(shè)備的write函數(shù):
const struct file_operations def_blk_fops = { .open = blkdev_open, ... ... .read = do_sync_read, .write = do_sync_write, ... ... };
這是我們最為熟悉的文件系統(tǒng)類型,它的文件就是我們一般理解的文件,對應(yīng)實(shí)際磁盤中按照特定格式組織并管理的區(qū)域。對這類文件的讀寫操作,都會(huì)按照固定規(guī)則轉(zhuǎn)化為對應(yīng)磁盤的讀寫操作。
應(yīng)用程序如果打開的是一個(gè)ext4文件系統(tǒng)的文件,則會(huì)調(diào)用ext4的write函數(shù):
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的性能,無論打開的是設(shè)備文件還是磁盤文件,一般情況IO會(huì)先寫入到系統(tǒng)緩存中并直接返回,IO生命周期結(jié)束。后續(xù)系統(tǒng)刷新緩存或者主動(dòng)調(diào)用sync,數(shù)據(jù)才會(huì)被真正寫入到塊設(shè)備中。有意思的是,針對塊設(shè)備的稱為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
當(dāng)打開文件時(shí)候指定了O_DIRECT標(biāo)志,則指定文件的IO為direct IO,它會(huì)繞過系統(tǒng)緩存直接發(fā)送給塊設(shè)備。在發(fā)送給塊設(shè)備之前,虛擬文件系統(tǒng)會(huì)將write函數(shù)參數(shù)表示的IO轉(zhuǎn)化為dio,在其中封裝了一個(gè)個(gè)bio結(jié)構(gòu),接著調(diào)用submit_bio將這些bio提交到通用塊層進(jìn)行處理。
do_blockdev_direct_IO -> dio_bio_submit -> submit_bio
核心結(jié)構(gòu)
1.bio/request
bio是Linux通用塊層和底層驅(qū)動(dòng)的IO基本單位,可以看到它的最重要的幾個(gè)屬性,一個(gè)bio就可以表示一個(gè)完整的IO操作:
struct bio { sector_t bi_sector; //io的起始扇區(qū) ... ... struct block_device *bi_bdev; //對應(yīng)的塊設(shè)備 ... ... bio_end_io_t *bi_end_io; //io結(jié)束的回調(diào)函數(shù) ... ... struct bio_vec *bi_io_vec; //內(nèi)存page列表 ... ... };
request代表一個(gè)獨(dú)立的IO請求,是通用塊層和驅(qū)動(dòng)層進(jìn)行IO傳遞的結(jié)構(gòu),它容納了一組連續(xù)的bio。通用塊層提供了很多IO調(diào)度策略,將多個(gè)bio合并生成一個(gè)request,以提高IO的效率。
2.gendisk
每個(gè)塊設(shè)備都對應(yīng)一個(gè)gendisk結(jié)構(gòu),它定義了設(shè)備名、主次設(shè)備號(hào)、請求隊(duì)列,和設(shè)備的相關(guān)操作函數(shù)。通過add_disk,我們就真正在系統(tǒng)中定義一個(gè)塊設(shè)備。
3.request_queue
這個(gè)即是日常所說的IO請求隊(duì)列,通用塊層將IO轉(zhuǎn)化為request并插入到request_queue中,隨后底層驅(qū)動(dòng)從中取出完成后續(xù)IO處理。
struct request_queue {
... ...
struct elevator_queue *elevator; //調(diào)度器
request_fn_proc *request_fn; //請求處理函數(shù)
make_request_fn *make_request_fn; //請求入隊(duì)函數(shù)
... ...
softirq_done_fn *softirq_done_fn; //軟中斷處理
struct device *dev;
unsigned long nr_requests;
... ...
};
處理流程
在收到上層文件系統(tǒng)提交的bio后,通用塊層最主要的功能就是根據(jù)bio創(chuàng)建request,并插入到request_queue中。
在這個(gè)過程中會(huì)對bio進(jìn)行一系列處理:當(dāng)bio長度超過限制會(huì)被分割,當(dāng)bio訪問地址相鄰則會(huì)被合并。
request創(chuàng)建后,根據(jù)request_queue配置的不同elevator調(diào)度器,request插入到對應(yīng)調(diào)度器隊(duì)列中。在底層設(shè)備驅(qū)動(dòng)程序從request_queue取出request處理時(shí),不同elevator調(diào)度器返回request策略不同,從而實(shí)現(xiàn)對request的調(diào)度。
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調(diào)度器
... ...
}
請求隊(duì)列
Linux中提供了不同類型的request_queue,一個(gè)是本文主要涉及的single-queue,另外一個(gè)是multi-queue。single-queue是在早期的硬件設(shè)備(例如機(jī)械硬盤)只能串行處理IO的背景下創(chuàng)建的,而隨著更快速的SSD設(shè)備的普及,single-queue已經(jīng)無法發(fā)揮底層存儲(chǔ)的性能了,進(jìn)而誕生了multi-queue,它優(yōu)化了很多機(jī)制使IOPS達(dá)到了百萬級(jí)別以上。至于multi-queue和single-queue的詳細(xì)區(qū)別,本篇不做討論。
每個(gè)隊(duì)列都可以配置不同的調(diào)度器,常見的有noop、deadline、cfq等。不同的調(diào)度器會(huì)根據(jù)IO類型、進(jìn)程優(yōu)先級(jí)、deadline等因素,對request請求進(jìn)一步進(jìn)行合并和排序。我們可以通過sysfs進(jìn)行配置,來滿足業(yè)務(wù)場景的需求:
#/sys/block/sdx/queue scheduler #調(diào)度器配置 nr_requests #隊(duì)列深度 max_sectors_kb #最大IO大小
在IO經(jīng)過通用塊層的處理和調(diào)度后,就進(jìn)入到了設(shè)備驅(qū)動(dòng)層,就開始需要和存儲(chǔ)硬件進(jìn)行交互。
以scsi驅(qū)動(dòng)為例:在scsi的request處理函數(shù)scsi_request_fn中,循環(huán)從request_queue中取request,并創(chuàng)建scsi_cmd下發(fā)給注冊到scsi子系統(tǒng)的設(shè)備驅(qū)動(dòng)。需要注意的是,scsi_cmd中會(huì)注冊一個(gè)scsi_done的回調(diào)函數(shù)。
static void scsi_request_fn(struct request_queue *q)
{
for (;;) {
... ...
req = blk_peek_request(q); //從request_queue中取出request
... ...
cmd->scsi_done = scsi_done; //指定cmd完成后回調(diào)
rtn = scsi_dispatch_cmd(cmd); //下發(fā)將request對應(yīng)的scsi_cmd
... ...
}
}
int scsi_dispatch_cmd(struct scsi_cmnd *cmd)
{
... ...
rtn = host->hostt->queuecommand(host, cmd);
... ...
}
軟中斷
每個(gè)request_queue都會(huì)注冊軟中斷號(hào),用來進(jìn)行IO完成后的下半部處理,scsi驅(qū)動(dòng)中注冊的為: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); ... ... }
硬中斷
當(dāng)存儲(chǔ)設(shè)備完成IO后,會(huì)通過硬件中斷通知設(shè)備驅(qū)動(dòng),此時(shí)設(shè)備驅(qū)動(dòng)程序會(huì)調(diào)用scsi_done回調(diào)函數(shù)完成scsi_cmd,并最終觸發(fā)BLOCK_SOFTIRQ軟中斷。
void __blk_complete_request(struct request *req) { ... ... raise_softirq_irqoff(BLOCK_SOFTIRQ); ... ... }
而BLOCK_SOFTIRQ軟中斷的處理函數(shù)就是之前注冊的scsi_softirq_done,通過自下而上層層回調(diào),到達(dá)bio_end_io,完成整個(gè)IO的生命周期。
-> scsi_finish_command -> scsi_io_completion -> scsi_end_request -> blk_update_request -> req_bio_endio -> bio_endio
以上,我們很粗略地漫游了Linux中一個(gè)塊設(shè)備IO的生命周期,這是一個(gè)很復(fù)雜的過程,其中很多機(jī)制和細(xì)節(jié)只是點(diǎn)到為止,但是我們有了對整個(gè)IO路徑的整體的認(rèn)識(shí)。當(dāng)我們再遇到IO相關(guān)問題的時(shí)候,可以更加快速地找到關(guān)鍵部分,并深入研究解決。
服務(wù)電話: 400-678-1800 (周??周五 09:00-18:00)
商務(wù)合作: 0571-87770835
市場反饋: marketing@woqutech.com
地址: 杭州市濱江區(qū)濱安路1190號(hào)智匯中?A座1101室