《操作系统导论》学习笔记(一):操作系统概览
程序:指令和数据的集合,一般作为目标文件保存在磁盘中。
进程:程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
可执行程序位于磁盘中,需要将静态程序加载到内存生成动态的进程,CPU才可以不停地取指执行。进程的创建过程涉及到许多陌生的概念,诸如进程控制块、进程队列、用户空间等待等,下面将从虚拟地址空间开始阐述。
虚拟地址空间:在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中,这个沙盘就是虚拟地址空间(virtual address space)。虚拟地址空间由内核空间(kernel space)和用户空间(user space)两部分组成。
程序包含代码及数据,编译链接生成的可执行文件时汇编形式,装载到内存中是二进制文件。程序是通过变量访问数据,但在二进制下是没有变量的概念,只能通过内存地址访问数据。如果全部变量都通过地址访问,这样是低效且不现实的。因而,需要根据不同变量的性质进行分区
操作系统在创建进程时会在内核空间配备一个进程控制块(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使用权。
《操作系统导论》实验一:模拟进程状态转换
(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挂在对应头指针后组成队列。
下面是进程的创建过程:
创建进程时,操作系统为进程生成唯一的进程控制块(PCB)并挂在进程队列,用户空间开辟内存,装入程序的变量及代码,栈内保存 main()
的参数argc/argv
,清空寄存器内容,main()
入口地址送到程序计数器PC;CPU执行程序main()
的指令,遇到return
语句返回操作系统;操作系统释放进程内容,并将进程从进程队列中移除。
(1) 头文件及函数原型
#include
#define int pid_t
pid_t fork( void);
返回值:成功调用一次则返回两个值,父进程返回子进程PID,子进程返回0;调用失败则返回-1。
(2) 函数说明
主函数main()运行时会自动创建进程,称为父进程;fork()系统调用用于创建一个新进程,称为子进程。创建子进程时,子进程会在内核中拥有自己的进程控制块(task_struct),从而拥有不同于父进程的PID。但同时,子进程复制父进程其余一切(堆栈、代码段等),而fork()作为系统调用保存在父进程和子进程的栈中,因而会返回两次,产生两个返回值。
// 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不同返回值输出下面两行内容。
(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。
(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;
}
子进程重载字符统计程序,统计p3.c的行数为32
、单词数为123
、字节数为966
。
在计算机科学中,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
#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;
}
系统调用(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在某一时刻只能运行一个进程,如果不能实现CPU切换运行不同进程,创建再多的进程及子进程也是得到不运行。为了提高CPU的利用率满足用户需求,因此引出 CPU虚拟化技术 — 通过中断机制让多个进程并发执行,分时使用一个CPU。
《操作系统导论》学习笔记(三):CPU虚拟化(机制)