🚀 前言
? ? 書接第十四章:linux0.11內核源碼修仙傳第十四章——進程調度之fork函數,在這一節博客中已經通過fork進程創建了一個新的進程1,并且可以被調度,接下來接著主線繼續走下去。希望各位給個三連,拜托啦,這對我真的很重要!!!
目錄
- 🚀 前言
- 🏆硬盤基本信息的賦值
- 🏆硬盤分區表的設置
- 📃硬盤分區表(Disk Partition Table,DPT)
- 📃代碼實現
- 🏆加載根文件系統
- 📃mount_root 整體解讀
- 📃內存中用于文件系統的數據結構
- 文件信息初始化
- 超級塊初始化
- inode信息讀取
- 記錄位圖信息
- 記錄inode位圖信息
- 🎯總結
- 📖參考資料
🏆硬盤基本信息的賦值
? ? 好久沒回顧 main
函數了,來回顧一下:
void main(void)
{···mem_init(main_memory_start,memory_end);trap_init();blk_dev_init();chr_dev_init();tty_init();time_init();sched_init();buffer_init(buffer_memory_end);hd_init();floppy_init();sti();move_to_user_mode();if (!fork()) { /* we count on this going ok */init();}for(;;) pause();
}
? ? 這里面前面的一堆初始化已經看完了,fork函數也運行了,成功創建了進程1。進程1會返回0,。現在壓力來到了init
函數:
void init(void)
{int pid,i;setup((void *) &drive_info);(void) open("/dev/tty0",O_RDWR,0);(void) dup(0);(void) dup(0);printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,NR_BUFFERS*BLOCK_SIZE);printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);if (!(pid=fork())) {close(0);if (open("/etc/rc",O_RDONLY,0))_exit(1);execve("/bin/sh",argv_rc,envp_rc);_exit(2);}if (pid>0)while (pid != wait(&i))/* nothing */;while (1) {if ((pid=fork())<0) {printf("Fork failed in init\r\n");continue;}if (!pid) {close(0);close(1);close(2);setsid();(void) open("/dev/tty0",O_RDWR,0);(void) dup(0);(void) dup(0);_exit(execve("/bin/sh",argv,envp));}while (1)if (pid == wait(&i))break;printf("\n\rchild %d died with code %04x\n\r",pid,i);sync();}_exit(0); /* NOTE! _exit, not exit() */
}
? ? ok,fine,里面內容很多,沒關系,一點點來,這一篇博文來看第一行 setup
函數的內容。
struct drive_info { char dummy[32]; } drive_info;void init(void)
{setup((void *) &drive_info);···
}
? ? 先來看看傳入的參數:drive_info
。這個變量是來自內存 0x90080 的數據,設置是在main函數的最開始。詳情可以看博客:linux0.11內核源碼修仙傳第二章——setup.s,內存里的存放位置如下所示:
? ? 接下來來看setup
函數:
static inline _syscall1(int,setup,void *,BIOS)
? ? 好的,這又是一個系統調用,返回類型是int
,有一個參數是 void *
類型的BIOS。有關于系統調用可以參考這篇博客:linux0.11內核源碼修仙傳第十四章——進程調度之fork函數。其實直白一點,可以直接去系統調用的sys_call_table
里面找對應的函數,這里講結論,它會直接調用 sys_setup
函數:
int sys_setup(void * BIOS)
{···hd_info[0].cyl = *(unsigned short *) BIOS; // 總柱面數hd_info[0].head = *(unsigned char *) (2+BIOS); // 磁頭數hd_info[0].wpcom = *(unsigned short *) (5+BIOS); // 寫入補償hd_info[0].ctl = *(unsigned char *) (8+BIOS); // 控制字節hd_info[0].lzone = *(unsigned short *) (12+BIOS); // 邏輯區域起始柱面hd_info[0].sect = *(unsigned char *) (14+BIOS); // 每磁道扇區數BIOS += 16; ···
}
? ? 上面是這個函數的第一部分,對硬盤基本信息的賦值。BIOS是來自內存 0x90080 處的數據,包括柱面數、磁頭數、扇區數等信息。不了解這些信息的可以看參考資料[3]。其實這里本來是個循環的,循環賦值所有硬盤信息進hd_info這個結構體數組,但是這里只考慮一個盤的情況,因此去掉了循環。最后BIOS加了16可以看上面內存的圖,硬盤1和硬盤2參數之間隔了16字節。這里是準備到下一個硬盤了,但是這里沒有了,所以不作考慮。
? ? 最終結果如下所示:
🏆硬盤分區表的設置
📃硬盤分區表(Disk Partition Table,DPT)
? ? 硬盤分區表用于定義硬盤的分區信息。分區表存儲在硬盤的某個特定區域,通常是硬盤的第一個扇區,稱為主引導記錄(Master Boot Record, MBR),現代的分區表格式則主要是GPT(GUID Partition Table)。其作用主要是告訴操作系統硬盤上有多少個分區,每個分區的大小和位置。
📃代碼實現
? ? 在linux0.11中的做法如下所示:
int sys_setup(void * BIOS)
{···hd[0].start_sect = 0;hd[0].nr_sects = hd_info[i].head*hd_info[i].sect*hd_info[i].cyl;struct buffer_head *bh = bread(0x300 + drive*5,0);struct partition *p = 0x1BE + (void *)bh->b_data;for (int i=1;i<5;i++,p++) {hd[i].start_sect = p->start_sect;hd[i].nr_sects = p->nr_sects;}brelse(bh);···
}
? ? 其實仔細看就能看出來,只是給一個新的結構體數組 hd
做賦值,而且這個結構體里面就兩個成員:start_sect
和nr_sects
,也就是開始扇區和總扇區數來記錄。循環里面一共4次,加上最開始初始化的,因此一共是五個分區,最后結果如下所示:
? ? 這些信息從哪里獲取呢,就是在硬盤的第一個扇區的 0x1BE 偏移處,這里存儲著該硬盤的分區信息,只要把這個地方的數據拿到就 OK 了。
? ? 所以 bread 就是干這事的,從硬盤讀取數據:
struct buffer_head *bh = bread(0x300 + drive*5,0);
? ? 第一個參數 0x300 是第一塊硬盤的主設備號,就表示要讀取的塊設備是硬盤一。第二個參數 0 表示讀取第一個塊,一個塊為 1024 字節大小,也就是連續讀取硬盤開始處 0 ~ 1024 字節的數據。拿到這部分數據后,再取 0x1BE 偏移處,就得到了分區信息:
struct partition *p = 0x1BE + (void *)bh->b_data;
? ? 從硬盤的視角來看分區。0號塊本來是一個超級塊,可以參考這篇博客:linux0.11內核源碼修仙傳第十五章——文件系統,現在在里面多放一個分區信息。下面是示意圖:
? ? 至于如何從硬盤中讀取指定位置(塊)的數據,也就是 bread 函數的內部實現,這部分略微復雜,先埋個坑日后再細聊。
🏆加載根文件系統
? ? 最后setup
函數還有一部分:
int sys_setup(void * BIOS)
{···rd_load(); // 不用管mount_root();···
}
? ? 其中 rd_load
是當有 ramdisk 時,也就是虛擬內存盤,才會執行。虛擬內存盤是通過軟件將一部分內存(RAM)模擬為硬盤來使用的一種技術,此處當不存在,因此這行代碼無用。
? ? mount_root
是加載根文件系統,有了它之后,操作系統才能從一個根開始找到所有存儲在硬盤中的文件,所以它是文件系統的基石,很重要:
void mount_root(void)
{int i,free;struct super_block * p;struct m_inode * mi;if (32 != sizeof (struct d_inode))panic("bad i-node size");for(i=0;i<NR_FILE;i++)file_table[i].f_count=0;if (MAJOR(ROOT_DEV) == 2) {printk("Insert root floppy and press ENTER");wait_for_keypress();}for(p = &super_block[0] ; p < &super_block[NR_SUPER] ; p++) {p->s_dev = 0;p->s_lock = 0;p->s_wait = NULL;}if (!(p=read_super(ROOT_DEV)))panic("Unable to mount root");if (!(mi=iget(ROOT_DEV,ROOT_INO)))panic("Unable to read root i-node");mi->i_count += 3 ; /* NOTE! it is logically used 4 times, not 1 */p->s_isup = p->s_imount = mi;current->pwd = mi;current->root = mi;free=0;i=p->s_nzones;while (-- i >= 0)if (!set_bit(i&8191,p->s_zmap[i>>13]->b_data))free++;printk("%d/%d free blocks\n\r",free,p->s_nzones);free=0;i=p->s_ninodes+1;while (-- i >= 0)if (!set_bit(i&8191,p->s_imap[i>>13]->b_data))free++;printk("%d/%d free inodes\n\r",free,p->s_ninodes);
}
📃mount_root 整體解讀
? ? 從整體上來說,mount_root
這個函數就是要把硬盤中的數據以文件系統的格式進行解讀,加載到內存中設計好的數據結構,這樣操作系統就可以通過內存中的數據,以文件系統的方式訪問硬盤中的一個個文件。首先回顧一下硬盤中的文件系統格式,區別于之前我們的博客里介紹的文件系統,哪個是ext2的格式,但是linux-0.11是MINIX
文件系統,但是都是大同小異的,這里貼出來這個文件系統的格式:
? ? 簡單看看這個文件系統:
引導塊:啟動區,當然不一定所有的硬盤都有啟動區,但我們還是得預留出這個位置,以保持格式的統一。
超級塊:用于描述整個文件系統的整體信息,我們看它的字段就知道了,有后面的 inode 數量,塊數量,第一個塊在哪里等信息。
inode位圖:inode的使用情況。
塊位圖:塊的使用情況。
i 結點:inode存放每個文件或目錄的元信息和索引信息。
塊:存放文件數據。
? ? 可是硬盤中憑什么就有了這些信息呢?這就是個雞生蛋蛋生雞的問題了。你可以先寫一個操作系統,然后給一個硬盤做某種文件系統類型的格式化,這樣你就得到一個有文件系統的硬盤了,有了這個硬盤,你的操作系統就可以成功啟動了。
📃內存中用于文件系統的數據結構
文件信息初始化
? ? 下面來逐步看,就只看第一個循環目前:
void mount_root(void)
{for(i=0;i<64;i++)file_table[i].f_count=0;···
}
? ? 這個循環就干了一件事,把 64 個 file_table
里的 f_count
清零。來看看具體這個 file_table
,其表示進程所使用的文件,進程每使用一個文件都需要記錄在這里面,包括文件類型,inode索引信息,引用次數f_count
,現在沒有被引用,所以先將其都置為0。來看看代碼里的具體實現:
struct file file_table[NR_FILE];struct file {unsigned short f_mode;unsigned short f_flags;unsigned short f_count;struct m_inode * f_inode;off_t f_pos;
};
? ? 來看一個file_table
的使用案例。比如現在有如下的命令:echo "hello" > 0
。這個命令表示將字符串“hello”寫入到0號文件描述符。這個0號文件就是file_table[0]對應的文件。這個文件在硬盤哪里呢?注意到其中有個f_inode
成員,通過這個即可找到indoe信息,inode里面包含了一個文件所需要的全部信息,包括文件大小,文件類型,文件所在硬盤號等。此事已在前面有所記載。
超級塊初始化
? ? 接著看這個函數后面的內容:
void mount_root(void)
{···for(p = &super_block[0] ; p < &super_block[8] ; p++) {p->s_dev = 0;p->s_lock = 0;p->s_wait = NULL;}···
}
? ? 這又是一個初始化的操作。super_block
就是我們之前講的超級塊,其作用也和之前的超級塊一樣,里面存的這個把設備的信息,通過這個超級塊就可以掌握整個設備的文件系統全局了。
s_dev
:超級塊對應的設備號,置為 0 表示未使用。
s_lock
:超級塊鎖,置為 0(未鎖定)
s_wait
:等待隊列指針,置為NULL
? ? 來看接下來的操作:
void mount_root(void)
{···if (!(p=read_super(0)))panic("Unable to mount root");···
}
? ? 接下來就是讀取硬盤的超級塊信息到內存中,read_super
函數就是讀取硬盤中的超級塊。
inode信息讀取
? ? 接下來讀取根目錄的inode信息。
int ROOT_DEV = 0;
#define ROOT_INO 1void mount_root(void)
{···if (!(mi=iget(ROOT_DEV, ROOT_INO)))panic("Unable to read root i-node");mi->i_count += 3 ; // 邏輯上使用4次,初始為1···
}
? ? iget
函數會獲取根inode,同時會增加inode引用次數,表示inode被使用。
? ? 接下來就是將 inode 設置當前進程(新建的進程1)的根目錄和工作目錄:
void mount_root(void)
{···p->s_isup = p->s_imount = mi;current->pwd = mi; // 當前工作目錄(m_inode指針)current->root = mi; // 根目錄(m_inode指針)···
}
記錄位圖信息
? ? 超級塊下面是塊位圖:
void mount_root(void)
{···free=0;i=p->s_nzones; // 文件系統的總磁盤塊數while (-- i >= 0)if (!set_bit(i&8191,p->s_zmap[i>>13]->b_data))free++;···
}
? ? 首先來看看結構體p的內容:s_zmap 是位圖數組,每個元素指向一個頁,b_data是頁的內存地址。用于管理塊的占用狀態。i>>13
是因為一頁是 2^13 個塊,超出了就是其他頁,就是其他索引了。
? ? 其次來搞清楚set_bit
函數的含義:
#define set_bit(bitnr,addr) ({ \
register int __res __asm__("ax"); \
__asm__("bt %2,%3;setb %%al":"=a" (__res):"a" (0),"r" (bitnr),"m" (*(addr))); \
__res; })
? ? 這個不是設置位!!!是檢查位的值。若 addr
的第 bitnr
位為 1,返回 1;否則返回 0。
? ? 8191換算成二進制是這個:1 1111 1111 1111
,共13個1,這是取出低13位的值。為什么是13呢?那是因為在早期文件系統,如 MINIX。每個位圖頁管理8192(2^13)個塊。i&8191
表示位圖頁內偏移。合起來就是檢查每個塊對應的位是否為0,0就是空閑,就遞增free
變量。通過 i >> 13
和 i & 8191
分頁定位位圖中的位。
? ? free變量的含義就是統計文件系統中未被占用的磁盤塊(空閑塊)數量。
記錄inode位圖信息
? ? 最后就是inode位圖:
void mount_root(void)
{···free=0;i=p->s_ninodes+1;while (-- i >= 0)if (!set_bit(i&8191,p->s_imap[i>>13]->b_data))free++;···
}
? ? 做法和目標同上面的塊位圖,只是i的值不一樣。
🎯總結
? ? 這篇博客就主要講了setup函數的內容,獲硬盤信息以及加載根文件。
📖參考資料
[1] linux源碼趣讀
[2] 一個64位操作系統的設計與實現
[3] 硬盤基本知識(磁道、扇區、柱面、磁頭數、簇、MBR、DBR)
[4] 分區表(Partition Table)是計算機硬盤驅動器上一個重要的數據結構