zl程序教程

您现在的位置是:首页 >  系统

当前栏目

Linux 0.11-进程2的创建-34

Linux进程 创建 34 0.11
2023-09-14 09:02:33 时间

Linux 0.11-进程2的创建-34


进程2的创建

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

图片

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

图片

虽然此时进程1打开的0,1,2三个文件描述符指向的都是一个字符设备文件,即显示屏,但是三个文件描述符服务于三种不同情境,例如: 接收键盘输入时,会调用0号文件描述符,向屏幕输入时,会调用1号文件描述符,2号文件描述符用于异常信息输出。

到此为止,标志着进程 1 的工作基本结束了,准确说是能力建设的工作结束了,接下来就是控制流程创建新的进程了,我们继续往下看。

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() */
}

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

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)四步走,接下来我们一点点拆解。

linux是如何将进程创建和执行过程分开的—fork和exec

fork

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

图片

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

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

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

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

struct task_struct {
    ...
    struct file *filp[NR_OPEN];
    ...
};

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

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

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

整体看就是如图所示。

图片

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

close

好了,我们继续看。

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 对应的系统调用函数,很简单。

int sys_close(unsigned int fd) {   
    ...
    current->filp[fd] = NULL;
    ...
}

open

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

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 程序继续运行,这就是下一章的重点!

void init(void) {
    ...
    if (!(pid=fork())) {
        ...
        execve("/bin/sh",argv_rc,envp_rc);
        ...
    }
    ...
}

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

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

欲知后事如何,且听下回分解。


转载

本文转载至闪客图解操作系统系列文章