本文为学习操作系统源码
(低并发编程)所作笔记,仅供学习参考,不做任何商业用途,若有侵权,请联系删除。
第四部分 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)) ; 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); }
|
我们就只讲第一行代码 setup
的一部分,硬盘信息的获取。
1 2 3 4 5 6 7 8
| struct drive_info { char dummy[32]; } drive_info;
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_sect 和
nr_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索引才是),就是我们通常说的文件描述符。比如有如下命令。
就表示把 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) { ... 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 中常说的
stdin、stdout、stderr。
那这个 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); }
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)) ; 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); }
|
别急,我们一点点看,我仍然是去掉了一些错误校验的旁路分支。
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
可执行文件),如何讲可执行文件中的代码和数据加载到内存并运行。