随想录(文件系统的第一个用户程序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,用户侧的代码也可以做得很复杂。

 

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