《操作系统导论》第一部分 CPU虚拟化 P1 进程,进程API

C1 抽象:进程

进程:进程就是正在运行的程序,程序本身是没有生命周期的,它只是存在于磁盘上的一些指令或静态数据。操作系统让这些字节运动了起来,让程序发挥了作用

在实际使用计算机时,会同时运行很多程序,如浏览器,邮件,音乐播放器等等,一个正常的系统可能有上百个进程同时运行,每个进程都需要CPU为它提供服务,但是CPU是有限的,操作系统通过虚拟化CPU来提供这种服务

通过让一个进程只运行一个时间片,然后切换到其它进程,操作系统提供了存在多个CPU的假象,这就是时分共享CPU技术,潜在的开销就是性能损失,因为CPU必须共享,每个进程的速度就会慢一点
与时分共享对应的是空分共享,资源在空间上被划分给希望使用它的人,如磁盘空间就是一个空分共享资源

要实现CPU的虚拟化,就得需要一些低级机制高级智能

低级机制:即机制,机制是一些低级方法或者协议,如能让操作系统停止运行一个程序,并开始在给定的CPU上运行另一个程序,即分时机制

高级智能:即策略,策略是在操作系统内做出某种决定的算法,如给定一组要在CPU上运行的程序,调度策略会决定有限运行哪个程序,可能利用历史信息(哪个程序在一分钟内运行更多),工作负载知识(运行什么类型的程序),性能指标(能否优化)来做出决定

1.1 抽象:进程

操作系统为正在运行的程序提供的抽象,就是所谓的进程

机器状态:程序在运行时可以读取或更新的内容

进程的构成:
1,进程的机器状态有一个明显的组成部分,就是它的内存,指令存在于内存中,正在运行的程序读取和写入的数据也在内存中,因此进程可以访问的内存是该进程的一部分
2,进程的机器状态另一部分是寄存器,许多指令给明确的读取或更新寄存器,因此它们对与执行该进程很重要

1.2 进程API

操作系统的所有接口必须包括以下的内容,现代操作系统都必须以某种形式提供这些API:
1,创建:操作系统必须包含一些创建新进程的方法,如在shell中键入命令或双击应用程序图标时,会调用操作系统来创建新进程,运行指定的程序
2,销毁:由于存在创建进程的接口,因此为了终止某些进程,系统提供了一个强制销毁进程的接口
3,等待:有时等待进程停止运行是有用的,因此经常提供某种接口
4,状态:通常也有一些接口可以获得有关进程的状态信息,如运行了多长时间,处于什么状态
5,其它控制:除了销毁或等待进程外,有时还可能有其它控制,如大多数操作系统提供某种方法来暂停进程一段时间,然后继续运行

1.3 进程创建:更多细节

程序如何转化为进程?

操作系统运行程序的第一件事是将程序代码和所有的静态数据从磁盘加载到内存中,加载到进程的地址空间中

程序最初是以某种可执行格式存放在磁盘中的,因此将程序和静态数据加载到内存的过程中,需要操作系统从磁盘读取这些字节,并将它们存放在内存的某处
在这里插入图片描述
在将程序和静态数据加载到内存后,操作系统还需要为程序的运行时栈分配一些内存。操作系统也可能会用参数初始化栈,如将参数填入main()函数,即argc和argv

操作系统也可能为程序的堆分配一些内存,如malloc()用来申请空间,操作系统在堆中分配更多的内存给进程,以满足这些调用

通过将代码和静态数据加载到内存中,通过创建和初始化栈以及执行与I/O设置相关的其它工作,OS终于为程序执行搭好了舞台。然后它的最后一项任务就是启动程序,在入口处运行,即跳转到main(),OS将CPU的控制权转移到新创建的进程中,从而进程开始执行

1.4 进程状态

进程在给定的时间可能处于不同的状态:
1,运行:在运行状态下,进程正在处理器上运行,它正在执行指令
2,就绪:在就绪状态下,进程已经准备好,但由于某种原因,操作系统选择不在此时运行
3,阻塞:在阻塞状态下,一个进程执行了某种操作,直到发生了其它事件时才会准备运行,如当进程向磁盘发起I/O请求时,它会被阻塞,因此其它进程可以使用处理器

在这里插入图片描述
假设有两个进程p0和p1,p0需要发起I/O,一开始p0处于运行状态,p1处于就绪状态,当p0发起I/0时,p0会被阻塞,OS发现p0不使用CPU了并开始运行p1,I/O完成后将p0移回就绪状态,最后p1结束,将p0从就绪变为运行

上述这个简单的例子中,操作系统也需要做出很多决定,首先操作系统要在p0发起I/O时阻塞p0并运行p1,这样可以通过保持CPU繁忙来提高资源利用率,当I/O完成后,操作系统决定继续运行完p1,再运行p0,这些类型的决策由操作系统调度程序完成

1.5 数据结构

操作系统是一个程序,和其它程序一样,它有一些关键的数据结构来跟踪各种相关信息,如为了跟踪每个进程的状态,操作系统可能为所有就绪的进程保留某种进程列表,以及当前正在运行的进程的一些附加信息。操作系统还必须跟踪被阻塞的进程,当I/O完成后,操作系统应该唤醒正确的进程,让它们再次运行

将存储关于进程的信息的个体结构称为进程控制块 PCB

当一个进程停止时,它的寄存器中的值将被保存到寄存器上下文中,通过恢复这些寄存器,操作系统可以恢复运行该进程,称为上下文切换

除了运行,就绪,阻塞外,还有一些其它的状态:
1,有时系统会有一个初始状态,表示进程在创建时处于的状态
2,一个进程可以处于已经退出但尚未清理的最终状态(僵尸进程)

C2 插叙:进程API

UNIX操作系统通过一对系统调用fork()和exec()来创建进程,该进程还可以通过系统调用wait()来等待其创建的子进程执行完成

2.1 fork()系统调用

对fork()的说明:
1,fork()是创建进程的系统调用
2,c程序一开始,会产生一个进程,当遇到fork()时,会创建一个子进程
3,子进程与父进程共存,一起向下执行程序
4,子进程创建成功后,fork()会返回两个值,一个代表父进程,是一串数字,这串数字是子进程的PID,一个代表子进程,为0

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


int main(int argc, char const *argv[])
{
	//输出父进程PID
	printf((int)getpid);
	//创建子进程,且rc接收fork()的返回值
	//这时存在两个进程都会接着执行下面的程序,
	//所以rc会有两种值,一种是父进程的返回值,即子进程地址
	//另一种是子进程的返回值,即0
	int rc = fork();
	

	if(rc == 0) {
	//子进程
	//会输出子进程的PID
		printf("我是子进程");
		printf((int)getpid());
	}

	if(rc != 0) {
	//父进程执行
	//会输出父进程的PID
		printf("我是父进程");
		printf((int)getpid());
	}
	
	return 0;
}

如果要操作某个进程(如终止进程)就需要通过PID(进程描述符)指明

fork()之后,对操作系统来说这时看起来有两个完全一样的程序在运行,并会从fork()返回不同的值

子进程并不是完全拷贝了父进程,它有着自己的地址空间,寄存器,程序计数器等,且与父进程从fork()得到的返回值不同

上述的程序如果运行在单CPU上,那么子进程和父进程此时都有可能运行,即上述程序中的输出顺序可能会不同,因为父子进程都抢着执行代码,所以结果会是随机的

CPU调度程序决定了某个时刻哪个进程能被执行,由于CPU调度程序很复杂,所以无法确定哪个进程先运行,在多线程和并发中就存在着大量的不确定性

2.2 wait()系统调用

有时候需要父进程等待子进程执行完毕,这项任务由wait()系统调用完成

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


int main(int argc, char const *argv[])
{
	
	printf((int)getpid);
	
	int rc = fork();
	
	if(rc == 0) {
	//子进程
	//会输出子进程的PID
		printf("我是子进程");
		printf((int)getpid());
	}

	if(rc != 0) {
	//父进程执行
	//会输出父进程的PID
		wait(NULL);
		printf("我是父进程");
		printf((int)getpid());
	}
	
	return 0;
}

使用wait()系统调用使得上述的程序输出结果变得确定了

由于在父进程中调用了wait(),所以如果父进程先运行时,他会等待自己的子进程运行完毕,wait()才返回到父进程,父进程才会做出输出操作

2.3 exec()系统调用

fork()只是在想运行相同程序的拷贝时有用,exec()系统调用可以让子进程执行与父进程不同的程序

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


int main(int argc, char const *argv[])
{
	
	printf((int)getpid);
	
	int rc = fork();
	
	if(rc == 0) {
	//子进程
		printf("我是子进程");
		printf((int)getpid());

		char *myargs[3];
		myargs[0] = strdup("wc");
		myargs[1] = strdup("test.c");
		myargs[2] = NULL;
		//子进程调用execvp()来运行字符串计数程序wc
		//针对文件test.c,从而得到该文件多少行,多少单词,多少字节
		execvp(myargs[0], myargs);
	}

	if(rc != 0) {
	//父进程执行
		wait(NULL);
		printf("我是父进程");
		printf((int)getpid());
	}
	
	return 0;
}

exec()系统调用给定可执行程序的名称和它需要的参数后,exec()会从可执行程序中加载代码和静态数据,并用它们覆盖当前进程的代码段,静态数据,堆,栈,调用exec()的进程除了进程号外,其它的部分都被exec()要执行的程序替换。exec()没有创建新进程,而是将当前运行的程序替换成不同的运行程序

2.4 设计这些API的理由

分离fork()和exec()是非常有用的,这给了shell在fork之后exec之前运行代码的机会,这些代码可以在运行新程序前改变环境

shell是一个用户程序,它首先显示一个提示符,等待输入命令,输入后,shell调用fork()创建新进程,并调用exec的某个变体执行这个程序,调用wait等待该命令完成,子进程结束后,shell从wait返回,再次输出一个提示符,等待下一个命令

看一个例子:

prompt> wc test.c >file1.txt

wc的结果会被重定向到文件file1中,当完成了子进程的创建后,shell在调用exec前先关闭了标准输出,打开了文件file1,接着结果就被输出到了file1内,而不是打印在屏幕上

2.5 其它API

UNIX中还有很多与进程交互的方式,如通过kill()向进程发送信号,包括要求进程睡眠,终止等,或ps命令查看当前运行的进程,top展示当前系统中进程消耗资源的情况

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值