linux0.12-7-1

文章详细阐述了Linux内核初始化的过程,从main.c的内存设置、硬件初始化到进程0和init进程的创建。主要内容包括内存管理、设备初始化、中断处理,以及通过fork()创建新进程。init进程会安装根文件系统,执行配置文件,并启动shell登陆程序。
摘要由CSDN通过智能技术生成

[272页]

第7章 初始化程序

1、main.c主要内核初始化工作。
2、如果能完全理解这里调用的所有程序,那么看完这张内容后应该对Linux内核有了大致的了解。
3、 有一定的C语言知识
4、 需要GNU gcc手册在身边作为参考,因为在内核代码很多地方使用gcc的扩展特性。
例如内联(inline)函数、内联(内嵌)汇编语句等。

7-1 main.c程序

7-1-1 功能描述

1、
(a)main.c程序首先利用前面setup.s程序取得的系统参数设置、系统的根文件设备

以及一些内存全局变量。这些内存变量指明了主内存的开始地址、系统所拥有的内存容量
和作为高速缓冲区内存的末端地址。

如果还定义了虚拟盘,则主内存将适当减少。

整个内存的映像示意图如图7-1所示
内核程序+高速缓冲+虚拟盘+主内存区。

(b)高速缓冲部分还要扣除被显示和ROM BIOS占用的部分。

高速缓冲区是用于磁盘等块设备临时存放数据的地方,
以1K(1024)字节为一个数据块单位。

©主内存区域的内存由内存管理模块mm通过分页机制进行管理分配,

以4K字节为一个内存页面单位。

(d)内核程序可以自由访问高速缓冲中的数据,但需要通过mm才能使用分配到内存页面。
2、main.c进行所有方面的硬件初始化工作

包括陷阱门、块设备、字符设备和tty,还包括人工设置第一个任务task 0。
待所有初始化工作完成后程序就设置中断允许标志以开启中断,
并切换到任务0中运行。
作者建议深入的看,遇到看不懂暂时先放一放。

3、整个内核完成初始化后,内核将执行权切换到用户模式(任务0),即CPU从0特权级

切换到了第3特权级。此时main.c的主程序就工作在任务0中。然后系统第一次调用进程创建
函数fork(),创建出一个用于运行init()的子进程(通常被称为init进程)。

4、 系统整个初始化过程如图7-2所示

看作者
(a)main.c程序首先确定如何分配使用系统物理内存
(b)调用内核各部分的初始化函数分别对内存管理、中断处理、块设备和字符设备、进程管理以及硬盘和软盘等硬件进行初始化处理。
©程序把自己"手工"移动到任务0(进程0)中运行,并使用fork()调用首次创建出进程1(init进程),
(d)init()函数将继续进行应用环境的初始化并执行shell登陆程序。
(e)而原进程0则会在系统空闲时被调度执行,因此进程0通常也被称为idle进程。

5、 init()函数可分为4个部分:安装根文件系统、显示系统信息、

运行系统初始资源配置文件rc中的命令、执行用户登陆shell程序。

(a)代码首先调用系统调用setup((void *) &drive_info);用来收集硬盘设备分区表信息并安装根文件系统。

在安装根文件系统之前,系统会先判断是否需要建立虚拟盘。若编译内核时设置了虚拟盘的大小,
并在前面内核初始化过程中已经开辟了一块内存用在虚拟盘,则内核就会首先尝试把根文件系统加载到内存的虚拟盘区中。

(b)打开一个终端设备tty0,并复制其文件描述符以产生标准输入stdin、标准输出stdout和错误输出stderr设备。

内核随后利用这些描述符在终端上显示一些系统信息,例如高速缓冲区中缓冲块总数、注内存区空闲内存总字节等。

©新建一个进程2,并在其中为建立用户交互使用环境而执行一些初始配置操作,即在用户可以使用shell命令行环境之前,

内核调用/bin/sh程序运行了配置文件etc/rc中设置的命令。rc文件的作业与DOS系统根目录中的AUTOEXEC.BAT文件类似。
这段代码首先通过关闭文件描述符0,并立刻打开文件/etc/rc,从而把标志输入stdin定向到/etc/rc文件上。
这样,所有的标准输入数据都将从该文件中读取。然后内核以非交互形式执行/bin/sh,从而实现执行/etc/rc文件中的命令。
当该文件中的命令执行完毕后,/bin/sh就会立刻退出。因此进程2也就随之结束。

(d)init()函数的最后一部分,等待进程2退出,创建新的进程,执行参数_exit(execve(“/bin/sh”,argv,envp));等待新进程退出,如此循环。

带有’-'标志会在/bin/sh执行时通知它这不是一次普通的运行,而是作为登陆shell运行/bin/sh的。

6、fork()是内联函数,因为创建进程1之前,要求进程0的用户堆栈干净,所以fork()不能以函数形式进行调用。

作者展开代码,且解释。
static inline int fork(void)
{
long __res;
asm volatile(“int $0x80”:“=a”(__res):“0”(__NR_fork));
if (__res>=0)
return (int)__res;
errno = -__res;
return -1;
}
进程init和进程0实际上同时使用着内核代码区内(小于1MB的物理内存)相同的代码和数据物理内存页面(640KB),
只是执行的代码不在一处。当进程init操作用户堆栈时,内核才会分配内存页给进程init。

当进程init或进程2执行过execve()调用后,进程2的代码和数据区位于系统的主内存。

7-1-2 代码注释

佩服赵老师保姆式注释,如果学不会只能怪您自己了。!!!(-:

/*
 *  linux/init/main.c
 *
 *  (C) 1991  Linus Torvalds
 */
//是为了包括定义在unistd.h中的内嵌汇编代码等信息。
#define __LIBRARY__ 
//*.h头文件所在的默认目录是include/,则在代码中就不必明确指明其位置。如果不是UNIX的
//标准头文件,则需要指明所在的目录,并用双引号括住。unistd.h是标准符号常数与类型文件。
//其中定义了各种符号常数和类型,并声明了各种函数。如果还定义了符号__LIBRARY__,则还会
//包含系统调用号和内嵌汇编代码syscall0()等。
#include <unistd.h>
#include <time.h>//时间类型头文件。其中主要定义了tm结构和一些有关时间的函数原形。

/*
我们需要下面这些内嵌语句-从内核空间创建进程将导致没有写时复制(COPY ON WRITE)!!!
知道执行一个execve调用。这对堆栈可能带来问题。处理方法是在fork()调用后不让main()
使用任何堆栈。因此就不能由函数调用-这意味着fork也要使用内嵌的代码,否则我们在从
fork()退出时就要使用堆栈了。

实际上只要pause和fork需要使用内嵌方式,以保证从main()中不会弄乱堆栈,但是我们同时还
定义了其他一些函数。
*/
//fork()展开后
static inline _syscall0(int,fork)
//pause()系统调用;暂停进程的执行,直到收到一个信号。 
static inline _syscall0(int,pause)
//setup(void *BIOS)系统调用,仅用于linux初始化(仅在这个程序中被调用)。
static inline _syscall1(int,setup,void *,BIOS)
//sync()系统调用:更新文件系统。
static inline _syscall0(int,sync)

#include <linux/tty.h>
#include <linux/sched.h>
#include <linux/head.h>
#include <asm/system.h>
#include <asm/io.h>

#include <stddef.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#include <linux/fs.h>

#include <string.h>

static char printbuf[1024];//静态字符串数组,用作内核显示信息的缓存。

extern char *strcpy();
extern int vsprintf();//送格式化输出到一字符串中
extern void init(void);//函数原形,初始化
extern void blk_dev_init(void);//块设备初始化子程序
extern void chr_dev_init(void);//字符设备初始化
extern void hd_init(void);//硬盘初始化程序
extern void floppy_init(void);//软驱初始化程序
extern void mem_init(long start, long end);//内存管理初始化
extern long rd_init(long mem_start, int length);//虚拟盘初始化
extern long kernel_mktime(struct tm * tm);//计算系统开机启动时间(秒)。
//内核专用sprintf()函数。该函数用于产生格式化信息并输出到指定缓冲区str中。参数'*fmt'
//指定输出将采用的格式,参见标志C语言书籍。该子程序正好是vsprintf如何使用的一个简单
//例子。函数使用vsprintf()将格式化字符串放入str缓冲区,参见第179行上的printf()函数。
static int sprintf(char * str, const char *fmt, ...)
{
	va_list args;
	int i;

	va_start(args, fmt);
	i = vsprintf(str, fmt, args);
	va_end(args);
	return i;
}

/*
 * 以下这些数据是在内核引导期间由setup.s程序设置的。
 */
 //下面三行分别将指定的线性地址强行转换为给定数据类型的指针,并获取指针所指内容。由于内核代码
 //被映射到从物理地址零开始的地方,因此这些线性地址正好也是对应的物理地址。
 //这些指定地址处内存值的含义请参见表6-3(setup程序读取并保存的参数)。
 //drive_info结构请参见下面第125行。
#define EXT_MEM_K (*(unsigned short *)0x90002)//1MB以后的扩展内存大小KB
#define CON_ROWS ((*(unsigned short *)0x9000e) & 0xff)//选的的控制台屏幕行、列数。
#define CON_COLS (((*(unsigned short *)0x9000e) & 0xff00) >> 8)//
#define DRIVE_INFO (*(struct drive_info *)0x90080)//硬盘参数表32字节内容。
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)//根文件系统所在设备号。
#define ORIG_SWAP_DEV (*(unsigned short *)0x901FA)//交换文件所在设备号。

/*
是啊,是啊,下面这段程序很差劲,但我不知道如何正确地实现,而且好像
它还能运行。如果有关于实时时钟更多资料,那我很感兴趣。这些都是试探
出来的,另外还看了一些bios程序,呵!
 */
//这段宏读取CMOS实时时钟信息。outb_p和inb_p是include/asm/io.h中定义的端口输入输出宏。
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \			//0x70是写地址端口号。
inb_p(0x71); \						//0x71是读数据端口号。
})
//定义宏。将BCD码转换二进制数据值。
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)

//该函数取CMOS时钟信息作为开机时间,保存到全局变量startup_time(秒)中。参见后面
//CMOS内存列表说明。其中调用的函数kernel_mktime()用于计算从1970年1月1日0时起到
//开机当日经过的秒数,作为开机时间。
static void time_init(void)
{
	struct tm time;

	do {
		time.tm_sec = CMOS_READ(0);
		time.tm_min = CMOS_READ(2);
		time.tm_hour = CMOS_READ(4);
		time.tm_mday = CMOS_READ(7);
		time.tm_mon = CMOS_READ(8);
		time.tm_year = CMOS_READ(9);
	} while (time.tm_sec != CMOS_READ(0));
	BCD_TO_BIN(time.tm_sec);
	BCD_TO_BIN(time.tm_min);
	BCD_TO_BIN(time.tm_hour);
	BCD_TO_BIN(time.tm_mday);
	BCD_TO_BIN(time.tm_mon);
	BCD_TO_BIN(time.tm_year);
	time.tm_mon--;
	startup_time = kernel_mktime(&time);
}
//下面定义一些局部变量。
static long memory_end = 0;			//机器具有的物理内存容量(字节数)。
static long buffer_memory_end = 0;	//告诉缓冲区末端地址。
static long main_memory_start = 0;	//主内存(将用于分页)开始的位置。
static char term[32];				//终端设置字符串(环境参数)。

//读取并执行/etc/rc文件时所使用的命令行参数和环境参数。
static char * argv_rc[] = { "/bin/sh", NULL };
static char * envp_rc[] = { "HOME=/", NULL ,NULL };
//运行登陆shell时所使用的命令行参数和环境参数。
//"-"是传递给shell程序sh的一个标志。通过识别该标志,sh程序
//会作为登陆shell执行。其执行过程与在shell提示符下执行sh不一样。
static char * argv[] = { "-/bin/sh",NULL };
static char * envp[] = { "HOME=/usr/root", NULL, NULL };

struct drive_info { char dummy[32]; } drive_info;//用于存放硬盘参数表信息。

//内核初始化主程序。初始化结束后将以任务0(idle任务即空闲任务)的身份运行。
void main(void)		/* 这里确实是void,没错。 */
{			/* 在startup程序(head.s)中就是这样假设的 */
/*
此时中断仍被禁止着,做完必要的设置后就将其开启。
 */
//首先保存根文件系统设备号和交换文件设备号,并根据setup.s程序中获取的信息设置控制台终端
//屏幕行、列数环境变量TERM,并用其设置初始init进程中执行etc/rc文件和shell程序使用的
//环境变量,以及复制内存0x90080处的硬盘参数表。
//其中ROOT_DEV已在前面包含进的include/linux/fs.h文件第206行上被声明为extern int,
//而SWAP_DEV在include/linux/mm.h文件内也作了相同声明。这里mm.h文件并没有显示地列在
//本程序前面,因为前面包含进的include/linux/sched.h文件中已经包含有它。 
 	ROOT_DEV = ORIG_ROOT_DEV;//声明在fs/super.c 定义在bootsetup.s
 	SWAP_DEV = ORIG_SWAP_DEV;//声明在mm/swap.c  定义在bootsetup.s
	sprintf(term, "TERM=con%dx%d", CON_COLS, CON_ROWS);
	envp[1] = term;	
	envp_rc[1] = term;
 	drive_info = DRIVE_INFO;//复制内存0x90080处的硬盘参数表。
//接着根据机器物理内存容量设置高速缓冲区和主内存区的位置和范围。
//高速缓存末端地址->buffer_memory_end;机器内存容量->memory_end;
//主内存开始地址->main_memory_start;
	memory_end = (1<<20) + (EXT_MEM_K<<10);//内存大小=1MB+扩展内存(k)*1024字节。
	memory_end &= 0xfffff000;//忽略不到4KB(1页)的内存数。
	if (memory_end > 16*1024*1024)//如果内存量超过16MB,则按16MB计。
		memory_end = 16*1024*1024;
	if (memory_end > 12*1024*1024) //如果内存>12MB,则设置缓冲区末端=4MB
		buffer_memory_end = 4*1024*1024;
	else if (memory_end > 6*1024*1024)//如果内存>6MB,则设置缓冲区末端=2MB
		buffer_memory_end = 2*1024*1024;
	else//否则设置缓冲区末端=1MB
		buffer_memory_end = 1*1024*1024;
	main_memory_start = buffer_memory_end;//主内存起始位置=缓冲区末端。
	
//如果在Makefile文件中定义了内存虚拟盘符号RAMDISK,则初始化虚拟盘。此时主内存将减少。
//参见kernel/blk_drv_ramdisk.c。
#ifdef RAMDISK
	main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
//以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看,
//若实在看不下去了,就先放一放,继续看下一个初始化调用。--作者保姆式的注释和关系。
	mem_init(main_memory_start,memory_end);//主内存区初始化。
	trap_init();//陷阱门(硬件中断量)初始化。
	blk_dev_init();//块设备初始化。
	chr_dev_init();//字符设备初始化。
	tty_init();//tty初始化。
	time_init();//设置开机启动时间。
	sched_init();//调度程序初始化
	buffer_init(buffer_memory_end);//缓冲管理初始化,建内存链表等。
	hd_init();//硬盘初始化。
	floppy_init();//软驱动初始化
	sti();//所有初始化工作都完了,于是开启中断。
//下面过程通过在堆栈中设置的参数,利用中断返回指令启动任务0执行。	
	move_to_user_mode();//移到用户模式下执行。
	if (!fork()) {		/* 在新建的子进程(任务1即init进程)中执行。 */
		init();
	}
//下面代码开始以任务0的身份运行。	
/*
注意!!对于任何其他的任务,'pause()'将意味着我们必须等待收到一个信号才会返回就绪态,但 
任务0(task0)是唯一例外情况,因为任务0在任何空闲时间里都会被激活,
因此对于任务0'pause()'仅意味着我们返回来查看是否有其他任务可以运行,如果没有的话我们就回到合理,
一直循环执行'pause()'
 */
	for(;;)
		__asm__("int $0x80"::"a" (__NR_pause):"ax");
}

//下面函数产生格式化信息并输出到标准输出设备stdout(1),这里是指屏幕上显示。参数'*fmt'
//指定输出将采用的格式,参见标准C语言书籍。该子程序正好是vsprintf如何使用的一个简单
//例子。该程序使用vsprintf()将格式化的字符串放入printbuf缓冲区,然后用write()将缓冲
//去的内容输出到标准设备(1--stdout)。vsprintf()函数的实现建kernel/vsprintf.c
static int printf(const char *fmt, ...)
{
	va_list args;
	int i;

	va_start(args, fmt);
	write(1,printbuf,i=vsprintf(printbuf, fmt, args));
	va_end(args);
	return i;
}
//在main()中已经进行了系统初始化,包括内存管理、各种硬件设备和驱动程序。init()函数
//运行在任务0第1次创建的子进程(任务1)中。它首先对第一个将要执行的程序(shell)
//的环境进行初始化,然后以登陆shell方式加载该程序并执行之。
void init(void)
{
	int pid,i;
//setup()是一个系统调用。用于读取硬盘参数包括分区表信息并加载虚拟盘(若存在的话)和
//安装根文件系统设备。该函数用25行上的宏定义,对应函数时sys_setup(),在块设备子目录
//kernel/blk_drv/hd.c,74行。
	setup((void *) &drive_info);
//下面以读写访问方式打开设备"/dev/tty0",它对应终端控制台。由于这是第一次打开文件
//操作,因此产生的文件句柄号(文件描述符)肯定是0.该句柄是UNIX类操作系统默认的控制
//台标志输入句柄stdin。这里再把它以读和写的方式分别打开是为了复制产生标准输出(写)
//句柄stdou和标志出差输出句柄stderr。函数前面的"(void)"前缀用于表示强制函数无需返回值。
	(void) open("/dev/tty1",O_RDWR,0);
	(void) dup(0);//复制句柄,产生句柄1号--stdout标准输出设备。
	(void) dup(0);//复制句柄,产生句柄2号--stderr标志出错输出设备。
	
//下面打印缓冲区块数和总字节数,每块1024字节,以及主内存区空闲内存字节数。	
	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);
	
	//函数_exit()退出时的出错码 1-操作未允许;2--文件或目录不存在。
	if (!(pid=fork())) {//if语句里面是task2
	//关闭句柄0(stdin)并立刻打开/etc/rc文件的作业是把标志输入stdin重定向到/etc/rc文件。
	//这样shell程序/bin/sh就可以运行rc文件中的设置的命令。
		close(0);
		if (open("/etc/rc",O_RDONLY,0))
			_exit(1);//若打开文件失败,则退出。
		execve("/bin/sh",argv_rc,envp_rc);//替换成/bin/sh程序并执行。
		_exit(2);//若execve()执行失败则退出。
	}
	//下面还是父进程(1)执行语句。wait()等待子进程停止或终止,返回值应是子进程的进程号
	//(pid)。这三局的作用是父进程等待子进程的结束。&i是存放返回状态信息的位置。
	//如果wait()返回值不等于子进程号,则继续等待。
	if (pid>0)
		while (pid != wait(&i))
			/* nothing */;
			
//如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建一个子进程,
//如果出错,则显示"初始化程序创建子进程失败"信息并继续执行。对于所创建的子进程将关闭所有
//以前还遗留的句柄(stdin,stdou,stderr),新创建一个会后并设置进程组号,
//然后重新打开/dev/tty0作为stdin,并复制成stdou和stderr。再次执行系统解释程序/bin/sh。但这
//次执行所选用的参数和环境数组另选了一套。然后父进程再次运行wait()等待。
//如果子进程又停止了执行,则在标准输出上显示出错信息"子进程pid停止了运行,返回码时i",
//然后继续重试下……,形成"大"死循环。
	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/tty1",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);	/* 注意! 是_exit(),非exit() */
	//_exit()和exit()都用于正常终止一个函数。但_exit()直接是一个sys_exit系统调用,而
	//exit()则通常是普通函数库中的一个函数。它会先执行一些清除操作,例如调用执行各终止处理程序、
	//关闭所有标准IO等,然后调用sys_exit。
}

7-1-3 其他信息

1、 CMOS信息

(a)0x70是地址端口,0x71是数据端口。
(b)表7-1 CMOS 64字节信息简表

2、 调用fork()创建新进程

(a)fork是创建新进程,但需要用exec()簇函数取执行其他不同的程序。
(b)子进程pid=0,父进程pid=子进程的pid号。
©当程序执行完或有必要终止时就可以调用exit()来退出,
而父进程则可以使用wait()调用来查看或等待子进程的退出,并获取被终止进程的退出状态信息。

3、 关于会话期的概念

_不理解进程组和会话期。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值