引言:操作系统的核心概念——作业
在计算机科学领域,操作系统(Operating System, OS)扮演着资源管理者的角色,而“作业”(Job)则是操作系统管理用户任务的基本单位。简单来说,作业是指用户在一次计算任务或事务处理中,要求计算机系统完成的全部工作。它从用户提交源代码或可执行程序开始,经过编译、链接、加载、执行,直到最终输出结果并结束的整个生命周期。
理解作业的概念对于深入掌握操作系统的工作原理至关重要。作业不仅仅是简单的程序执行,它涉及复杂的调度算法、资源分配策略以及进程管理机制。本文将详细解析作业的定义、提交过程、调度机制,以及从代码到运行的幕后推手,帮助读者全面理解这一核心概念。
一、作业的定义与分类
1.1 作业的基本定义
在操作系统中,作业(Job)是指用户要求计算机系统完成的一个计算任务。它通常包括用户程序、数据以及控制信息(如作业控制语言JCL)。作业是操作系统进行资源分配和调度的基本单位。
例如,一个用户编写了一个C语言程序,想要计算斐波那契数列的前10项,他需要将源代码、输入数据以及如何运行的控制命令一起提交给操作系统。这个完整的任务就是一个作业。
1.2 作业的分类
根据处理方式的不同,作业可以分为两大类:
1.2.1 批处理作业(Batch Job)
批处理作业是指用户将作业提交给系统后,系统按照一定的顺序自动处理,用户无需干预。这种作业通常用于处理大量相似的任务,如银行的月末结算、科学计算等。
特点:
- 无需人工交互
- 作业排队等待处理
- 适合处理大量数据
1.2.2 交互式作业(Interactive Job)
交互式作业是指用户通过终端与系统进行实时交互的作业。用户输入命令,系统立即响应并返回结果。例如,使用文本编辑器编写代码、在命令行中运行程序等。
特点:
- 需要用户实时参与
- 响应时间短
- 适合调试和开发
二、作业的生命周期:从提交到完成
作业的生命周期可以分为四个主要阶段:提交、进入内存、执行和完成。每个阶段都涉及操作系统的重要机制。
2.1 作业提交(Job Submission)
作业提交是用户将作业提交给操作系统的第一个步骤。用户通过作业控制语言(Job Control Language, JCL)或命令行界面(CLI)指定作业的参数,如程序名、输入数据、输出要求等。
示例:在Linux系统中提交一个批处理作业
假设我们有一个C语言程序fibonacci.c,用于计算斐波那契数列。我们可以通过以下步骤提交作业:
- 编写源代码:
“`c
#include
int main() {
int n = 10;
int first = 0, second = 1, next;
printf("斐波那契数列前 %d 项:\n", n);
for (int i = 0; i < n; i++) {
printf("%d ", first);
next = first + second;
first = second;
second = next;
}
printf("\n");
return 0;
}
2. **编译源代码**:
```bash
gcc fibonacci.c -o fibonacci
- 提交作业:
在批处理系统中,用户可能需要编写一个作业控制脚本。例如,在Slurm作业调度系统中,可以编写一个提交脚本
submit_job.sh: “`bash #!/bin/bash #SBATCH –job-name=fibonacci_job # 作业名称 #SBATCH –output=fibonacci.out # 输出文件 #SBATCH –error=fibonacci.err # 错误文件 #SBATCH –ntasks=1 # 任务数 #SBATCH –time=00:01:00 # 运行时间限制
# 运行程序 ./fibonacci
4. **提交命令**:
```bash
sbatch submit_job.sh
2.2 作业进入内存(Job Admission)
作业提交后,操作系统会将其放入作业队列中。作业调度器(Job Scheduler)根据系统的资源状况(如内存、CPU时间等)决定是否将作业调入内存。
作业调度器的工作流程:
- 检查系统资源是否足够(如内存空间、I/O设备等)。
- 如果资源充足,将作业从外存(如硬盘)调入内存。
- 为作业创建进程或线程,准备执行。
2.3 作业执行(Job Execution)
作业进入内存后,操作系统会将其转换为一个或多个进程,并交给进程调度器(Process Scheduler)进行CPU调度。进程调度器根据调度算法(如先来先服务、时间片轮转、优先级调度等)分配CPU时间。
示例:进程调度算法
假设系统中有三个进程(作业),它们的到达时间和执行时间如下:
| 进程 | 到达时间 | 执行时间 |
|---|---|---|
| P1 | 0 | 5 |
| P2 | 1 | 3 |
| P3 | 2 | 8 |
先来先服务(FCFS)调度:
- P1先执行,执行时间为5个单位。
- P2等待5个单位后开始执行,执行时间为3个单位。
- P3等待8个单位后开始执行,执行时间为8个单位。
- 平均等待时间:(0 + 5 + 8) / 3 = 4.33
最短作业优先(SJF)调度:
- P2(执行时间3)先执行。
- P1(执行时间5)执行。
- P3(执行时间8)最后执行。
- 平均等待时间:(3 + 0 + 8) / 3 = 3.67
2.4 作业完成(Job Completion)
作业执行完毕后,操作系统会回收其占用的资源(如内存、I/O设备等),并将输出结果返回给用户。作业完成阶段包括:
- 终止进程。
- 释放资源。
- 将输出结果写入指定的文件或设备。
三、作业调度算法详解
作业调度算法决定了哪个作业将被选中进入内存执行。常见的作业调度算法包括:
3.1 先来先服务(FCFS)
FCFS按照作业提交的顺序进行调度。这是最简单的调度算法,但可能导致短作业等待长作业,从而降低系统吞吐量。
示例: 假设有三个作业A、B、C,到达时间和执行时间如下:
| 作业 | 到达时间 | 执行时间 |
|---|---|---|
| A | 0 | 10 |
| B | 1 | 2 |
| C | 2 | 1 |
按照FCFS:
- A先执行,执行时间为10。
- B等待9个单位后执行,执行时间为2。
- C等待11个单位后执行,执行时间为1。
- 平均等待时间:(0 + 9 + 11) / 3 = 6.67
3.2 最短作业优先(SJF)
SJF选择执行时间最短的作业优先执行。这可以减少平均等待时间,但需要预知作业的执行时间。
示例: 使用上面的数据,按照SJF:
- C(执行时间1)先执行。
- B(执行时间2)执行。
- A(执行时间10)最后执行。
- 平均等待时间:(2 + 0 + 10) / 3 = 4
3.3 优先级调度(Priority Scheduling)
优先级调度根据作业的优先级进行调度。优先级可以由用户指定或系统根据作业的特性动态计算。
示例: 假设三个作业的优先级如下(数值越小优先级越高):
| 作业 | 到达时间 | 执行时间 | 优先级 |
|---|---|---|---|
| A | 0 | 10 | 3 |
| B | 1 | 2 | 1 |
| C | 2 | 1 | 2 |
按照优先级调度:
- B(优先级1)先执行。
- C(优先级2)执行。
- A(优先级3)最后执行。
- 平均等待时间:(2 + 0 + 10) / 3 = 4
3.4 时间片轮转(Round Robin)
时间片轮转主要用于分时系统,每个作业被分配一个固定的时间片(如10ms),时间片用完后,作业被放回队列末尾,等待下一次调度。
示例: 假设时间片为2ms,三个作业的执行时间分别为5ms、3ms、8ms。
调度过程:
- P1执行2ms,剩余3ms。
- P2执行2ms,剩余1ms。
- P3执行2ms,剩余6ms。
- P1执行2ms,剩余1ms。
- P2执行1ms,完成。
- P3执行2ms,剩余4ms。
- P1执行1ms,完成。
- P3执行4ms,完成。
四、从代码到运行的幕后推手
从用户编写代码到程序在操作系统中运行,涉及多个步骤和幕后推手。
4.1 编译与链接
用户编写的源代码(如C语言)需要经过编译器编译成目标代码,然后通过链接器链接成可执行文件。
示例:编译和链接过程
预处理:处理宏定义和头文件。
gcc -E fibonacci.c -o fibonacci.i编译:将预处理后的代码编译成汇编代码。
gcc -S fibonacci.i -o fibonacci.s汇编:将汇编代码转换成机器代码(目标文件)。
gcc -c fibonacci.s -o fibonacci.o链接:将目标文件与库文件链接成可执行文件。
gcc fibonacci.o -o fibonacci
4.2 程序加载
当用户执行可执行文件时,操作系统的加载器(Loader)负责将程序从外存加载到内存中,并为程序分配内存空间。
加载过程:
- 分配内存:为程序的代码段、数据段、堆栈段分配内存。
- 加载代码:将可执行文件的代码段和数据段加载到内存中。
- 初始化:初始化程序计数器(PC)和栈指针(SP)。
4.3 进程创建与调度
加载完成后,操作系统会创建一个进程来执行程序。进程是操作系统进行资源分配和调度的基本单位。
进程创建过程:
- 分配进程控制块(PCB):PCB是操作系统用于管理进程的数据结构,包含进程ID、状态、程序计数器、寄存器值、内存指针等信息。
- 分配资源:为进程分配内存、文件描述符等资源。
- 初始化:初始化进程的上下文,包括程序计数器、栈指针等。
4.4 CPU调度
进程创建后,进程调度器根据调度算法分配CPU时间。调度器决定哪个进程在何时使用CPU。
示例:Linux系统中的进程调度
Linux系统使用完全公平调度器(Completely Fair Scheduler, CFS)来调度进程。CFS的目标是让每个进程获得公平的CPU时间。
CFS的核心思想:
- 维护一个红黑树(Red-Black Tree),按进程的虚拟运行时间(vruntime)排序。
- 选择vruntime最小的进程执行。
- 进程运行时,vruntime增加。
- 当进程vruntime增加后,可能会被其他进程抢占。
代码示例:查看进程调度信息
在Linux系统中,可以通过以下命令查看进程的调度信息:
# 查看进程的调度策略和优先级
chrt -p <PID>
# 查看进程的调度统计信息
cat /proc/<PID>/sched
五、作业管理与监控
5.1 作业状态
作业在其生命周期中会经历多种状态,常见的状态包括:
- 提交状态(Submit):作业已提交,但尚未进入系统。
- 等待状态(Wait):作业在队列中等待调度。
- 运行状态(Run):作业正在执行。
- 完成状态(Finish):作业执行完毕。
- 异常状态(Error):作业因错误而终止。
5.2 作业监控工具
操作系统提供了多种工具来监控作业的运行状态。
示例:Linux系统中的作业监控
ps命令:查看当前进程的状态。
ps aux | grep fibonaccitop命令:实时显示系统中各个进程的资源占用情况。
tophtop命令:增强版的top命令,提供更友好的界面。
htop作业调度系统监控:在高性能计算集群中,使用作业调度系统(如Slurm)监控作业状态。
squeue -u <username> # 查看用户作业队列 scontrol show job <jobid> # 查看作业详细信息
六、作业调度的高级主题
6.1 多处理器调度
在多处理器系统中,作业调度变得更加复杂。调度器需要考虑处理器亲和性(Processor Affinity),即尽量让进程在同一个处理器上运行,以减少缓存失效。
6.2 实时调度
实时系统要求作业在规定的时间内完成。实时调度算法包括:
- 硬实时调度:必须满足截止时间,否则会导致严重后果。
- 软实时调度:尽量满足截止时间,但偶尔错过是可以接受的。
6.3 负载均衡
在多处理器或多计算机系统中,负载均衡是确保所有处理器都充分利用的关键。调度器需要将作业分配到负载较轻的处理器或节点上。
七、总结
作业是操作系统管理用户任务的基本单位,其生命周期包括提交、进入内存、执行和完成。作业调度算法决定了哪个作业优先进入内存执行,常见的算法包括FCFS、SJF、优先级调度和时间片轮转。从代码到运行的幕后推手包括编译器、链接器、加载器和进程调度器。
理解作业的管理与调度对于优化系统性能、提高资源利用率至关重要。无论是批处理作业还是交互式作业,操作系统都通过复杂的机制确保任务高效、公平地执行。希望通过本文的详细解析,读者能够对操作系统中的作业有更深入的理解,并在实际应用中更好地利用系统资源。
