MINIX3操作系统分析

文章目录

  • 何为微内核???
  • 对MINIX3操作系统整体印象
  • MINIX3操作系统分析
    • 源代码组织
    • 进程
      • 系统初始化
      • MINIX3的中断处理
      • MINIX3的进程间通信
      • 系统任务
      • 时钟任务
    • 输入输出系统
      • 块设备驱动程序
      • 字符设备驱动程序
    • 存储管理
    • 文件系统

本来打算在3月8号还书之前把《操作系统设计与实现》这本书看完的,但是拖到4月4号才看完,趁着清明节,记下笔记。

何为微内核???

这本书实现的是一个微内核的操作系统。刚开始的时候不懂微内核是什么意思,只知道微内核操作系统就是只将必要的功能放到内核中。比如对于本书的MINIX操作系统来说,内核负责进程调度、进程间通信、时钟管理和系统任务,其它的内容,例如设备驱动程序、文件系统、内存管理等内容都放在了用户空间中,为什么要进行这样的设计呢?
在宏内核中,也就是Linux那样的操作系统,内核实现了很多的功能,并且提供了一些接口供应用程序使用(例如我们使用的read()还有write()等系统调用)。这些功能都是内核的一部分,我们使用这些系统调用的时候,一般来说都会进入内核态执行,这样就能使用内核当中的代码来访问硬件了,这就是宏内核。
而在微内核中,内核虽然也提供了同样的功能的系统调用,但是这些系统调用并不是去直接访问硬件的,而是将这次访问交给别的程序去执行
不理解的人可能会认为这个和宏内核是一样的,但是其实不然。在宏内核中,一个系统调用一般不会只由一个函数来完成,在执行的过程中会去调用很多的函数,这里最大的特点在于,这些都是内核态的程序,它们都属于同一个地址空间,或者就叫它内核进程的地址空间(不知道这么说对不对)。而上面所说的交给别的程序去执行,是交给了另一个进程去执行,两部分代码不是同一个进程。这样可以将一个系统调用按照层次来划分,每一层只要保证给上层的接口是不变的(在MINIX3中就是接收的消息类型),就可以进行替换。

对MINIX3操作系统整体印象

书陆陆续续看了有一个月,很多的特性也都记不清楚,现在我还能记住的一些特性,或许就是MINIX3操作系统的精髓吧?

  1. 实际上只有两个系统调用send()receive()
  2. 一共有7种消息供进程间通信使用
  3. 系统可以在32位保护模式和16位实模式之间切换
  4. 设备驱动程序、文件系统是独立的进程,它们初始化完毕后就循环等待消息并进行处理
  5. 进程表被分为了3个部分,分别存在于内核、进程、存储管理器和文件系统中

MINIX3操作系统分析

这里使用的是http://www.minix3.org网站上minix-3.1.0-book.iso.bz2这个镜像中提取出来的代码。

源代码组织

系统被分成了很多独立的进程,每个进程都自己的头文件,其中定义了全局变量、宏和函数。在定义全局变量的时候,glo.h中的全局变量都冠以EXTERN这个宏(定义在src/include/minix/const.h中),所以在大多数的源文件中,全局变量都是外部声明,而在table.c这个源文件中,EXTERN宏被定义为空,为全局变量分配了存储空间。

进程

系统初始化

书里面没有讲系统是如何引导的,这里我也就不总结了(源码中src/boot/目录下是引导程序),直接来看初始化。引导完毕后的代码执行流程如下,位于src/kernel/mpx386.s中。

......
121:	MINIX:				! this is the entry point for the MINIX kernel
122:		jmp	over_flags	! skip over the next few bytes
......
176:	! Call C startup code to set up a proper environment to run main().
177:		push	edx
178:		push	ebx
179:		push	SS_SELECTOR
180:		push	DS_SELECTOR
181:		push	CS_SELECTOR
182:		call	_cstart		! cstart(cs, ds, mds, parmoff, parmlen)
183:		add	esp, 5*4
......
204:		jmp	_main			! main()
......

在汇编中引用C符号需要在其前面加上_前缀,所以_cstart实际上是src\kernel\start.c中的cstart函数,其中调用prot_init()函数来进行保护模式的初始化。

21:	PUBLIC void cstart(cs, ds, mds, parmoff, parmsize)
......
48:	/* Initialize protected mode descriptors. */
49:	prot_init();

main()函数在src\kernel\main.c中,主要任务是初始化中断、进程表和特权表、调用announce()restart()函数。对restart()的调用将启动第一个任务,控制权将不再返回到main()中。

31:		PUBLIC void main()
......
44:			/* Initialize the interrupt controller. */
45:			intr_init(1);
......		/*初始化内核中的进程表和特权表*/
165:		announce();				/* print MINIX startup banner */
166:		restart();

restart()调用的是汇编程序_restart,位于src/kernel/mpx386.s中,最后使用中断返回指令iret指令执行下一个进程。

382:	_restart:
......
397:	o16	pop	gs
398:	o16	pop	fs
399:	o16	pop	es
400:	o16	pop	ds
401:	popad
402:	add	esp, 4		! skip return adr
403:	iretd			! continue process

MINIX3的中断处理

当32位Intel处理器收到一个中断时,会新建立一个栈供中断处理程序使用,栈的位置由TSS(任务状态段)中的位置决定,在MINIX3中,中断处理程序将使用0级的栈基址和栈指针,这个栈就是内核进程表的stackframe_s结构的结尾处。当中断发生时,就会执行相应的中断处理程序,在MINIX3中就是hwint_masterhwint_master这两个宏定义的代码,首先调用save保存寄存器,然后执行相应的中断处理程序,最后的ret将返回到_restart或者restart1处进行中断返回,上面的代码位于src/kernel/mpx386.s中。

216:	#define hwint_master(irq)	\
217:		call	save			/* save interrupted process state */;\
218:		push	(_irq_handlers+4*irq)	/* irq_handlers[irq]		  */;\
219:		call	_intr_handle		/* intr_handle(irq_handlers[irq]) */;\
220:		pop	ecx							    ;\
......
228:		ret				/* restart (another) process      */
......
323:	save:
......
339:		jmp	RETADR-P_STACKBASE(eax)

MINIX3的进程间通信

MINIX3的进程间通信主要使用三条原语,send(dest, &message)用来向dest进程发送一条消息,receive(source, &message)用来接收一条来自source进程的消息,sendrev(src_dst, &message)用来发送一条消息,并等待一个进程的应答。主要代码位于src/kernel/proc.c中。
对于发送消息的进程来说,如果dest进程正在等待发送进程的消息,或者等待任何消息,那么就通过内核将消息复制到接收进程;否则就将发送进程阻塞。

200:	PRIVATE int mini_send(caller_ptr, dst, m_ptr, flags)
......
224:		if ( (dst_ptr->p_rts_flags & (RECEIVING | SENDING)) == RECEIVING &&
225:			 (dst_ptr->p_getfrom == ANY || dst_ptr->p_getfrom == caller_ptr->p_nr)) {
226:		/* Destination is indeed waiting for this message. */
227:		CopyMess(caller_ptr->p_nr, caller_ptr, m_ptr, dst_ptr,
228:			dst_ptr->p_messbuf);
229:		if ((dst_ptr->p_rts_flags &= ~RECEIVING) == 0) enqueue(dst_ptr);
230:		} else if ( ! (flags & NON_BLOCKING)) {
231:		/* Destination is not waiting.  Block and dequeue caller. */
232:		caller_ptr->p_messbuf = m_ptr;
233:		if (caller_ptr->p_rts_flags == 0) dequeue(caller_ptr);
234:		caller_ptr->p_rts_flags |= SENDING;
235:		caller_ptr->p_sendto = dst;
236:
237:		/* Process is now blocked.  Put in on the destination's queue. */
238:		xpp = &dst_ptr->p_caller_q;		/* find end of list */
239:		while (*xpp != NIL_PROC) xpp = &(*xpp)->p_q_link;	
240:		*xpp = caller_ptr;			/* add caller to end */
241:		caller_ptr->p_q_link = NIL_PROC;	/* mark new end of list */
242:		}

对于接收消息的进程来说,它首先会检查通知,然后才是向本进程发送的消息,如果没有等待接收的消息或者通知,就将自己阻塞。

251:	PRIVATE int mini_receive(caller_ptr, src, m_ptr, flags)
......
269:		/* Check to see if a message from desired source is already available.
270:		* The caller's SENDING flag may be set if SENDREC couldn't send. If it is
271:		* set, the process should be blocked.
272:		*/
273:		if (!(caller_ptr->p_rts_flags & SENDING)) {
......
290:			/* Found a suitable source, deliver the notification message. */
291:			BuildMess(&m, src_proc_nr, caller_ptr);	/* assemble message */
292:			CopyMess(src_proc_nr, proc_addr(HARDWARE), &m, caller_ptr, m_ptr);
......
297:			/* Check caller queue. Use pointer pointers to keep code simple. */
298:			xpp = &caller_ptr->p_caller_q;
299:			while (*xpp != NIL_PROC) {
300:				if (src == ANY || src == proc_nr(*xpp)) {
301:					/* Found acceptable message. Copy it and update status. */
302:					CopyMess((*xpp)->p_nr, *xpp, (*xpp)->p_messbuf, caller_ptr, m_ptr);
303:					if (((*xpp)->p_rts_flags &= ~SENDING) == 0) enqueue(*xpp);
304:					*xpp = (*xpp)->p_q_link;		/* remove from queue */
305:					return(OK);				/* report success */
306:				}
307:				xpp = &(*xpp)->p_q_link;		/* proceed to next */
308:			}
309:		}

通知消息是在接收进程处理的时候构建并拷贝的,如果接收进程没有等待通知,那么就将该通知位置位。

328:	PRIVATE int mini_notify(caller_ptr, dst)
......
331:	{
332:		register struct proc *dst_ptr = proc_addr(dst);
333:		int src_id;				/* source id for late delivery */
334:		message m;				/* the notification message */
335:
336:		/* Check to see if target is blocked waiting for this message. A process 
337:		* can be both sending and receiving during a SENDREC system call.
338:		*/
339:		if ((dst_ptr->p_rts_flags & (RECEIVING|SENDING)) == RECEIVING &&
340:			! (priv(dst_ptr)->s_flags & SENDREC_BUSY) &&
341:			(dst_ptr->p_getfrom == ANY || dst_ptr->p_getfrom == caller_ptr->p_nr)) {
342:
343:			/* Destination is indeed waiting for a message. Assemble a notification 
344:			* message and deliver it. Copy from pseudo-source HARDWARE, since the
345:			* message is in the kernel's address space.
346:			*/ 
347:			BuildMess(&m, proc_nr(caller_ptr), dst_ptr);
348:			CopyMess(proc_nr(caller_ptr), proc_addr(HARDWARE), &m, 
349:				dst_ptr, dst_ptr->p_messbuf);
350:			dst_ptr->p_rts_flags &= ~RECEIVING;	/* deblock destination */
351:			if (dst_ptr->p_rts_flags == 0) enqueue(dst_ptr);
352:			return(OK);
353:		 } 
354:
355:		/* Destination is not ready to receive the notification. Add it to the 
356:		* bit map with pending notifications. Note the indirectness: the system id 
357:		* instead of the process number is used in the pending bit map.
358:		*/ 
359:		src_id = priv(caller_ptr)->s_id;
360:		set_sys_bit(priv(dst_ptr)->s_notify_pending, src_id); 
361:		return(OK);
362:	}

系统任务

将驱动程序放到内核外部的结果就是它们不能执行特权指令,也就无法进行I/O操作,所以在MINIX3的内核中为驱动程序提供了一组服务,这些服务由系统任务来实现。虽然系统任务属于内核的一部分,但是它也是一个单独的进程,所以它的用一个循环实现,代码在src/kernel/system.c中。

59:	PUBLIC void sys_task()
60:	{
61:		/* Main entry point of sys_task.  Get the message and dispatch on type. */
62:		static message m;
63:		register int result;
64:		register struct proc *caller_ptr;
65:		unsigned int call_nr;
66:		int s;
67:
68:		/* Initialize the system task. */
69:		initialize();
70:
71:		while (TRUE) {
72:		/* Get work. Block and wait until a request message arrives. */
73:		receive(ANY, &m);	
......
99:		}
100:}

时钟任务

对于分时系统来说时钟是很必要的。虽然它是驱动程序,但又不同于传统意义的块设备和字符设备,它也属于内核的一部分,并作为一个单独的进程来执行,只接收时钟中断,代码在src\kernel\clock.c中。

69:	PUBLIC void clock_task()
70:	{
71:		/* Main program of clock task. If the call is not HARD_INT it is an error.
72:		*/
73:		message m;			/* message buffer for both input and output */
74:		int result;			/* result returned by the handler */
75:
76:		init_clock();			/* initialize clock task */
77:
78:		/* Main loop of the clock task.  Get work, process it. Never reply. */
79:		while (TRUE) {
80:
81:			/* Go get a message. */
82:			receive(ANY, &m);	
83:
84:			/* Handle the request. Only clock ticks are expected. */
85:			switch (m.m_type) {
86:				case HARD_INT:
87:					result = do_clocktick(&m);	/* handle clock tick */
88:					break;
89:				default:				/* illegal request type */
90:			kprintf("CLOCK: illegal request %d from %d.\n", m.m_type,m.m_source);
91:			}
92:		}
93:	}

输入输出系统

MINIX3系统中的每一类I/O设备都有一个单独的设备驱动程序。这些驱动程序是完整的进程,每个都有自己的状态、寄存器和堆栈等,它们使用消息通信的方式和上层进程以及系统任务进行通信。每种设备驱动程序一般都可以执行下面几种操作:

  1. open
  2. close
  3. read
  4. write
  5. ioctl
  6. scattered_io(这个我没怎么见过,书中说是读写多个块使用的)

块设备驱动程序

所有的块设备驱动程序共用的代码在src\drivers\libdriver\driver.c中,和系统任务和时钟任务类似,块设备驱动程序的主程序也是循环接收消息,根据消息类型,通过函数指针调用该驱动程序的相应处理函数。

73:	PUBLIC void driver_task(dp)
......
75:	{
......
81:		/* Get a DMA buffer. */
82:		init_buffer();
83:
84:		/* Here is the main loop of the disk task.  It waits for a message, carries
85:		* it out, and sends a reply.
86:		*/
87:		while (TRUE) {
88:
89:			/* Wait for a request to read or write a disk block. */
90:			if(receive(ANY, &mess) != OK) continue;
91:
92:			device_caller = mess.m_source;
93:			proc_nr = mess.PROC_NR;
94:
95:			/* Now carry out the work. */
96:			switch(mess.m_type) {
97:			case DEV_OPEN:		r = (*dp->dr_open)(dp, &mess);	break;
98:			case DEV_CLOSE:		r = (*dp->dr_close)(dp, &mess);	break;
99:			case DEV_IOCTL:		r = (*dp->dr_ioctl)(dp, &mess);	break;
100:		case CANCEL:		r = (*dp->dr_cancel)(dp, &mess);break;
101:		case DEV_SELECT:	r = (*dp->dr_select)(dp, &mess);break;
102:
103:		case DEV_READ:	
104:		case DEV_WRITE:	  r = do_rdwt(dp, &mess);	break;
105:		case DEV_GATHER: 
106:		case DEV_SCATTER: r = do_vrdwt(dp, &mess);	break;
......
123:		}
......
129:		if (r != EDONTREPLY) {
130:			mess.m_type = TASK_REPLY;
131:			mess.REP_PROC_NR = proc_nr;
132:			/* Status is # of bytes transferred or error code. */
133:			mess.REP_STATUS = r;	
134:			send(device_caller, &mess);
135:		}
136:	}
137:}

比如对于RAM盘和硬盘来说,上面的函数在各自的main()中被调用。

src\drivers\memory\memory.c
73:		PUBLIC int main(void)
74:		{
75:			/* Main program. Initialize the memory driver and start the main loop. */
76:			m_init();			
77:			driver_task(&m_dtab);		
78:			return(OK);				
79:	}
src\drivers\at_wini\at_wini.c
291:	PUBLIC int main()
292:	{
293:		/* Set special disk parameters then call the generic main loop. */
294:		init_params();
295:		driver_task(&w_dtab);
296:		return(OK);
297:	}

字符设备驱动程序

字符设备驱动程序处理键盘,也处理显示器,是MINIX3中规模最大的驱动程序,但是它仍然是和上述程序一样的结构,入口点在src\drivers\tty\tty.c中。

157:	PUBLIC void main(void)
158:	{
......
168:		/* Initialize the TTY driver. */
169:		tty_init();
......
176:		/* Final one-time keyboard initialization. */
177:		kb_init_once();
......
181:		while (TRUE) {
......
188:			/* Get a request message. */
189:			receive(ANY, &tty_mess);
......
293:		}
294:	}

存储管理

我感觉这部分实现得挺草率的,把进程管理和存储管理组合成了一个叫pm(进程管理器)的进程,而且没有什么有用的东西,主程序的入口在src\servers\pm\main.c中,call_vec最终调用相应的程序执行系统调用。

42:	PUBLIC int main()
43:	{
......
49:		pm_init();			/* initialize process manager tables */
50:
51:		/* This is PM's main loop-  get work and do it, forever and forever. */
52:		while (TRUE) {
53:			get_work();		/* wait for an PM system call */
......
68:				result = (*call_vec[call_nr])();
......
93:		}
94:		return(OK);
95:	}

文件系统

其实无论是什么样的文件系统,都需要存储至少两种类型的文件,即目录文件和常规文件,而且都实现了一些基本的操作(创建、删除、打开、关闭、读、写、上锁等)。MINIX3的文件系统是用索引节点(inode)的实现方式,也作为一个单独的进程来实现,进程入口点在src\servers\fs\main.c中,call_vec最终调用相应的程序执行系统调用。

41:	PUBLIC int main()
42:	{
......
50:		fs_init();
51:
52:		/* This is the main loop that gets work, processes it, and sends replies. */
53:		while (TRUE) {
54:			get_work();		/* sets who and call_nr */
......
84:				error = (*call_vec[call_nr])();
......
93:		}
94:		return(OK);				/* shouldn't come here */
95:	}

文件系统中用了一个叫做高速缓存的东西,其实就是在内存中保留一些文件的数据块,这样能提高速度。为了能快速的判断某个文件的内容是否在高速缓存中,MINIX3使用了哈希表,通过设备号和块号来计算哈希值,并且使用链地址法来处理冲突。没有使用的高速缓冲块被链接在LRU(最近最少使用)链表中,如果需要分配一个新的缓冲块,就从LRU的头部取出一个块,删除它原来所在的哈希链表,插入到新的哈希链表头部,在使用完毕后放入LRU的尾部。

你可能感兴趣的:(操作系统,读书笔记)