Queues Extra: Difference between revisions
pc>Yuron No edit summary |
W81054ch [PHRhYmxlIGNsYXNzPSJ0d3BvcHVwIj48dHI+PHRkIGNsYXNzPSJ0d3BvcHVwLWVudHJ5dGl0bGUiPkdyb3Vwczo8L3RkPjx0ZD51c2VyPGJyIC8+YnVyZWF1Y3JhdDxiciAvPmludGVyZmFjZS1hZG1pbjxiciAvPnN5c29wPGJyIC8+PC90ZD48L3RyPjwvdGFibGU+] (talk | contribs) m (1 revision imported) |
||
(2 intermediate revisions by 2 users not shown) | |||
Line 9: | Line 9: | ||
*Consumer – the process taking items out of the queue | *Consumer – the process taking items out of the queue | ||
Start with an empty queue; both producer and consumer are (asynchronously) running processes but neither has yet used this queue. | Start with an empty queue; both producer and consumer are (asynchronously) running processes but neither has yet used this queue. You can work through the scenario if you wish. (And we encourage you to do so!) | ||
You can work through the scenario if you wish. ( | |||
Clicking any of the ‘symbols’ {A-E} will try to insert that item into the queue; clicking “Output” will try to take the next item out of the queue. These are your [[System_Calls|system calls]]. | Clicking any of the ‘symbols’ {A-E} will try to insert that item into the queue; clicking “Output” will try to take the next item out of the queue. These are your [[System_Calls|system calls]]. | ||
<blockquote> | <blockquote> | ||
The “Consumer” area simply shows a history of items | The “Consumer” area simply shows a history of items which have been read. | ||
which have been read. | |||
</blockquote> | </blockquote> | ||
{{#widget:FIFO}} | {{#widget:FIFO}} | ||
The producer tries to put an item into the queue (“pipe”). | The producer tries to put an item into the queue (“pipe”). It succeeds and carries on running. Maybe it puts more items in, too. | ||
It succeeds and carries on running. Maybe it puts more items in, too. | |||
Now imagine the consumer tries to remove some items. At first it | Now imagine the consumer tries to remove some items. At first it succeeds and continues to run. If it keeps trying the queue will become empty. Notice that items in the “Pipe” – which represents an area of RAM – don’t move. | ||
succeeds and continues to run. If it keeps trying the queue will | |||
become empty. Notice that items in the “Pipe” – which | |||
represents an area of RAM – don’t move. | |||
If the consumer tries again it is unable to proceed. At this point | If the consumer tries again it is unable to proceed. At this point | ||
Line 39: | Line 32: | ||
In the demonstration, the blocked state is indicated in red. | In the demonstration, the blocked state is indicated in red. | ||
</blockquote> | </blockquote> | ||
Conversely, if the producer manages to fill the buffer and then tries | Conversely, if the producer manages to fill the buffer and then tries to enter another item, it has to <strong>sleep</strong> (it blocks) until the consumer <strong>wakeups</strong> it on the removal of an item. | ||
to enter another item, it has to <strong>sleep</strong> (it blocks) until the | |||
consumer <strong>wakeups</strong> it on the removal of an item. | |||
In an ideal world, the queue stays part-full so no one blocks – but | In an ideal world, the queue stays part-full so no one blocks – but the world is not always perfect. | ||
the world is not always perfect. | |||
---- | ---- | ||
=== I/O queues === | === I/O queues === | ||
A common use of queues is to buffer I/O streams. In these cases one | A common use of queues is to buffer I/O streams. In these cases one of the ‘processes’ may be an [[Interrupts|Interrupt Service Routine]] (ISR) rather than a free-running process. The objective is to use the buffers to ‘smooth out’ the rate | ||
of the ‘processes’ may be an [[Interrupts|Interrupt Service | of flow to keep the I/O devices working at maximum efficiency. The principles are still the same although there is a difference in the detail of <strong>sleep</strong> and <strong>wakeup</strong>. | ||
Routine]] (ISR) rather than a free-running process. The | |||
objective is to use the buffers to ‘smooth out’ the rate | |||
of flow to keep the I/O devices working at maximum efficiency. The | |||
principles are still the same although there is a difference in the | |||
detail of <strong>sleep</strong> and <strong>wakeup</strong>. | |||
==== Receiver ==== | ==== Receiver ==== | ||
Here the <em>producer</em> is an input device. When data arrives it causes | Here the <em>producer</em> is an input device. When data arrives it causes an interrupt and the ISR adds it to the queue before returning to… whatever it was that was interrupted. | ||
an interrupt and the ISR adds it to the queue before returning | |||
There is some embarrassment if the buffer gets full because there is | There is some embarrassment if the buffer gets full because there is nothing which can be done – you can’t stop the user typing, for example – so this should be prevented by higher level protocols. | ||
nothing which can be done – you can’t stop the user typing, for | |||
example – so this should be prevented by higher level protocols. | |||
==== Transmitter ==== | ==== Transmitter ==== | ||
Here the <em>consumer</em> is an output device. It does not know when to send so the assumption is that if there is something sleeping (queue not empty) and the device is not busy, then send the next item. | Here the <em>consumer</em> is an output device. It does not know when to send so the assumption is that if there is something sleeping (queue not empty) and the device is not busy, then send the next item. | ||
The interrupt is therefore caused by “transmitter | The interrupt is therefore caused by “transmitter available” (rather than the receiver’s <em>data</em> available). | ||
available” (rather than the receiver’s <em>data</em> available). | |||
This is okay until the queue is empty, at which point the next interrupt needs to <strong>sleep</strong> until there is something in the queue again. In this case this is done by selectively disabling that particular interrupt and returning; it will not now trouble us for a while. | This is okay until the queue is empty, at which point the next interrupt needs to <strong>sleep</strong> until there is something in the queue again. In this case this is done by selectively disabling that particular interrupt and returning; it will not now trouble us for a while. | ||
When the producer (a normal process in this case) puts new data into | When the producer (a normal process in this case) puts new data into the buffer its <strong>wakeup</strong> enables the transmitter interrupt again. As soon as it can (typically after the [[System_Calls|system call]] from the producer returns, as system calls usually disable <em>all</em> interrupts for their duration) the transmitter ISR will get the output stream | ||
the buffer its <strong>wakeup</strong> enables the transmitter interrupt again. As | |||
soon as it can (typically after the [[System_Calls|system call]] from | |||
the producer returns, as system calls usually disable <em>all</em> interrupts | |||
for their duration) the transmitter ISR will get the output stream | |||
busy again. | busy again. | ||
---- | ---- | ||
=== Hazards === | === Hazards === | ||
Because two, potentially independent processes may be writing to and | Because two, potentially independent processes may be writing to and reading from a buffer in memory, there could be a possibility that they may interfere in a dangerous way. For example, if a <em>count</em> of items is maintained, with the writer incrementing a variable and the reader decrementing it, then the increment and decrement operations need to be ensured to be [[Atomicity|atomic]]. | ||
reading from a buffer in memory, there could be a possibility that | |||
they may interfere in a dangerous way. For example, if a <em>count</em> of | |||
items is maintained, with the writer incrementing a variable and the | |||
reader decrementing it, then the increment and decrement operations | |||
need to be ensured to be [[Atomicity|atomic]]. | |||
<blockquote> | <blockquote> | ||
Thought experiment: if not, what could happen? | Thought experiment: if not, what could happen? | ||
Line 93: | Line 64: | ||
Some form of '''[[mutual exclusion]]''' may be needed. | Some form of '''[[mutual exclusion]]''' may be needed. | ||
A mutex-free FIFO implementation is possible with careful [[Operation Ordering|ordering of the operations]]. If the writer only ever <em>modifies</em> the tail pointer and the reader only ever <em>modifies</em> the head pointer it | A mutex-free FIFO implementation is possible with careful [[Operation Ordering|ordering of the operations]]. If the writer only ever <em>modifies</em> the tail pointer and the reader only ever <em>modifies</em> the head pointer it can be made safe. | ||
can be made safe. | |||
<syntaxhighlight lang="bash"> | <syntaxhighlight lang="bash"> | ||
Load write pointer | Load write pointer | ||
Line 135: | Line 105: | ||
For a further hint, see [[Memory_Barrier|memory barrier]]. | For a further hint, see [[Memory_Barrier|memory barrier]]. | ||
(When dealing with unsynchronised processes, sometimes the <strong>order</strong> in | (When dealing with unsynchronised processes, sometimes the <strong>order</strong> in which operations are performed can make the difference between correct code and code with subtle, <strong>non-deterministic</strong> fault cases.) | ||
which operations are performed can make the difference between correct | |||
code and code with subtle, <strong>non-deterministic</strong> fault cases.) | |||
---- | ---- | ||
{{PageGraph}} | {{PageGraph}} | ||
{{Category|IO}} | {{Category|IO}} | ||
{{Category|Processes}} | {{Category|Processes}} |
Latest revision as of 10:03, 5 August 2019
Depends on | Queues • Process States |
---|
A deeper look at interaction of processes with queues.
Definitions:
- Producer – the process putting items into the queue
- Consumer – the process taking items out of the queue
Start with an empty queue; both producer and consumer are (asynchronously) running processes but neither has yet used this queue. You can work through the scenario if you wish. (And we encourage you to do so!)
Clicking any of the ‘symbols’ {A-E} will try to insert that item into the queue; clicking “Output” will try to take the next item out of the queue. These are your system calls.
The “Consumer” area simply shows a history of items which have been read.
The producer tries to put an item into the queue (“pipe”). It succeeds and carries on running. Maybe it puts more items in, too.
Now imagine the consumer tries to remove some items. At first it succeeds and continues to run. If it keeps trying the queue will become empty. Notice that items in the “Pipe” – which represents an area of RAM – don’t move.
If the consumer tries again it is unable to proceed. At this point the consumer process becomes blocked and the scheduler is called. This is referred to as “sleep” or “wait” and it will persist as long as the queue is empty.
The producer, we hope, continues to run and eventually puts another item into the queue. The ‘put’ process checks to see if there is a process waiting (sleeping) and, if there is, it sends a “signal” (or “wakeup”) which unblocks the blocked process. In this case the wakeup will alter the consumer’s state to ready; the consumer will run again sometime in the (near) future. This will be very quick in the demo!
In the demonstration, the blocked state is indicated in red.
Conversely, if the producer manages to fill the buffer and then tries to enter another item, it has to sleep (it blocks) until the consumer wakeups it on the removal of an item.
In an ideal world, the queue stays part-full so no one blocks – but the world is not always perfect.
I/O queues
A common use of queues is to buffer I/O streams. In these cases one of the ‘processes’ may be an Interrupt Service Routine (ISR) rather than a free-running process. The objective is to use the buffers to ‘smooth out’ the rate of flow to keep the I/O devices working at maximum efficiency. The principles are still the same although there is a difference in the detail of sleep and wakeup.
Receiver
Here the producer is an input device. When data arrives it causes an interrupt and the ISR adds it to the queue before returning to… whatever it was that was interrupted.
There is some embarrassment if the buffer gets full because there is nothing which can be done – you can’t stop the user typing, for example – so this should be prevented by higher level protocols.
Transmitter
Here the consumer is an output device. It does not know when to send so the assumption is that if there is something sleeping (queue not empty) and the device is not busy, then send the next item.
The interrupt is therefore caused by “transmitter available” (rather than the receiver’s data available).
This is okay until the queue is empty, at which point the next interrupt needs to sleep until there is something in the queue again. In this case this is done by selectively disabling that particular interrupt and returning; it will not now trouble us for a while.
When the producer (a normal process in this case) puts new data into the buffer its wakeup enables the transmitter interrupt again. As soon as it can (typically after the system call from the producer returns, as system calls usually disable all interrupts for their duration) the transmitter ISR will get the output stream busy again.
Hazards
Because two, potentially independent processes may be writing to and reading from a buffer in memory, there could be a possibility that they may interfere in a dangerous way. For example, if a count of items is maintained, with the writer incrementing a variable and the reader decrementing it, then the increment and decrement operations need to be ensured to be atomic.
Thought experiment: if not, what could happen?
Some form of mutual exclusion may be needed.
A mutex-free FIFO implementation is possible with careful ordering of the operations. If the writer only ever modifies the tail pointer and the reader only ever modifies the head pointer it can be made safe.
Load write pointer
Store data at write pointer
Increment write pointer
Store write pointer (so reader can see it)
Exercises: What could go wrong if the store operations were reversed?
AnswerIf the code were written like this:
Load write pointer Store write pointer (so reader can see it) Store data at write pointer Increment write pointerThe reader could try to read the data any time after the second statement, including before it has been written to memory.
If the timing were unfortunate then undefined data would be read. In practice this would cause an intermittent fault in the software – very hard to find and debug.
What is the equivalent (correct!) code sequence for the reader?
AnswerLoad read pointer Load data at read pointer Increment read pointer Store read pointer (so writer can see it)The danger here is even more subtle: the problem occurs because, once the read pointer is written back the location (in a cyclic buffer) can be reused and thus overwritten with a new value. The correct value must have been loaded before this is allowed.
Try to work these out before revealing the answers!
For a further hint, see memory barrier.
(When dealing with unsynchronised processes, sometimes the order in which operations are performed can make the difference between correct code and code with subtle, non-deterministic fault cases.)