1. 程式人生 > 其它 >結合原始碼的作業系統學習記錄(3)--系統呼叫

結合原始碼的作業系統學習記錄(3)--系統呼叫

  • 系統呼叫過程

    當應用程式經過庫函式向核心發出一箇中斷呼叫 int 0x80 時,就開始執行一個系統呼叫,eax 存放呼叫號,ebx,,ecx,edx依次存放攜帶的引數(最多 3 個)。根據傳入引數的不同,有對應的 __syscall0,__syscall3 等巨集定義

  • __syscall0

    位於 include/unistd.h 中

    #define _syscall0(type,name) \
    type name(void) \
    { \
    long __res; \
    __asm__ volatile ("int $0x80" \	// 呼叫系統中斷
    	: "=a" (__res) \	// 返回值
    	: "0" (__NR_##name)); \	// 輸入的是系統中斷呼叫號
    if (__res >= 0) \
    	return (type) __res; \
    errno = -__res; \
    return -1; \
    }
    

    在系統呼叫之前需要儲存現場,在使用者態想核心態轉換之前,將暫存器的值壓入核心棧中,

    在 sched.c 中定義了中斷呼叫的函式 system_call:

    set_system_gate(0x80,&system_call);
    

    跟進 system_call 的定義,這一部分是彙編實現的

    reschedule:
    	push ret_from_sys_call # 將ret_from_sys_call 的地址入棧
    	jmp _schedule
    
    _system_call:
    	cmpl $nr_system_calls-1,%eax	# 如果呼叫號超出範圍的話就置 eax 為 -1 並退出 
    	ja bad_sys_call
    	push %ds	# 儲存原段暫存器值
    	push %es	
    	push %fs
    	pushl %edx
    	pushl %ecx		# %ebx,%ecx,%edx 中存放著傳入的引數
    	pushl %ebx		
    	movl $0x10,%edx		# ds,es 指向核心資料段(全域性描述符表中資料段描述符)
    	mov %dx,%ds		
    	mov %dx,%es
    	movl $0x17,%edx		# fs 指向區域性資料段(區域性描述符表中資料段描述符)
    	mov %dx,%fs
    	call _sys_call_table(,%eax,4)		# 呼叫地址 = _sys_call_table + %eax * 4
    	pushl %eax		# 系統呼叫號入棧
    	movl _current,%eax		# 取當前任務(程序)pcb 地址
    	# 如果當前任務不在就緒狀態,或在就緒狀態但時間片用完,則重新執行排程程式
    	cmpl $0,state(%eax)		# state
    	jne reschedule
    	cmpl $0,counter(%eax)		# counter
    	je reschedule
    # 當從系統呼叫 c 函式返回後,對訊號量進行識別處理
    ret_from_sys_call:
    	movl _current,%eax		# 判斷程序是不是 task0,如果是則不必進行訊號量方面的處理
    	cmpl _task,%eax
    	je 3f		# 向前跳到標籤 3f
    	# 通過對原呼叫程式程式碼選擇符的檢查來判斷呼叫程式是否是超級使用者
    	# 如果是超級使用者則退出中斷,否則進行訊號量處理
    	# 比較選擇符是否為普通使用者程式碼段的選擇符 0x000f (RPL=3,區域性表,第1 個段(程式碼段))
    	cmpw $0x0f,CS(%esp)		# was old code segment supervisor ?
    	jne 3f
    	# 如果原堆疊段選擇符不為 0x17(也即原堆疊不在使用者資料段中),則退出
    	cmpw $0x17,OLDSS(%esp)		# was stack segment = 0x17 ?
    	jne 3f
    	# 首先取當前任務結構中的訊號點陣圖( 32 位,每位代表 1 種訊號)
    	# 然後用任務結構中的訊號阻塞(遮蔽)碼,阻塞不允許的訊號位,取得數值最小的訊號值,再把原始號點陣圖中該訊號對應的為復位
    	# 最後將該訊號值作為引數之一呼叫 do_signal
    	movl signal(%eax),%ebx		# 取訊號點陣圖
    	movl blocked(%eax),%ecx		# 取阻塞
    	notl %ecx		# 每位取反
    	andl %ebx,%ecx		# 獲得許可的訊號點陣圖
    	bsfl %ecx,%ecx		# 從低位(位0)開始掃描點陣圖,看是否有 1 的位,若有,則 ecx 保留該位的偏移值
    	je 3f
    	btrl %ecx,%ebx		# 復位該訊號(ebx 含有原 signal 點陣圖)
    	movl %ebx,signal(%eax)		# 重新儲存signal 點陣圖資訊
    	incl %ecx		#  將訊號調整為從1 開始的數(1-32)。
    	pushl %ecx		# 訊號值入棧作為呼叫 do_signal 的引數之一
    	call _do_signal
    	popl %eax		# 彈出訊號值
    3:	popl %eax
    	popl %ebx
    	popl %ecx
    	popl %edx
    	pop %fs
    	pop %es
    	pop %ds
    	iret
    

    呼叫 call _sys_call_table(,%eax,4) 時的核心棧

  • read

    函式實現在 fs/read_write.c 的 sys_read 中,fd 是檔案控制代碼,buf 是緩衝區,count 是要讀的位元組數

    int sys_read(unsigned int fd,char * buf,int count)
    {
    	struct file * file;
    	struct m_inode * inode;
    
        // 如果 fd 的值大於程式最多開啟檔案數(NR_OPEN)或者 count < 0 或者這個 fd 的檔案結構指標為空,則返回出錯碼並退出
    	if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))
    		return -EINVAL;
    	if (!count)
    		return 0;
        // 驗證存放資料的緩衝區的記憶體限制
    	verify_area(buf,count);
    	inode = file->f_inode;
        // 取檔案對應的 i 結點,如果是讀管道檔案模式,則進行讀管道操作,返回讀取的位元組數
    	if (inode->i_pipe)
    		return (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;
        // 如果是字元型檔案,則執行讀字元裝置操作,返回讀取位元組數
    	if (S_ISCHR(inode->i_mode))
    		return rw_char(READ,inode->i_zone[0],buf,count,&file->f_pos);
        // 如果是塊裝置檔案,則執行塊裝置讀操作,並返回讀取的位元組數
    	if (S_ISBLK(inode->i_mode))
    		return block_read(inode->i_zone[0],&file->f_pos,buf,count);
        // 如果是目錄檔案或者常規檔案,則首先驗證 count 並進行調整
        // (若 count + 檔案當前讀寫指標值 > 檔案大小,則重新設定 count  = 檔案大小 - 當前讀寫的指標值)
        // 成功後執行檔案讀操作,返回讀取位元組數並退出
    	if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode)) {
    		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);
    	}
        // 否則列印結點的檔案屬性,並返回錯誤碼並退出
    	printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);
    	return -EINVAL;
    }
    

    以 file_read 為例,位於 fd/file_dev.c 中。i 結點確定裝置號,filp 結構確定檔案中當前讀寫指標的位置,buf 指定緩衝區地址,count 為讀取位元組數,最後的返回值為實際讀取的位元組數或出錯號。

    int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
    {
    	int left,chars,nr;
    	struct buffer_head * bh;
    
        // 讀取位元組數小於等於 0 則直接返回
    	if ((left=count)<=0)
    		return 0;
    	while (left) {
            // bmap 根據 i 結點和檔案結構資訊,讀資料塊檔案當前讀寫位置在裝置上對應的邏輯塊號(nr)
    		if (nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE)) {
    			if (!(bh=bread(inode->i_dev,nr)))
    				break;
    		} else
    			bh = NULL;
            // 計算檔案讀寫指標在資料塊中的偏移值 nr
    		nr = filp->f_pos % BLOCK_SIZE;
            // 可讀資料和要讀資料取 min
    		chars = MIN( BLOCK_SIZE-nr , left );
            // 調整讀寫檔案指標,指標前移此次讀取位元組數 chars
    		filp->f_pos += chars;
            // 剩餘位元組數減去 chars
    		left -= chars;
            // 若從裝置上讀到了資料,則將 p 指向資料塊緩衝區中開始讀的位置,並且複製 chars 位元組到 buf 中,否則向 buf 中填入 char 個 0
    		if (bh) {
    			char * p = nr + bh->b_data;
    			while (chars-->0)
    				put_fs_byte(*(p++),buf++);
    			brelse(bh);
    		} else {
    			while (chars-->0)
    				put_fs_byte(0,buf++);
    		}
    	}
        // 修改該 i 結點的訪問時間為當前時間
    	inode->i_atime = CURRENT_TIME;
        // 返回讀取的位元組數
    	return (count-left)?(count-left):-ERROR;
    }
    

    接著看從底層讀取檔案資料的函式 bread,位於 fs/buffer.c 中

    struct buffer_head * bread(int dev,int block)
    {
    	struct buffer_head * bh;
    	
        // 在高速緩衝中申請一塊緩衝區,如果返回 NULL,直接宕機
    	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;
    }
    

    繼續往下跟 ll_rw_block 函式,位於 kernel/blk_drv/ll_rw_blk.c 中。ll_rw_block 實際上呼叫了 make_request 函式讀取

    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);
    }
    

    make_request:

    static void make_request(int major,int rw, struct buffer_head * bh)
    {
    	struct request * req;
    	int rw_ahead;
    
    // WRITEA / READA 是特殊情況,如果緩衝區已經上鎖,就直接退出,否則就執行一般的讀寫操作
    // READ 和 WRITE 後面的 A 表示 Ahead,提前預讀/寫,如果緩衝區正在使用,也就是被上了鎖,就放棄預讀/寫
    	if (rw_ahead = (rw == READA || rw == WRITEA)) {
    		if (bh->b_lock)
    			return;
    		if (rw == READA)
    			rw = READ;
    		else
    			rw = WRITE;
    	}
    	if (rw!=READ && rw!=WRITE)
    		panic("Bad block dev command, must be R/W/RA/WA");
    	// 緩衝區上鎖
        lock_buffer(bh);
        // 如果緩衝區是寫並且資料不髒,或者命令是讀並且資料更新過,則不需新增請求,直接 unlock 並退出
    	if ((rw == WRITE && !bh->b_dirt) || (rw == READ && bh->b_uptodate)) {
    		unlock_buffer(bh);
    		return;
    	}
    repeat:
    // 不能讓佇列中全都是寫請求項,需要為讀請求保留一些空間
    // 讀操作是優先的,請求佇列的後三分之一是為讀準備的
        
    // 請求項是從請求陣列末尾開始搜尋空項並填入的,讀請求可以從佇列末尾開始操作,寫請求只能從佇列 2/3 處填入
    	if (rw == READ)
    		req = request+NR_REQUEST;
    	else
    		req = request+((NR_REQUEST*2)/3);
    // 搜尋空請求項,request 結構的 dev 欄位為 -1 時,表示該項未被佔用
    	while (--req >= request)
    		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;	// 命令(READ/WRITE)
    	req->errors=0;	// 操作時產生的錯誤次數
    	req->sector = bh->b_blocknr<<1;	// 起始扇區(1塊=2扇區)
    	req->nr_sectors = 2;	// 讀寫扇區數
    	req->buffer = bh->b_data;	// 資料緩衝區
    	req->waiting = NULL;	// 任務等待操作執行完成的地方
    	req->bh = bh;	// 緩衝區頭指標
    	req->next = NULL;	// 指向下一請求項
    	add_request(major+blk_dev,req);	// 將此次請求項加入請求佇列中
    }
    

    繼續跟進 add_request,看一下加入請求佇列的過程,dev 時指定的塊裝置,req 時請求項的資訊

    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;	// 清緩衝區的髒標記
        // 如果 dev 的當前請求欄位為空,則表示目前該裝置沒有請求項,本次時第一個請求項
        // 因此可將塊裝置當前的請求指標直接指向請求項,並立刻執行響應裝置的請求函式
    	if (!(tmp = dev->current_request)) {
    		dev->current_request = req;
    		sti();	// 開中斷
    		(dev->request_fn)();	// 執行裝置請求函式,實際執行了 do_hd_request
    		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();
    }
    

    插入佇列之後,看一下最終執行操作的 do_hd_request,讀操作的分支

    	else if (CURRENT->cmd == READ)
    	{
    		hd_out (dev, nsect, sec, head, cyl, WIN_READ, &read_intr);
    	}
    

    繼續跟進 hd_out ,該函式的作用是向硬碟控制器傳送命令塊。其中,drive 時硬碟號(0-1),nsect 是讀寫扇區數,sect 是起始扇區,head 是磁頭號,cyl 是柱面號,cmd 是命令碼,*intr_addr() 是硬碟中斷處理程式中將呼叫的 c 處理函式。

    static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect,
    		unsigned int head,unsigned int cyl,unsigned int cmd,
    		void (*intr_addr)(void))
    {
    	register int port asm("dx");	// port 變數對應暫存器dx。
    
        // 不支援的讀寫
    	if (drive>1 || head>15)
    		panic("Trying to write bad sector");
        // 等待一段時間後仍未就緒則出錯宕機
    	if (!controller_ready())
    		panic("HD controller not ready");
    	do_hd = intr_addr;		// do_hd 函式指標將在硬碟中斷程式中被呼叫。
    	outb_p (hd_info[drive].ctl, HD_CMD);	// 向控制暫存器(0x3f6)輸出控制位元組。
    	port = HD_DATA;		// 置dx 為資料暫存器埠(0x1f0)。
    	outb_p (hd_info[drive].wpcom >> 2, ++port);	// 引數:寫預補償柱面號(需除4)。
    	outb_p (nsect, ++port);	// 引數:讀/寫扇區總數。
    	outb_p (sect, ++port);	// 引數:起始扇區。
    	outb_p (cyl, ++port);		// 引數:柱面號低8 位。
    	outb_p (cyl >> 8, ++port);	// 引數:柱面號高8 位。
    	outb_p (0xA0 | (drive << 4) | head, ++port);	// 引數:驅動器號+磁頭號。
    	outb (cmd, ++port);		// 命令:硬碟控制命令。
    }
    

    到現在,從驅動到硬碟控制器的讀處理就完成了,再回去看 ll_rw_block 之後的處理,接著是 wait_on_buffer。

    static inline void wait_on_buffer(struct buffer_head * bh)
    {
    	cli();		// 關中斷。
    	while (bh->b_lock)	// 如果已被上鎖,則程序進入睡眠,等待其解鎖。
    		sleep_on(&bh->b_wait);
    	sti();		// 開中斷。
    }
    

    具體分析一下 sleep_on,這裡主要是一些程序排程的處理了,將當前程序掛載到睡眠佇列 p 中(此時任務置為不可中斷的等待狀態),並讓睡眠佇列頭的指標指向當前任務。

    void sleep_on(struct task_struct **p)
    {
    	struct task_struct *tmp;
    
    	if (!p)
    		return;
        // 如果當前任務是 0 則宕機
    	if (current == &(init_task.task))
    		panic("task[0] trying to sleep");
        // tmp 指向已經在等待佇列上的任務,等待佇列頭指標指向當前任務
    	tmp = *p;
    	*p = current;
        // // 將當前任務置為不可中斷的等待狀態。
    	current->state = TASK_UNINTERRUPTIBLE;
        // 重新排程
    	schedule();
        // 只有當這個等待任務唄喚醒時,排程程式才會回到這裡
        // 若還存在等待的任務,則也將其置為就緒狀態(喚醒)
    	if (tmp)
    		tmp->state=0;
    }
    

    當硬碟讀好資料之後,給系統傳送中斷,執行 hd_interrupt

    _hd_interrupt:
    	push eax
    	push ecx
    	push edx
    	push ds
    	push es
    	push fs
    	mov eax,10h ; ds,es 置為核心資料段。
    	mov ds,ax
    	mov es,ax
    	mov eax,17h ; fs 置為呼叫程式的區域性資料段。
    	mov fs,ax
    ; 由於初始化中斷控制晶片時沒有采用自動EOI,所以這裡需要發指令結束該硬體中斷。
    	mov al,20h
    	out 0A0h,al ; EOI to interrupt controller ;//1 ;// 送從8259A。
    	jmp l3 ; give port chance to breathe
    l3: jmp l4 ; 延時作用。
    l4: xor edx,edx
    	xchg edx,dword ptr _do_hd ; do_hd 定義為一個函式指標,將被賦值read_intr()或 write_intr()函式地址。(kernel/blk_drv/hd.c)
    ; 放到edx 暫存器後就將do_hd 指標變數置為NULL。
    	test edx,edx ; 測試函式指標是否為Null。
    	jne l5 ; 若空,則使指標指向C 函式unexpected_hd_interrupt()。
    	mov edx,dword ptr _unexpected_hd_interrupt ; (kernel/blk_drv/hdc,237)。
    l5: out 20h,al ; 送主8259A 中斷控制器EOI 指令(結束硬體中斷)。
    	call edx ; "interesting" way of handling intr.
    	pop fs ; 上句呼叫do_hd 指向的C 函式。
    	pop es
    	pop ds
    	pop edx
    	pop ecx
    	pop eax
    	iretd
    

    顯然在讀操作中最終會呼叫 read_intr 函式,

    static void read_intr(void)
    {
        // 如果硬碟執行命令後出錯
    	if (win_result()) {
    		bad_rw_intr();	// 讀寫硬碟失敗處理
    		do_hd_request();	// 再次請求硬碟作相應處理
    		return;
    	}
    	port_read(HD_DATA,CURRENT->buffer,256);	// // 將資料從資料暫存器口讀到請求結構緩衝區。
    	CURRENT->errors = 0;		// 清出錯次數。
    	CURRENT->buffer += 512;	// 調整緩衝區指標,指向新的空區。
    	CURRENT->sector++;		// 起始扇區號加1,
        // 如果需要讀出的扇區數還沒讀完,則再次置硬碟呼叫 c 函式指標為  read_intr()
    	if (--CURRENT->nr_sectors) {
    		do_hd = &read_intr;
    		return;
    	}
    	end_request (1);		// 若全部扇區資料已經讀完,則處理請求結束事宜,
    	do_hd_request ();		// 執行其它硬碟請求操作。
    }
    

    終於結束了,最後再看一下 end_request

    extern inline void end_request(int uptodate)
    {
    	DEVICE_OFF(CURRENT->dev);	// 關閉裝置
        // 讀寫成功,b_uptodate 置 1,解鎖緩衝區
    	if (CURRENT->bh) {
    		CURRENT->bh->b_uptodate = uptodate;
    		unlock_buffer(CURRENT->bh);
    	}
        // 如果 b_uptodate 標誌為 0,則顯示裝置錯誤資訊
    	if (!uptodate) {
    		printk(DEVICE_NAME " I/O error\n\r");
    		printk("dev %04x, block %d\n\r",CURRENT->dev,
    			CURRENT->bh->b_blocknr);
    	}
    	wake_up(&CURRENT->waiting);	// 喚醒等待該請求項的程序
    	wake_up(&wait_for_request);	// 喚醒等待請求的程序
    	CURRENT->dev = -1; // 釋放該請求項
    	CURRENT = CURRENT->next; // 從請求連結串列中刪除該請求項
    }
    
  • write

    write 函式的部分邏輯和 read 相似,首先看入口的 sys_write 函式,對比可以看出,流程基本相同,就是將對應的處理函式換了一下

    int sys_write (unsigned int fd, char *buf, int count)
    {
    	struct file *file;
    	struct m_inode *inode;
    
    // 如果檔案控制代碼值大於程式最多開啟檔案數NR_OPEN,或者需要寫入的位元組計數小於0,或者該控制代碼的檔案結構指標為空,則返回出錯碼並退出。
    	if (fd >= NR_OPEN || count < 0 || !(file = current->filp[fd]))
    		return -EINVAL;
    // 若需讀取的位元組數count 等於0,則返回0,退出
    	if (!count)
    		return 0;
    // 取檔案對應的i 節點。若是管道檔案,並且是寫管道檔案模式,則進行寫管道操作,若成功則返回
    // 寫入的位元組數,否則返回出錯碼,退出。
    	inode = file->f_inode;
    	if (inode->i_pipe)
    		return (file->f_mode & 2) ? write_pipe (inode, buf, count) : -EIO;
    // 如果是字元型檔案,則進行寫字元裝置操作,返回寫入的字元數,退出。
    	if (S_ISCHR (inode->i_mode))
    		return rw_char (WRITE, inode->i_zone[0], buf, count, &file->f_pos);
    // 如果是塊裝置檔案,則進行塊裝置寫操作,並返回寫入的位元組數,退出。
    	if (S_ISBLK (inode->i_mode))
    		return block_write (inode->i_zone[0], &file->f_pos, buf, count);
    // 若是常規檔案,則執行檔案寫操作,並返回寫入的位元組數,退出。
    	if (S_ISREG (inode->i_mode))
    		return file_write (inode, file, buf, count);
    // 否則,顯示對應節點的檔案模式,返回出錯碼,退出。
    	printk ("(Write)inode->i_mode=%06o\n\r", inode->i_mode);
    	return -EINVAL;
    }
    

    同樣以 file_write 為例,跟進檢視一下

    int file_write(struct m_inode * inode, struct file * filp, char * buf, int count)
    {
    	off_t pos;
    	int block,c;
    	struct buffer_head * bh;
    	char * p;
    	int i=0;
    
        // 如果是要向檔案後新增資料,則將檔案讀寫指標移到檔案尾部,否則就在檔案讀寫指標寫入
    	if (filp->f_flags & O_APPEND)
    		pos = inode->i_size;
    	else
    		pos = filp->f_pos;
        // 若已寫入位元組數 i 小於需要寫入的位元組數 count
    	while (i<count) {
            // 建立資料塊號(pos/BLOCK_SIZE)在裝置上對應的邏輯塊,並返回在裝置上的邏輯塊號
    		if (!(block = create_block(inode,pos/BLOCK_SIZE)))
    			break;
            // 根據該邏輯塊號讀取裝置上的相應資料塊
    		if (!(bh=bread(inode->i_dev,block)))
    			break;
            // 求出檔案讀寫指標在資料塊中的偏移值c,
    		c = pos % BLOCK_SIZE;
            // 將 p 指向讀出資料塊緩衝區中開始讀取的位置
    		p = c + bh->b_data;
            // 髒標記,表示緩衝區已修改
    		bh->b_dirt = 1;
            // 可寫的長度
    		c = BLOCK_SIZE-c;
            // 在可寫長度和能寫長度中取小的
    		if (c > count-i) c = count-i;
            // 檔案讀寫指標前移此次需要寫入的位元組數,如果當前檔案讀寫指標位置超過了檔案大小,就修改 i 結點中檔案大小欄位,並標記 i 已修改
    		pos += c;
    		if (pos > inode->i_size) {
    			inode->i_size = pos;
    			inode->i_dirt = 1;
    		}
            // 已寫入的位元組數累加到此次寫入的位元組數c
    		i += c;
            // 從使用者緩衝區 buf 中複製 c 個位元組到高速緩衝區中
          // p 指向開始位置處,然後釋放該緩衝區
    		while (c-->0)
    			*(p++) = get_fs_byte(buf++);
    		brelse(bh);
    	}
        // 更新檔案修改時間
    	inode->i_mtime = CURRENT_TIME;、
        // 如果此次操作不是在檔案尾新增資料,則把檔案讀寫指標調整到當前讀寫位置,並更改 i 節點修改時間為當前時間。
    	if (!(filp->f_flags & O_APPEND)) {
    		filp->f_pos = pos;
    		inode->i_ctime = CURRENT_TIME;
    	}
        // 返回寫入的位元組數
    	return (i?i:-1);
    }
    

    其中建立邏輯塊的 create_block 實際上是呼叫的 _bmap 函式,

    inode :檔案的 i 結點,block :檔案中的資料塊號,create:建立標誌

    // 建立檔案資料塊block 在裝置上對應的邏輯塊,並返回裝置上對應的邏輯塊號。
    int create_block(struct m_inode * inode, int block)
    {
    	return _bmap(inode,block,1);
    }
    
    static int _bmap(struct m_inode * inode,int block,int create)
    {
    	struct buffer_head * bh;
    	int i;
    
    	if (block<0)
    		panic("_bmap: block<0");
        // 如果塊號大於 直接塊數 + 間接塊數 + 二次間接塊數,超出檔案系統表示範圍,則宕機。
    	if (block >= 7+512+512*512)
    		panic("_bmap: block>big");
        // 如果該塊號小於 7,則使用直接塊表示
    	if (block<7) {
            // 如果傳入的建立標誌位為1,並且 i 結點中對應改塊的邏輯塊欄位為 0
    		if (create && !inode->i_zone[block])
                // 則向相應裝置申請一磁碟塊,並將盤上的邏輯塊號填入邏輯塊欄位中
    			if (inode->i_zone[block]=new_block(inode->i_dev)) {
                    // 更新 i 結點時間和髒標記
    				inode->i_ctime=CURRENT_TIME;
    				inode->i_dirt=1;
    			}
    		return inode->i_zone[block];
    	}
        // 如果該塊號在 7 ~ 7+512 中,說明這是一次間接塊
    	block -= 7;
    	if (block<512) {
            同上
    		if (create && !inode->i_zone[7])
    			if (inode->i_zone[7]=new_block(inode->i_dev)) {
    				inode->i_dirt=1;
    				inode->i_ctime=CURRENT_TIME;
    			}
    		if (!inode->i_zone[7])
    			return 0;
            // 讀取裝置上的一次間接塊
    		if (!(bh = bread(inode->i_dev,inode->i_zone[7])))
    			return 0;
            // 取該簡介快上第 block 項中的邏輯塊號(盤塊號)
    		i = ((unsigned short *) (bh->b_data))[block];
    		if (create && !i)
                // 申請一磁碟塊
    			if (i=new_block(inode->i_dev)) {
                    // 簡介快中的第 block 項等於新邏輯塊塊號
    				((unsigned short *) (bh->b_data))[block]=i;
    				bh->b_dirt=1;
    			}
            // 釋放該簡介塊
    		brelse(bh);
    		return i;
    	}
    	block -= 512;
        // 申請一磁碟塊用於存放二次簡接塊
    	if (create && !inode->i_zone[8])
    		if (inode->i_zone[8]=new_block(inode->i_dev)) {
    			inode->i_dirt=1;
    			inode->i_ctime=CURRENT_TIME;
    		}
        // 若此時 i 結點二次間接塊欄位為 0,表明申請磁碟塊失敗
    	if (!inode->i_zone[8])
    		return 0;
        // 讀取該二次間接塊的一級塊
    	if (!(bh=bread(inode->i_dev,inode->i_zone[8])))
    		return 0;
        // 取該二次間接塊的一級塊上第(block/512)項中的邏輯塊號
    	i = ((unsigned short *)bh->b_data)[block>>9];
        // 如果二次間接塊的一級塊上第(block/512)項中的邏輯塊號為 0 的話,則需申請一磁碟塊作為二次間接塊的二級塊
    	if (create && !i)
    		if (i=new_block(inode->i_dev)) {
                // 讓二次間接塊的一級塊中第(block/512)項等於該二級塊的塊號
    			((unsigned short *) (bh->b_data))[block>>9]=i;
    			bh->b_dirt=1;
    		}
    	brelse(bh);
    	if (!i)
    		return 0;
        // (讀取二次間接塊的二級塊)獲取二級索引對應的資料
    	if (!(bh=bread(inode->i_dev,i)))
    		return 0;
        // 取該二級塊上第 block 項中的邏輯塊號
    	i = ((unsigned short *)bh->b_data)[block&511];
    	if (create && !i)
    		if (i=new_block(inode->i_dev)) {
    			((unsigned short *) (bh->b_data))[block&511]=i;
    			bh->b_dirt=1;
    		}
    	brelse(bh);
    	return i;
    }
    

    跟進建立塊的 new_block 操作,根據當前塊的使用情況申請一個新的塊並標記已使用,然後把超級塊的資訊寫回到硬碟,並返回新建的塊號

    int new_block(int dev)
    {
    	struct buffer_head * bh;
    	struct super_block * sb;
    	int i,j;
    
        // 從裝置 dev 中取超級塊
    	if (!(sb = get_super(dev)))
    		panic("trying to get new block from nonexistant device");
        // 掃描邏輯塊點陣圖,尋找空閒邏輯塊,獲取放置該邏輯塊的塊號
    	j = 8192;
    	for (i=0 ; i<8 ; i++)
            // s_zmap[i]為資料塊點陣圖的快取 
    		if (bh=sb->s_zmap[i])
    			if ((j=find_first_zero(bh->b_data))<8192)
    				break;
        // 如果全部掃描完還沒找到,或者點陣圖所在緩衝塊無效,則退出
    	if (i>=8 || !bh || j>=8192)
    		return 0;
        // 設定新邏輯塊對應邏輯塊點陣圖中的 bit 位,若對應 bit 位已置為,則宕機
        // 
    	if (set_bit(j,bh->b_data))
    		panic("new_block: bit already set");
        // 更新髒標記,該點陣圖對應的 buffer 需要回寫
    	bh->b_dirt = 1;
        // 如果新邏輯塊大於該裝置上的總邏輯塊數,則說明指定邏輯塊在對應裝置上不存在,申請失敗
        // 點陣圖存在多個塊中,i 位第 i 個塊,每個塊對應的點陣圖管理者 8192 個數據塊
    	j += i*8192 + sb->s_firstdatazone-1;
    	if (j >= sb->s_nzones)
    		return 0;
        // 讀取裝置上的該新邏輯塊資料,失敗則宕機
    	if (!(bh=getblk(dev,j)))
    		panic("new_block: cannot get block");
        // 新塊的引用計數應為1。否則宕機
    	if (bh->b_count != 1)
    		panic("new block: count is != 1");
        // 將該新邏輯塊清零
    	clear_block(bh->b_data);
        // 更新已更新標記和髒標記,並釋放對應緩衝區
    	bh->b_uptodate = 1;
    	bh->b_dirt = 1;
    	brelse(bh);
    	return j;
    }
    

    在 create_block 之後,需要用 bread 把塊的內容讀進來存到 buffer 中(新塊內容都是 0),接著就可以把資料寫到 buffer 中。

    在寫檔案的時候資料不是直接到硬碟的,只是放在快取裡,系統會有執行緒定期更新快取到硬碟。

  • sync

    可以實時將資料同步到硬碟

    int sys_sync(void)
    {
    	int i;
    	struct buffer_head * bh;
    
        // 將所有 i 節點寫入高速緩衝
    	sync_inodes();		
        // 掃描所有高速緩衝區,對於已被修改的緩衝塊產生的寫請求,將緩衝區中的資料與裝置中同步
    	bh = start_buffer;
    	for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
            // 等待緩衝區解鎖
    		wait_on_buffer(bh);
    		if (bh->b_dirt)
                // 寫裝置塊請求,等待底層驅動寫回到硬碟,不一定立刻寫入        
    			ll_rw_block(WRITE,bh);
    	}
    	return 0;
    }
    

    先看 sync_inodes 函式,該函式把 inode_table 中(程序開啟檔案對應的 inode 節點)寫入到 buffer 中。

    void sync_inodes(void)
    {
    	int i;
    	struct m_inode * inode;
    
        // 指標首先指向 i 節點表指標陣列首項 
    	inode = 0+inode_table;
        // 掃描 i 節點表陣列指標
    	for(i=0 ; i<NR_INODE ; i++,inode++) {
            // 等待該 i 節點可用
    		wait_on_inode(inode);
            // i 結點已修改
            // 管道內容直接存在在記憶體中,所以不需要寫
    		if (inode->i_dirt && !inode->i_pipe)
                // 寫盤
    			write_inode(inode);
    	}
    }
    

    繼續跟進寫盤操作

    static void write_inode(struct m_inode * inode)
    {
    	struct super_block * sb;
    	struct buffer_head * bh;
    	int block;
    
        // 先對該節點加鎖,如果改節點沒在修改中活著該節點的裝置號為0,則解鎖並退出
    	lock_inode(inode);
    	if (!inode->i_dirt || !inode->i_dev) {
    		unlock_inode(inode);
    		return;
    	}
        // 獲取該 i 節點的超級快
    	if (!(sb=get_super(inode->i_dev)))
    		panic("trying to write inode without device");
        // 該節點的邏輯塊號 = 2(啟動塊+超級塊) + inode 點陣圖佔用的塊數 + 邏輯塊點陣圖佔用的塊數 + inode 的相對偏移(i 節點號-1)/每塊含有的i 節點數
    	block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks +
    		(inode->i_num-1)/INODES_PER_BLOCK;
        // 讀取該 i 節點所在的邏輯塊
    	if (!(bh=bread(inode->i_dev,block)))
    		panic("unable to read i-node block");
        // 將該 i 節點資訊複製到邏輯塊對應該 i 節點的項中
    	((struct d_inode *)bh->b_data)
    		[(inode->i_num-1)%INODES_PER_BLOCK] =
    			*(struct d_inode *)inode;
        // 更新緩衝區的髒標記和 i 節點修改標記
    	bh->b_dirt=1;
    	inode->i_dirt=0;
        // 釋放並解鎖
    	brelse(bh);
    	unlock_inode(inode);
    }
    

    現在 i 節點的內容以及在 buffer 中了,然後遍歷 buffer,通過 ll_rw_block 寫回硬碟。

  • 用於在檔案系統中建立硬連結(需要一些檔案系統的知識)

    int sys_link(const char * oldname, const char * newname)
    {
    	struct dir_entry * de;
    	struct m_inode * oldinode, * dir;
    	struct buffer_head * bh;
    	const char * basename;
    	int namelen;
    
        // 取原始檔路徑名對應的 i 節點
    	oldinode=namei(oldname);
    	if (!oldinode)
    		return -ENOENT;
        // 如果原路徑名對應的是一個目錄名,則釋放該 i 節點
    	if (S_ISDIR(oldinode->i_mode)) {
    		iput(oldinode);
    		return -EPERM;
    	}
        // 查詢新路徑名的最頂層目錄的 i 節點,並返回最後的檔名及其長度
        // 如果沒找到,則釋放原路徑名的 i 節點
    	dir = dir_namei(newname,&namelen,&basename);
    	if (!dir) {
    		iput(oldinode);
    		return -EACCES;
    	}
        // 如果新路徑名中不包括檔名,則釋放原路徑名 i 節點和新路徑名目錄的 i 節點
    	if (!namelen) {
    		iput(oldinode);
    		iput(dir);
    		return -EPERM;
    	}
        // 如果新路徑名目錄的裝置號與原路徑名的裝置號不一樣,則也不能建立連線
    	if (dir->i_dev != oldinode->i_dev) {
    		iput(dir);
    		iput(oldinode);
    		return -EXDEV;
    	}
        // 如果使用者沒有在新目錄中寫的許可權,則也不能建立連線
    	if (!permission(dir,MAY_WRITE)) {
    		iput(dir);
    		iput(oldinode);
    		return -EACCES;
    	}
        // 查詢該新路徑名是否已經存在,如果存在,則也不能建立連線
    	bh = find_entry(&dir,basename,namelen,&de);
    	if (bh) {
    		brelse(bh);
    		iput(dir);
    		iput(oldinode);
    		return -EEXIST;
    	}
        // 在新目錄中新增一個目錄項,若失敗則釋放該目錄的 i 節點和原路徑名的i 節點
    	bh = add_entry(dir,basename,namelen,&de);
    	if (!bh) {
    		iput(dir);
    		iput(oldinode);
    		return -ENOSPC;
    	}
        // 如果上面的判斷都過了,就開始建立硬連結
        // 設定該目錄項的 i 節點號等於原路徑的 i 節點號,並更新包含該新添目錄項的高速緩衝區的髒標記,然後釋放目錄的 i 節點
    	de->inode = oldinode->i_num;
    	bh->b_dirt = 1;
    	brelse(bh);
    	iput(dir);
        // 原 i 節點的應用計數+1,更新修改時間和髒標記
    	oldinode->i_nlinks++;
    	oldinode->i_ctime = CURRENT_TIME;
    	oldinode->i_dirt = 1;
    	iput(oldinode);
    	return 0;
    }
    
  • 對應就是刪除硬連結的系統呼叫

    int sys_unlink(const char * name)
    {
    	const char * basename;
    	int namelen;
    	struct m_inode * dir, * inode;
    	struct buffer_head * bh;
    	struct dir_entry * de;
    
    // 如果找不到對應路徑名目錄的 i 節點,則返回出錯碼。
    	if (!(dir = dir_namei(name,&namelen,&basename)))
    		return -ENOENT;
    // 如果最頂端的檔名長度為 0,則說明給出的路徑名最後沒有指定檔名,釋放該目錄 i 節點,
    // 返回出錯碼,退出。
    	if (!namelen) {
    		iput(dir);
    		return -ENOENT;
    	}
    // 如果在該目錄中沒有寫的許可權,則釋放該目錄的 i 節點,返回訪問許可出錯碼,退出。
    	if (!permission(dir,MAY_WRITE)) {
    		iput(dir);
    		return -EPERM;
    	}
    // 如果對應路徑名上最後的檔名的目錄項不存在,則釋放包含該目錄項的高速緩衝區,釋放目錄的 i 節點,返回檔案已經存在出錯碼,退出。否則 dir 是包含要被刪除目錄名的目錄 i 節點,de 是要被刪除目錄的目錄項結構。
    	bh = find_entry(&dir,basename,namelen,&de);
    	if (!bh) {
    		iput(dir);
    		return -ENOENT;
    	}
    // 取該目錄項指明的 i 節點。若出錯則釋放目錄的 i 節點,並釋放含有目錄項的高速緩衝區,
    // 返回出錯號。
    	if (!(inode = iget(dir->i_dev, de->inode))) {
    		iput(dir);
    		brelse(bh);
    		return -ENOENT;
    	}
    // 如果該目錄設定了受限刪除標誌並且使用者不是超級使用者,並且程序的有效使用者 id 不等於被刪除檔名 i 節點的使用者id,並且程序的有效使用者 id 也不等於目錄 i 節點的使用者 id,則沒有許可權刪除該檔名。則釋放該目錄 i 節點和該檔名目錄項的 i 節點,釋放包含該目錄項的緩衝區,返回出錯號。
    	if ((dir->i_mode & S_ISVTX) && !suser() &&
    	    current->euid != inode->i_uid &&
    	    current->euid != dir->i_uid) {
    		iput(dir);
    		iput(inode);
    		brelse(bh);
    		return -EPERM;
    	}
    // 如果該指定檔名是一個目錄,則也不能刪除,釋放該目錄 i 節點和該檔名目錄項的 i 節點,
    // 釋放包含該目錄項的緩衝區,返回出錯號。
    	if (S_ISDIR(inode->i_mode)) {
    		iput(inode);
    		iput(dir);
    		brelse(bh);
    		return -EPERM;
    	}
    // 如果該 i 節點的連線數已經為0,則顯示警告資訊,修正其為 1。
    	if (!inode->i_nlinks) {
    		printk("Deleting nonexistent file (%04x:%d), %d\n",
    			inode->i_dev,inode->i_num,inode->i_nlinks);
    		inode->i_nlinks=1;
    	}
    // 將該檔名的目錄項中的 i 節點號欄位置為 0,表示釋放該目錄項,並設定包含該目錄項的緩衝區已修改標誌,釋放該高速緩衝區。
    	de->inode = 0;
    	bh->b_dirt = 1;
    	brelse(bh);
    // 該 i 節點的連線數減 1,置已修改標誌,更新改變時間為當前時間。最後釋放該 i 節點和目錄的 i 節點,返回0(成功)。
    	inode->i_nlinks--;
    	inode->i_dirt = 1;
    	inode->i_ctime = CURRENT_TIME;
    	iput(inode);
    	iput(dir);
    	return 0;
    }
    
  • sys_close

    負責關閉一個檔案,主要步驟:

    1. 根據檔案描述符,把指標陣列的對應項置空
    2. 如果指向的 file 結構體沒有其他程序在使用,則這個 file 結構體可以重用,但他指向的 i 節點需要寫回到硬碟。
    int sys_close(unsigned int fd)
    {	
    	struct file * filp;
    
        // 若檔案控制代碼值大於程式同時能開啟的檔案數,則返回出錯碼。
    	if (fd >= NR_OPEN)
    		return -EINVAL;
        // 清除 close_on_exec 標記,該標記表示 fork+exec 時關閉該檔案
    	current->close_on_exec &= ~(1<<fd);
        // 若該檔案控制代碼的檔案結構指標為 NULL,報錯
    	if (!(filp = current->filp[fd]))
    		return -EINVAL;
        // 置該檔案控制代碼的檔案結構指標為 NULL
    	current->filp[fd] = NULL;
        // 若在關閉檔案之前,對應檔案結構中的控制代碼引用計數已經為 0,說明核心出錯
    	if (filp->f_count == 0)
    		panic("Close: file count is 0");
        // 否則將對應檔案結構的控制代碼引用計數減 1,如果還不為 0,則返回 0(成功)。
    	if (--filp->f_count)
    		return (0);
        // 若已等於 0,說明該檔案已經沒有控制代碼引用,則釋放該檔案 i 節點,返回0。
    	iput(filp->f_inode);
    	return (0);
    }
    

    跟進 inode 的釋放操作 iput

    void iput(struct m_inode * inode)
    {
    	if (!inode)
    		return;
        // 用程序在使用這個 i 節點則等待
    	wait_on_inode(inode);
    	if (!inode->i_count)
    		panic("iput: trying to free free inode");
        // 如果是管道 i 節點,則喚醒等待該管道的程序,引用次數減 1
    	if (inode->i_pipe) {
    		wake_up(&inode->i_wait);
            // 如果還有引用則返回
    		if (--inode->i_count)
    			return;
            // 對於 pipe 節點,其 i_size 存放著實體記憶體頁地址
    		free_page(inode->i_size);
    		inode->i_count=0;
    		inode->i_dirt=0;
    		inode->i_pipe=0;
    		return;
    	}
        // 如果 i 節點對應的裝置號為 0,則將此節點的引用計數遞減 1,返回。
    	if (!inode->i_dev) {
    		inode->i_count--;
    		return;
    	}
        // 如果是塊裝置檔案的 i 節點,此時邏輯塊欄位 0 中是裝置號,則重新整理該裝置。並等待 i 節點解鎖。
    	if (S_ISBLK(inode->i_mode)) {
    		sync_dev(inode->i_zone[0]);
    		wait_on_inode(inode);
    	}
    repeat:
    	if (inode->i_count>1) {
    		inode->i_count--;
    		return;
    	}
        // 如果該 i 節點的連結數為 0,則釋放該 i 節點的所有邏輯塊,並釋放該 i 節點
    	if (!inode->i_nlinks) {
    		truncate(inode);
    		free_inode(inode);
    		return;
    	}
        // 如果該 i 節點已作過修改,則先更新該 i 節點,並等待該 i 節點解鎖
    	if (inode->i_dirt) {
    		write_inode(inode);	/* we can sleep - so do again */
    		wait_on_inode(inode);
    		goto repeat;
    	}
    	inode->i_count--;
    	return;
    }
    
  • sys_exit

    用於程序退出,實際上呼叫了 do_exit 函式

    int sys_exit(int error_code)
    {
         return do_exit((error_code&0xff)<<8);
    }
    

    do_exit 的流程在程序呼叫中分析過,這裡重點看 free_page_tables 的實現過程。 free_page_tables 的作用是根據指定的線性地址和頁表個數,釋放對應記憶體頁表所指定的記憶體塊,並置表項空閒。其中:

    • 頁目錄位於實體地址 0 開始處,共 1024 項,佔 4k 位元組,每個目錄項指定一個頁表
    • 頁表從實體地址 0x1000 處開始(緊接著目錄空間),每個頁表有 1024 項,也佔 4k 記憶體
    • 每個頁表項對應一頁實體記憶體(4K),目錄項和頁表項的大小均為 4 個位元組

    from 是其實地址,size 是釋放長度

    int free_page_tables(unsigned long from,unsigned long size)
    {
    	unsigned long *pg_table;
    	unsigned long * dir, nr;
    
        // 要釋放的記憶體塊的地址需以 4M 為邊界
    	if (from & 0x3fffff)
    		panic("free_page_tables called with wrong alignment");
    	if (!from)
    		panic("Trying to free up swapper memory space");
        // 計算所佔頁目錄項數(4M 的整數倍),也即所佔頁數
    	size = (size + 0x3fffff) >> 22;
        // 計算起始目錄項,對應的目錄項號=from>>22
        // 因每項佔 4 位元組,並且由於頁目錄是從實體地址 0 開始,因此實際的目錄項指標 = 目錄項號 << 2,也即 from >> 20
        // 與上 0xffc 確保指標範圍有效
    	dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
        // size 現在是需要被釋放記憶體的目錄項數
    	for ( ; size-->0 ; dir++) {
            // 如果該目錄項無效( P 位 = 0),則繼續
    		if (!(1 & *dir))
    			continue;
            // 取目錄項中頁表地址
    		pg_table = (unsigned long *) (0xfffff000 & *dir);
            // 每個頁表有 1024 個頁項
    		for (nr=0 ; nr<1024 ; nr++) {
                // 若該頁表項有效( P 位 = 1),則釋放對應記憶體頁
    			if (1 & *pg_table)
    				free_page(0xfffff000 & *pg_table);
    			*pg_table = 0;// 該頁表項內容清零。
    			pg_table++;// 指向頁表中下一項。
    		}
            // 釋放該頁表所佔記憶體頁面
    		free_page(0xfffff000 & *dir);
    		*dir = 0;
    	}
    	invalidate();	// 重新整理頁變換高速緩衝
    	return 0;
    }