操作系统导论-进程概述

进程概述

相关概念

Q1:什么是进程?

进程就是运行中的程序。程序是存在磁盘上的一些指令的合集,包含一些系统调用、过程调用以及静态数据等。程序是没有生命周期的,谁听说过某某某程序到了晚上8点就不执行了。而进程是有生命周期的。

Q2:为什么需要进程/为什么需要虚拟化?

日常使用计算机的时候,可能同时会处理多种事情,比如说一边跑着深度学习框架,一边看视频、玩LOL。系统中存在多个进程同时进行,这时候就需要虚拟化virtualization,让CPU提供这样一种假象,一个进程只运行一个时间片,然后就切换到其他进程。进程是操作系统提供的抽象。

Q3:程序如何转换成进程的?

OS 运行必须做的一件事就是把代码和所有的静态数据加载到内存中,加载到进程的地址空间中。C语言程序使用栈存放局部变量、函数参数和返回地址。操作系统分配这些内存,并提供给进程。 操作系统也可能会用参数初始化栈。对于动态开辟的空间比如说malloc、realloc以及calloc开辟的空间则是存放在堆区,跟链表、树、散列表等一样。同样的操作系统也会分配相当的内存给进程。

操作系统导论-进程概述_第1张图片

进程API

1、fork()

系统调用,创建进程,示例代码

#include 
#include 
#include 

int main(int argc, char **argv) {
    printf("hello world (pid : %d)\n", (int)getpid());
    int rc = fork();
    if (rc < 0) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
        printf("hello, I am child (pid : %d)\n", (int)getpid());
    } else {
        printf("hello, I am parent of %d (pid : %d)\n", rc, (int)getpid());
    }

    return 0;
}

2、wait()

子进程与父进程输出顺序可能不一定,但是可以使用wait(NULL)来确定要delay的父/子进程

#include 
#include 
#include 
#include 

int main() {
    printf("hello world (pid : %d)\n", (int)getpid());
    int rc = fork();
    if (rc < 0) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
        printf("hello, I am child (pid : %d)\n", (int)getpid());
    } else {
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc : %d) (pid : %d)\n", rc, wc, (int)getpid());
    }

    return 0;
}

3、exec族函数

系统调用,可以让各个进程执行不同的任务。

//通过man手册查询到的exec家族函数及参数列表如下
int execl(const char *path, const char *arg, .../* (char  *) NULL */);
int execlp(const char *file, const char *arg, ...  /* (char  *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

#include 
#include 
#include 
#include 
#include 

int main(int argc, char **argv) {
    printf("hello world. (pid : %d)\n", (int)getpid());
    int rc = fork();
    if (rc < 0) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
        printf("hello, I am child (pid : %d)\n", (int)getpid());
        char *myargs[3];
        myargs[0] = strdup("wc");
        myargs[1] = strdup("p3.c");
        myargs[2] = NULL;
        execvp(myargs[0], myargs);
        printf("This should not print out!\n");
    } else {
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc : %d) (pid : %d)\n", rc, wc, (int)getpid());
    }

    return 0;
}

Q : 过程调用与系统调用区别?

过程调用,简言之就是在一个程序中调用另一个程序,或者说另一段程序。系统调用就是在用户程序中调用系统提供的功能子程序,说白了就是程序员用的API。过程调用更多的是静态调用,调用程序和被调用程序往往在一个程序中。并且过程函数在编译之后对应的逻辑地址是不变的。系统调用是一种动态调用,系统调用的处理代码独立于程序之外。

**注意:**系统调用也是一种过程调用,但是是一种特殊的过程调用,操作系统使用陷入指令隐藏了这部分系统调用内部实现。

应对虚拟化的挑战-受限直接执行

为了让程序执行受限制的操作,操作系统把执行过程分成内核态和用户态。目的是使得进程既能够执行I/O请求和其他操作又不至于完全获得操作系统的控制权。

**用户态:**应用程序不能完全访问硬件资源

**内核态:**操作系统可以访问全部资源,包括硬件资源。

为了使得系统在用户态与内核态的切换,操作系统提供陷入指令。**陷入指令:**负责用户态到内核态,以及内核态到用户态的切换。

内核态 硬件 用户态
在进程列表上创建条目
为程序分配内存
将程序加载到内存中
根据 argv 设置程序栈
用寄存器/程序计数器填充内核栈
从陷阱返回
从内核栈恢复寄存器
转向用户模式 跳到 main
运行 main
……
调用系统调用 陷入操作系统
将寄存器保存到内核栈
转向内核模式
跳到陷阱处理程序
处理陷阱
做系统调用的工作
从陷阱返回
从内核栈恢复寄存器
转向用户模式
跳到陷阱之后的程序计数器
……从 main 返回
陷入(通过 exit())
释放进程的内存将进程
从进程列表中清除

Q:不同进程之间是如何切换的呢?

主要有以下几种方式

1、协作方式:等待系统调用。运行时间过长的进程被假定会定期放弃 CPU

2、非协作方式:操作系统控制,利用时钟中断,就是让正在执行的进程停止,赋予另外一个进程操作系统的部分控制权,然后再进行切换。也是最主要的手段,因此在操作系统启动的时候,时钟中断也会启动。3、保存和恢复上下文:操作系统要做的就是为当前正在执行的进程保存一些寄存器 的值(例如,到它的内核栈),并为即将执行的进程恢复一些寄存器的值(从它的内核栈)。 这样一来,操作系统就可以确保最后执行从陷阱返回指令时,不是返回到之前运行的进程, 而是继续执行操一个进程。所谓的上下文切换,就是程序状态的加载和程序状态 的保存。

时钟中断执行流程如下,以两个进程A、B为例:

内核态 硬件 用户态
进程 A……
时钟中断
将寄存器(A)保存到内核栈(A)
转向内核模式
跳到陷阱处理程序
处理陷阱
调用 switch()例程
将寄存器(A)保存到进程结构(A)
将进程结构(B)恢复到寄存器(B)
从陷阱返回(进入 B)
从内核栈(B)恢复寄存器(B)
转向用户模式
跳到 B 的程序计数器
进程 B……

在此协议中,有两种类型的寄存器保存/恢复:

第一种是发生时钟中断的时候。 在这种情况下,运行进程的用户寄存器由硬件隐式保存,使用该进程的内核栈。

第二种是当操作系统决定从 A 切换到 B。在这种情况下,内核寄存器被软件(即 OS)明确地保存, 但这一被存储在该进程的进程结构的内存中。后一个操作让系统从好像刚刚由 A 陷入内核, 变成好像刚刚由 B 陷入内核。

//测量系统调用成本,执行0字节读取,记录时间
/**
	int gettimeofday(struct timeval *tv, struct timezone *tz);这是获取当前时间系统调用,返回的是微妙
	数据结构如下:
		struct timeval {
               time_t      tv_sec;    
               suseconds_t tv_usec; 
        };
*/
#include 
#include 
#include 

int main() {
    int fd = open("./test", O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU);
    if (fd == -1) {
        fprintf(stderr, "open file failed\n");
        exit(1);
    }
    char buff[10];
    struct timeval start, end;
    for (int i = 0; i < 10; i++) {
        gettimeofday(&start, NULL);
        read(fd, buff, 0);
        gettimeofday(&end, NULL);
        printf("Use : %ld\n", end.tv_usec - start.tv_usec);
        printf("%ld : %ld\n", end.tv_usec, start.tv_usec);
    }
    close(fd);
    return 0;
}


//上下文切换,第一个进程向第一个管道写入数据,然后等待第二个数据的读取。第二个管道写入,第一个管道读取,计算上下文切换时间差。
#include 
#include 
#include 
#include 

int main() {
    int pi1[2], pi2[2];
    char buff1[30], buff2[30];
    int p1 = pipe(pi1);
    int p2 = pipe(pi2);
    if (p1 < 0 || p2 < 0) {
        fprintf(stderr, "pipe failed\n");
        exit(1);
    }
    struct timeval start, end;
    int rc = fork();
    for (int i = 0; i < 10; i++) {
        if (rc < 0) {
            fprintf(stderr, "fork failed\n");
            exit(1);
        } else if (rc == 0) {
            read(pi1[0], buff1, 25);
            gettimeofday(&end, NULL);
            printf("child : %ld\n", end.tv_usec - atol(buff1));
            gettimeofday(&start, NULL);
            sprintf(buff2, "%ld", start.tv_usec);
            write(pi2[1], buff2, 25);
        } else {
            gettimeofday(&start, NULL);
            sprintf(buff1, "%ld", start.tv_usec);
            write(pi1[1], buff1, 25);
            read(pi2[0], buff2, 25);
            gettimeofday(&end, NULL);
            printf("father : %ld\n", end.tv_usec - atol(buff2));
        }
    }

    return 0;
}

你可能感兴趣的:(体系结构,系统架构)