隨想錄(文件系統的第一個用戶程序shell)

【 聲明:版權所有,歡迎轉載,請勿用於商業用途。  聯繫信箱: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,用戶側的代碼也可以做得很複雜。

 

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