你管这破玩意儿叫操作系统源码(八)

本文为学习操作系统源码 (低并发编程)所作笔记,仅供学习参考,不做任何商业用途,若有侵权,请联系删除。

第四部分 shell程序的到来

第三十一回 | 拿到硬盘信息

第31回 | 拿到硬盘信息 (qq.com)

上一个大部分的名字叫一个新进程的诞生,讲述了进程 0 调用了 fork 函数创建了一个新的进程 —— 进程 1,并且使其达到了可以被调度的状态,fork 就算正式完成了自己的使命。

1
2
3
4
5
6
7
8
void main(void) {
...
move_to_user_mode();
if (!fork()) {
init();
}
for(;;) pause();
}

由于 fork 函数一调用,就又多出了一个进程,子进程(进程 1)会返回 0,父进程(进程 0)返回子进程的 ID,所以 init 函数只有进程 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
void init(void) {
int pid,i;
setup((void *) &drive_info);
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
if (!(pid=fork())) {
open("/etc/rc",O_RDONLY,0);
execve("/bin/sh",argv_rc,envp_rc);
}
if (pid>0)
while (pid != wait(&i))
/* nothing */;
while (1) {
if (!pid=fork()) {
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;
sync();
}
_exit(0); /* NOTE! _exit, not exit() */
}

我们就只讲第一行代码 setup 的一部分,硬盘信息的获取。

1
2
3
4
5
6
7
8
struct drive_info { char dummy[32]; } drive_info;

// drive_info = (*(struct drive_info *)0x90080);

void init(void) {
setup((void *) &drive_info);
...
}

先看入参。

drive_info 是来自内存 0x90080 的数据,这部分是由之前 第5回 | 进入保护模式前的最后一次折腾内存 讲的 setup.s 程序将硬盘 1 的参数信息放在这里了,包括柱面数、磁头数、扇区数等信息。

setup 是个系统调用,会通过中断最终调用到 sys_setup 函数。关于系统调用的原理,在 第25回 | 通过 fork 看一次系统调用 中已经讲得很清楚了,此处不再赘述。

所以直接看 sys_setup 函数,我仍然是对代码做了少许的简化,去掉了日志打印和错误判断分支,并且仅当作只有一块硬盘,去掉了一层 for 循环。

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
int sys_setup(void * BIOS) {

hd_info[0].cyl = *(unsigned short *) BIOS;
hd_info[0].head = *(unsigned char *) (2+BIOS);
hd_info[0].wpcom = *(unsigned short *) (5+BIOS);
hd_info[0].ctl = *(unsigned char *) (8+BIOS);
hd_info[0].lzone = *(unsigned short *) (12+BIOS);
hd_info[0].sect = *(unsigned char *) (14+BIOS);
BIOS += 16;

hd[0].start_sect = 0;
hd[0].nr_sects =
hd_info[0].head * hd_info[0].sect * hd_info[0].cyl;

struct buffer_head *bh = bread(0x300, 0);
struct partition *p = 0x1BE + (void *)bh->b_data;
for (int i=1;i<5;i++,p++) {
hd[i].start_sect = p->start_sect;
hd[i].nr_sects = p->nr_sects;
}
brelse(bh);

rd_load();
mount_root();
return (0);
}

先看第一部分,硬盘基本信息的赋值的操作。

1
2
3
4
5
6
7
8
9
10
int sys_setup(void * BIOS) {
hd_info[0].cyl = *(unsigned short *) BIOS;
hd_info[0].head = *(unsigned char *) (2+BIOS);
hd_info[0].wpcom = *(unsigned short *) (5+BIOS);
hd_info[0].ctl = *(unsigned char *) (8+BIOS);
hd_info[0].lzone = *(unsigned short *) (12+BIOS);
hd_info[0].sect = *(unsigned char *) (14+BIOS);
BIOS += 16;
...
}

刚刚说了,入参 BIOS 是来自内存 0x90080 的数据,这部分是由之前 第5回 | 进入保护模式前的最后一次折腾内存 讲的 setup.s 程序将硬盘 1 的参数信息放在这里了,包括柱面数、磁头数、扇区数等信息。

所以,一开始先往 hd_info 数组的 0 索引处存上这些信息。我们假设就只有一块硬盘,所以这个数组也只有一个元素。

这个数组里的结构就是 hd_i_struct,就表示硬盘的参数。

1
2
3
4
5
struct hd_i_struct {
// 磁头数、每磁道扇区数、柱面数、写前预补偿柱面号、磁头着陆区柱面号、控制字节
int head,sect,cyl,wpcom,lzone,ctl;
};
struct hd_i_struct hd_info[] = {};

最终效果就是这样。

看第二部分,硬盘分区表的设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static struct hd_struct {
long start_sect;
long nr_sects;
} hd[5] = {}

int sys_setup(void * BIOS) {
...
hd[0].start_sect = 0;
hd[0].nr_sects =
hd_info[0].head * hd_info[0].sect * hd_info[0].cyl;
struct buffer_head *bh = bread(0x300, 0);
struct partition *p = 0x1BE + (void *)bh->b_data;
for (int i=1;i<5;i++,p++) {
hd[i].start_sect = p->start_sect;
hd[i].nr_sects = p->nr_sects;
}
brelse(bh);
...
}

只看最终效果,就是给 hd 数组的五项附上了值。

这表示硬盘的分区信息,每个分区用 start_sectnr_sects,也就是开始扇区和总扇区数来记录。

这些信息是从哪里获取的呢?就是在硬盘的第一个扇区的 0x1BE 偏移处,这里存储着该硬盘的分区信息,只要把这个地方的数据拿到就 OK 了。

所以 bread 就是干这事的,从硬盘读取数据。

1
struct buffer_head *bh = bread(0x300, 0);

第一个参数 0x300 是第一块硬盘的主设备号,就表示要读取的块设备是硬盘一。第二个参数 0 表示读取第一个块,一个块为 1024 字节大小,也就是连续读取硬盘开始处 0 ~ 1024 字节的数据。

拿到这部分数据后,再取 0x1BE 偏移处,就得到了分区信息。

1
struct partition *p = 0x1BE + (void *)bh->b_data;

就这么点事。

至于如何从硬盘中读取指定位置(块)的数据,也就是 bread 函数的内部实现,那是相当复杂的,涉及到与缓冲区配合的部分,还有读写请求队列的设置,以及中断。

OK,目前我们已经把硬盘的基本信息存入了 hd_info[],把硬盘的分区信息存入了 hd[],我们继续往下看。

1
2
3
4
5
6
int sys_setup(void * BIOS) {
...
rd_load();
mount_root();
return (0);
}

就剩两个函数了。

其中 rd_load 是当有 ramdisk 时,也就是虚拟内存盘,才会执行。虚拟内存盘是通过软件将一部分内存(RAM)模拟为硬盘来使用的一种技术,一种小玩法而已,我们就先当做没有,否则很影响看主流程的心情。

mount_root 直译过来就是加载根,再多说几个字是加载根文件系统,有了它之后,操作系统才能从一个根开始找到所有存储在硬盘中的文件,所以它是文件系统的基石,很重要。

为了加载根文件系统,或者说所谓的加载根文件系统,就是把硬盘中的数据加载到内存里,以文件系统的数据格式来解读这些信息。

所以第一,需要硬盘本身就有文件系统的信息,硬盘不能是裸盘,这个不归操作系统管,你为了启动我的 Linux 0.11,必须拿来一块做好了文件系统的硬盘来。

第二,需要读取硬盘的数据到内存,那就必须需要知道硬盘的参数信息,这就是我们本讲所做的事情的意义。

第三十二回 | 加载根文件系统

第32回 | 加载根文件系统 (qq.com)

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
void mount_root(void) {
int i,free;
struct super_block * p;
struct m_inode * mi;

for(i=0;i<64;i++)
file_table[i].f_count=0;

for(p = &super_block[0] ; p < &super_block[8] ; p++) {
p->s_dev = 0;
p->s_lock = 0;
p->s_wait = NULL;
}
p=read_super(0);
mi=iget(0,1);

mi->i_count += 3 ;
p->s_isup = p->s_imount = mi;
current->pwd = mi;
current->root = mi;
free=0;
i=p->s_nzones;
while (-- i >= 0)
if (!set_bit(i&8191,p->s_zmap[i>>13]->b_data))
free++;

free=0;
i=p->s_ninodes+1;
while (-- i >= 0)
if (!set_bit(i&8191,p->s_imap[i>>13]->b_data))
free++;
}

从整体上说,它就是要把硬盘中的数据,以文件系统的格式进行解读,加载到内存中设计好的数据结构,这样操作系统就可以通过内存中的数据,以文件系统的方式访问硬盘中的一个个文件了。

硬盘中的文件系统格式是怎样的

首先硬盘中的文件系统,无非就是硬盘中的一堆数据,我们按照一定格式去解析罢了。Linux-0.11 中的文件系统是 MINIX 文件系统,它就长成这个样子。

每一个块结构的大小是 1024 字节,也就是 1KB,硬盘里的数据就按照这个结构,妥善地安排在硬盘里。

引导块就是我们系列最开头说的启动区,当然不一定所有的硬盘都有启动区,但我们还是得预留出这个位置,以保持格式的统一。

超级块用于描述整个文件系统的整体信息,我们看它的字段就知道了,有后面的 inode 数量,块数量,第一个块在哪里等信息。有了它,整个硬盘的布局就清晰了。

inode 位图和块位图,就是位图的基本操作和作用了,表示后面 inode 和块的使用情况,和我们之前讲的内存占用位图 mem_map[] 是类似的。

再往后,inode 存放着每个文件或目录的元信息和索引信息,元信息就是文件类型、文件大小、修改时间等,索引信息就是大小为 9 的 i_zone[9] 块数组,表示这个文件或目录的具体数据占用了哪些块。

其中块数组里,0~6 表示直接索引,7 表示一次间接索引,8 表示二次间接索引。当文件比较小时,比如只占用 2 个块就够了,那就只需要 zone[0] 和 zone[1] 两个直接索引即可。

再往后,就都是存放具体文件或目录实际信息的了。如果是一个普通文件类型的 inode 指向的块,那里面就直接是文件的二进制信息。如果是一个目录类型的 inode 指向的块,那里面存放的就是这个目录下的文件和目录的 inode 索引以及文件或目录名称等信息。

好了,文件系统格式的说明,我们就简单说明完毕了,MINIX 文件系统已经过时,你可以阅读我之前写的 图解 | 你管这破玩意叫文件系统?来全面了解一个 ext2 文件系统的来龙去脉,基本思想都是一样的。

内存中用于文件系统的数据结构有哪些

1
2
3
4
5
6
7
8
9
10
11
12
13
struct file {
unsigned short f_mode;
unsigned short f_flags;
unsigned short f_count;
struct m_inode * f_inode;
off_t f_pos;
};

void mount_root(void) {
for(i=0;i<64;i++)
file_table[i].f_count=0;
...
}

把 64 个 file_table 里的 f_count 清零。

这个 file_table 表示进程所使用的文件,进程每使用一个文件,都需要记录在这里,包括文件类型、文件 inode 索引信息等,而这个 f_count 表示被引用的次数,此时还没有引用,所以设置为零。

而这个 file_table 的索引(当然准确说是进程的filp索引才是),就是我们通常说的文件描述符。比如有如下命令。

1
echo "hello" > 0

就表示把 hello 输出到 0 号文件描述符。

0 号文件描述符是哪个文件呢?就是 file_table[0] 所表示的文件。

这个文件在哪里呢?注意到 file 结构里有个 f_inode 字段,通过 f_inode 即可找到它的 inode 信息,inode 信息包含了一个文件所需要的全部信息,包括文件的大小、文件的类型、文件所在的硬盘块号,这个所在硬盘块号,就是文件的位置咯。

1
2
3
4
5
6
7
8
9
10
11
struct super_block super_block[8];
void mount_root(void) {
...
struct super_block * p;
for(p = &super_block[0] ; p < &super_block[8] ; p++) {
p->s_dev = 0;
p->s_lock = 0;
p->s_wait = NULL;
}
...
}

又是把一个数组 super_block 做清零工作。

这个 super_block 存在的意义是,操作系统与一个设备以文件形式进行读写访问时,就需要把这个设备的超级块信息放在这里。

这样通过这个超级块,就可以掌控这个设备的文件系统全局了。

果然,接下来的操作,就是读取硬盘的超级块信息到内存中来。

1
2
3
4
5
void mount_root(void) {
...
p=read_super(0);
...
}

read_super 就是读取硬盘中的超级块。

接下来,读取根 inode 信息。

1
2
3
4
5
6
struct m_inode * mi;
void mount_root(void) {
...
mi=iget(0,1);
...
}

然后把该 inode 设置为当前进程(也就是进程 1)的当前工作目录和根目录。

1
2
3
4
5
6
void mount_root(void) {
...
current->pwd = mi;
current->root = mi;
...
}

然后记录块位图信息。

1
2
3
4
5
6
7
void mount_root(void) {
...
i=p->s_nzones;
while (-- i >= 0)
set_bit(i&8191, p->s_zmap[i>>13]->b_data);
...
}

最后记录 inode 位图信息。

1
2
3
4
5
6
void mount_root(void) {
...
i=p->s_ninodes+1;
while (-- i >= 0)
set_bit(i&8191, p->s_imap[i>>13]->b_data);
}

就完事了。

其实整体上就是把硬盘中文件系统的各个信息,搬到内存中。之前的图可以说非常直观了。

有了内存中的这些结构,我们就可以顺着根 inode,找到所有的文件了。

我们继续往下看 init 函数。

1
2
3
4
5
6
void init(void) {
setup((void *) &drive_info);
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
}

看到这相信你也明白了。

之前 setup 函数的一番折腾,加载了根文件系统,顺着根 inode 可以找到所有文件,就是为了下一行 open 函数可以通过文件路径,从硬盘中把一个文件的信息方便地拿到。

第三十三回 | 打开终端设备文件

第33回 | 打开终端设备文件 (qq.com)

open 函数会触发 0x80 中断,最终调用到 sys_open 这个系统调用函数,相信你已经很熟悉了。

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
44
45
46
47
48
49
50
open.c

struct file file_table[64] = {0};

int sys_open(const char * filename,int flag,int mode) {
struct m_inode * inode;
struct file * f;
int i,fd;
mode &= 0777 & ~current->umask;

for(fd=0 ; fd<20; fd++)
if (!current->filp[fd])
break;
if (fd>=20)
return -EINVAL;
current->close_on_exec &= ~(1<<fd);

f=0+file_table;
for (i=0 ; i<64 ; i++,f++)
if (!f->f_count) break;
if (i>=64)
return -EINVAL;

(current->filp[fd]=f)->f_count++;

i = open_namei(filename,flag,mode,&inode);

if (S_ISCHR(inode->i_mode))
if (MAJOR(inode->i_zone[0])==4) {
if (current->leader && current->tty<0) {
current->tty = MINOR(inode->i_zone[0]);
tty_table[current->tty].pgrp = current->pgrp;
}
} else if (MAJOR(inode->i_zone[0])==5)
if (current->tty<0) {
iput(inode);
current->filp[fd]=NULL;
f->f_count=0;
return -EPERM;
}
if (S_ISBLK(inode->i_mode))
check_disk_change(inode->i_zone[0]);

f->f_mode = inode->i_mode;
f->f_flags = flag;
f->f_count = 1;
f->f_inode = inode;
f->f_pos = 0;
return (fd);
}

这么大一坨别怕,我们慢慢来分析,我先用一张图来描述这一大坨代码的作用。

第一步,在进程文件描述符数组 filp 中找到一个空闲项。还记得进程的 task_struct 结构吧,其中有一个 filp 数组的字段,就是我们常说的文件描述符数组,这里先找到一个空闲项,将空闲地方的索引值即为 fd。

1
2
3
4
5
6
7
8
9
int sys_open(const char * filename,int flag,int mode) {
...
for(int fd=0 ; fd<20; fd++)
if (!current->filp[fd])
break;
if (fd>=20)
return -EINVAL;
...
}

由于此时当前进程,也就是进程 1,还没有打开过任何文件,所以 0 号索引处就是空闲的,fd 自然就等于 0。

第二步,在系统文件表 file_table 中找到一个空闲项。一样的玩法。

1
2
3
4
5
6
7
8
9
10
int sys_open(const char * filename,int flag,int mode) {
int i;
...
struct file * f=0+file_table;
for (i=0 ; i<64; i++,f++)
if (!f->f_count) break;
if (i>=64)
return -EINVAL;
...
}

注意到,进程的 filp 数组大小是 20,系统的 file_table 大小是 64,可以得出,每个进程最多打开 20 个文件,整个系统最多打开 64 个文件。

第三步,将进程的文件描述符数组项和系统的文件表项,对应起来。代码中就是一个赋值操作。

1
2
3
4
5
int sys_open(const char * filename,int flag,int mode) {
...
current->filp[fd] = f;
...
}

第四步,根据文件名从文件系统中找到这个文件。其实相当于找到了这个 tty0 文件对应的 inode 信息。

1
2
3
4
5
6
7
8
9
int sys_open(const char * filename,int flag,int mode) {
...
// filename = "/dev/tty0"
// flag = O_RDWR 读写
// 不是创建新文件,所以 mode 没用
// inode 是返回参数
open_namei(filename,flag,mode,&inode);
...
}

接下来判断 tty0 这个 inode 是否是字符设备,如果是字符设备文件,那么如果设备号是 4 的话,则设置当前进程的 tty 号为该 inode 的子设备号。并设置当前进程tty 对应的tty 表项的父进程组号等于进程的父进程组号。

最后第五步,填充 file 数据。其实就是初始化这个 f,包括刚刚找到的 inode 值。最后返回给上层文件描述符 fd 的值,也就是零。

1
2
3
4
5
6
7
8
9
10
int sys_open(const char * filename,int flag,int mode) {
...
f->f_mode = inode->i_mode;
f->f_flags = flag;
f->f_count = 1;
f->f_inode = inode;
f->f_pos = 0;
return (fd);
...
}

打开文件,返回给上层的是一个文件描述符,然后操作系统底层进行了一系列精巧的构造,使得一个进程可以通过一个文件描述符 fd,找到对应文件的 inode 信息。

好了,我们接着再往下看两行代码。接下来,两个一模一样的 dup 函数,什么意思呢?

其实,刚刚的 open 函数返回的为 0 号 fd,这个作为标准输入设备

接下来的 dup 为 1 号 fd 赋值,这个作为标准输出设备

再接下来的 dup 为 2 号 fd 赋值,这个作为标准错误输出设备

熟不熟悉?这就是我们 Linux 中常说的 stdinstdoutstderr

那这个 dup 又是什么原理呢?非常简单,首先仍然是通过系统调用方式,调用到 sys_dup 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int sys_dup(unsigned int fildes) {
return dupfd(fildes,0);
}

// fd 是要复制的文件描述符
// arg 是指定新文件描述符的最小数值
static int dupfd(unsigned int fd, unsigned int arg) {
...
while (arg < 20)
if (current->filp[arg])
arg++;
else
break;
...
(current->filp[arg] = current->filp[fd])->f_count++;
return arg;
}

我仍然是把一些错误校验的旁路逻辑去掉了。

那这个函数的逻辑非常单纯,就是从进程的 filp 中找到下一个空闲项,然后把要复制的文件描述符 fd 的信息,统统复制到这里

那根据上下文,这一步其实就是把 0 号文件描述符,复制到 1 号文件描述符,那么 0 号和 1 号文件描述符,就统统可以通过一条路子,找到最终 tty0 这个设备文件的 inode 信息了。

那下一个 dup 就自然理解了吧,直接再来一张图。

ok,进程 1 的 init 函数的前四行就讲完了,此时进程 1 已经比进程 0 多了与 外设交互的能力,具体说来是 tty0 这个外设(也是个文件,因为 Linux 下一切皆文件)交互的能力,这句话怎么理解呢?什么叫多了这个能力?

因为进程 fork 出自己子进程的时候,这个 filp 数组也会被复制,那么当进程 1 fork 出进程 2 时,进程 2 也会拥有这样的映射关系,也可以操作 tty0 这个设备,这就是“能力”二字的体现。

而进程 0 是不具备与外设交互的能力的,因为它并没有打开任何的文件,filp 数组也就没有任何作用。

进程 1 刚刚创建的时候,是 fork 的进程 0,所以也不具备这样的能力,而通过 setup 加载根文件系统,open 打开 tty0 设备文件等代码,使得进程 1 具备了与外设交互的能力,同时也使得之后从进程 1 fork 出来的进程 2 也天生拥有和进程 1 同样的与外设交互的能力。

第三十四回 | 进程2的创建

第34回 | 进程2的创建 (qq.com)

到此为止,标志着进程 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
void init(void) {
...
if (!(pid=fork())) {
close(0);
open("/etc/rc",O_RDONLY,0);
execve("/bin/sh",argv_rc,envp_rc);
_exit(2);
}
if (pid>0)
while (pid != wait(&i))
/* nothing */;
while (1) {
if (!(pid=fork())) {
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() */
}

别急,我们一点点看,我仍然是去掉了一些错误校验的旁路分支。

1
2
3
4
5
6
7
8
9
10
void init(void) {
...
if (!(pid=fork())) {
close(0);
open("/etc/rc",O_RDONLY,0);
execve("/bin/sh",argv_rc,envp_rc);
_exit(2);
}
...
}

先看这个第一段,我们先尝试口述翻译一遍。

1. fork 一个新的子进程,此时就是进程 2 了。

2. 在进程 2 里关闭(close) 0 号文件描述符。

3. 只读形式打开(open) rc 文件。

4. 然后执行(execve) sh 程序。

听起来还蛮合逻辑的,创建进程(fork)、关闭(close)、打开(open)、执行(execve)四步走,接下来我们一点点拆解。

fork

fork 前面讲过了,就是将进程的 task_struct 结构进行一下复制,比如进程 0 fork 出进程 1 的时候。

之后,新进程再重写一些基本信息,包括元信息和 tss 里的寄存器信息。再之后,用 copy_page_tables 复制了一下页表(这里涉及到写时复制的伏笔)。

比如进程 0 复制出进程 1 的时候,页表是这样复制的。

而这里的进程 1 fork 出进程 2,也是同样的流程,不同之处在于两点细节:

第一点,进程 1 打开了三个文件描述符并指向了 tty0,那这个也被复制到进程 2 了,具体说来就是进程结构 task_struct 里的 flip[] 数组被复制了一份。

1
2
3
4
5
struct task_struct {
...
struct file *filp[NR_OPEN];
...
};

而进程 0 fork 出进程 1 时是没有复制这部分信息的,因为进程 0 没有打开任何文件。这也是刚刚说的与外设交互能力的体现,即进程 0 没有与外设交互的能力,进程 1 有,哎,其实就是这个 flip 数组里有没有东西而已嘛~

第二点,进程 0 复制进程 1 时页表的复制只有 160 项,也就是映射 640K,而之后进程的复制,统统都是复制 1024 项,也就是映射 4M 空间。

1
2
3
4
5
int copy_page_tables(unsigned long from,unsigned long to,long size) {
...
nr = (from==0)?0xA0:1024;
...
}

整体看就是如图所示。

除此之外,就没有别的区别了。

close

好了,我们继续看。

1
2
3
4
5
6
7
8
9
10
void init(void) {
...
if (!(pid=fork())) {
close(0);
open("/etc/rc",O_RDONLY,0);
execve("/bin/sh",argv_rc,envp_rc);
_exit(2);
}
...
}

fork 完之后,后面 if 里面的代码都是进程 2 在执行了。

close(0) 就是关闭 0 号文件描述符,也就是进程 1 复制过来的打开了 tty0 并作为标准输入的文件描述符,那么此时 0 号文件描述符就空出来了。

下面是 close 对应的系统调用函数,很简单。

1
2
3
4
5
int sys_close(unsigned int fd) {   
...
current->filp[fd] = NULL;
...
}

open

下来 open 函数以只读形式打开了一个叫 /etc/rc 的文件,刚好占据了 0 号文件描述符的位置。

1
2
3
4
5
6
7
8
9
void init(void) {
...
if (!(pid=fork())) {
...
open("/etc/rc",O_RDONLY,0);
...
}
...
}

这个 rc 文件表示配置文件,具体什么内容,取决于你的硬盘里这个位置处放了什么内容,与操作系统内核无关,所以我们暂且不用管。

此时,进程 2 与进程 1 几乎完全一样,只不过进程 2 通过 close 和 open 操作,将原来进程 1 的指向标准输入的 0 号文件描述符,重新指向了 /etc/rc 文件。

execve

好,接下来进程 2 就将变得不一样了,会通过一个经典的,也是最难理解的 execve 函数调用,使自己摇身一变,成为 /bin/sh 程序继续运行,这就是下一章的重点!

1
2
3
4
5
6
7
8
9
void init(void) {
...
if (!(pid=fork())) {
...
execve("/bin/sh",argv_rc,envp_rc);
...
}
...
}

这里就包含着操作系统究竟是如何加载并执行一个程序的原理,包括如何从文件系统中找到这个文件,如何解析一个可执行文件(在现代的 Linux 里称作 ELF 可执行文件),如何讲可执行文件中的代码和数据加载到内存并运行。


你管这破玩意儿叫操作系统源码(八)
https://www.spacezxy.top/2023/04/06/OperatingSystem/Operating-system-source-code-8/
作者
Xavier ZXY
发布于
2023年4月6日
许可协议