/init/main.c
是Linux操作系统启动过程的核心部分,它负责初始化硬件、设备、内存和系统服务,以及启动第一个用户进程,为后续的系统运行奠定基础。
详细解析
1. 内联函数定义
fork
,pause
,setup
,sync
: 这些函数被声明为内联,意味着它们在编译时会被直接嵌入到调用点,而不是通过常规函数调用的方式执行。这在内核上下文中特别重要,因为避免了不必要的栈操作和上下文切换,提高了效率。尤其是fork
和pause
,由于它们在main
函数中的关键作用,需要确保不会破坏栈的完整性。
1.1 fork
内联函数分析
系统调用宏定义
#define __NR_setup 0 /* used only by init, to get system going */
#define __NR_exit 1
#define __NR_fork 2
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
此宏定义 _syscall0
的功能是简化创建无参数系统调用函数的过程,其具体作用如下:
- 参数解析:
type
: 指定系统调用函数的返回值类型。name
: 系统调用函数的名称。
- 函数体详解:
- 定义一个长整型变量
__res
用于存储系统调用的结果。 - 使用内联汇编
__asm__
插入一条int $0x80
指令,这是 Linux 中触发系统调用的标准方式。 - 汇编指令使用寄存器传递参数和接收结果,其中
"=a" (__res)
表示将结果存储到eax
寄存器(即__res
),而"0" (__NR_##name)
表示从eax
寄存器读取系统调用编号,这里__NR_##name
是根据name
动态生成的系统调用编号。
- 定义一个长整型变量
- 错误处理:
- 如果系统调用成功,即
__res >= 0
,则将__res
转换为type
类型并返回。 - 如果系统调用失败,即
__res < 0
,则将-__res
的值赋给全局变量errno
,表示错误代码,并返回-1
。
- 如果系统调用成功,即
1.2 setup
内联函数分析
这个宏提供了一种便捷方式来封装系统调用,避免了每次手动编写相似的汇编代码和错误处理逻辑。例如,可以这样使用:_syscall1(int, read, int, fd)
, 生成一个 read
函数,用于读取文件描述符 fd
的数据。
#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; \
}
-
参数解析:
type
: 指定系统调用返回值的数据类型。name
: 系统调用函数的名称。atype
: 系统调用单个参数的数据类型。a
: 系统调用的参数。
-
功能实现:
- 定义了一个函数
name
接受参数a
类型为atype
。 - 使用内嵌汇编指令
"int $0x80"
触发 Linux 系统调用机制。 __NR_##name
是系统调用编号,它根据name
动态生成,如__NR_open
对应 open 系统调用的编号。- 参数
a
被传递到寄存器b
中,供系统调用使用。 - 调用结果存储在
__res
变量中。 - 如果
__res
大于等于 0,表示系统调用成功,将__res
强制转换为type
类型并返回。 - 如果
__res
小于 0,表示系统调用失败,将-__res
的值赋给全局变量errno
表示错误码,函数返回-1
。
- 定义了一个函数
2. 系统初始化与配置
-
硬件与内存初始化:
EXT_MEM_K
和DRIVE_INFO
用于读取系统BIOS提供的扩展内存信息和磁盘驱动器信息。memory_end
,buffer_memory_end
,main_memory_start
: 这些变量用于确定系统可用的内存范围,以及分配给缓冲区的内存大小。根据系统总内存的不同,分配给缓冲区的内存也会相应调整。
-
实时钟初始化 (
time_init
)- 通过访问CMOS寄存器读取系统时间,然后将其从二进制编码的十进制(BCD)格式转换为二进制格式,最后计算出系统启动的时间戳。
3. 设备与资源初始化
hd_init
,floppy_init
,blk_dev_init
,chr_dev_init
: 这些函数分别用于初始化硬盘、软盘、块设备和字符设备,确保系统可以访问和管理这些硬件资源。
3.1 hd_init
分析
此函数主要负责硬盘设备的初始化工作,确保系统可以正确地与硬盘交互。
void hd_init(void)
{
blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
set_intr_gate(0x2E,&hd_interrupt);
outb_p(inb_p(0x21)&0xfb,0x21);
outb(inb_p(0xA1)&0xbf,0xA1);
}
blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
这一行代码将硬盘设备的请求处理函数设置为 DEVICE_REQUEST
。在Linux设备驱动模型中,blk_dev 是一个数组,存储了所有注册的块设备信息。MAJOR_NR
表示硬盘设备的主设备号,request_fn
是该设备结构体中的一个成员,用于指定当有I/O
请求到达时应该调用哪个函数来处理这些请求。这里的 DEVICE_REQUEST
就是这个处理函数,它负责调度和执行具体的读写操作。
set_intr_gate(0x2E, &hd_interrupt);
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))
#define set_intr_gate(n,addr) \
_set_gate(&idt[n],14,0,addr)
set_intr_gate
函数来设置一个中断服务程序(ISR
)。0x2E 是硬盘设备的中断请求线(IRQ
)编号,在这里被映射到处理器的中断向量表中。hd_interrupt
是实际的中断处理函数,当硬件检测到中断信号时,CPU会跳转到这个函数以处理中断事件,例如完成的数据传输或错误报告。
outb_p(inb_p(0x21) & 0xfb, 0x21);
这一行代码通过I/O端口 0x21 来访问并修改硬盘控制器的寄存器。inb_p 和 outb 分别是从I/O端口读取字节和向I/O端口写入字节的函数。首先,从端口 0x21 读取当前值,然后与 0xfb 进行按位与运算,目的是清除特定的比特位。这通常是为了禁用某个功能或设置控制器进入某种模式。
3.2 blk_dev_init
分析
该函数功能是初始化块设备请求队列,NR_REQUEST
为32
,将其设备标识符设为-1
,表示未指定设备,并将其下一个请求指针设为NULL
表示队列末尾。这样,函数将块设备的请求队列初始化为空队列状态。
void blk_dev_init(void)
{
int i;
for (i=0 ; i<NR_REQUEST ; i++) {
request[i].dev = -1;
request[i].next = NULL;
}
}
4. main
函数流程
main
函数是程序的入口点,它首先禁用中断,进行必要的初始化,然后重新启用中断。- 执行一系列初始化函数:
mem_init
、trap_init
、blk_dev_init
、chr_dev_init
、tty_init
、time_init
、sched_init
、buffer_init
,这些函数分别用于内存管理、陷阱处理、设备初始化、终端初始化、时间管理、调度初始化和缓冲区初始化。 - 创建一个子进程(
init
),这是Linux系统中的第一个用户级进程,负责后续的系统初始化和进程管理。
5. init
函数细节
init
函数用于创建一个守护进程,关闭标准输入、输出和错误流,然后执行/bin/sh
shell。- 如果
fork
失败或执行execve
失败,会打印错误信息并退出。 - 监控子进程的运行状态,当子进程终止时,会输出相关信息并同步文件系统。
6. main.c
源码
/*
* linux/init/main.c
*
* (C) 1991 Linus Torvalds
*/
#define __LIBRARY__
#include <unistd.h>
#include <time.h>
/*
* we need this inline - forking from kernel space will result
* in NO COPY ON WRITE (!!!), until an execve is executed. This
* is no problem, but for the stack. This is handled by not letting
* main() use the stack at all after fork(). Thus, no function
* calls - which means inline code for fork too, as otherwise we
* would use the stack upon exit from 'fork()'.
*
* Actually only pause and fork are needed inline, so that there
* won't be any messing with the stack from main(), but we define
* some others too.
*/
static inline _syscall0(int,fork)
static inline _syscall0(int,pause)
static inline _syscall1(int,setup,void *,BIOS)
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>
static char printbuf[1024];
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);
extern long startup_time;
/*
* This is set up by the setup-routine at boot-time
*/
#define EXT_MEM_K (*(unsigned short *)0x90002)
#define DRIVE_INFO (*(struct drive_info *)0x90080)
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)
/*
* Yeah, yeah, it's ugly, but I cannot find how to do this correctly
* and this seems to work. I anybody has more info on the real-time
* clock I'd be interested. Most of this was trial and error, and some
* bios-listing reading. Urghh.
*/
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \
inb_p(0x71); \
})
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
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;
struct drive_info { char dummy[32]; } drive_info;
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO;
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end &= 0xfffff000;
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;
#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();
time_init();
sched_init();
buffer_init(buffer_memory_end);
hd_init();
floppy_init();
sti();
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();
}
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;
}
static char * argv_rc[] = { "/bin/sh", NULL };
static char * envp_rc[] = { "HOME=/", NULL };
static char * argv[] = { "-/bin/sh",NULL };
static char * envp[] = { "HOME=/usr/root", NULL };
void init(void)
{
int pid,i;
setup((void *) &drive_info);
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
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())) {
close(0);
if (open("/etc/rc",O_RDONLY,0))
_exit(1);
execve("/bin/sh",argv_rc,envp_rc);
_exit(2);
}
if (pid>0)
while (pid != wait(&i))
/* nothing */;
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();
}
_exit(0); /* NOTE! _exit, not exit() */
}