讀取一個文件的時候,操作系統發生了什麼

今天分享一下讀取文件的過程。linux萬物皆文件,任意文件的操作,都是通過統一的函數開始,所以我們就從read函數,分析針對一般文件的讀取過程。

int sys_read(unsigned int fd,char * buf,int count){
	struct file * file;
	struct m_inode * inode;
	// 通過fd拿到file和inode結構體
	if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))
		return -EINVAL;
	inode = file->f_inode;
	...
	/*
		f_pos表示當前的讀取指針,i_size表示整個文件大小
		下面代碼判斷讀的長度是否大於剩下的可讀長度,是的話只取剩下的部分
	*/
	if (count+file->f_pos > inode->i_size)
		count = inode->i_size - file->f_pos;
	// 到底了
	if (count<=0)
		return 0;
	return file_read(inode,file,buf,count);
}

下面是進程結構體和文件系統結構體的關係。
在這裏插入圖片描述
file_read函數是對一般文件進行讀取的函數。

int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
{
	int left,chars,nr;
	struct buffer_head * bh;

	if ((left=count)<=0)
		return 0;
	while (left) {
		// bmap取得該文件偏移對應的硬盤塊號,然後讀進來
		if (nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE)) {
			if (!(bh=bread(inode->i_dev,nr)))
				break;
		} else
			bh = NULL;
		// 偏移
		nr = filp->f_pos % BLOCK_SIZE;
		// 讀進來的數據中,可讀的長度和還需要讀的長度,取小的,如果還沒讀完繼續把塊從硬盤讀進來
		chars = MIN( BLOCK_SIZE-nr , left );
		filp->f_pos += chars; // 更新偏移指針
		left -= chars; // 更新還需藥讀取的長度
		if (bh) {
			char * p = nr + bh->b_data;
			while (chars-->0)
				put_fs_byte(*(p++),buf++); //複製到buf裏 
			brelse(bh);
		} else {
			// 沒有數據則複製0
			while (chars-->0)
				put_fs_byte(0,buf++);
		}
	}
	// 更新訪問時間
	inode->i_atime = CURRENT_TIME;
	// 返回讀取的長度,如果一個都沒讀則返回錯誤
	return (count-left)?(count-left):-ERROR;
}

上面的函數代碼看起來很多,但是邏輯其實比較清晰。他主要是根據當前的讀指針位置,算出對應文件內容所在的硬盤塊,接着把文件在硬盤中的數據塊讀進來內存,然後複製到用戶空間。所以現在的問題有兩個。
1 根據讀指針計算文件內容在硬盤的位置。我們知道一個文件對應一個inode。inode裏記錄了文件內容的一些信息。如圖。
在這裏插入圖片描述
我們看到inode裏記錄了文件每個數據塊的邏輯塊號在硬盤中對應的塊號。所以我們根據讀指針和硬盤邏輯塊的大小算出邏輯塊號。然後根據邏輯塊號從inode的映射表中找到對應的硬盤塊號。
2 根據硬盤塊號,把數據讀取出來。讀取函數是bread(block read)。

struct buffer_head * bread(int dev,int block)
{
	struct buffer_head * bh;
	// 先從buffer鏈表中獲取一個buffer
	if (!(bh=getblk(dev,block)))
		panic("bread: getblk returned NULL\n");
	// 之前已經讀取過並且有效,則直接返回
	if (bh->b_uptodate)
		return bh;
	// 返回讀取硬盤的數據
	ll_rw_block(READ,bh);
	//ll_rw_block會鎖住bh,所以會先阻塞在這然後等待喚醒 
	wait_on_buffer(bh);
	// 底層讀取數據成功後會更新該字段爲1,否則就是讀取出錯了
	if (bh->b_uptodate)
		return bh;
	brelse(bh);
	return NULL;
}

我們分三部分分析bread函數。
1 根據設備號和塊號從buffer鏈表中獲取緩存的數據,操作系統在硬盤上面實現了一層緩存系統。對於文件的讀寫進行了緩存處理。比如我們讀取了一個文件的某一部分內容,如果下次繼續讀取這部分內容,則不需要再從硬盤讀取,直接從緩存中讀取就行。這樣就提高了讀取的速度,因爲我們知道硬盤的讀取是非常慢的操作。當然操作系統會對數據的有效性進行維護(b_uptodate字段等於1說明有效)。
2 如果緩存失效,則調用ll_rw_block函數進行硬盤讀取。
3 因爲硬盤讀取非常慢,所以這時候進程會阻塞。通過wait_on_buffer函數實現進程的阻塞。等到進程被喚醒的時候再次通過b_uptodate字段判斷是否讀取成功。b_uptodate字段會在數據讀取成功的時候設置爲1.

static inline void wait_on_buffer(struct buffer_head * bh)
{
	cli();
	while (bh->b_lock)
		sleep_on(&bh->b_wait);
	sti();
}

我們繼續分析ll_rw_block函數,看看操作系統是如何對硬盤的數據進行讀取的。

void ll_rw_block(int rw, struct buffer_head * bh)
{
	unsigned int major;

	if ((major=MAJOR(bh->b_dev)) >= NR_BLK_DEV ||
	!(blk_dev[major].request_fn)) {
		printk("Trying to read nonexistent block-device\n\r");
		return;
	}
	// 新建一個讀寫硬盤數據的請求
	make_request(major,rw,bh);
}

ll_rw_block函數的邏輯非常簡單,直接調用make_request。分析這個函數之前我們先了解一下struct request結構體和一些硬盤讀取的內容。硬盤對應上層的讀寫操作,維護了一個結構體struct blk_dev_struct。
在這裏插入圖片描述
該結構體記錄了請求硬盤操作的任務隊列和處理函數。struct request結構體則記錄了請求硬盤任務的一些上下文。比如操作的類型(讀或寫),讀取的扇區、扇區數、保存讀寫數據的指針。接下來我們繼續分析make_request函數。

static void make_request(int major,int rw, struct buffer_head * bh)
{
	struct request * req;
	int rw_ahead;
	...
	// 請求隊列1/3用於讀,2/3用於寫
repeat:
	if (rw == READ)
		req = request+NR_REQUEST;
	else
		req = request+((NR_REQUEST*2)/3);
	/* find an empty request */
	while (--req >= request)
		// 小於0說明該結構沒有被使用
		if (req->dev<0)
			break;
	// 沒有找到可用的請求結構
	if (req < request) {
		// 預讀寫則直接返回
		if (rw_ahead) {
			unlock_buffer(bh);
			return;
		}
		// 阻塞等待可用的請求結構
		sleep_on(&wait_for_request);
		// 被喚醒後重新查找
		goto repeat;
	}

	req->dev = bh->b_dev;
	req->cmd = rw;
	req->errors=0;
	req->sector = bh->b_blocknr<<1; // 一塊等於兩個扇區所以乘以2,即左移1位,比如要讀地10塊,則讀取第二十個扇區
	req->nr_sectors = 2;// 一塊等於兩個扇區,即讀取的扇區是2
	req->buffer = bh->b_data;
	req->waiting = NULL;
	req->bh = bh;
	req->next = NULL;
	// 插入請求隊列
	add_request(major+blk_dev,req);
}

該函數就是生成一個struct request節點插入到請求硬盤操作的隊列中。繼續看add_request

static void add_request(struct blk_dev_struct * dev, struct request * req)
{
	struct request * tmp;

	req->next = NULL;
	cli();
	if (req->bh)
		req->bh->b_dirt = 0;
	// 當前沒有請求項,插入隊列,開始處理請求
	if (!(tmp = dev->current_request)) {
		dev->current_request = req;
		sti();
		(dev->request_fn)();
		return;
	}
	// 如果已經在處理隊列中的請求,那麼使用電梯算法插入相應的位置,等待處理。
	for ( ; tmp->next ; tmp=tmp->next)
		if ((IN_ORDER(tmp,req) ||
		    !IN_ORDER(tmp,tmp->next)) &&
		    IN_ORDER(req,tmp->next))
			break;
	req->next=tmp->next;
	tmp->next=req;
	sti();
}

不管是第一個任務節點還是後續的任務節點。都由request_fn對應的函數逐個進行處理。硬盤操作對應的處理函數是do_hd_request。do_hd_request函數根據request結構體中的上下文,對硬盤控制器發送操作命令,比如需要讀取的操作類型、讀取的扇區等。並且設置回調函數read_intr(因爲我們分析的是讀取操作)。這時候進程就阻塞了。等到硬盤控制器從硬盤中讀取數據成功後,會觸發中斷。在中斷處理函數中會執行剛纔我們設置的回調read_intr。read_intr函數從硬盤控制器的數據寄存器中把數據讀取進來。如果還沒讀取完畢,則繼續等待後續硬盤中斷。如果全部讀取成功則喚醒進程。

	// 讀寫數據成功,數據有效位置1
	CURRENT->bh->b_uptodate = uptodate;
	unlock_buffer(CURRENT->bh);
inline void unlock_buffer(struct buffer_head * bh)
{
	if (!bh->b_lock)
		printk(DEVICE_NAME ": free buffer being unlocked\n");
	bh->b_lock=0;
	// 喚醒等待的進程
	wake_up(&bh->b_wait);
}

至此,文件的讀取整個過程就分析完了。最後順便說一下文件寫入的過程,其實和讀取的過程很類似。如果是修改文件之前的內容,則先把這塊內容讀取到內存,然後修改內存的數據,最後回寫硬盤。如果是追加性寫入,則先在硬盤申請一個新的數據塊,並且修改位圖、inode信息。然後把新塊讀取到內存,接着修改內存數據,最後回寫到硬盤。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章