Linux0.12核心讀書筆記
阿新 • • 發佈:2019-01-24
實踐 一.準備工作 1.程式碼下載 http://oldlinux.org/Linux.old/kernel/0.1x/linux-0.12.tar.gz 讀書筆記 第六章 引導啟動程式 Boot 1.PC在電源開啟後,80x86 CPU將進入真實模式,並從地址0xFFFF0開始自動執行程式程式碼,這裡通常是BIOS的地址,它首先執行某些系統檢測,然後在0地址處初始化中斷向量,注意這裡的中斷向量號是BIOS定義的,與作業系統,即CPU指定的向量號是不同的;然後BIOS讀取啟動裝置的第一個扇區的512位元組到地址0x7C00(即07C0:0000)處,並跳轉到此處開始執行。 2.boot/bootsect.S 1)把0x7c00開始的512位元組程式碼,複製到0x90000開始的512位元組處 2)設定軟碟機最大讀取扇區數目,BIOS中斷0x1E的中斷向量值是軟碟機引數表地址,其向量值位於記憶體0x0000:0x78處,該地址處儲存的一個四位元組是一個地址,該地址處的12位元組就是軟碟機引數表,把該地址的12位元組複製到0x9FeF4處,然後把0x1E的中斷向量值指向此地址(以little endian的形式) ------------------------------------------------------ | 512 Byte | 4 * 512 Byte | Stack Size | 12 Byte | ------------------------------------------------------ 0x90000 setup.S 0x9FeF4 0x9FF00 3)使用BIOS中斷0x13從磁碟第2扇區讀取4個扇區到0x90200處,這就是setup模組 4)使用BIOS中斷0x13獲得每磁軌扇區數 5)從磁碟中第5個扇區開始讀取system模組,到地址0x10000,長度是0x30000 6)判斷root_dev是否被指定,root_dev表示根檔案系統的裝置號,它的具體格式可以參考P213,表6-2,如果它沒有被指定,要根據每磁軌扇區數來判斷是1.44M驅動器,還是1.2M驅動器來確定裝置號,裝置號的格式參考P207的註釋 7)跳轉到setup模組的地址處 http://oldlinux.org/Linux.old/docs 從硬碟啟動的問題:boot程式要識別活動分割槽的檔案系統型別,並能夠訪問到核心Image 3.setup.S 1)使用BIOS中斷0x15獲取擴充套件記憶體的大小,並存儲在0x90002開始的2位元組處,所謂擴充套件記憶體是從0x100000(1M)開始的記憶體 2)使用BIOS中斷0x10,獲取螢幕游標位置,並儲存在記憶體0x90000處的2位元組處 3)獲取當前顯示卡的顯示模式 4)獲取第一塊硬碟的硬碟引數,儲存到0x90080處,獲取第二塊硬碟的硬碟引數,儲存到0x90090處 5)判斷是否有第二塊硬碟,如果沒有,把第二塊硬碟引數清零 6)把system模組從0x10000移動到0x00000,即記憶體0地址處,長度為0x80000,注意0x80000超過了真實模式下段長度的最大值,所以這裡是採用每次複製0x8000,即32k的方式 7)載入idt,載入gdt,開啟記憶體的第20位地址線 8)重新程式設計8259A,設定中斷向量號,這裡說明一下對於中斷的理解,外部裝置發生中斷後,通過中斷控制器8259A通知CPU(通過一個訊號),CPU產生中斷,在保護模式下,會呼叫相應的中斷門進行處理。但是IBM PC BIOS把中斷向量號放在了0x08-0x0F,0x70-0x77,詳見P19表2-2,與Intel指定的中斷向量號不同,所以在作業系統中需要重新對中斷控制器的中斷請求號與中斷向量號的對應關係進行程式設計,按照80x86的說明,中斷向量號被設定在0x20-0x2f,詳見P165表5-2 9)開啟CR0的PE位,進入保護模式 10)jmpi 0,8,其中8是段選擇符,0是段中的偏移量,8所指定的段就是system所在的位置0x0 進入setup.S後,bootsect.S所在的0x90000的512位元組就沒用了,setup.S通過BIOS讀取系統引數放置到該地址處,然後把system模組複製到0地址處,最後進入保護模式,並跳轉到0地址。 8259A的程式設計方法 詳見P235 4.head.s 1)重新載入idt和gdt,idt中有256個表項,在後面各個模組初始化的時候,會安裝各自的中斷處理程式;gdt也有256項。在載入完成之後,需要重新載入各個段暫存器,尤其是CS段,需要通過一個jmp指令來載入它,讓它們的描述符使用新的gdt 2)通過向0地址寫資料,然後和0x10000比較的方法,來判斷是否成功打開了A20地址線 3)檢查是否存在協處理器,方法是執行協處理器(80287/80387)初始化命令,然後讀取協處理器的狀態字,如果為0,說明協處理器執行命令正常,否則說明協處理器不存在,需要設定cr0暫存器的EM模擬位,並復位MP協處理器存在位 4)啟動分頁機制,執行這一步會導致從0地址開始5頁,即5*4K範圍內的程式碼段被覆蓋,作為頁目錄和頁表,頁表中填寫的最後一個頁基地址是從0xFFF000,0xFFF000+0x1000(4K)正好等於0x1000000,即16M,也就是說,4個頁表覆蓋了16M的地址範圍。這裡一直有一個疑問,在頁表中的基地址指的是實體地址還是虛擬地址呢? 5)跳轉到main函式 中斷對於堆疊的影響 中斷髮生的時候,如果沒有發生特權級變換,那麼會把eflags,cs,eip,error_code依次入棧。如果發生了特權級變換,那麼需要把ss,esp,elfags,cs,eip,error_code依次入棧,同時使用iretd作為中斷返回的語句。該語句將把棧中內容,存入對應的暫存器中,但是對於eflags有些不同,只有當CPL=0的時候,elfags暫存器中的IOPL才能被改變,只有當CPL>2 ] ; struct { long * a; short b; } stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 }; 按照little-endian的順序,0x10應該是ss段的選擇符,& user_stack [PAGE_SIZE>>2] 則是esp的值,它指向stack陣列的最高位,向低地址擴充套件 ds則被設定為0x10選擇符,即在head.s中定義的gdt中的段描述符 _gdt: .quad 0x0000000000000000 /* NULL descriptor */ .quad 0x00c09a0000000fff /* 16Mb 程式碼段*/ .quad 0x00c0920000000fff /* 16Mb 資料段*/ .quad 0x0000000000000000 /* TEMPORARY - don't use */ .fill 252,8,0 /* space for LDT's and TSS's etc */ 進入main.c之後,呼叫move_to_user_mode,它在system.h中定義 #define move_to_user_mode() \ __asm__ ("movl %%esp,%%eax\n\t" \ "pushl $0x17\n\t" \ //指定ss描述符,0x17指定ldt中的第二個描述符,但是ldt是在哪裡載入的呢? "pushl %%eax\n\t" \ //esp入棧 "pushfl\n\t" \ //eflags入棧 "pushl $0x0f\n\t" \ //cs 入棧,這裡同樣是載入ldt中描述符,而且是第一個描述符 "pushl $1f\n\t" \ //eip入棧,$1f表示前向標號1 "iret\n" \ //經過iret後,從特權級0轉換到任務0的特權級3,同時跳轉到標號1處 "1:\tmovl $0x17,%%eax\n\t" \ "mov %%ax,%%ds\n\t" \ "mov %%ax,%%es\n\t" \ "mov %%ax,%%fs\n\t" \ "mov %%ax,%%gs" \ :::"ax") 在sched_init中,呼叫了lldt(0),所以使用0x17作為ss的選擇符是有道理的,在sched.h中有如下的定義: /* * Entry into gdt where to find first TSS. 0-nul, 1-cs, 2-ds, 3-syscall * 4-TSS0, 5-LDT0, 6-TSS1 etc ... */ #define FIRST_TSS_ENTRY 4 #define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1) #define _TSS(n) ((((unsigned long) n)get_free_page(swap.c)//從0x100000即1M開始的15M記憶體,是所謂的主記憶體,由作業系統進行管理,它把這些記憶體分成頁,並使用一個數組mem_map來管理,如果某頁使用了,那麼設定陣列中對應序號為1,如果沒有使用設定為0 //get_free_page的邏輯是根據mem_map找到一個沒有使用過的頁,然後計算出它對應的實體地址(LOW_MEM+4K*index),最後把這4K的地址清零,如果沒有找到一個空閒的頁,執行swap_out操作 ----->swap_out()//該函式用來從記憶體中換出一個空閒頁,線上性地址空間中,低64M記憶體是程式碼和資料等存放的地址,所以虛擬記憶體是從64M開始的,也就是說FIRST_VM_PAGE=(64M)>>12,線性地址空間範圍是4G,除了64M以外,還有可用的虛擬頁數為1024*1024-FIRST_VM_PAGE;pg_dir是在head.s中定義的標號,它是0地址,在0地址中存放的是頁目錄表 //從FIRST_VM_PAGE中可以算出它對應頁目錄中的第幾項,從這項開始在pg_dir中查詢頁目錄項是否有P位置位的,如果有,那麼從這個頁目錄項的高20位中可以得到一個實體地址,它指向一個頁表,該頁表一共有1024項,每項指向一個4K的頁面,對這個頁表的每一項呼叫try_to_swap_out函式,嘗試把它換出記憶體 ----->try_to_swap_out()//該函式檢查當前頁表項的P位是否置位,檢查mem_map中對應的位元組是否為1,如果都確認,那麼就可以交換了,首先得到一個交換號,然後把頁面資料寫到交換空間中。 //write_swap_page這個函式用於寫交換頁,它使用到了SWAP_DEV裝置號,從前面的分析我們知道它實際上是一個硬碟裝置。 /* 其實分頁管理把4G的線性地址分成了1024*1024個頁,每個頁都有一個頁表項對應,每1024個頁表項使用一個頁目錄項來對應,也就是說只要指定了頁目錄項,和對應的頁表中的專案,就確定了線性地址空間中的頁的線性地址,而儲存在頁表項中的內容,要麼是這個虛擬頁對應的實體地址,要麼是交換頁號。而如果要把所有的頁目錄表和頁表項都放在記憶體中的話需要佔用1024*1024*4 + 1024*4的大小 */ ------------------------------------------------------------- 有個疑問,CPU是如何區分線性地址和實體地址的,在訪問線性地址的時候,會發生地址轉換,在轉換的過程中,使用的是實體地址,CPU怎麼知道此時用的就是實體地址,而不是線性地址呢? 在核心態,在構建頁目錄和頁表的時候,線性地址與實體地址是一一對應的,也就是說線性地址等於實體地址 ------------------------------------------------------------- copy_process中還儲存把用於任務切換的暫存器的值寫入到task的tss中,同時指定了任務在核心態執行的時候ss和esp指向task結構所在頁的頂端 至此fork過程算是結束了 從move_to_user_mode函式開始,該函式會從核心態,即特權級0,切換到特權級為3的任務0,任務0通過靜態變數init_task說明了ldt和tss。 接著int 80系統呼叫,使得後面的入棧出棧操作都是在任務0的核心態堆疊進行的,並不影響任務0的使用者態堆疊,在fork函式中複製了任務0的task_struct,並把當前的暫存器儲存在新建立的task_struct的tss中,其中eax儲存的值為0,這正是fork之後,子程序返回值為0的關鍵。在建立新的task_struct之後,int 80把新建立的pid作為返回值,返回到sys_call之後會進行任務排程,如果它導致任務切換至新建立的任務1執行,那麼CPU將會把TSS中儲存的各個暫存器中的值恢復到相應暫存器中並載入相應的ldt,此時eax作為返回值被返回給main函式,這時的返回值就成為0,而不同於其父程序任務0的值 ------------------------------------------------------------- 為什麼任務塊線上性地址空間的地址都是以64M逐漸增加的? ------------------------------------------------------------- ldt,gdt中存放的基地址都是線性地址 memory.c中的copy_page_tables函式,其主要作用是複製頁目錄項及其頁表,其中比較費解的是如下的幾句: from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */ to_dir = (unsigned long *) ((to>>20) & 0xffc); size = ((unsigned) (size+0x3fffff)) >> 22; 其中from和to都是線性地址,計算from_dir的這一句,如果寫成如下的這句可能更好理解: from_dir = (unsigned long *) ((from>>22)>22得到的是from線性地址對應的是第幾個頁目錄項,然後左移2位,則是計算這個目錄項的地址,因為每個目錄項都佔用4位元組,所以from_dir得到的是from線性地址對應的頁目錄項的地址 ------------------------------------------------------------- 從這裡看所有的任務塊的頁目錄項顯然也是存放在0地址開始的4K的,那麼就有一個問題,難道每個任務的線性地址空間就只有64M麼? ------------------------------------------------------------- ------------------------------------------------------------- 一個程序的頁目錄和頁表是否可能缺頁 完全可能,此時需要第一次載入資料到物理頁中 ------------------------------------------------------------- mm目錄中給出了兩個層次的對映,一個是線性地址到實體地址的對映,一個是線性地址到交換頁的對映。只要給定了一個線性地址,那麼就確定了其頁目錄項的地址,如果該地址沒有內容,那麼就會發生缺頁中斷,如果有內容,那麼它就指向了一個頁表,通過線性地址的中間10bit又可以得到一個頁表項的地址,該地址中的內容可能有值,也可能沒有值 交換頁的機制是分配一個頁作為對映點陣圖,把其中的每一個bit用來表示一個交換頁4k是否使用,把這個bit的序號作為交換頁的序號儲存在頁表項中,並且序號為0,即對映點陣圖的序號 也就是說一個線性地址確定了頁表項的內容之後,該內容要麼是一個物理頁地址,要麼是一個交換頁的序號 signal.c fork.c檔案中有一個verify_area函式,它的作用是檢查一段記憶體區域是否可寫,因為在80386中,特權級0在寫特權級3的頁面的時候,不會產生頁防寫,這導致使用者程序防寫失效,但是在80486以後,intel加上了特權級0在寫特權級3的頁面的時候,也會產生頁防寫的功能。verify_area函式就是實現這個功能的,它的引數是程序中的記憶體地址,及長度。 這裡特別需要說明使用者程序中的記憶體地址,實際上就是所謂的邏輯地址,由於可執行檔案在編譯的時候,其中的變數,函式地址都是相對地址,這樣可以在載入可執行檔案的時候,把它載入到任意地址,這個所謂的相對地址就是使用者程序的邏輯地址,在核心中,需要根據程序所在的線性空間地址,計算出這個邏輯地址的線性地址,繼而算出它所在頁首的線性地址。 任務結構中blocked欄位,表示訊號阻塞點陣圖,如果1置位,表示對應的訊號將被阻塞,如果0置位表示對應的訊號不被阻塞,其中SIGKILL和SIGSTOP是不能被阻塞的,也就是說它們在blocked欄位中相應位中為0 下面函式中引數addr是使用者程序的地址,我們可以看到在彙編程式碼中使用的是fs:offset,段加偏移量的方式,這樣就訪問到邏輯地址的線性地址。 extern inline void put_fs_long(unsigned long val,unsigned long * addr) { __asm__ ("movl %0,%%fs:%1"::"r" (val),"m" (*addr)); } 為什麼使用fs段暫存器呢,因為在int80系統呼叫中,sys_call.s檔案中,把ds,es指向了核心的資料段,而fs指向了當前程序ldt的資料段,所以這裡可以使用fs來引用使用者空間資料段。 好不容易看懂了signal.c中的do_signal函式, 1.呼叫與被呼叫 首先該函式是在sys_call.s中的ret_from_sys_call呼叫的,該函式是所有中斷呼叫退出的時候呼叫的函式,其中也包括系統呼叫system_call,它是通過入棧的eax與其他呼叫區分的,system_call函式在根據呼叫號呼叫了相應的c函式之後,把返回的結果eax入棧,而其他的中斷呼叫則是直接把-1入棧。 2.被中斷的系統呼叫重新啟動的問題 ret_from_sys_call函式會對signal進行處理,即呼叫do_signal函式,但是system_call系統呼叫呼叫c函式的過程中,有可能導致所在程序sleep,在收到非阻塞的訊號喚醒該程序的時候,這個系統呼叫就要告訴do_signal是否要重新啟動這個系統呼叫了,因為它被中斷了,do_signal中的程式碼如下; if ((orig_eax != -1) && //就是通過這個eax來區分是否系統呼叫int80 ((eax == -ERESTARTSYS) || (eax == -ERESTARTNOINTR))) {//這表示系統呼叫的處理函式要求重新啟動系統呼叫 if ((eax == -ERESTARTSYS) && ((sa->sa_flags & SA_INTERRUPT) || signr < SIGCONT || signr > SIGTTOU))//這種情況下,不用重新啟動系統呼叫 *(&eax) = -EINTR; else {//這是重新啟動系統呼叫的程式碼,eip會使用者態的ip,它減掉2,表示eip移動到指向系統呼叫的程式碼 *(&eax) = orig_eax; *(&eip) = old_eip -= 2; } } 3.對呼叫訊號控制代碼的預處理 首先需要明白兩件事情 1)由於do_signal是在中斷呼叫中呼叫的,所以它是在核心態執行的,它用的棧是使用者程序的核心棧,而不是使用者程序的使用者棧 2)所謂的呼叫訊號控制代碼,是通過設定核心棧上的eip來實現的,因為在中斷呼叫結束後,棧中的值會被彈回到使用者程序的相應暫存器中,所以只需要設定棧中相應暫存器的值就可以實現對訊號控制代碼的呼叫 下面來說明訊號控制代碼的問題,使用者實際上是通過Libc庫中的函式來呼叫int 80系統呼叫,進而實現各種功能的,對於設定訊號控制代碼來說,程式碼如下: void (*signal)(int sig,__sighandler_t func)(int) { void (*res)(); register int __fooebx __asm ("bx") = sig; __asm__("int $80":"=a" (res): "0" (__NR_signal), "r" (__fooebx), "c" (func), "d" ((long)__sig_restore)); return res; } 從該函式中,我們可以看到eax暫存器儲存了功能號__NR_signal,ebx儲存了訊號號,ecx儲存了訊號控制代碼,edx儲存了__sig_restore,這是一個用來恢復暫存器的函式,後面會詳細說明。 在sys_call.s中,我們可以看到如下的程式碼: _system_call: push %ds push %es push %fs pushl %eax # save the orig_eax pushl %edx pushl %ecx # push %ebx,%ecx,%edx as parameters pushl %ebx # to the system call movl $0x10,%edx # set up ds,es to kernel space mov %dx,%ds mov %dx,%es movl $0x17,%edx # fs points to local data space mov %dx,%fs cmpl _NR_syscalls,%eax jae bad_sys_call call _sys_call_table(,%eax,4) pushl %eax 這裡是根據功能號,呼叫相應的c函式的程式碼,在呼叫c函式之前,把eax,edx(__sig_restore),ecx(handler),ebx(signr)都入棧了,而signal的c函式原型是 int sys_signal(int signum, long handler, long restorer) 根據c函式的入棧規則,sys_signal函式可以取到它的引數 訊號處理後暫存器的恢復 我們先來說說訊號處理在整個系統呼叫中的位置 1.進入系統呼叫 ----- 2.系統呼叫呼叫c函式處理 \____這裡都是處於核心態 3.do_signal處理訊號 -----/ 4.呼叫signal_handler -----\____這裡都是處於使用者態 5.呼叫sig_restore -----/ 6.返回到使用者程序呼叫系統呼叫之後的一條指令 從程式碼入手看看這些呼叫是如何跳轉的 *(&eip) = sa_handler; //這裡把要呼叫的訊號控制代碼複製給棧引數eip,在中斷呼叫結束的時候,這個棧引數會彈出給eip暫存器,實現從核心態到使用者態的轉換,從而進入使用者程序的訊號控制代碼處理函式 longs = (sa->sa_flags & SA_NOMASK)?7:8; *(&esp) -= longs;//這裡移動使用者態棧的esp,7個或者8個4位元組 verify_area(esp,longs*4); tmp_esp=esp; put_fs_long((long) sa->sa_restorer,tmp_esp++); put_fs_long(signr,tmp_esp++); if (!(sa->sa_flags & SA_NOMASK)) put_fs_long(current->blocked,tmp_esp++); put_fs_long(eax,tmp_esp++); put_fs_long(ecx,tmp_esp++); put_fs_long(edx,tmp_esp++); put_fs_long(eflags,tmp_esp++); put_fs_long(old_eip,tmp_esp++); current->blocked |= sa->sa_mask; return(0); /* Continue, execute handler */ 經過上述調整之後,使用者態棧的狀態如下: ------------- high address | old_eip | | eflags | | edx | | ecx | | eax | | blocked |(可能沒有這個四位元組) | signr | low address | restorer | blocked = old_mask; return -EINTR; } /* we're not restarting. do the work */ *(&restart) = 1; *(&old_mask) = current->blocked; current->blocked = set; (void) sys_pause(); /* return after a signal arrives */ return -ERESTARTNOINTR; /* handle the signal, and come back */ } 該函式實際上是包在Libc的庫函式中,通過int80系統呼叫呼叫的,其呼叫程式碼如下: int sigsuspend(sigset_t *sigmask) { int res; register int __fooebx __asm__ ("bx") = 0; __asm__("int $0x80" :"=a" (res) :"0" (__NR_sigsuspend), "r" (__fooebx), "c" (0), "d" (*sigmask) :"bx","cx"); if (res >= 0) return res; errno = -res; return -1; } 前面說過system_call首先把edx,ecx,ebx入棧作為呼叫的c函式sys_sigsuspend的引數,顯然這裡的暫存器edx對應set,ecx對應old_mask,ebx對應restart,在程序第一次呼叫sigsuspend的時候,進入sys_sigsuspend,因為restart為0,所以不會進入if語句,而會到後面的程式碼,restart棧變數被賦值為1,old_mask棧變數的值被賦值為當前程序的blocked,當前執行緒的blocked被賦值為set,然後呼叫sys_pause,該函式將導致系統重新排程,當前程序就此被掛起,噹噹前程序收到一個訊號的時候,它被喚醒,並從return語句開始往後執行。由於該c函式返回了-ERESTARTNOINTR,所以do_signal函式會重新啟動該系統呼叫,也就是說,在訊號處理函式執行完成後,返回到使用者態的時候,sigsuspend函式中的嵌入式彙編程式碼會被再次執行 下面我們再來看引數的問題: 從system_call函式看起,函式呼叫及引數的情況如下: 1.system_call pushl %edx pushl %ecx # push %ebx,%ecx,%edx as parameters pushl %ebx 2.呼叫_sys_call_table call _sys_call_table(,%eax,4) // sys_suspend函式被呼叫,棧變數被修改 pushl %eax 3.呼叫do_signal 4. popl %eax popl %ebx popl %ecx popl %edx 執行這段程式碼後,棧上被修改的值就被彈入到相應的暫存器中 5.系統呼叫被重新啟動,即第二次執行sigsuspend中的嵌入式彙編程式碼 此時的ebx,ecx,edx已經不是開始原來的初值了,而是已經修改過的值了,在第二次進入到suspend系統呼叫中的時候,發現是restart,會立即返回-EINTR,在do_signal函式中也因為這個返回值不會再次修改eip的值而導致重新啟動,最終該程序因為收到一個訊號而被喚醒。 signal與sigaction的區別 兩個函式的定義如下: void (*signal(int _sig, void (*_func)(int)))(int); 這個函式定義看起來還是挺複雜的,其實複雜的地方在於當把一個函式指標作為一個函式的返回值的時候,同時又要把這個函式指標的原型表現出來。 使用如下的定義可以簡化該定義: typedef void (*sigfunc)(int);//這裡顯然定義了一種函式指標型別 sigfunc signal(int _sig,sigfunc); struct sigaction { void (*sa_handler)(int); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }; int sigaction(int sig, struct sigaction *act, struct sigaction *oldact); 我們先來看看跟signal相關的一些過程 1.中斷髮生 2.ret_from_sys_call函式被呼叫 3.取當前程序的signal和block,判斷可以進行處理的signal,block是訊號遮蔽mask,其中位設定為1表示該訊號被遮蔽,設定為0表示該訊號可以進行處理 4.把當前要處理的signal,在當前程序的signal點陣圖中復位 5.呼叫do_signal函式,其中會把處理這個signal的handler儲存起來,然後把當前程序的這個signal的handler恢復預設,設定為NULL 6.進入使用者態,呼叫signal的handler,在handler的內部一般會把自己重新設定給這個signal作為signal handler 這裡可能出現的問題是第6步,因為在進入到使用者態以後,該函式可能在把自己重新設定為signal handler的之前,又發生了同樣的signal訊號,此時實際上發生了訊號處理的巢狀,這時對signal訊號呼叫它的訊號處理handler,將是預設的操作,而不是使用者定義的,由此導致訊號的丟失。 那麼sigaction函式是如何避免這種情況的發生的呢? 在sigaction結構中有一個sa_mask欄位,在這個欄位中可以設定訊號遮蔽位,注意這個結構在程序中是每個訊號都有一個的,在do_signal函式中,預設情況下會把自己的訊號加入到這裡,同時會把這個sa_mask加入到程序的blocked欄位中,這樣一來,在第6步進入到使用者態呼叫處理handler的時候,又發生了同樣的訊號,那麼它雖然被設定到程序的signal上,但是將會被blocked,也就是被阻塞發生,同時,在當前的handler處理結束之後,sa_restorer將會把程序原來的blocked,也就是沒有合併當前訊號的sa_mask的blocked欄位重新恢復到程序中,這樣程序將在下次呼叫的時候發現又發生了signal訊號,從而避免了該訊號的丟失。 塊裝置 1.塊裝置的讀寫是靠中斷驅動的,而不是在單個程序中進行的 2.塊裝置的幾種情況 1)正常讀寫 2)發生異常 3)讀寫錯誤 4)超時 3.塊裝置的reset 4.扇區號到磁頭號,柱面號,當前磁軌扇區號的轉換 軟盤驅動程式 1.基本流程 1)觸發方式與硬碟是相同的,即使用kernel/blk_drv/ll_rw_blk.c檔案中的ll_rw_page和ll_rw_block函式來啟動軟盤裝置的請求處理 2)軟盤的讀寫,首先需要啟動馬達,然後尋道,然後啟動DMA進行讀寫,讀寫完成之後,DMA裝置會通過中斷控制器向CPU傳送中斷請求。這裡的問題是啟動馬達和尋道操作都是非同步進行的,驅動程式中採用了兩種方式來處理,對於啟動馬達操作,是啟動了一個定時器,在定時器超時之後,查詢軟盤控制器的狀態;對於尋道操作,則是設定一箇中斷函式,軟盤驅動器在執行完成尋道操作之後,會產生一箇中斷,CPU根據中斷號會呼叫中斷函式 3)幾種情況的處理 a.正常情況的處理 正常情況下,啟動馬達,並啟動定時器,定時器超時後,判斷是否需要尋道,如果不需要尋道,則馬上啟動DMA,如果需要尋道,則設定尋道完成後的中斷函式,該中斷函式檢查控制器狀態,並啟動DMA開始讀寫資料;DMA傳輸資料完成之後,會發生中斷,中斷函式將檢查驅動器狀態,並做出相應處理。 b.錯誤狀態 在通過result檢查驅動器狀態,並發現狀態錯誤的情況下,會給錯誤計數加一,在錯誤計數小於MAX_ERRORS/2的時候,將進入recalibrate狀態進行校準,如果錯誤技術大於MAX_ERRORS/2,那麼將進入reset狀態進行驅動器重置。在錯誤次數超過MAX_ERRORS的時候,將會終止當前請求,啟動下一個請求 c.recalibrate的處理 重新校正命令是軟盤驅動器傳送的定位命令,它將導致磁頭移動到0磁軌,由於這個過程較長,所以它是一個非同步操作,第一步是傳送校準命令,第二步是在校準完成後,傳送中斷請求。 d.reset的處理 reset處理是要求軟盤驅動器重新設定驅動器的各項引數 e.選定驅動器 由於支援4個軟盤驅動器,所以有一個選定的操作,在啟動某個驅動器馬達之後,將設定selected標誌,表示已經有驅動器在處理了,在達到錯誤計數最大數目,讀寫過程中防寫,或者DMA讀寫完成的時候,將呼叫deselect操作,該操作將把selected標誌復位,並喚醒那些wait_on_floppy_select等待軟盤驅動器選定的程序。這裡又涉及另外一個函式floppy_change f. floppy_change 該函式呼叫了floppy_on函式,該函式通過設定DOR,啟動指定的驅動器的馬達,並關閉其他的驅動器,同時啟動了一個計時器,在啟動馬達,即計時器沒有超時的時候,當前程序休眠,在計時器超時之後,喚醒程序,如果程序發現當前已經有驅動器被selected,那麼就sleep在wait_on_floppy_select上,直到deselect操作把該程序喚醒 g.關於馬達的啟動和關閉 馬達的啟動和關閉是通過設定DOR暫存器中的高四位來實現的,ticks_to_floppy_on函式設定了moff_timer和mon_timer,分別用於關閉和啟動定時器超時,在moff_timer超時的時候,將關閉對應的馬達,在mon_timer超時的時候,將喚醒等待啟動馬達完成的程序。 4)軟盤驅動的幾種命令 軟盤驅動器主要用到三個暫存器,分別是主狀態暫存器,資料暫存器和數字輸出暫存器(DOR),其中DOR控制馬達的啟動和關閉,驅動器的選擇,是否允許使用DMA等操作。主狀態暫存器和資料暫存器是配對使用的,通過資料暫存器傳送命令和引數資料,通過主狀態暫存器查詢命令的執行結果,使用到的命令有:recalibrate命令,尋道命令,讀資料命令,寫資料命令,檢測中斷狀態命令,設定驅動器引數命令 大致分為三種命令,一種是查詢狀態命令,如查詢主狀態暫存器,檢測中斷狀態,一種是阻塞式命令,即傳送命令後,即時查詢驅動器狀態暫存器,就可以得到命令結果的,例如:讀寫命令,一種是非同步式的命令,例如,啟動關閉馬達,校準,重置,尋道,這種非同步命令,一般通過中斷或者定時器的方式,並通過查詢狀態判斷是否正確完成了 由於軟盤驅動程式較為複雜,所以從這裡我們也可以看到一般塊裝置驅動程式的編寫方法,首先要掌握暫存器的使用,其次要了解驅動程式讀寫資料的一般步驟,最後,要對意外情況進行處理,例如,非同步操作,超時,重新校準,重置等 字元裝置 tty實際上是對一組成對的輸入輸出裝置的抽象,tty既包含輸入,也包含輸出,所以這個概念可以應用在任何慢速的輸入輸出邏輯中 uart uart晶片的這個結構圖很有意思,它包括了讀寫訊號選通引腳,8根資料引腳,復位引腳,中斷引腳,中斷遮蔽引腳,提供時鐘的晶振引腳,很有代表性哦 波特率是一種描述調製訊號能力的單位,它表示每秒調製訊號的個數,位元率表示每秒傳輸資料的bit數,在兩相調製下,二者是相等的,即碼元為0或者1,即bit 只要是裝置,都有一個等待的程序佇列,在裝置忙的時候需要休眠,在裝置空閒的時候需要喚醒 fs inode_table是對儲存在裝置中的檔案系統的inode的快取,inode結構中的i_count表示系統對這個快取節點的引用,當這個值為0的時候,表示inode_table中的這個inode節點無效,而i_nlinks是檔案系統中目錄項對inode的引用次數,當這個值為0的時候,表示在檔案系統中這個inode節點可以刪除了 inode.c中對外的介面包括:invalidate_inodes,sync_inodes,bmap,create_block,iput,get_empty_inode,get_pipe_inode,iget.實際上,inode通過在記憶體中快取inode節點來達到快速訪問inode的目的,inode_table即是這個緩衝區。所以,這就涉及緩衝區與裝置的同步問題,在提供上述介面的時候,首先是針對緩衝區的操作,如果發現緩衝區的inode與裝置的inode不同步,則需要同步之,這可能就涉及裝置操作. _bmap是其中很重要的一個函式,它根據資料序號,返回某個inode節點資料塊的邏輯塊號。這裡就涉及兩個問題,一個是可能需要裝置操作,如果這個inode節點的資料序號對應的邏輯塊在裝置上還沒有分配,或者甚至用來索引資料的資料塊都沒有分配,那麼就需要裝置分配資料塊,同時當前程序必須等待這個過程的完成而進入sleep狀態,另外一個是inode包含的資料塊序號與邏輯塊號的對應問題 inode節點中的i_dev表示inode節點所在的裝置,而字元裝置和塊裝置使用inode節點的i_zone[0]來表示這個inode節點所代表的裝置號 在超級塊或者邏輯塊dirty之後,什麼時候同步資料,在哪個程序同步資料 bitmap.c 超級塊的資料欄位 s_ninods 表示inode的塊數 s_nzones 表示邏輯塊的塊數,包括超級塊,inode對映塊和資料對映塊 s_firstdatazone 表示第一個資料塊對應的盤塊號,這裡涉及兩個序號空間,一個是資料塊序號空間,一個是盤塊號空間,資料塊是在inode塊之後的,它是從0開始的,同時序號0又是不使用的序號,盤塊也是從0開始的,0對應引導塊,後面依次是超級塊,inode對映塊,資料塊對映塊,inode塊,資料塊,這兩個序號空間之間的對應關係是序號為1的資料塊,對應序號為s_firstdatazone的盤塊,那麼序號為nr的資料塊,對應nr+s_firstdatazone-1的盤塊號 之所以存在這樣一個轉換是因為,某個資料塊的序號就是該資料塊在資料塊對映塊中的位移,通常情況下,首先可以得到這個位移,也就知道了資料塊的序號,通過轉換就可以得到它對應的盤塊號,繼而可以讀寫盤塊資料 另外一個序號空間就是inode序號,同樣的,inode序號也是從0開始的,序號0不使用,inode序號就是該inode在inode對映塊中的位移,得到這個位移,就得到了inode的序號,由於每個inode節點只有32位元組,所以通過類似上面的演算法,可以算出該inode節點所在的盤塊號,繼而讀寫該塊資料 還有一個序號空間就是inode節點中的資料序號,這裡實際上是一個對映表,inode節點中給出了資料序號0到6,對應的盤塊號,同時給出了一級索引塊的盤塊號,和二級索引塊的盤塊號 分配一個數據塊的過程: 1.超級塊的s_zmap欄位中每一位表示對應序號的資料塊是否使用,通過這個欄位得到一個沒有使用的資料塊的盤塊號 2.從buffer的hash表中獲取一個空閒的buffer塊 3.把這個空閒塊的b_blocknr欄位設定為得到的資料塊序號,即建立buffer緩衝與裝置上資料的盤塊號的對映 分配一個inode的過程: 一般都是在得知一個inode的序號之後,要求一個inode節點 1.從inode_table中查詢是否有對應裝置和對應序號的inode,如果有就把該節點的引用數加一 2.如果沒有就在inode_table中,找一個空閒節點,並根據inode的序號讀取裝置上該序號的inode內容到空閒節點上。 buffer.c 從breada函式可以看出,buffer_head結構中的b_count欄位用來表示引用次數,在提前預讀,而不是使用該buffer的情況下,這個次數是要恢復回去的,而b_uptodate用來表示該欄位是否與裝置是相同的,也就是說在b_count為0的情況下,b_uptodate仍然可能是1 另外一點,在buffer初始化的時候,在高速緩衝中分配buffer,buffer的數目可能小於總的資料塊個數 namei.c 基本概念 檔案方式字 在inode中有一個i_mode的欄位,它表示檔案的存取許可權,檔案型別和set-user-id,set-group-id 有效使用者ID,有效組ID 這兩個概念首先是程序裡的概念,即是程序的有效使用者ID,程序的有效組ID。在執行一個程式檔案的時候,一般情況下,有效使用者ID就是實際使用者ID,即當前登入的使用者ID,有效組ID就是當前使用者所屬的組ID,但是如果該檔案的檔案方式字中set-user-id置位,那麼程序的有效使用者ID就會設定為檔案的宿主ID,同樣,如果set-group-id置位,那麼程序的有效組ID就會設定為檔案宿主所在組的ID。 關於namei.c中find_entry中一段程式碼的解釋: if (namelen==2 && get_fs_byte(name)=='.' && get_fs_byte(name+1)=='.') { /* '..' in a pseudo-root results in a faked '.' (just change namelen) */ if ((*dir) == current->root) namelen=1; else if ((*dir)->i_num == ROOT_INO) { ---------------------------------------------------------begin主要是這一段 /* '..' over a mount-point results in 'dir' being exchanged for the mounted directory-inode. NOTE! We set mounted, so that we can iput the new dir */ sb=get_super((*dir)->i_dev); if (sb->s_imount) { iput(*dir); (*dir)=sb->s_imount; (*dir)->i_count++; } }-------------------------------------------------------------------------------------------------end } find_entry的函式原型如下: static struct buffer_head * find_entry(struct m_inode ** dir, const char * name, int namelen, struct dir_entry ** res_dir) 它的具體功能是在dir這個node上查詢名字為name的direntry,上面擷取的程式碼是為了處理一種特殊情況,當前所在目錄是mount過的一個檔案系統,而dir指向這個檔案系統的根節點,而name為字串"..",我們先來看看mount一個檔案系統的過程 super.c檔案的sys_mount函式,其過程如下: 從裝置檔案路徑得到裝置檔案對應的inode,從這個inode的i_zone[0]得到裝置號,從這個裝置號中讀取檔案系統的超級塊,從要mount到的路徑得到對應的inode,把超級塊的s_imount指標指向這個inode 從這裡我們可以看到宿主檔案系統中的inode與新mount上的檔案系統是對應起來的,如果要取mount上的檔案系統的根目錄的上一級,那麼就是取宿主檔案系統中的inode的上一級,恰好在建立目錄inode的時候,它必然有兩個entry,一個是.,一個是..,所以上面的程式碼使用超級塊指向的宿主檔案系統中的inode替換原來的inode是沒問題的。 實際上inode.c中的iget函式中的如下的程式碼和上面是一個意思: if (inode->i_mount) { int i; for (i = 0 ; i= NR_SUPER) { printk("Mounted inode hasn't got sb\n"); if (empty) iput(empty); return inode; } iput(inode); dev = super_block[i].s_dev; nr = ROOT_INO; inode = inode_table; continue; } follow_link函式 首先說明一下軟連線和硬連線的區別,軟連線是這樣一個inode,它的資料內容實際上是一個字串表示的路徑,所以它可以是跨檔案系統的;硬連線實際上是一個目錄中的entry,該entry指定了一個inode和一個名字,名字即是硬連線的名字,inode即是它所指向的檔案,該檔案的nlink連線數因此被加一,由於指定了一個inode的序號,所以硬連線必須是指向同一個檔案系統內部的。總的來說,軟連線是一個inode,而硬連線只是一個entry follow_link函式就是從inode中找到資料中的字串路徑,並根據這個路徑找到相應inode的函式,第一步找到資料中的路徑是好理解的,第二部根據路徑找相應的inode就有點說法,一般來說,根據路徑查詢inode是通過namei函式進行的,但是namei的引數路徑,一般都是使用者空間的資料,而這裡得到的路徑是核心空間的,在follow_link函式中通過修改fs來達到目的 get_dir函式是從一個基準目錄的inode節點和一個給定的path,得到這個給定path中目錄部分的inode節點 dir_namei函式呼叫get_dir函式,返回給定path中目錄部分的inode,和path中除了目錄部分剩餘的字串 _namei函式呼叫dir_name函式,得到給定path的inode,如果有剩餘的字串,則在這個inode的entry中查詢是否有對應的entry,得到entry後,根據entry中的inode序號得到inode節點 umask基礎知識 umask是建立檔案的時候,賦予檔案的預設許可權。對於檔案來說,系統不允許預設執行許可權,必須使用chmod修改,所以它的最大許可權只能是6,即只有讀寫許可權,而對於目錄,則可以賦予執行許可權,umask的作用是設定在建立檔案的時候,遮蔽掉的許可權,例如umask設定為002,表示把other的寫許可權遮蔽,那麼檔案對應的許可權就是664,對於目錄則是775.