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

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

程序创建进程


程序:指令和数据的集合,一般作为目标文件保存在磁盘中。
进程:程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
可执行程序位于磁盘中,需要将静态程序加载到内存生成动态的进程,CPU才可以不停地取指执行。程序在硬盘中主要包含代码(code)及静态数据(static data),而相比之下,进程似乎多了堆(heap)和(stack)。不止如此,每个进程还拥有自己的身份证——进程控制块。这些是怎么来的呢?下面将从虚拟地址空间开始逐一阐明。

1. 虚拟地址空间

在这里插入图片描述
虚拟内存:内存管理的一种方式, 磁盘上划分出一块空间由操作系统管理,物理内存耗尽时可以充当物理内存来使用。虚拟内存可以通过页表(page table)映射到物理内存。

虚拟地址空间:在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中,这个沙盘就是虚拟地址空间(virtual address space)。虚拟地址空间由内核空间(kernel space)和用户空间(user space)两部分组成。
在这里插入图片描述

2.进程控制块

操作系统在创建进程时会在内核空间配备一个进程控制块(PCB),
包含描述进程当前情况及管理进程全部信息的数据结构。在Linux操作系统中的进程控制块实际是一个task_struct结构体,放在sched.h,以下简要介绍。
在这里插入图片描述
(1) 进程状态(process state):进程的常见运行状态包括就绪(Ready)、阻塞(Blocked)及运行(Running)。

enum proc_state { READY, RUNNING, READY };

在这里插入图片描述
就绪(Ready):进程拥有运行所需的所有资源,等待分配CPU。
运行(Running):进程运行在CPU上,即CPU正在运行该进程包含的指令。
阻塞(Blocked)/等待(Waiting):进程在运行时发生CPU以外事件的请求(如I/O请求)从而放弃CPU使用权。
Simulation Homework: 模拟进程状态转换

(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):采用指针将进程控制块之间相互链接
在这里插入图片描述
进程链表组织方式是系统设置就绪队列头指针、阻塞队列头指针、运行队列头指针,按照进程的状态将进程的PCB挂在对应头指针后组成队列

3.用户空间分区

源程序包含代码及数据,编译链接生成的可执行文件时汇编形式,装载到内存中是二进制文件。程序是通过变量访问数据,但在二进制下是没有变量的概念,只能通过内存地址访问数据。如果全部变量都通过地址访问,这样是低效且不现实的。因而,需要根据不同变量的性质进行分区,比如局部变量内容短小,需要频繁访问,但是生命周期很短,通常只在一个方法内存活,就专门从内存划分出一块较小区域命名为栈(stack),由编译器分配与回收,效率高。又比如较大的结构体可能不需要太频繁的访问,但生命周期较长,通常很多个方法中都会用到,就划出另一较大区域命名为堆(heap),由程序员自主分配回收。
在这里插入图片描述
在这里插入图片描述

4.创建进程

在这里插入图片描述
创建进程时,操作系统为进程生成唯一的进程控制块(PCB)并挂在进程队列,内存中开辟空间存放程序的变量及代码,栈内装入输入的参数argc/argv,清空寄存器内容,main()入口地址送到程序计数器PC;CPU执行程序main()的指令,遇到return语句返回操作系统;操作系统释放进程内容,并将进程从进程队列中移除。

说清楚程序是如何变成进程的,下面说说如何进程中操作进程—API。

进程创建进程—接口API/C库函数

1. 创建子进程fork()

(1) 头文件

#include <unistd.h>

(2) 函数原型

#define int pid_t 
pid_t fork( void);

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

(3) 函数说明
主函数main()运行时会自动创建进程,称为父进程;fork()系统调用用于创建一个新进程,称为子进程。创建子进程时,子进程会在内核中拥有自己的进程控制块(task_struct),从而拥有不同于父进程的PID。但同时,子进程复制父进程其余一切(堆栈、代码段等),而fork()作为系统调用保存在父进程和子进程的栈中,因而会返回两次,产生两个返回值。
在这里插入图片描述

// p1.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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. 阻塞当前进程wait()

(1) 头文件

#include <sys/wait.h>

(2) 函数原型

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

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

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

// p2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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. exec()函数簇

(1) 头文件

#include <unistd.h>

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

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

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

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

// p3.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

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;
}

在这里插入图片描述
子进程重载字符统计程序,统计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
在这里插入图片描述
在这里插入图片描述
(2) 控制台输入./p4,则会在后台创建子进程统计p4.c的行数、单词数及字节数,然后创建p4.output并写入,输入cat p4.output可显示内容。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/wait.h>

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;
}

在这里插入图片描述
Code Homework: 进程API的使用

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

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

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

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

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

发布了21 篇原创文章 · 获赞 8 · 访问量 1495

猜你喜欢

转载自blog.csdn.net/K_Xin/article/details/104636421