34.进程2的创建

34.进程2的创建

书接上回,上回书咱们说到,进程 1 通过 open 函数建立了与外设交互的能力,具体其实就是打开了 tty0 这个设备文件,并绑定了标准输入 0,标准输出 1 和 标准错误输出 2 这三个文件描述符。

图片

同时我们看到源码中用 printf 函数,调用 write 函数,向 1 号文件描述符输出了字符串的效果。

图片

到此为止,标志着进程 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 文件。

到目前为止,进程 2 与进程 1 的区别,仅仅是将 0 号文件描述符重新指向了 /etc/rc 文件,其他的没啥区别。

而这个 rc 文件是干嘛的,现在还不用管,肯定是后面 sh 程序要用到的,到时候在说。

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 可执行文件),如何讲可执行文件中的代码和数据加载到内存并运行。

加载到内存并运行又包含着虚拟内存等相关的知识。所以这里面的水很深,了解了这个函数,再加上 fork 函数,基本就可以把操作系统全部核心逻辑都串起来了。

updatedupdated2024-05-102024-05-10