一、进程
进程就是运行中的程序,程序本身是没有生命周期的,它只是存储在磁盘上的一些指令(或者一些静态数据),操作系统将这些指令和数据加载到内存中,使其运行起来。
1.1 虚拟化CPU技术
根据我们平时使用计算机的经验表明,计算机往往可以同时运行多个进程,实际上,一个正常的系统可能会有上百个进程同时在运行,但物理CPU是少量的,那么操作系统是如何提供几乎有无数个CPU的假象的呢——这就是虚拟化CPU技术。
如何实现虚拟化CPU?
操作系统通过让一个进程只运行一个时间片(毫秒级),然后切换到其他进程的做法,造成了有多个CPU的假象,这就是常说的时分共享CPU
技术。
显然,这会造成性能损失,因为CPU必须被多个进程共享,每个进程的运行就会慢一点。
补充知识:
时分共享是操作系统共享资源的最基本的技术之一,允许资源由一个实体使用一小段时间,然后切换,这样循环下去,资源就可以被许多人共享。
空分共享是资源在空间上被划分给希望使用它的人,比如,磁盘空间是一个空分共享资源,一旦将块分配给文件,在用户删除文件之前,其他人不可能使用这个块空间。
1.2 进程的机器状态
进程还可以理解为操作系统为程序提供的一个抽象。对于一个进程,在任何时候,我们都可以知道它在运行过程中访问或影响的系统的不同部分,反过来说,被影响和访问的系统部分也可以用来概括该进程。
为了理解进程的构成,我们必须了解它的机器状态
——程序在运行时可以读取或更新的内容。
在任意时刻,物理计算机的哪些部分对执行程序很重要?
进程的机器状态有个很重要的组成部分,就是进程的内存
。指令存在在内存中,正在运行的程序读取和写入的数据也在内存中。
进程的机器状态的另一部分是寄存器
。进程的许多指令明确地读取或更新寄存器。因此,他们对于执行该进程很重要。
有一些非常特殊的寄存器构成了该机器状态的一部分。例如,程序计数器(Program Counter)告诉我们程序当前正在执行哪个指令,栈指针(Stack Pointer)和相关的帧指针(Frame Pointer)用于管理函数参数栈、局部变量和返回地址。
1.3 进程API
操作系统所有的接口必须包含的内容有:
- 创建 (Create):操作系统必须包含一些创建新进程的方法。比如,在Shell中输入命令或者双击应用图标时,所有现代操作系统都会以某种形式提供这些API。
- 销毁 (Destory):由于存在创建进程的接口,因此操作系统还要提供强制销毁进程的接口。尽管很多进程会在运行完成后自己退出,但是,如果它们不退出,用户希望能够强制终止它们。因此停止失控进程的接口很有用。
- 等待 (Wait):有时会有等待进程停止运行的需求,因此操作系统会提供相关接口。
- 其他控制(Miscellaneous control):除了杀死或等待进程外,有时还可能有其他控制。比如,大多数操作系统提供某种方法来暂停进程(停止运行一段时间),然后恢复(继续运行)。
- 状态(Statu):通常也有一些接口可以获得有关进程的状态信息,例如运行了多长时间,或者处于什么状态。
1.4 如何创建一个进程?
程序如何转化为一个进程?换个说法,操作系统是如何启动一个程序的?进程的创建实际上如何进行?
程序首先要被加载进内存
首先,程序必须被操作系统加载进内存,程序最初是以某个可执行格式驻留在磁盘上(disk,或者在某些现代系统中,在基于闪存的SSD上)。因此,将程序和静态数据加载到内存中的过程,需要操作系统从磁盘读取这些字节,并把它们放在内存的某处。
在早期的(或简单的)操作系统中,程序要全部加载进内存之后才会开始执行程序 。但是现代操作系统惰性(Lazily)执行该过程,即仅在程序执行期间需要加载的代码或数据片段,才会加载。这个惰性加载涉及到分页和交换机制,这块是内存虚拟化的内容。
为程序分配运行时内存
- C程序用栈存放局部变量、函数参数和返回地址。操作系统也可能会用参数初始化栈,它会将参数填入main()函数,这个参数就是argc 和 argv数组。
- 操作系统也可能为程序的堆(heap)分配一些内存,对于C程序来说,堆用于显示请求的动态分配数据。程序会通过调用malloc()来请求内存,并通过调用free()来明确地释放它。数据结构(如链表、散列表、树和其他数据结构)需要堆空间。一开始堆很小,但随着程序的运行,通过malloc()库请求更多内存,操作系统可能会参与分配更多的内存给进程。
IO设置相关工作
操作系统还要执行一些其他的初始化任务,尤其是输入输出相关的任务。比如,在UNIX系统中,默认情况下每个进程都有3个打开的文件描述符,用于标准输入、输出和错误。这些描述符让程序轻松读取来自终端的输入以及打印输出到屏幕。
启动程序
前面的工作完成之后,操作系统还有最后一项任务:启动程序,在入口处运行,即main()函数,通过跳转到main()例程,操作系统(OS)将CPU的控制权转移到新建的进程中,从而程序开始执行了。
1.5 进程状态
进程在任意时间可能会处于不同的状态,简单来讲,进程可以处于下面3种状态之一:
- 运行 (running):在运行状态下,进程正在CPU上运行,这意味着它正在执行指令。
- 就绪 (ready):在就绪状态下,进程已经准备好运行,但由于某种原因,操作系统选择不在此时运行
- 阻塞 (blocked):在阻塞状态下,一个进程执行了某个操作,需要等待其它事件发生才能继续执行(比如此时程序发起IO请求,需要等待IO完成才能继续执行),这时它就会被阻塞,其它进程可以使用CPU。
状态转换
进程的上下文切换
操作系统是一个程序,和其他程序一样,它有一些关键的数据结构来跟踪各种相关的信息。
比如,为了跟踪每个进程的状态,操作系统可能会为所有就绪的进程保留某种进程列表
,以及跟踪当前正在运行的进程的一些附加信息。操作系统还必须以某种方式跟踪被阻塞的进程。当IO事件完成时,操作系统应该确保唤醒正确的进程,让它准备好再次运行。
操作系统会追踪进程的一些重要信息。对于停止的进程,寄存器上下文将保存其寄存器的内容。当一个进程停止时,它的寄存器将被保存在这个内存位置。通过恢复这些寄存器(将它们的值放回实际的物理寄存器中),操作系统可以恢复运行该进程。这被称为上下文切换
(Context Switch)。
进程除了运行、就绪和阻塞之外,还有其他一些进程可以处于的状态。有时候系统会有一个初始状态,表示进程在创建时处于的状态——新建态
。
另外,一个进程可以处于已退出但尚未清理的最终状态(在基于UNIX的系统中,这称为僵尸状态
)。这个最终状态非常有用,因为它允许其他进程(通常是创建该进程的父进程)检查进程的返回代码,并查看感刚刚结束的进程是否成功执行(在UNIX系统中,程序成功执行返回0,否则返回非0)。完成后,父进程将进行最后一次调用(例如,wait()),以等待子进程的完成,并告诉操作系统它可以清理这个正在结束的进程的所有相关数据结构。
二、章节作业
模拟作业
:模拟作业以模拟器的形式出现,你运行它以确保理解上述内容。模拟器通常是Python程序,它们让你能够生成不同的问题(使用不同的随机种子),也让程序为你解决问题,以便你检查答案。使用-h或-help参数运行任何模拟器,将提供有关模拟器所有选项的更多信息。
程序process-run.py 让你能够查看程序运行时进程状态如何改变,是在使用CPU(例如,执行相加指令)还是执行I/O(例如,向磁盘发送请求并等待它完成)。
process-run.py 代码
#! /usr/bin/python2
from __future__ import print_function
import sys
from optparse import OptionParser
import random
# to make Python2 and Python3 act the same -- how dumb
def random_seed(seed):
try:
random.seed(seed, version=1)
except:
random.seed(seed)
return
# process switch behavior
SCHED_SWITCH_ON_IO = 'SWITCH_ON_IO'
SCHED_SWITCH_ON_END = 'SWITCH_ON_END'
# io finished behavior
IO_RUN_LATER = 'IO_RUN_LATER'
IO_RUN_IMMEDIATE = 'IO_RUN_IMMEDIATE'
# process states
STATE_RUNNING = 'RUNNING'
STATE_READY = 'READY'
STATE_DONE = 'DONE'
STATE_WAIT = 'WAITING'
# members of process structure
PROC_CODE = 'code_'
PROC_PC = 'pc_'
PROC_ID = 'pid_'
PROC_STATE = 'proc_state_'
# things a process can do
DO_COMPUTE = 'cpu'
DO_IO = 'io'
DO_IO_DONE = 'io_done'
class scheduler:
def __init__(self, process_switch_behavior, io_done_behavior, io_length):
# keep set of instructions for each of the processes
self.proc_info = {}
self.process_switch_behavior = process_switch_behavior
self.io_done_behavior = io_done_behavior
self.io_length = io_length
return
def new_process(self):
proc_id = len(self.proc_info)
self.proc_info[proc_id] = {}
self.proc_info[proc_id][PROC_PC] = 0
self.proc_info[proc_id][PROC_ID] = proc_id
self.proc_info[proc_id][PROC_CODE] = []
self.proc_info[proc_id][PROC_STATE] = STATE_READY
return proc_id
# program looks like this:
# c7,i,c1,i
# which means
# compute for 7, then i/o, then compute for 1, then i/o
def load_program(self, program):
proc_id = self.new_process()
for line in program.split(','):
opcode = line[0]
if opcode == 'c': # compute
num = int(line[1:])
for i in range(num):
self.proc_info[proc_id][PROC_CODE].append(DO_COMPUTE)
elif opcode == 'i':
self.proc_info[proc_id][PROC_CODE].append(DO_IO)
# add one compute to HANDLE the I/O completion
self.proc_info[proc_id][PROC_CODE].append(DO_IO_DONE)
else:
print('bad opcode %s (should be c or i)' % opcode)
exit(1)
return
def load(self, program_description):
proc_id = self.new_process()
tmp = program_description.split(':')
if len(tmp) != 2:
print('Bad description (%s): Must be number <x:y>' % program_description)
print(' where X is the number of instructions')
print(' and Y is the percent change that an instruction is CPU not IO')
exit(1)
num_instructions, chance_cpu = int(tmp[0]), float(tmp[1])/100.0
for i in range(num_instructions):
if random.random() < chance_cpu:
self.proc_info[proc_id][PROC_CODE].append(DO_COMPUTE)
else:
self.proc_info[proc_id][PROC_CODE].append(DO_IO)
# add one compute to HANDLE the I/O completion
self.proc_info[proc_id][PROC_CODE].append(DO_IO_DONE)
return
def move_to_ready(self, expected, pid=-1):
if pid == -1:
pid = self.curr_proc
assert(self.proc_info[pid][PROC_STATE] == expected)
self.proc_info[pid][PROC_STATE] = STATE_READY
return
def move_to_wait(self, expected):
assert(self.proc_info[self.curr_proc][PROC_STATE] == expected)
self.proc_info[self.curr_proc][PROC_STATE] = STATE_WAIT
return
def move_to_running(self, expected):
assert(self.proc_info[self.curr_proc][PROC_STATE] == expected)
self.proc_info[self.curr_proc][PROC_STATE] = STATE_RUNNING
return
def move_to_done(self, expected):
assert(self.proc_info[self.curr_proc][PROC_STATE] == expected)
self.proc_info[self.curr_proc][PROC_STATE] = STATE_DONE
return
def next_proc(self, pid=-1):
if pid != -1:
self.curr_proc = pid
self.move_to_running(STATE_READY)
return
for pid in range(self.curr_proc + 1, len(self.proc_info)):
if self.proc_info[pid][PROC_STATE] == STATE_READY:
self.curr_proc = pid
self.move_to_running(STATE_READY)
return
for pid in range(0, self.curr_proc + 1):
if self.proc_info[pid][PROC_STATE] == STATE_READY:
self.curr_proc = pid
self.move_to_running(STATE_READY)
return
return
def get_num_processes(self):
return len(self.proc_info)
def get_num_instructions(self, pid):
return len(self.proc_info[pid][PROC_CODE])
def get_instruction(self, pid, index):
return self.proc_info[pid][PROC_CODE][index]
def get_num_active(self):
num_active = 0
for pid in range(len(self.proc_info)):
if self.proc_info[pid][PROC_STATE] != STATE_DONE:
num_active += 1
return num_active
def get_num_runnable(self):
num_active = 0
for pid in range(len(self.proc_info)):
if self.proc_info[pid][PROC_STATE] == STATE_READY or \
self.proc_info[pid][PROC_STATE] == STATE_RUNNING:
num_active += 1
return num_active
def get_ios_in_flight(self, current_time):
num_in_flight = 0
for pid in range(len(self.proc_info)):
for t in self.io_finish_times[pid]:
if t > current_time:
num_in_flight += 1
return num_in_flight
def check_for_switch(self):
return
def space(self, num_columns):
for i in range(num_columns):
print('%10s' % ' ', end='')
def check_if_done(self):
if len(self.proc_info[self.curr_proc][PROC_CODE]) == 0:
if self.proc_info[self.curr_proc][PROC_STATE] == STATE_RUNNING:
self.move_to_done(STATE_RUNNING)
self.next_proc()
return
def run(self):
clock_tick = 0
if len(self.proc_info) == 0:
return
# track outstanding IOs, per process
self.io_finish_times = {}
for pid in range(len(self.proc_info)):
self.io_finish_times[pid] = []
# make first one active
self.curr_proc = 0
self.move_to_running(STATE_READY)
# OUTPUT: headers for each column
print('%s' % 'Time', end='')
for pid in range(len(self.proc_info)):
print('%14s' % ('PID:%2d' % (pid)), end='')
print('%14s' % 'CPU', end='')
print('%14s' % 'IOs', end='')
print('')
# init statistics
io_busy = 0
cpu_busy = 0
while self.get_num_active() > 0:
clock_tick += 1
# check for io finish
io_done = False
for pid in range(len(self.proc_info)):
if clock_tick in self.io_finish_times[pid]:
io_done = True
self.move_to_ready(STATE_WAIT, pid)
if self.io_done_behavior == IO_RUN_IMMEDIATE:
# IO_RUN_IMMEDIATE
if self.curr_proc != pid:
if self.proc_info[self.curr_proc][PROC_STATE] == STATE_RUNNING:
self.move_to_ready(STATE_RUNNING)
self.next_proc(pid)
else:
# IO_RUN_LATER
if self.process_switch_behavior == SCHED_SWITCH_ON_END and self.get_num_runnable() > 1:
# this means the process that issued the io should be run
self.next_proc(pid)
if self.get_num_runnable() == 1:
# this is the only thing to run: so run it
self.next_proc(pid)
self.check_if_done()
# if current proc is RUNNING and has an instruction, execute it
instruction_to_execute = ''
if self.proc_info[self.curr_proc][PROC_STATE] == STATE_RUNNING and \
len(self.proc_info[self.curr_proc][PROC_CODE]) > 0:
instruction_to_execute = self.proc_info[self.curr_proc][PROC_CODE].pop(0)
cpu_busy += 1
# OUTPUT: print what everyone is up to
if io_done:
print('%3d*' % clock_tick, end='')
else:
print('%3d ' % clock_tick, end='')
for pid in range(len(self.proc_info)):
if pid == self.curr_proc and instruction_to_execute != '':
print('%14s' % ('RUN:'+instruction_to_execute), end='')
else:
print('%14s' % (self.proc_info[pid][PROC_STATE]), end='')
# CPU output here: if no instruction executes, output a space, otherwise a 1
if instruction_to_execute == '':
print('%14s' % ' ', end='')
else:
print('%14s' % '1', end='')
# IO output here:
num_outstanding = self.get_ios_in_flight(clock_tick)
if num_outstanding > 0:
print('%14s' % str(num_outstanding), end='')
io_busy += 1
else:
print('%10s' % ' ', end='')
print('')
# if this is an IO start instruction, switch to waiting state
# and add an io completion in the future
if instruction_to_execute == DO_IO:
self.move_to_wait(STATE_RUNNING)
self.io_finish_times[self.curr_proc].append(clock_tick + self.io_length + 1)
if self.process_switch_behavior == SCHED_SWITCH_ON_IO:
self.next_proc()
# ENDCASE: check if currently running thing is out of instructions
self.check_if_done()
return (cpu_busy, io_busy, clock_tick)
#
# PARSE ARGUMENTS
#
parser = OptionParser()
parser.add_option('-s', '--seed', default=0, help='the random seed', action='store', type='int', dest='seed')
parser.add_option('-P', '--program', default='', help='more specific controls over programs', action='store', type='string', dest='program')
parser.add_option('-l', '--processlist', default='', help='a comma-separated list of processes to run, in the form X1:Y1,X2:Y2,... where X is the number of instructions that process should run, and Y the chances (from 0 to 100) that an instruction will use the CPU or issue an IO', action='store', type='string', dest='process_list')
parser.add_option('-L', '--iolength', default=5, help='how long an IO takes', action='store', type='int', dest='io_length')
parser.add_option('-S', '--switch', default='SWITCH_ON_IO', help='when to switch between processes: SWITCH_ON_IO, SWITCH_ON_END', action='store', type='string', dest='process_switch_behavior')
parser.add_option('-I', '--iodone', default='IO_RUN_LATER', help='type of behavior when IO ends: IO_RUN_LATER, IO_RUN_IMMEDIATE', action='store', type='string', dest='io_done_behavior')
parser.add_option('-c', help='compute answers for me', action='store_true', default=False, dest='solve')
parser.add_option('-p', '--printstats', help='print statistics at end; only useful with -c flag (otherwise stats are not printed)', action='store_true', default=False, dest='print_stats')
(options, args) = parser.parse_args()
random_seed(options.seed)
assert(options.process_switch_behavior == SCHED_SWITCH_ON_IO or options.process_switch_behavior == SCHED_SWITCH_ON_END)
assert(options.io_done_behavior == IO_RUN_IMMEDIATE or options.io_done_behavior == IO_RUN_LATER)
s = scheduler(options.process_switch_behavior, options.io_done_behavior, options.io_length)
if options.program != '':
for p in options.program.split(':'):
s.load_program(p)
else:
# example process description (10:100,10:100)
for p in options.process_list.split(','):
s.load(p)
assert(options.io_length >= 0)
if options.solve == False:
print('Produce a trace of what would happen when you run these processes:')
for pid in range(s.get_num_processes()):
print('Process %d' % pid)
for inst in range(s.get_num_instructions(pid)):
print(' %s' % s.get_instruction(pid, inst))
print('')
print('Important behaviors:')
print(' System will switch when ', end='')
if options.process_switch_behavior == SCHED_SWITCH_ON_IO:
print('the current process is FINISHED or ISSUES AN IO')
else:
print('the current process is FINISHED')
print(' After IOs, the process issuing the IO will ', end='')
if options.io_done_behavior == IO_RUN_IMMEDIATE:
print('run IMMEDIATELY')
else:
print('run LATER (when it is its turn)')
print('')
exit(0)
(cpu_busy, io_busy, clock_tick) = s.run()
if options.print_stats:
print('')
print('Stats: Total Time %d' % clock_tick)
print('Stats: CPU Busy %d (%.2f%%)' % (cpu_busy, 100.0 * float(cpu_busy)/clock_tick))
print('Stats: IO Busy %d (%.2f%%)' % (io_busy, 100.0 * float(io_busy)/clock_tick))
print('')
tips
:第一行的代码是表示python程序所在的绝对路径,这个需要根据自己python程序所在的位置做修改。
问题
- 1.用以下标志运行程序: ./process-run.py -l 5:100,5:100。CPU利用率(CPU使用时间的百分比)应该是多少?为什么你知道这一点?利用-c参数查看你的答案是否正确。
运行结果截图:
先解释一下参数:
- -l 表示显示进程列表,展示出所有的进程
- 5:100 表示指定进程是"5:100",这意味着该进程由5条指令组成,并且每条指令是CPU指令的可能性是100%
这条命令有两个5:100,表示同时运行两个进程。
答案:CPU利用率应该为100%,因为两个进程所有指令都是CPU指令。
检查答案是否正确 :
-
- 现在用这些参数运行: ./process-run.py -l 4:100,1:0。这些参数指定了一个包含4条指令的进程(都要使用CPU),并且只是简单地发出IO请求并等待它完成。完成这两个进程需要多长时间?(默认每个CPU指令用1个时钟时间,一个IO请求5个时钟时间)用-c检查你的答案是否正确。
运行结果截图:
答案:11个时钟时间,第一个进程花费4个时钟时间,进程结束后,第二个进程发起IO请求花费一个时钟时间,然后处理IO请求花费5个时钟时间,最后IO结束返回花费1个时钟时间。
- 现在用这些参数运行: ./process-run.py -l 4:100,1:0。这些参数指定了一个包含4条指令的进程(都要使用CPU),并且只是简单地发出IO请求并等待它完成。完成这两个进程需要多长时间?(默认每个CPU指令用1个时钟时间,一个IO请求5个时钟时间)用-c检查你的答案是否正确。
检查答案 :
-
- 现在交换进程的顺序: ./process-run.py -l 1:0,4:100.现在发生了什么?交换顺序是否重要?为什么?同样,用-c看看你的答案是否正确。
运行截图:
答案:第一个进程先运行,发起IO请求进入阻塞状态,CPU此时会调度第二个进程运行。交换顺序很重要,因为如果先运行的进程进入阻塞之后,可以调度后面的程序占用CPU,CPU的利用率变高,时间效率也会变高。
检查答案:
对比两个命令的CPU使用率(-p 参数):
- 现在交换进程的顺序: ./process-run.py -l 1:0,4:100.现在发生了什么?交换顺序是否重要?为什么?同样,用-c看看你的答案是否正确。
-
- 现在探索另一些参数。一个重要的参数是 -S,它决定了当进程发出I/O请求时系统如何反应。将参数设置为SWITCH_ON_END,表示进程进程I/O操作时,系统不会切换到另一个进程,而是等待进程完成。当你运行以下两个进程时,会发生什么情况?一个执行I/O,另一个执行CPU工作。(-l 1:0,4:100 -c -S SWITCH_ON_END)