【 聲明:版權所有,歡迎轉載,請勿用於商業用途。 聯繫信箱:feixiaoxing @163.com】
熟悉linux的同學都知道,linux的啓動順序就是uboot -> linux -> shell。但是很少人研究linux是怎麼調用shell程序的。今天,可以藉助早期linux 0.11的內核版本分析一下。linux 0.11的地址可以參考這個鏈接,https://github.com/tinyclub/linux-0.11-lab。
1、內核態跳轉到用戶態,main.c
內核態跳轉到用戶態,os是通過代碼move_to_user_mode()來完成的。執行完這段代碼的意義就是,從下一行代碼開始,當前代碼就是用戶態的程序了。只不過,這是一個特殊的用戶態程序。普通的用戶程序會有兩個地址空間,一個指向內核,一個指向自己。而這個特殊的用戶程序就只有一個空間,即內核空間。當然,有的同學會說,普通的用戶程序不能執行內核代碼,必須通過系統調用纔可以,爲什麼這個用戶程序可以?其實這個只要設置一下tlb屬性就可以了。
void main(void)
{
/*
* other init code :-)
*/
move_to_user_mode();
if (!fork()) { /* we count on this going ok */
init();
}
/*
* NOTE!! For any other task 'pause()' would mean we have to get a
* signal to awaken, but task0 is the sole exception (see 'schedule()')
* as task 0 gets activated at every idle moment (when no other tasks
* can run). For task0 'pause()' just means we go check if some other
* task can run, and if not we return here.
*/
for(;;) pause();
}
另外,關於move_to_user_mode,這個也是有意思的,大家可以看看怎麼實現的,特別是這麼一句"pushl $1f\n\t"
#define move_to_user_mode() \
__asm__ (
"movl %%esp,%%eax\n\t" \
"pushl $0x17\n\t" \
"pushl %%eax\n\t" \
"pushfl\n\t" \
"pushl $0x0f\n\t" \
"pushl $1f\n\t" \
"iret\n" \
"1:\tmovl $0x17,%%eax\n\t" \
"movw %%ax,%%ds\n\t" \
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")
ps:
如果是用戶態進入系統態一般用int或者是sysenter。返回的話就用iret。
2、加載文件系統
os通過setup((void *) &drive_info)實現文件系統的加載。當然加載文件系統可以放在模式切換前,也可以放在切換後,這一塊問題不大。加載文件系統的意義在於,有了fs之後,os可以簡單地通過路徑就可以實現數據的訪問和讀寫了。
在linux的後期演進過程中,文件系統逐步變成了根文件系統。其實一個硬盤、ram、fd裏面可以有多個文件系統。至於哪一個是根文件系統,都是os自己指定的,不同的文件系統之間也可以切換爲根文件系統,這一點上沒有多大難度。
3、打開串口
串口的打開,是通過open("/dev/tty0",O_RDWR,0)函數實現的,這一塊比較簡單,就是一個系統調用。
4、創建子進程
子進程的創建是通過pid=fork()實現。如果是子進程,返回爲0;如果是父進程,返回爲子進程的id號。fork只是clone一下父進程的內容,本身沒有加載任何代碼的東西。
5、準備加載用戶代碼
execve("/bin/sh",argv_rc,envp_rc)來完成用戶空間的加載。這也是一個系統調用。簡單來說,execve只負責系統空間的加載,但是不負責實際代碼和數據的載入。這麼做的好處就是在內存空間不富裕的時候,只載入必要的代碼段和數據段。
6、page異常
在execve返回到用戶側的時候,就會產生異常。因爲雖然execve分配了代碼空間,但是沒有實際載入數據,所以會產生一個缺頁異常。這部分的內容是通過do_no_page來完成的,如果os發現當前的空間確實有數據,那麼就會盡可能地從fs加載數據進來,當然如果本身空間就是非法的,這個時候就會rais真正的異常。
7、等待子進程結束
第一次執行sh的時候,fs只完成一部分工作,執行完就退出。這個時候父進程就會在(pid != wait(&i)進行等待。一般來說,這個sh會完成一些腳本的初始化工作,類似於linux rc文件。執行結束後,sh就退出來了。雖然後面也會執行sh,但是兩者的輸入參數是不一樣的,差別也就在這個地方。
8、重複啓動子進程、等待子進程
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();
}
9、不可達的代碼
在0.11中_exit(0)代碼是不可達的。看待上面的循環邏輯,大家就可以明白,父進程會一直監視sh有沒有退出。如果sh退出之後,那麼會重啓一個新的shell。所以,_exit(0)是永遠也不會執行到的。
ps:
linux 0.11從哪方面講,對自己的操作系統水平和認知提升都是大有裨益的。文件系統裏面的程序,可以自己寫一個簡單的syscall + helloworld程序就可以了,把流程捋一遍,就知道系統的整個執行流程了。另外,清華大學陳渝老師的ucore也不錯,有對應的代碼https://github.com/chyyuu/ucore_os_lab,還有視頻教程https://www.bilibili.com/video/av6538245/,簡單的安裝virtualbox虛擬機、配置一下ubuntu + qemu + gdb,就可以調試開發,非常適合用來練習,對自己認識bootloader、kernel、fs很有幫助。
文件系統非常重要,如果是bios啓動的話,很有可能內核也會放在文件系統裏面,即bios先讀取文件系統,執行kernel,再加載完整的文件系統。後期的windows、linux,都是這麼來做的。比如,windows就是ntoskrnl.exe,而linux則是bzImage,兩者邏輯是一樣的。
用戶側的代碼通常都放在文件系統裏面,簡單地來看,就是讀取一個文件,copy到內存,創建一個線程,分配一些時間片,開始調度和切換。用戶程序訪問資源都需要syscall,如果是內核來做這樣事情,可以直接call function,連syscall都節省了。普通的用戶側代碼如果沒有syscall、沒有內核調用,其實執行起來是很容易的事情,它的內存和內核也是分開來的。當然,如果使用到了動態庫、libc,用戶側的代碼也可以做得很複雜。