main.c 功能描述

之前 setup 在 0x90000 ~ 0x901FF 保存了一些重要的机器参数,其中包括主内存区的开始地址,内存大小和作为高速缓冲区内存的末端地址,如果 Makefile 中还定义了 RAMDISK 虚拟盘,则主内存区还会减小。

高速缓冲是用于供磁盘等块设备临时存放数据的地方,以 1KB 为一个数据块单位,其中包含了显存及其 BIOS 占用的区域。现在 main.c 将会用这些参数来划分内存区域

内存划分

之后调用一堆初始化函数对内存、陷阱门、块设备、字符设备、tty、时间、进程调度、缓冲区、硬盘、软驱进行初始化,并完成进程 0 的创建,从内核态切换为用户态。

此时第一次调用 fork 函数创建用于运行 init 函数的子进程 1,init 函数主要作用为

  • 安装根文件系统
  • 显示系统信息
  • 执行资源配置文件
  • 执行登录 shell 程序

流程图如下:

内存划分


代码分析

*.h 头文件没有指明路径默认在 include 目录下

1
2
3
4
// Line 7
#define __LIBRARY__ // 定义了该符号表示会包含系统调用号及一些宏定义如 syscall0 等
#include <unistd.h> // 定义了各种符号常数与类型,声明了函数
#include <time.h> // 时间类型头文件

__always_inline 与 syscall 等在 include/unistd.h 中都有定义,_syscall 后面的数字表示定义的函数有几个参数,括号中的前两个参数为函数返回值类型及函数名,之后的每两个参数代表该函数参数类型及参数名

1
2
3
4
5
// Line 24
__always_inline _syscall0(int,fork)
__always_inline _syscall0(int,pause)
__always_inline _syscall1(int,setup,void *,BIOS)
__always_inline _syscall0(int,sync)

比如 _syscall1(int,setup,void *,BIOS) 表示定义了 int setup(void *BIOS) 这么一个函数,具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// include/unistd.h Line 267
#define __always_inline inline __attribute__((always_inline)) // 该函数需要内联处理

// Line 60 取部分用到的常数
#define __NR_fork 2
#define __NR_pause 29
#define __NR_setup 0
#define __NR_sync 36

// Line 147
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \ // 将结果从 eax 寄存器赋值给变量 __res
: "0" (__NR_##name)); \ // '0' 表示使用与第 0 条操作表达式相同的寄存器,即 eax = __NR_XXX
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}

#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}

对于 gcc AT&T 内嵌汇编不太熟悉的话,可以参考这篇 csdn 博文。以 _syscall0(int,fork) 为例,_syscall0(int,fork) 后产生了这么一个函数:

1
2
3
4
5
6
7
8
int fork(){
long __res;
__asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_fork));
if (__res >= 0)
return (int) __res;
errno = -__res;
return -1;
}

然后包含一堆头文件,引用一些初始化函数,定义一些常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Line 29
#include <linux/tty.h> // 定义有关 tty_ip,串行通信方面的常数等
#include <linux/sched.h> // 进程调度程序头文件
#include <linux/head.h> // 定义了段描述符的结构等
#include <asm/system.h> // 以宏的形式定义了许多有关设置或修改描述符/中断门等的嵌入式汇编子程序
#include <asm/io.h> // 定义了对 IO 端口的操作函数

#include <stddef.h> // 标准定义头文件
#include <stdarg.h> // 标准参数头文件
#include <fcntl.h> // 文件控制头文件
#include <sys/types.h> // 定义了基本的系统数据类型

#include <linux/fs.h> // 文件系统头文件

static char printbuf[1024]; // 内核显示信息的缓存

extern int vsprintf(); // vsprintf.c
extern void init(void); // 就在本程序中
extern void blk_dev_init(void); // 块设备初始化 blk_drv/ll_rw_blk.c
extern void chr_dev_init(void); // 字符设备初始化 chr_drv/tty_io.c
extern void hd_init(void); // 硬盘初始化 blk_drv/hd.c
extern void floppy_init(void); // 软驱初始化 blk_drv/floppy.c
extern void mem_init(long start, long end); // 内存管理初始化 mm/memory.c
extern long rd_init(long mem_start, int length); // 虚拟盘初始化 blk_drv/ramdisk
extern long kernel_mktime(struct tm * tm); // 系统开机时间 kernel/mktime.c
extern long startup_time;

#define EXT_MEM_K (*(unsigned short *)0x90002) // 1MB 后扩展内存的大小(KB)
#define DRIVE_INFO (*(struct drive_info *)0x90080) // 硬盘参数表
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC) // 根文件系统所在设备号

接下来定义读取 CMOS 时钟信息的宏及 time_init 函数,从 CMOS 中读出的信息都是 BCD 码的形式,用 4 bits(半个字节)表示一个 10 进制数。于是定义一个 BCD_TO_BIN 的宏,val&15 取 10 进制个位,val>>4 取 10 进制十位,乘 10 与个位相加得到实际的十进制对应的二进制数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Line 69
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \ // 向 0x70 端口输出要读取 CMOS 的内存位置
inb_p(0x71); \ // 从 0x71 读取一个字节
})

#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10) // BCD 码转二进制数值

static void time_init(void)
{
struct tm time; // tm 结构定义在 time.h 中

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)); // do-while 循环读取时钟信息,将误差锁定在 1s 内
BCD_TO_BIN(time.tm_sec); // 读出来都是 BCD 码,转成二进制数值
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--; // 让月份范围从 1 ~ 12 减为 0 ~ 11
startup_time = kernel_mktime(&time); // 计算开机时间
}

outb_p 与 inb_p 定义在 include/asm/io.h 中,分别表示向 io 端口输出信息及从 io 端口读取信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// include/asm/io.h Line 11
#define outb_p(value,port) \
__asm__ ("outb %%al,%%dx\n" \
"\tjmp 1f\n" \ // jmp 1f 相当于 nop
"1:\tjmp 1f\n" \
"1:"::"a" (value),"d" (port))

#define inb_p(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al\n" \
"\tjmp 1f\n" \
"1:\tjmp 1f\n" \
"1:":"=a" (_v):"d" (port)); \
_v; \
})

main 之前最后还定义了一些用于内存划分的变量

1
2
3
4
5
6
// Line 98
static long memory_end = 0; // 机器的物理内存容量
static long buffer_memory_end = 0; // 高速缓冲区末端地址
static long main_memory_start = 0; // 主内存开始位置

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

main 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Line 104
void main(void)
{
ROOT_DEV = ORIG_ROOT_DEV; // 存储根设备号
drive_info = DRIVE_INFO; // 存储硬盘参数表
memory_end = (1<<20) + (EXT_MEM_K<<10); // 内存大小 = 1MB + 扩展内存大小 * 1024
memory_end &= 0xfffff000; // 4K 对齐
if (memory_end > 16*1024*1024) // 内存大小超过 16 MB,限制为 16 MB
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024) // 内存大小在 12 ~ 16 MB 之间,缓冲区末端 = 4 MB
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024) // 内存大小在 6 ~ 12 之间,缓冲区末端 = 2 MB
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;// 小于等于 6 MB,缓冲区末端 = 1 MB
main_memory_start = buffer_memory_end; // 主内存区域起始位置设置为缓冲区结束位置
#ifdef RAMDISK // 如果定义了 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(); // 初始化完成,开启之前被屏蔽的中断
move_to_user_mode(); // 转移到用户态
if (!fork()) { // 先生成一个子进程 1,继续往下执行
init(); // 在进程 1 中继续初始化
}
for(;;) pause(); // pause 调用进程调度函数,切换回进程 1 执行
}

之前有说过 pause 的进程需等待一个信号才能被激活,进程 0 是个例外,当没有其他进程在运行时,会激活进程 0

接下来声明了一个 printf 函数,为 init 中输出信息做准备,并设置了一些配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Line 152
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;
}
// 读取执行 /etc/rc 文件使用的命令行参数及环境参数
static char * argv_rc[] = { "/bin/sh", NULL }; // 命令行参数数组
static char * envp_rc[] = { "HOME=/", NULL, NULL }; // 环境数组
// 运行登录 shell 使用的命令行参数及环境参数
static char * argv[] = { "-/bin/sh",NULL }; // 命令行参数数组
static char * envp[] = { "HOME=/usr/root", NULL, NULL }; // 环境数组

进程 1 执行的 init 函数,该函数首先对第一个将要执行的 shell 程序的环境进行初始化,然后以登录 shell 的方式加载并执行。如果运行登录 shell 的进程结束了,进程 1 又会重新新建一个进程,继续运行登录 shell。这里的登录 shell 指的就是开机完成用户可以键入用户名密码登录系统的 shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Line 169
void init(void)
{
int pid,i;

setup((void *) &drive_info); // 读取硬盘参数,安装根文件系统设备,加载虚拟盘
(void) open("/dev/tty0",O_RDWR,0); // 以读写访问方式打开设备 /dev/tty0,句柄为 0(stdin)
(void) dup(0); // 复制句柄 0,产生 1 号句柄,用于标准输出(stdout)
(void) dup(0); // 复制句柄 0,产生 2 号句柄,用于标准出错输出(stderr)
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); // 打印主内存空闲内存字节数
if (!(pid=fork())) { // 新建进程 2(作为进程 1 的子进程)
close(0); // 关闭标准输入流
if (open("/etc/rc",O_RDONLY,0)) //立即打开 /etc/rc 将 stdin 重定向到 /etc/rc 文件
_exit(1);
execve("/bin/sh",argv_rc,envp_rc);// 执行 /bin/sh,从 /etc/rc 读入命令并执行
_exit(2);
}
if (pid>0) // 以下是父进程执行的代码,&i 是存放返回信息的位置
while (pid != wait(&i)) // 父进程(进程 1)等待进程 2 运行结束,环境初始化完毕
/* nothing */;
while (1) {
if ((pid=fork())<0) { // 创建进程 n,作为登录 shell
printf("Fork failed in init\r\n");
continue;
}
if (!pid) { // 进程 n 执行的代码部分
close(0);close(1);close(2); // 关闭以前遗留的三个标准流句柄
setsid(); // 创建新的会话
(void) open("/dev/tty0",O_RDWR,0); // 重新打开 /dev/tty0 作为 stdin
(void) dup(0); // stdout
(void) dup(0); // stderr
_exit(execve("/bin/sh",argv,envp)); // 打开登录 shell
}
while (1) // 对于进程 1,等待进程 n(登录 shell) 运行结束
if (pid == wait(&i))
break; // 子进程 n 结束了,进程 1 又会回到上面新建一个进程运行登录 shell
printf("\n\rchild %d died with code %04x\n\r",pid,i);
sync(); // 同步,刷新缓冲区
}
_exit(0);
}