《操作系统导论》学习笔记(二):CPU虚拟化(进程)

《操作系统导论》学习笔记(一):操作系统概览

目录

    • 进程
        • 1. 虚拟地址空间
        • 2. 用户空间分区
        • 3. 内核空间的进程控制块
    • 子进程
        • 1. f o r k ( ) fork() fork() 创建子进程
        • 2. w a i t ( ) wait() wait() 阻塞当前进程
        • 3. e x e c ( ) exec() exec() 函数簇
        • 4. API 有什么用?
        • 5. 系统命令、接口API和系统调用关系

进程

《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第1张图片
程序:指令和数据的集合,一般作为目标文件保存在磁盘中。
进程:程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。

可执行程序位于磁盘中,需要将静态程序加载到内存生成动态的进程,CPU才可以不停地取指执行。进程的创建过程涉及到许多陌生的概念,诸如进程控制块、进程队列、用户空间等待等,下面将从虚拟地址空间开始阐述。

1. 虚拟地址空间

《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第2张图片
虚拟地址空间:在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中,这个沙盘就是虚拟地址空间(virtual address space)。虚拟地址空间由内核空间(kernel space)和用户空间(user space)两部分组成。

下图为内核空间和用户空间的具体构成:
《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第3张图片

2. 用户空间分区

程序包含代码及数据,编译链接生成的可执行文件时汇编形式,装载到内存中是二进制文件。程序是通过变量访问数据,但在二进制下是没有变量的概念,只能通过内存地址访问数据。如果全部变量都通过地址访问,这样是低效且不现实的。因而,需要根据不同变量的性质进行分区

  • 局部变量内容短小,需要频繁访问,但是生命周期很短,通常只在一个方法内存活,就专门从内存划分出一块较小区域命名为栈(stack),由编译器分配与回收,效率高。
  • 较大的结构体可能不需要太频繁的访问,但生命周期较长,通常很多个方法中都会用到,就划出另一较大区域命名为堆(heap),由程序员自主使用内存API函数分配回收。

《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第4张图片
《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第5张图片

3. 内核空间的进程控制块

操作系统在创建进程时会在内核空间配备一个进程控制块(PCB),包含描述进程当前情况及管理进程全部信息的数据结构。在Linux操作系统中的进程控制块实际是一个task_struct结构体,放在sched.h,以下简要介绍。
《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第6张图片
(1) 进程状态(process state):进程的核心运行状态包括就绪(Ready)、阻塞(Blocked)及运行(Running)。

enum proc_state {
      READY, RUNNING, READY };

《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第7张图片
就绪(Ready):进程拥有运行所需的所有资源,等待分配CPU。
运行(Running):进程运行在CPU上,即CPU正在运行该进程包含的指令。
阻塞(Blocked)/等待(Waiting):进程在运行时发生CPU以外事件的请求(如I/O请求)从而放弃CPU使用权。

《操作系统导论》实验一:模拟进程状态转换

(2) 进程标识符(process identifier/number):描述本进程的唯一标识符,用来区别其他进程。

int pid;		// Process ID

(3) 程序计数器(program counter)及寄存器信息(registers):进程切换时的入口地址及需要保存的信息

// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
     
  int eip;		// 程序计数器(PC),存放下一个CPU指令存放的内存地址
  int ebx;		// 基址寄存器, 在内存寻址时存放基地址
  int ecx;		// 计数器(counter),loop循环的内定计数器
  int edx;		// 用来放整数除法产生的余数
  int esi;		// 源变址寄存器
  int edi;		// 目的变址寄存器
  int esp;		// 栈指针寄存器
  int ebp;		// 基址指针寄存器
};

(4)内存限制(memory limits)

char *mem;		// Start of process memory
uint sz;		// Size of process memory

(5)打开文件列表

struct file *ofile[NOFILE]; // Open files

(6) 进程指针(pointer):采用指针将进程控制块之间相互链接
《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第8张图片
进程链表组织方式是系统设置就绪队列头指针、阻塞队列头指针、运行队列头指针,按照进程的状态将进程的PCB挂在对应头指针后组成队列。

下面是进程的创建过程:
《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第9张图片
创建进程时,操作系统为进程生成唯一的进程控制块(PCB)并挂在进程队列,用户空间开辟内存,装入程序的变量及代码,栈内保存 main() 的参数argc/argv,清空寄存器内容,main()入口地址送到程序计数器PC;CPU执行程序main()的指令,遇到return语句返回操作系统;操作系统释放进程内容,并将进程从进程队列中移除。

子进程

1. f o r k ( ) fork() fork() 创建子进程

(1) 头文件及函数原型

#include 
#define int pid_t 
pid_t fork( void);

返回值:成功调用一次则返回两个值,父进程返回子进程PID,子进程返回0;调用失败则返回-1。

(2) 函数说明
主函数main()运行时会自动创建进程,称为父进程;fork()系统调用用于创建一个新进程,称为子进程。创建子进程时,子进程会在内核中拥有自己的进程控制块(task_struct),从而拥有不同于父进程的PID。但同时,子进程复制父进程其余一切(堆栈、代码段等),而fork()作为系统调用保存在父进程和子进程的栈中,因而会返回两次,产生两个返回值。
《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第10张图片

// p1.c
#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) {
     	// 子进程内rc=0
        printf("hello, I am child (pid:%d)\n", (int) getpid());
    } else {
     				// 父进程内rc为子进程ID  getpid()为父进程ID
        printf("hello, I am parent of %d (pid:%d)\n", rc, (int) getpid());
    }
    return 0;
}

在这里插入图片描述
父进程PID为3838,子进程PID为3829。创建子进程后,两个进程将执行fork()系统调用之后的下一条指令,因而不会再输出hello world,但可以根据rc不同返回值输出下面两行内容。

2. w a i t ( ) wait() wait() 阻塞当前进程

(1) 头文件及函数原型

#include 
#define int pid_t 
pid_t wait (int * status);

参数:status 不是NULL时,子进程的结束状态值会由参数 status 返回;如果不关心子进程结束状态可置status为NULL。
返回值:执行成功则返回子进程PID,失败则返回-1。

(2) 函数说明
不使用wait()时,父进程和子进程会同时运行;使用wait()后,父进程会等待子进程执行完毕并返回子进程PID后再执行。

// p2.c
#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) {
     	// 子进程内rc=0
        printf("hello, I am child (pid:%d)\n", (int) getpid());
	    sleep(1);			// 等待一段时间再退出当前进程
    } else {
     				// wc为子进程PID
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
	       rc, wc, (int) getpid());
    }
    return 0;
}

在这里插入图片描述
父进程PID为838,子进程PID为841,wait()返回值wc=841。

3. e x e c ( ) exec() exec() 函数簇

(1) 头文件及函数原型
exec指的是一组函数族,并不存在某一个具体的exec(),现选取execvp()作为例子了解。

#include 
int execvp(const char *file, char *const argv[]);

参数:file为需要运行的文件名,argv[]为输入的参数列表
返回值:执行成功则函数不会返回,执行失败则直接返回-1

(3) 函数说明
exec()函数簇可以使子进程摆脱和父进程内容的相似性,执行一个完全不同的程序。

// p3.c
#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];			// strdup()字符串拷贝库函数
        myargs[0] = strdup("wc");   // 程序: "wc" (字符统计)
        myargs[1] = strdup("p3.c"); // 参数: 需要统计的文件
        myargs[2] = NULL;           // 命令行结束标志
        execvp(myargs[0], myargs);  // 统计行、单词、字节数
        printf("this shouldn't print out");
    } else {
     
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
	       rc, wc, (int) getpid());
    }
    return 0;
}

《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第11张图片
子进程重载字符统计程序,统计p3.c的行数为32、单词数为123、字节数为966

4. API 有什么用?

在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器),诸如windows下的cmd、unix下的bash。打开shell时,shell相当于父进程,shell可以接受命令,然后利用fork() 创建子进程执行命令,执行完毕结束子进程返回shell,从而接受下一条命令。
(1) 控制台输入wc p3.c > newfile.rtf,则会在后台创建子进程统计p3.c的行数、单词数及字节数,并写入newfile.rtf
在这里插入图片描述
《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第12张图片
(2) 控制台输入./p4,则会在后台创建子进程统计p4.c的行数、单词数及字节数,然后创建p4.output并写入,输入cat p4.output可显示内容。

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
     
    int rc = fork();
    if (rc < 0) {
     
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
     
		// 重定向输出到文件
		close(STDOUT_FILENO); 
		open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);

		// 重载"wc"程序
        char *myargs[3];			// strdup()字符串拷贝库函数
        myargs[0] = strdup("wc");   // 程序: "wc" (字符统计)
        myargs[1] = strdup("p4.c"); // 参数: 需要统计的文件
        myargs[2] = NULL;           // 命令行结束标志
        execvp(myargs[0], myargs);  // 统计行、单词、字节数
    } else {
     
        int wc = wait(NULL);
		assert(wc >= 0);
    }
    return 0;
}

在这里插入图片描述
《操作系统导论》实验二:进程API的使用

5. 系统命令、接口API和系统调用关系

系统调用(System call):操作系统提供给用户程序调用的一组“特殊”接口,用户程序可以通过这组“特殊”接口获得操作系统内核提供的服务。例如,用户可以通过进程系统调用sys_fork()创建进程。

应用程序接口API(Application Programming Interface):程序员在用户空间下可以直接使用的函数接口,是一些预定义的函数,比如fork()函数,提供应用程序访问一组系统调用的能力。

系统命令:系统命令相对于API更高了一层,它实际上是一个可执行程序,它的内部引用了用户编程接口(API)来实现相应的功能
《操作系统导论》学习笔记(二):CPU虚拟化(进程)_第13张图片
在这里插入图片描述
过程如下:使用系统命令gcc -o p1 p1.c -Wall -Werror 调用gcc编译器编译生成可执行程序p1,然后使用系统命令./p1执行该程序创建进程,进程内运行API函数fork(),根据frok()的系统调用号寻找内核空间对应的系统调用的sys_fork()创建子进程。

本节介绍了进程及子进程的创建,但是单个CPU在某一时刻只能运行一个进程,如果不能实现CPU切换运行不同进程,创建再多的进程及子进程也是得到不运行。为了提高CPU的利用率满足用户需求,因此引出 CPU虚拟化技术 — 通过中断机制让多个进程并发执行,分时使用一个CPU。

《操作系统导论》学习笔记(三):CPU虚拟化(机制)

你可能感兴趣的:(操作系统,操作系统)