Cambridge AS & A Level · Part 3 · Chapter 20
System Software
How the operating system shares one CPU, one memory and slow I/O between many processes, and how a compiler turns source code into object code. Every figure, question, task and exam-style question from the chapter is here, with labs to try each idea.
Guru Jazzy🎯 Learning objectives
By the end of this chapter you should be able to:
- show how an operating system can make the most of the computer's resources
- describe how the user interface hides the complexity of the hardware from the user
- Show understanding of process management
- show understanding of virtual memory, paging and segmentation for memory management
- show how an interpreter can run a program without producing a translated version
- show understanding of the stages of compiling a program
- show how a language's grammar can be written as syntax diagrams or in Backus–Naur Form (BNF)
- show how Reverse Polish Notation (RPN) can be used to evaluate expressions
🧭 20.01 The purposes of an operating system (OS)
Chapter 8 (Section 8.02) listed what an operating system does. This chapter looks at some of those jobs in more depth. Start with four ideas:
- Booting. When the computer is switched on, the OS is still on disk, so there is no OS running yet. ROM holds a BIOS (basic input/output system) that starts a bootstrap program. The bootstrap loads the OS into memory and sets it running.
- Several programs at once. The OS can keep more than one program in memory. Only one can use the CPU at any instant; the others wait, ready to run when they get the chance. This is multi-programming (one user). A system with many users logged in at the same time is a time-sharing system.
- Two viewpoints. The internal viewpoint is how the OS organises its own work to use the resources well. The external viewpoint is the facilities it gives the user.
- Three key resources in the internal view: the CPU, the memory and the I/O system.
Operating system structure
The OS has to manage resources and provide facilities for users, so it runs in two modes:
The kernel runs all the time. The rest of the OS runs in user mode, and each part is only loaded when it is needed. One way to arrange this is a layered structure: application programs and utilities make system calls to the kernel, and each layer must be fully serviced by the layer below it, just like a network protocol stack.
A strict layered structure is hard to build in practice. A more flexible approach is a modular structure: the kernel calls on separate service modules (memory, file, device and scheduling management) only when it needs them.
Taken further this becomes a micro-kernel, where the kernel itself is cut down to the absolute minimum and everything else is a module.
🔌 20.02 The input/output (I/O) system
The I/O system is not only about a user typing or reading a screen. It also covers data moving to and from storage devices while a program runs. Each device is reached through its own device driver.
The bus structure gives the OS a choice. Small transfers can go through the CPU, but for large amounts of data the OS can arrange a direct transfer between memory and the I/O device, leaving the CPU free.
Why does this matter? A CPU typically runs at gigahertz frequencies, thousands of millions of clock cycles every second, so one second is a very long time for a computer system. Compare that with typical I/O speeds:
| Device | Data rate | Time to transfer 1 byte |
|---|---|---|
| Keyboard | 10 Bps | 0.1 s |
| Screen | 50 MBps | 2 × 10−8 s |
| Disk | 5 MBps | 2 × 10−7 s |
Table 20.01 Typical rates and times for data transfer
So the CPU must never sit idle while I/O happens. Managing CPU time is the job of the scheduler (next tab).
🔬 Lab: if one clock cycle lasted one second
Choose a clock speed. The table shows how many clock cycles pass while one byte moves, and how long that would feel if each cycle took one second.
| Device | Cycles per byte | Feels like… |
|---|
⚙️ 20.03 Process scheduling
Managing the CPU mostly means scheduling it so that it is used efficiently. Here the CPU is treated as a single unit; multiprocessor issues are left out.
Programs start out on disk. A user can submit a program as a job: the program plus instructions on how it should run.
- A long-term or high-level scheduler chooses which program on disk is moved into main memory.
- If memory gets overcrowded, a medium-term scheduler may move a program back out to disk.
- Once a program is in memory, a short-term or low-level scheduler decides when it gets the CPU.
🔄 Process states Figure 20.05
Chapter 8 defined a process as "a program being executed". A better definition includes its state from the moment it arrives in memory. At that point a process control block (PCB) is created, ready to hold the data the process needs while it runs. After that, the process moves between states:
- A new process arrives in memory and a PCB is created → ready.
- The dispatcher gives a ready process the CPU → running.
- A running process is halted by an interrupt → back to ready.
- A running process cannot continue until some event happens (I/O, perhaps) → waiting (also called suspended or blocked).
- A waiting process is told its event has completed → ready.
- A running process finishes → terminated.
A process can be split into parts that run separately, called threads. Each thread is then handled as an individual process.
🔬 Lab: drive a process through its life
Press an event. Only the legal transitions work. Try making a waiting process run directly and see what happens.
⚡ Interrupts
Some interrupts are caused by errors that end a running process early. Otherwise there are two reasons for an interrupt:
- A process alternates between bursts of CPU use and I/O. I/O takes far too long for the CPU to wait idle, so when a running process makes a system call that needs I/O, an interrupt moves it to the waiting state.
- The scheduler decides to halt the process, for one of the reasons in the scheduling algorithms below.
Whatever the reason, the kernel runs an interrupt-handling routine. It may have to decide the interrupt's priority. One thing it must do is save the current register values into the process's PCB, so the process can carry on exactly where it stopped when it next runs.
My thoughts
Without priorities the second interrupt would either be lost or would break into the first routine at an unsafe moment. So interrupts are given priorities. If the new one has a higher priority, the current handler is itself suspended (its registers saved) and the new one is serviced first, then the first resumes. If it has equal or lower priority it is held (masked) until the current routine finishes.
📋 Scheduling algorithms
The high-level scheduler also makes decisions, but here we look at the options for the low-level scheduler.
| Algorithm | Type | How it works |
|---|---|---|
| First come first served (FCFS) | Non-preemptive | Processes queue in a first-in first-out (FIFO) queue. Very inefficient on its own, but useful as part of a more complex scheme. |
| Round robin | Preemptive | Each process gets a fixed time slice, then is halted and goes to the back of a FIFO queue. Usually no priorities, but separate queues for different priority levels can each be scheduled round-robin. |
| Priority-based | Usually preemptive | More complicated: priorities may need re-working every time a process joins the ready queue or is halted, and judging priority needs some computation. |
Possible criteria for priority
- estimated time the process will take to run
- Estimated time remaining
- how long it has already spent in the ready queue
- whether it is I/O bound or CPU bound
More than one criterion may be used, and estimating run time is not easy. Take a process that prints employees' wage slips: it needs a lot of I/O and very little CPU. It makes sense to give it a high priority so its short CPU bursts happen quickly; it then spends most of its time waiting while the printer works, out of everyone's way.
🔬 Lab: scheduling simulator
Random processes arrive. Pick an algorithm and compare the Gantt chart and the average waiting time. (Priority 1 = highest.)
| Process | Arrives at | CPU burst | Priority |
|---|
| Process | Finishes at | Turnaround | Waiting time |
|---|
🎮 Preemptive or not?
🧠 20.04 Memory management
Memory management covers several jobs:
- providing protected memory for the OS kernel
- loading a program: deciding the memory addresses for the program, its procedures and its data
- in a multiprogramming system, coping with fragmentation of main memory (just like files on a hard disk); the medium-term scheduler may move a process out to ease the problem
- deciding how much memory each process sharing memory should get
Partitions and segments
An early approach was to partition memory and load the whole of a process into one partition. That wasted memory whenever the process was smaller than its partition. Dynamic partitioning let the partition size match the process, but the rule stayed "one process per partition".
Segmentation extended this to larger processes: the process was split into segments, and each segment was loaded into a dynamic partition. Two things limited it:
- segments did not have to be the same size;
- there was not always room for all of one process's segments at once, so segments were shuffled between disk and memory.
Together these caused fragmentation of both memory and disk, and the system slowed down.
📄 Paging and virtual memory
The modern approach is paging. The process is divided into equal-sized pages, and memory is divided into frames of the same size. Secondary storage can be divided into frames too.
All the pages could be loaded at once, but usually not every part of a program is needed at the same time; large programs have optional routes. A special case is a program whose address space is bigger than the memory itself. Paging then supports virtual memory.
The CPU passes addresses to a memory management unit (MMU), which finds the matching place in a page frame. An address has two parts: the page number and the offset from the start of the page. The MMU uses a page table.
- Left: a program of 48 instructions occupies three pages, the first 48 logical addresses. With 8-bit logical addresses, the top 4 bits are the page number (so only 16 pages) and the bottom 4 bits are the offset.
- Right: page frames with physical addresses. The first two program pages have been loaded; the frames used do not have to be next to each other.
- Centre: this process's page table (every process has its own). The page number is the index; the presence flag says whether the page is in memory; the third column is the frame number (it could instead hold the physical address of the frame's first item).
00001111 (not 00011111), and the bottom of page frame 3 should be 00110000 (not 01100000). Frame 3 runs from 00110000 to 00111111.Page faults and replacement
A process starts with all its pages on disk. One or more are loaded as it moves to the ready state. While it runs it will, sooner or later, need a page the page table says is not in memory: a page fault. Usually a page must be taken out of memory first to make room, chosen by a page replacement algorithm. The simple choice is first-in first-out; a more sensible one is least recently used (LRU), but that needs statistics of page use to be kept.
The overhead of virtual memory can hurt. The worst case is disk thrashing: code on one page needs another page that is on disk; once that is loaded, it almost immediately needs the first page again… pages are loaded and unloaded almost endlessly. Algorithms exist to guard against this, but it can still happen.
🔬 Lab: be the memory management unit
8-bit logical address = 4-bit page number + 4-bit offset. Use the page table to find the physical address, or spot a page fault.
| Page | Presence flag | Frame |
|---|
🔬 Lab: FIFO vs LRU page replacement
Each number is a page the process asks for. Green = new page loaded, red column = page fault. Try "thrash" with 2 frames.
First-in first-out
Least recently used
How to find out, and an answer
On Windows: Settings → System → About → Advanced system settings → Performance Settings → Advanced → Virtual memory shows the paging-file size. On macOS/Linux, sysctl vm.swapusage or free -h shows swap, and getconf PAGESIZE gives the page size (commonly 4096 bytes; Apple silicon uses 16 KB).
You might enlarge virtual memory when large programs (video editing, big datasets, virtual machines) run out of RAM, or shrink/move it when disk space is short or to reduce wear on an SSD. Constant thrashing is a sign that the real fix is more RAM.
🧑💻 20.05 Operating system facilities provided for the user
- User interface: a command line, a graphical display or voice recognition. Whatever the form, it lets the user interact with running programs.
- Device drivers: when a program uses a device, the OS provides the driver; the user just expects the device to work. (You might argue that printers don't always live up to this!)
- File system: the user chooses file names and a directory (folder) structure, but never has to organise the physical storage on the disk.
- Programming environment: a program can be written and run without the programmer knowing how the processor works.
- System calls: a running program is a kind of user too. If it needs to read a file, the request becomes a system call: the OS takes charge, finds the file and makes it available.
- Application programming interface (API): a set of calls, each doing a specific job such as creating a screen icon, each using one or more system calls. An API aims to make programs portable, so they run on different operating systems with minimal changes.
🛠️ 20.06 Translation software
Chapter 8 (Section 8.05) gave an overview of compilers and interpreters. Here we look inside a compiler, with a brief note on interpreters. Writing one is a specialist job and every team has its own methods, so these are general principles.
A compiler has a front end and a back end. The front end analyses the source code and, unless it finds errors, produces intermediate code that fully expresses the meaning (semantics) of the source. The back end takes that intermediate code and synthesises object code: the analysis–synthesis model.
Figure 20.07 assumes no errors. The source is read line by line and each line becomes matching intermediate code. An interpreter has the same analysis front end, but once a line is error-free and converted to intermediate code, it is executed straight away, so no translated version of the whole program is ever produced.
Front-end analysis stages
- Lexical analysis
- Syntax analysis
- Semantic analysis
- Intermediate code generation
1. Lexical analysis
The source code is just a sequence of characters. Each meaningful character or group of characters is a lexeme: an identifier chosen by the programmer, or a keyword, operator or symbol defined by the language. One approach is to remove all white space and comments, then take each line and identify every lexeme, a pattern-matching job that needs knowledge of the language.
Var Count : integer;
has five lexemes: Var Count : integer ;. And
PercentMark[Count] := Score * 10
has eight: PercentMark [ Count ] := Score * 10.
Each lexeme is then categorised so the line can be tokenised: Var and integer are keywords, Count is an identifier, and : and ; are distinct lexemes.
Every identifier gets an entry in the symbol table (it could have been called the identifier table). It holds attributes such as the data type, where it is declared and where it is assigned a value. Figure 20.08 shows it feeding syntax analysis, but later stages use it too, and most compilers are multi-pass, so the table is updated often.
Model answer
- Assembler: the table holds labels (and named variables/constants) with the address each one stands for. It is built in the first pass and used in the second to replace labels with addresses. Few attributes.
- Compiler: each record holds an identifier's name plus many attributes: data type, scope, where it is declared, where it is assigned, its eventual memory location. It is used and updated by several stages (syntax, semantic, code generation).
- Interpreter: similar attributes to a compiler's table, but built and updated on the fly as each line is analysed and executed, and it also holds the current values of variables while the program runs.
Another table supplies the token for each lexeme that is not an identifier; sometimes identifiers are replaced by tokens too. Whatever the scheme, the output of lexical analysis is a tokenised version of the source code.
Model answer
- In some languages white space is meaningful, e.g. indentation marks blocks in Python, and line ends end statements.
- Spaces inside string literals must be kept exactly.
- White space separates lexemes:
Var Countis two lexemes butVarCountis one identifier. - Keeping positions lets error messages report the exact line and column, and lets tools re-display or pretty-print the code.
🔬 Lab: lexical analyser
Type a line of code. Comments (//) and white space are removed, each lexeme is categorised and identifiers go into the symbol table.
| Index | Identifier | First seen at lexeme | Attributes (filled in later) |
|---|
2. Syntax analysis (parsing)
Parsing analyses the program's constructs. The result is a syntax tree or parse tree. For
y := 2 * x + 4
Read correctly, the shape of the tree makes sure that 2 * x is worked out before 4 is added.
3. Semantic analysis
Semantic analysis establishes the full meaning of the code. It builds an annotated abstract syntax tree: each identifier in the tree gets attributes, such as its data type, which are also recorded in the symbol table.
4. Intermediate code generation
A common intermediate code is three-address code. This statement uses five identifiers, so it needs five addresses:
y := a + (b * c - d) / e
It can be rewritten as four statements, each using at most three addresses:
temp := b * c temp := temp - d temp := temp / e y := a + temp
🔬 Lab: three-address code generator
Type an assignment with single-letter or named variables, numbers, + − * / ^ and brackets.
📐 Representation of the grammar of a language
Every programming language has a defined grammar, which both the programmer and the compiler writer must understand.
A syntax diagram is one way to present a grammar rule. Figure 20.10 says an identifier must start with a letter, followed by any combination of none or more letters or digits. The convention here: options are drawn above the main flow line and repetitions below it.
The alternative is Backus–Naur Form (BNF). A possible BNF definition of an identifier:
<Identifier> ::= <Letter>|<Identifier><Letter>|<Identifier><Digit> <Digit> ::= 0|1|2|3|4|5|6|7|8|9 <Letter> ::= <UpperCaseLetter>|<LowerCaseLetter> <UpperCaseLetter> ::= A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z <LowerCaseLetter> ::= a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z
|separates the options;::=reads "is defined as".- The definition of
<Identifier>is recursive: it uses itself. Without recursion it would have to list every possible combination after the first letter.
🔬 Lab: does it match the grammar?
Choose a rule, type a string and test it. The checker follows the BNF one symbol at a time.
⚙️ Back-end synthesis stages
If the front end found syntax errors, the only back-end job is to list them, each with an explanation and its location in the source.
With no errors, the main back-end stage is machine code generation from the intermediate code. This may include optimisation, which aims to make the program efficient. One kind tidies up inefficiencies that were in the source and were carried into the intermediate code:
x := (a + b) * (a - b) y := (a + 2 * b) * (a - b)
temp := (a - b) x := (a + b) * temp y := x + temp * b
Model answer
y = (a + 2b)(a − b) = ((a + b) + b)(a − b) = (a + b)(a − b) + b(a − b) = x + temp × b. ✔ The optimised version works out (a − b) once and reuses x, saving operations.
Another example: a statement inside a loop that does the same thing every time round. Optimisation moves it to just before the loop.
The other kind of optimisation happens after the machine code has been created, e.g. making efficient use of registers or memory.
🎮 Which stage does this?
🔁 Evaluation of expressions
An assignment statement often has an algebraic expression. It can be evaluated by first converting its infix form into Reverse Polish Notation (RPN). RPN is a postfix notation: it never needs brackets and has no rules of precedence.
Manually converting an expression between RPN and infix
Converting to RPN. Take a + b * c. Precedence matters, so convert b * c first:
a + b c *
then convert the two terms to get the final RPN:
a b c * +
Had it been (a + b) * c, where the brackets matter, the RPN would be a b + c *.
Converting from RPN. Take x 2 * y 3 * + 6 /. Scan until two operands are followed by an operator and combine them (brackets just for clarity):
(x * 2) y 3 * + 6 / (x * 2) (y * 3) + 6 / (x * 2) + (y * 3) 6 / ((x * 2) + (y * 3)) / 6
Precedence makes some brackets unnecessary, so the final version is (x * 2 + y * 3) / 6.
Using a syntax tree to convert an expression to RPN
In syntax analysis, a + b * c becomes a syntax tree. The lowest-precedence operator (+) goes at the root; if several share the same precedence, the first one is used.
The RPN is read off by a post-order traversal: start at the lowest leaf left of the root and visit left – right – root each time, giving a b c * +.
Using a stack with an RPN expression
To convert infix to RPN with a stack, use the shunting-yard algorithm. Picture the tokens as railway wagons shunted from the infix line to the RPN line, with a siding (the stack) in between. Examine each token in turn:
- An identifier goes straight through to the RPN line.
- An operator:
- if the stack is empty or its top operator has lower precedence, the operator goes onto the stack;
- if the top has equal or higher precedence, that operator is popped to the RPN line and the new one takes its place on the stack.
- When the infix line is empty, the operators left on the stack are popped one by one onto the RPN line.
| Infix line | Stack line | RPN line |
|---|---|---|
| a + b * c | ||
| + b * c | a | |
| b * c | + | a |
| * c | + | a b |
| c | + * | a b |
| + * | a b c | |
| + | a b c * | |
| a b c * + |
Table 20.02 Trace of the conversion process
Here + enters the stack first; * has higher precedence, so it joins it. At the end * is popped, then +. Had the expression been a * b + c, * would have entered first but been popped before + could enter.
a * b - c + d below), and it handles brackets by stacking ( and popping back to it at ). The lab does both.🔬 Lab: shunting-yard step by step
Type an infix expression (single letters, names or numbers; + − * / ^ and brackets). Step through and watch the three lines, then see the syntax tree and its post-order traversal.
| Infix line | Stack line | RPN line | Why |
|---|
🥞 Evaluating an RPN expression
A stack can evaluate RPN. Take x 2 * y 3 * + 6 / with x = 3 and y = 4. Values are pushed in turn. When the next item is an operator, the top two items are popped, the operator combines them, and the result is pushed. Then carry on.
The figure shows the stack after each step, with the operator above when one is used. (States with two values popped are not shown.) The answer is 3.
−, / and ^, the first value popped is the right-hand operand. With 18 then 6 on the stack, / gives 18 / 6, not 6 / 18.🔬 Lab: RPN stack machine
- Convert these infix expressions into RPN using the methods of Worked Examples 20.01, 20.02 and 20.03:
(x - y) / 4and3 * (2 + x / 7) - Convert these RPN expressions into infix (
^means "to the power of"):4 a b + c + d + e + *andy 2 ^ z 3 ^ + 6 / - Choose simple values for each variable in part 2, evaluate the infix version, then use the stack method on the RPN and check you get the same result.
Model answers
x y - 4 /and3 2 x 7 / + *4 * (a + b + c + d + e)and(y ^ 2 + z ^ 3) / 6- e.g. a = 1, b = 2, c = 3, d = 4, e = 5: infix 4 × 15 = 60; stack: 4, 1, 2 → 3, 3 → 6, 4 → 10, 5 → 15, then 4 × 15 = 60 ✔. And y = 4, z = 2: (16 + 8) / 6 = 4; stack: 4, 2 → 16, 2, 3 → 8, + → 24, 6, / → 4 ✔. (Paste them into the stack machine to check.)
🎲 Practice generator
RPN would be of little use on the simple processor of Chapter 6. Modern processors have stack instructions, so a compiler can convert expressions to RPN knowing the machine code can use the stack directly.
📌 Summary
- The operating system provides resource management, including scheduling of processes, memory management and control of the I/O system.
- For the user, the OS provides an interface, a file system and application programming interfaces.
- A modular approach gives a flexible structure for the OS.
- A process has five states: new, ready, running, waiting and terminated.
- A process may be interrupted by an error, a need for I/O, or the scheduling algorithm.
- A virtual memory system uses paging and a memory management unit that uses a page table.
- A compiler has a front end that analyses and a back end that synthesises.
- Backus–Naur Form represents the rules of a grammar.
- Reverse Polish Notation is used to evaluate expressions.
🃏 Key-term flashcards
Tap a card to flip it.
✅ Quick quiz
📝 Exam-style questions
Write your answer first, then open the model answer. These are my mark-scheme-style points, not official mark schemes.
(i) A process is a program that is being executed / loaded into memory, with its own process control block holding its state, register values and resources.
(ii) Any three, 2 marks each:
- New: the program has just been loaded into memory and its PCB created.
- Ready: in memory, able to run, waiting in the ready queue for the CPU.
- Running: currently has the CPU and is executing.
- Waiting / blocked: cannot continue until an event, e.g. I/O, completes.
- Terminated: has finished; its resources are being released.
(i) Any two: first come first served: non-preemptive · round robin: preemptive · shortest job first: non-preemptive · shortest remaining time: preemptive · priority-based: (usually) preemptive.
(ii) Any two: estimated execution time · estimated remaining time · time already spent in the ready queue · whether it is I/O bound or CPU bound.
(i)
- Partitioning: memory is divided into partitions (fixed or dynamic) and a whole process is loaded into one partition.
- Segmentation: a process is divided into segments of different sizes, each loaded into a dynamic partition.
- Paging: a process is divided into equal-sized pages that are loaded into page frames of the same size, not necessarily next to each other.
(ii) Any two: segments loaded into dynamic partitions · segments divided into pages (segmented paging) · paging used with virtual memory so only some pages/segments are in memory at once.
(i) Paging.
(ii) Any two, explained: a program larger than physical memory can still run, because only the pages in use need to be in memory · more processes can be in memory at once, improving multiprogramming · less loading time because unused parts of a program are never loaded · the programmer need not worry about how much physical memory there is.
(iii) Disk thrashing: loading one page forces out a page that is needed almost straight away, so pages are swapped in and out continually and the system spends its time paging instead of doing useful work.
(i) Front end: analyse the source code, report errors and produce intermediate code that expresses its full meaning. Back end: synthesise (optimised) object code from the intermediate code.
(ii) Any two: lexical analysis · syntax analysis · semantic analysis · intermediate code generation.
(iii) Any two: code optimisation · machine/object code generation · producing the error list · register allocation.
<Digit> ::= 0|1|2|3|4|5|6|7|8|9 <Sign> ::= +|- <Unsigned integer> ::= <Digit>|<Unsigned integer><Digit> <Signed integer> ::= <Sign><Unsigned integer>
Test it in the grammar lab on the Translation tab.
(c) a 6 + b c / +
(d) a + (3 * b - 6 * c), i.e. a + 3 * b - 6 * c
- (i) There is no semicolon at the end.
- (ii)
2is not a variable: a variable can only be made of letters. - (iii)
ccecontainse, which is not one of the allowed letters a–d.
<assignmentstatement> ::= <variable> = <variable> <operator> <variable> ; <variable> ::= <letter>|<letter><letter>|<letter><letter><letter> <letter> ::= a|b|c|d <operator> ::= +|-|*|÷
(c) <variable> ::= <letter>|<variable><letter> (a recursive definition).
(i) The interpreter stops at the line with the error, so it can be fixed and the program continued straight away without recompiling everything (and variable values can be inspected).
(ii) Only the object code is distributed; machine code is very hard to read or reverse-engineer, so customers never see the source code.
Cambridge International AS & A Level Computer Science 9608, Paper 31 Q1, June 2015
A program is a static set of instructions, e.g. stored in a file on disk. A process is a program in execution: loaded into memory with a PCB, a state, and allocated resources. One program can give rise to several processes.
- (i) The process is interrupted by the low-level scheduler: its time slice has run out, or a higher-priority process needs the CPU. It still has work to do, so it goes back to the ready queue.
- (ii) The CPU becomes free and the low-level scheduler (dispatcher) picks this process, e.g. it is next in the ready queue or has the highest priority.
- (iii) The process makes a request it must wait for, e.g. I/O or a resource that is not available, so it cannot continue even if given the CPU.
(c) When the event a blocked process is waiting for completes, it only becomes able to run. Another process is usually using the CPU, and the scheduler only chooses from the ready queue, so the process must join the ready queue and wait its turn by the scheduling algorithm. Moving it straight to running would bypass the scheduler and be unfair to processes already waiting.
(d) It decides which jobs on disk are loaded into memory (admitted to the ready queue), and so controls how many processes are in memory at once, aiming for a good mix of CPU-bound and I/O-bound processes.
Cambridge International AS & A Level Computer Science 9608, Paper 31 Q6, November 2015
Guru Jazzy · Cambridge AS & A Level Computer Science · Chapter 20 System Software · Figures from the Cambridge coursebook (Langfield & Duddell)