【ostep】02 虚拟化 CPU - 进程

进程 (process)

进程的抽象

进程是一种最基本的抽象。

进程的非正式定义非常简单:进程就 是运行中的程序。程序本身是没有生命周期的,它只是存在磁盘上面的一些指令(也可能 是一些静态数据)。是操作系统让这些字节运行起来,让程序发挥作用。

操作系统决定何时令 CPU 运行何地的指令,通过不断地切换内存中不同程序的指令,类抽象出同时执行多个进程的错觉。

可以很自然地联想到组原中的 IO 中断方式,他通过一种类似回调的方式,令 CPU 中断当前运行的程序,关中断,并将断点地址压栈,开中断,跳转至中断向量指向的内存空间,中断服务会保存当前的现场,例如寄存器状态等,服务结束后会进行现场的恢复

这保存和恢复像极了操作系统对进程的上下文切换。

为了搞清楚我们要保存和恢复什么东西,必须搞清楚一个进程会使用的哪些东西,或者说,在操作系统的抽象中,是什么构成了一个进程。这里有一个名词来描述他 —— 机器状态 (machine state)

机器状态包含主存和寄存器状态,主存状态就是进程用到的主存空间,同时进程也会用到寄存器(还有 PC 等特殊的寄存器)。

策略 (policy) 和机制 (mechanism),在实现操作系统时,策略和机制会被分为两个模块。可以这样理解,机制是策略的细节和组成部分,例如在程序的调度策略 (scheduling policy) 中,上下文切换操作被称为一种机制,而策略则是挑选哪个进程进行上下文切换。

描述进程

数据结构

xv6 的 proc 结构:

// the registers xv6 will save and restore$
// to stop and subsequently restart a process
struct context {
    
     
    int eip; 
    int esp; 
    int ebx; 
    int ecx; 
    int edx; 
    int esi;
    int edi; 
    int ebp;
};

// the different states a process can be in 
enum proc_state {
    
     UNUSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };

// the information xv6 tracks about each process 
// including its register context and state 
struct proc {
    
     
    char *mem;                  // Start of process memory
    uint sz;                    // Size of process memory
    char *kstack;               // Bottom of kernel stack ocessfor this process
    enum proc_state state;      // Process state 
    int pid;                    // Process ID
    struct proc *parent;        // Parent process
    void *chan;                 // If non-zero, sleeping on chan
    int killed;                 // If non-zero, have been killed
    struct file *ofile[NOFILE]; // Open files
    struct inode *cwd;          // Current directory 
    struct context context;     // Switch here to run process
    struct trapframe *tf;       // Trap frame for the // current interrupt
};

进程状态

一个进程有很多状态,主要是:运行(running)、就绪(ready)和阻塞(blocked)。

OS 会提供一些线程操作的 API,它们最少会包括这些:创建(create)、销毁(destroy)、等待(wait)、状态(state)和其他的控制接口(miscellaneous control)。

进程创建

OS 会将代码和静态数据加载到主存空间,不过在此之前还需要分配进程的栈空间(运行时栈 run time stack)。

还有一些其他的初始化任务,例如 IO,在 UNIX 中,每个进程默认拥有三个文件句柄,分别用于输入、输出和错误。

最后通过跳转到进程的入口地址,令 CPU 开始执行接下来的机器指令。

system call 实例

fork

fork 是 linux 下用于创建进程的系统调用。(注意是进程而非线程)

这个接口有些奇怪,不过非常符合 fork 的原意 —— 分叉,当在一个进程调用它时,当前进程和子进程将接连从 fork 调用返回,只不过子进程的返回值是 0 ,而父进程返回子进程的进程 id(当 返回值小于 0 时代表出现错误)。

下面给出一个最简单的创建进程的封装:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int createThread(void(*callback)(void)) {
    
    
    fflush(stdout); fflush(stdin); fflush(stderr);
    int rc = fork();
    if (rc < 0) {
    
    
        exit(1);
    } else if (rc == 0) {
    
     
        callback(); exit(0);
    }
    return rc;
}

由于进程创建时缓冲区会被完整拷贝,为了避免子进程缓冲区带有不必要的东西,我们首先刷新了它。

然后我们调用了 fork,主进程从 fork 返回后没有进入下面的任何一个条件分支,而子进程将进入第二个条件分支,调用 callback 后销毁。

这里子进程的销毁很重要,否则子进程也会从 createThread 函数返回,执行主进程的逻辑,除非我们有必要这样做,否则还是直接销毁他比较好。

wait

wait 用于等待一个子进程结束,被等待子进程按照创建顺序在依次调用中被等待,子进程结束后,父进程将从 wait 调用处返回。

还有一个 waitpid,可以提供一个具体的 pid 来等待,具体的查 man。

exec

exec 用于执行别的程序,调用 exec 后,当前进程将从某个给定的可执行程序中加载代码和静态数据,覆写自己的代码段和静态数据,堆栈也会重新初始化,同时将参数传递给该进程,开始执行。

示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "thread.h"

void thread() {
    
    
    execl("/bin/ls", "ls", "-al", NULL);
}

int main(int argc, char* argv[]) {
    
    
    createThread(thread);
    return 0;
}

exec 系列系统调用:exec[v|l][p][e]

v,表示给定参数以 char* 分别给出;

l,表示参数列表以 char** 给出。

p,表示会从 path 变量中寻找程序和命令,不用给出完整路径。

e,表示使用新的环境变量。

示例:

char* args[] = {
    
    "ls", "-al", NULL};
char *env[] = {
    
    "AA=aa","BB=bb",NULL};

execv("/bin/ls", args);
execvp("ls", args);
execvpe("ls", args, env);

execl("/bin/ls", "ls", "-al", NULL);
execlp("ls", "ls", "-al", NULL);
execle("ls", "ls", "-al", NULL, env);

shell 的实现原理通常是:首先通过 fork 创建一个子进程,然后在子进程调用 exec 来覆写当前进程,最后通过 wait 等待子进程结束。

fork 和 exec 允许我们在创建另一个子进程程序前做一些有意思的工作,例如重定向输出或输入流到某个文件:

$ wc ./thread.c > out

pipe

pipe 是管道,结构类似队列,可以实现跨进程的通信(进程之间的数据共享受限)。

c 原型:

int pipe(int[2] fd);

返回 -1 代表出错,0 代表成功。

调用返回后,fd 指向的内存空间将被顺序写入两个文件句柄(实际上位于内存中),一个用于读,另一个用于写,且几乎可以使用任何标准 I/O 函数进行处理。

例如 work8.c:

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "thread.h"

// 管道句柄
static int fd[2], readPipe, writePipe;

// 子进程 1
void child1() {
    
    
    char buf[1024] = {
    
    0};
    printf("child1 in:");
    fflush(stdout);

    while (scanf("%s", buf) != -1) {
    
    
        // 从 stdin 读,写入 pipe
        write(writePipe, buf, sizeof(buf));
        memset(buf, 0, sizeof(buf));

        // 让出 cpu,让子进程 2 输出
        // (这种做法并不稳定,应该通过进程共享内存实现进程同步锁,这里不展开)
        sleep(0);
        printf("child1 in: ");
        fflush(stdout);
    }
}

// 子进程 2
void child2() {
    
    
    char buf[1024];
    while (read(readPipe, buf, sizeof(buf))) {
    
    
        printf("child2 out: ");
        printf(buf);
        printf("\n");
        fflush(stdout);
    }
}

int main() {
    
    
    // 创建管道
    if (pipe(fd) != -1) {
    
    
        readPipe = fd[0];
        writePipe = fd[1];
    } else {
    
    
        printf("error");
        exit(1);
    }

    // 创建进程
    createThread(child1);
    createThread(child2);

    // 主进程守护进程 1
    wait();

    return 0;
}

进行读写时会阻塞,对读端,若无数据则等待,对写端,若已写入且未被读端取走,则等待。

dup 和 dup2

这里再介绍两个有意思的函数dupdup2

int dup (int oldfd)
int dup2 (int oldfd, int newfd)

shell 中允许使用一种特殊的语法,将即将运行的子进程的 stdout 定向到某个文件,

例如:

$ ls -alh > out.txt

然后我们看一下 out.txt:

ls 的输出内容被定向到 out.txt 中了。

$ cat out.txt
总用量 72K
drwxrwxr-x 2 devgaolihai devgaolihai 4.0K 12月 31 18:21 .
drwxrwxr-x 6 devgaolihai devgaolihai 4.0K 12月 31 17:58 ..
-rw-rw-r-- 1 devgaolihai devgaolihai  535 12月 28 18:38 05_fork.c
-rwxrwxr-x 1 devgaolihai devgaolihai  17K 12月 31 18:15 a.out
-rw-rw-r-- 1 devgaolihai devgaolihai  683 12月 31 18:16 dup.c
-rw-rw-r-- 1 devgaolihai devgaolihai    0 12月 31 18:21 out.txt
-rw-rw-r-- 1 devgaolihai devgaolihai  283 12月 31 15:48 thread.h
-rw-rw-r-- 1 devgaolihai devgaolihai  314 12月 29 22:19 threadTest.c
-rw-rw-r-- 1 devgaolihai devgaolihai  435 12月 30 21:55 work1.c
-rw-rw-r-- 1 devgaolihai devgaolihai  372 12月 31 17:35 work2.c
-rw-rw-r-- 1 devgaolihai devgaolihai  586 12月 31 17:11 work3.c
-rw-rw-r-- 1 devgaolihai devgaolihai  884 12月 31 17:29 work4.c
-rw-rw-r-- 1 devgaolihai devgaolihai  184 12月 31 17:35 work7.c
-rw-rw-r-- 1 devgaolihai devgaolihai 1.2K 12月 31 17:07 work8.c
-rwx------ 1 devgaolihai devgaolihai    9 12月 31 18:15 work8_test.txt

这个功能可以通过dup2实现,下面给出例子:

dup2 会将第二个参数代表的文件映射为第一个参数代表的文件。

#include <fcntl.h>
#include <stdio.h>
#include "thread.h"

// 子进程
void child() {
    
    
    printf("child pro");
}

int main() {
    
    
    // 相当于将 STDOUT_FILENO 变成 target
    // 以后对 STDOUT_FILENO 的操作全部变成对 target 的操作
    int target = open("./dup_out.txt", O_CREAT | O_TRUNC | O_RDWR, 0664);
    dup2(target, STDOUT_FILENO);
    createThread(child);

    return 0;
}

dup则是返回参数代表的文件的另一个文件句柄。

猜你喜欢

转载自blog.csdn.net/qq_16181837/article/details/112295623