ARM Cortex
Embedded System Multitasking
Programming Using
FreeRTOS
Andrew Eliasz
Feb 2013
FreeRTOS
Overview
FreeRTOS - The Who
● FreeRTOS was developed by a “very clever” software engineer Richard Barry ● As it says on the Lulu website who publish his books
● Richard Barry graduated with 1st Class Honors in Computing for Real Time
Systems.
● He's been directly involved in the start up of several companies, primarily
working in the industrial automation and aerospace and simulation markets.
● Barry is currently a director of Real Time Engineers Ltd. (owners of FreeRTOS)
and
● Head of Innovation at Wittenstein High Integrity Systems.
● Interestingly the only books on developing with FreeRTOS are those written by
Richard
● In this respect FreeRTOS has something in common with Micriums' uCOS operating
system
● The books on developing with uCOS II / uCOS III are written by the developer of
uCOS - Jean Labrosse
● Both FreeRTOS and uCOS have API (Application Programming Interface)s that are
FreeRTOS - Some claims
● Aims to:
● "Provide a free product that surpasses the quality and service demanded by
users of commercial alternatives"
● FreeRTOSTM is a market leading RTOS from Real Time Engineers Ltd. that supports
33 architectures and received 103000 downloads during 2012.
● It is professionally developed, strictly quality controlled, robust, supported, and free to
embed in commercial products without any requirement to expose your proprietary source code
● FreeRTOS has become the de facto standard RTOS for microcontrollers by removing
common objections to using free software, and in so doing, providing a truly compelling free software model
● Question: Which other operating systems use the FreeRTOS API ?
● Question: Are there any embedded operating systems that use the Posix API ( or a
subset of the Posix API ) ?
FreeRTOS - More Technical Claims
● FreeRTOS never performs a non-deterministic operation, such as walking a linked
list, from inside a critical section or interrupt
● Question: What does this mean ?
● FreeRTOS uses an efficient software timer implementation that does not use any
CPU time unless a timer actually needs servicing
● Software timers do not contain variables that need to be counted down to zero ● Question: Timers are, essentially, counters that generate a time even when the
timer overflows/reaches 0 [depending on whether it counts up or down]
● Lists of Blocked (pended) tasks do not require time consuming periodic servicing. ● Question: What does this mean ?
● The FreeRTOS queue usage model manages to combine simplicity with flexibility (in
a tiny code size) - attributes that are normally mutually exclusive
● Question: How is this possible ?
● FreeRTOS queues are base primitives on-top of which other communication and
synchronisation primitives are build
● The code re-use opportunities that result reduce overall code size
– This, in turn, assists testing and helps ensure robustness – Question: How would you test this claim ?
FreeRTOS - Technology Highlights
Wikipedia - FreeRTOS Description
●
FreeRTOS provides methods for multiple threads or tasks, mutexes,
semaphores and software timers.
●
A tick-less mode is provided for low power applications.
●Thread priorities are supported.
●
In addition there are four schemes of memory allocation provided
–
Allocate only
–
Allocate and free - using simple but fast, algorithm
–
A more complex fast allocate and free algorithm with memory
coalescence;
–
A C library supporting allocate and free with some mutual exclusion
protection
●
FreeRTOS has none of the features typical of operating systems such as
Linux or Microsoft Windows i.e. no support for
–
Device drivers
Wikipedia - FreeRTOS Description ctd.
●
FreeRTOS can be thought of as a 'thread library' rather than an 'operating
system'
–
Command line interface and POSIX like IO abstraction add-ons are
available
●
FreeRTOS+IO provides a Linux/POSIX like
open(), read(),
write(), ioctl() type interface to peripheral driver libraries
–
VxWorks from Wind River Systems has a proprietary API and also
supports a Posix API (i.e. the software was not designed with a Posix
API in mind)
–
The Rowebots RTOS was implemented with a Posix API in mind
●
FreeRTOS implements multiple threads by
●
Having the host program call a thread tick method at regular short
intervals
●
The thread tick method switches tasks depending on priority and a
round-robin scheduling scheme
–
Switching is driven via a hardware timer interrupt
Mastering FreeRTOS - Plan of Action
● FreeRTOS, although a tiny operating system, is far from simple
● Mostly written in generic C code, but, also some platform specific C and assembler ● This course is focused on ARM Cortex M3/M4 processors
● How much of the M3/M4 processor architecture do you need to understand ? ● Which compilers and IDEs can you use for developing applications ? - e.g.
– Keil (now owned by ARM) IDE – IAR
– Code Sourcery (Lite / commercial Eclipse based) - compiler is GCC – Code Red (widely favoured by NXP)
● Which drivers and protocols stacks will you be using ?
– Open source ? – Commercial ?
– Developed in house ?
● Complex systems and protocols
– USB ?
– Ethernet and TCP/IP ?
– CAN (Controller Area Network) ? – Rich GUIs ?
Mastering FreeRTOS - Plan of Action - ctd.
● Start off with something that works e.g.
● Evaluation board approximating the capabilities your project will need and for
which a port of FreeRTOS exists and for which there are some demonstrator programs that work
– Ideally there is a port that uses the IDE you will be using in your project
● Evaluate and organise the available documentation and forum discussions ● The published FreeRTOS books are good, but only take you so far
● Areas of “obscurity”
– Architecture specific details
– Complex libraries and peripherals are not part of the package
● USB ? ● TCP/IP ?
● LCD graphics libraries ? ● CAN bus support ?
– Libraries supplied with your IDE and Microcontroller vendor ?
● Have these libraries been integrated with FreeRTOS ? ● If not ... then who will integrate them ?
FreeRTOS - Analysis and Design Issues
● How to progress from initial requirements analysis and design to code specification
and implementation
● UML based requirements analysis and desing
● Hatley-Pirbhai or Ward-Mellor analysis and design ● How might you realise an object oriented design in C ? ● How is code to be “split up into modules”
● How is code to be partitioned into tasks ? ● Testing and integration
● MISRA guidelines ● Unit testing
● Code tracing and debugging
● How will the embedded systems targets be upgraded ? ● Patches and Bug fixes ?
● Enhancements ?
CMSIS-RTOS Standard
Based on
ARM Technical Documentation
as well as a Hitex Presentation
CMSIS
●
CMSIS represents an attempt by ARM to standardise many aspects
of device driver and associated library implementation across multiple
processors and multiple IDEs
●
The areas of standardization includes:
●
Hardware Abstraction Layer (HAL) for Cortex-M processor
registers
–
Standardized definitions for the SysTick, NVIC, System
Control Block registers, MPU registers, and core access
functions.
●
Standardized system exception namesso that RTOS and
middleware components can use system exceptions without
running into compatibility issues.
●
Standardized methods to organize header files- to facilitate
learning how to program new Cortex-M microcontroller products
and improve software portability.
CMSIS - ctd.
●
Common methods for system initialization to be used by each
MCU vendor e.g.
–
Standardized
SystemInit() function, provided in each device
driver library, for configuring the clock.
●
Standardized intrinsic functions - normally used to produce
instructions that cannot be generated by IEC/ISO C
–
Standardized intrinsic functions, improve software re-usability
and portability
●
Standardized ways to determine the system clock frequency
through a software variable
●
SystemFrequency, defined in the device driver. Allows RTOS to
CMSIS - Versions
● v1 - Core ● v2 - DSP ● v3 - RTOS
CMSIS - Organisation
●
The CMSIS follows a layered architecture approach
●Core Peripheral Access Layer
–
Provides name definitions, address definitions, and helper
functions to access core registers and core peripherals.
●
Device Peripheral Access Layer (MCU specific)
–
Deals with name definitions, address definitions, and driver
code to access peripherals.
●
Access Functions for Peripherals (MCU specific and optional)
–
Implements additional helper functions for peripherals as
CMSIS - Organisation - ctd.
CMSIS - RTOS Architecture
●
Kernel (RTK)
●Scheduling
●
Mutual exclusion
●Executive (RTE)
●
Inter-task communication & synchronisation
●
Memory management
●
RTOS
●
File management System
–
FAT file system
●
Networking
–
E.g. TCP/IP, CAN
●
Graphical User Interface support
–
E.g. OpenGL ES, Embedded Qt
CMSIS Components
●
The CMSIS consists of the following components:
●
CMSIS-CORE: provides an interface to Cortex-M0, Cortex-M3,
Cortex-M4, SC000, and SC300 processors and peripheral
registers
●
CMSIS-DSP: DSP library with over 60 functions in fixed-point
(fractional q7, q15, q31) and single precision floating-point (32-bit)
implementation
●
CMSIS-RTOS API: standardized programming interface for
real-time operating systems for thread control, resource, and real-time
management
●
CMSIS-SVD: System View Description XML files that contain the
programmer's view of a complete microcontroller system including
peripherals
●
The standard is fully scalable across the Cortex-M processor series of
CMSIS v3 Architecture
CMSIS - RTOS - Services
Mutual Exclusion
● Support for mutual exclusion comes from the implementation of mutexes and
Interthread Communication
● Multithreading interthread communication and synchronisation is provided by the implementation of primitives such as
● Signals
● Message queues ● Mailboxes
CMSIS - DSP Library
● The CMSIS-DSP library provides functions supporting ● Vector operations ● Matrix computing ● Complex arithmetic ● Filter functions ● Control functions ● PID controller ● Fourier transforms
● Most algorithms are available in floating-point and various fixed-point formats and are
optimized for the Cortex-M series processors.
● The Cortex-M4 processor implementation uses the ARM DSP SIMD (Single
Instruction Multiple Data) instruction set and floating-point hardware
● The CMSIS-DSP library, is written entirely in C
– The source code is provided and the algorithms can be adapted for specific
CMSIS SVD
●
The CMSIS-SVD System View Description XML specification
●
Describes the programmer's view of the microcontroller system
including the peripheral registers
●
SVD files can be used to create the CMSIS-CORE header files
which will include peripheral register and interrupt definitions.
●
CMSIS-DVD files can also be used to create peripheral
awareness dialogs for debuggers
●
ARM provides downloadable SVD files for many devices
●
CMSIS v3 Adaptation to some RTOS
Some RTOSes and IDEs to Consider
●RTOSes
●Free RTOS
●ChibiOS/RT
●Keil RTX
●IDEs
●IAR
●Keil
●
Atollic True Studio
●
Rowley Associates - Cross Works
CMSIS - Device Driver Architecture
● For each device, the MCU vendor should provide a header file that pulls-in additional
header files required by the device driver library and the Core Peripheral Access Layer
● Include the file device_family.h into the source code.
● In order to be CMSIS-compliant, the vendor implementation must use the predefined
exception and interrupt constants, core functions, and middleware functions for accessing device peripherals
● CMSIS-compliant device drivers will contain a startup-code file, which will include the
vector table for various supported compilers
● Typical CMSIS-compliant source code files
● device_family.h - the header file defining the device. ● core_cm3.h - the header file defining the device core. ● core_cm3.c - implements core intrinsic functions.
● system_device_family.h - implements device specific interrupt and peripheral
register definitions.
● system_device_family.c - impoements system functions and the initialization
code.
CMSIS - Device Driver Architecture
FreeRTOS View on CMSIS-RTOS
●
http://www.embedded.com/electronics-blogs/industry-comment/ , March 6
2012
●
While the hardware interface to a Cortex-M core is common across all
Cortex-M microcontrollers, the software interface to an RTOS is different in
every case.
●
POSIX, a common operating system interface in use for many years, is
generally not regarded as an optimal solution for smaller devices, such
as Cortex-M3 microcontrollers, so will CMSIS be a better choice?
●
The cynic might say that ARM has such a strong position in the
microcontroller market that, if it put its weight behind it, CMSIS RTOS will be
successful whether it technically merits the success or not.
●
Ultimately, however, its success (or otherwise) will depend on whether
customers want it.
●
For customers to want it, they have to see a benefit.
●
One clear benefit is that of having a single API for a set of two or more
FreeRTOS View on CMSIS-RTOS
●
The end-user perspective :
●
A common interface to multiple RTOS solutions will make software more
portable and facilitate migrating application code between different RTOS
suppliers.
●
From the engineering point of view, especially for high-volume products, the
selected microcontroller will be the lowest cost and smallest possible for the
task in hand.
●
The software running on the microcontroller will be trimmed down.
●
Where space, time, or responsiveness are at a premium, any software
that is not adding value to that application will be removed.
●
An RTOS abstraction layer will, by its very nature, not add to the RTOS
functionality, but will make the code bigger and slow down the execution
speed.
–
The RTOS abstraction layer will , therefore, always be a prime
candidate for removal.
●
A commercial RTOS is a financial investment and it is unlikely that
FreeRTOS View on CMSIS-RTOS
●
Open-source RTOS vendor perspective
●
Because of the financial investment required to select a new commercial
RTOS, CMSIS RTOS compliance will provide more mobility to, and
between, free open-source RTOS products.
–
Potentially a big benefit to open-source suppliers.
–
The potential investment made by commercial silicon, RTOS, and
tool vendors in CMSIS-RTOS-compliant infrastructure will be equally
applicable to open-source-compliant products
●
However, by its very nature, an RTOS abstraction layer will make the
RTOS larger and less responsive.
–
Counter productive as an RTOS should be designed to be small or
fast (or both).
–
Using the abstraction layer will not allow the RTOS to be used to its
full potential, or provide the maximum potential benefit to the end
user.
–
Maybe the abstraction layer will be removed in end products that
FreeRTOS View on CMSIS-RTOS
●
Commercial RTOS vendor perspective
●
The plus points listed above for an open-source RTOS vendor may very
well be negative points for a commercial vendors.
●
Commercial RTOS vendors invest in their products to differentiate them
in a number of different ways.
–
Performance, size, price, support, etc. are all attributes used to
position an RTOS.
–
Anything that homogenizes RTOS offerings in the market, and in so
doing reduces any differentiation gained by technical and/or
marketing investments, makes the RTOS market a tougher market in
which to work.
●
It may also make future investment harder to justify.
●
Tools and middleware will become the key differentiator, until they, too
FreeRTOS View on CMSIS-RTOS
●
The non-RTOS-affiliated middleware supplier
●
Supply software that must work with any RTOS their
customers might be using.
–
Need to provide their own abstraction layer to, and test
with, a number of different RTOS solutions.
–
Replacing these multiple abstraction layers with a single
abstraction layer is a benefit.
–
The benefit will only become a reality however if the end
users are actually using CMSIS RTOS in the products
they are designing and shipping.
FreeRTOS View on CMSIS-RTOS
●
The microcontroller vendor
●
Typically, provides software (predominantly drivers) that
must interface with any system their customers are using - a
standard such as CMSIS - RTOS is beneficial especially
when drivers other than those based on a simple model
need to be provided
●
The tool vendor
●
Provides products such as execution trace and profiling
tools, as well as kernel aware debugger plug-ins.
–
Generally such tools do not access the RTOS through
the RTOS's API, so will largely be unaffected.
–
Also, these tools are predominantly supplied by the
RTOS vendors themselves
●
Hence unlikely to want their tools to be used with any
Patterns of Inter-process
Communication and Synchronisation
A Generic Approach
Rationale
● There are a number of patterns for inter-process / inter-task communication ● These patterns apply to most pre-emptive multi-tasking operating systems
● Knowing these patterns makes it easier to structure code development for a variety
of operating system APIs
● In this course the target OS API is the FreeRTOS API
● The FreeRTOS specific examples and code snippets will be covered in a follow on
section
● The examples in this section will be given in pseudo-code and C.
● The examples will be general in the sense that they will not be targeted at any
particular operating system ( real time or otherwise ).
● From time to time specific operating system API interfaces will be mentioned to
illustrate real world code examples.
● The most important thing is to understand the underlying mechanisms, as well as the
various patterns of programming with the various operating system intertask communication primitives.
Tasks
● Real time and embedded systems need to be able to handle multiple inputs and
multiple outputs within relatively tight time constraints.
● A practical concurrent design decomposes the application into ● Small
● Sequential ● Schedulable ● program units.
● The design and implementation objective is to do this in such a way that when the
system is multi-tasking the design performance and timing requirements are satisfied.
● A typical RTOS kernel provides ● Task objects
● Task management services
Definition of a Task
● Formally a task is
● An independent thread of execution
● Competes with other concurrent tasks for processor execution time
● Is schedulable - competes for execution time based on some pre-determined
scheduling algorithm
● Upon creation is given a distinct set of parameters and supporting data
structures
– Name – Unique id
– Priority ( if associated with a preemptive scheduler) – Task control block
– Stack
– Task routine
● Typically, when a kernel starts,
● It creates its own set of system tasks and ● Allocates a suitable priority to each task
System Tasks
●
Typical system tasks include
●Startup ( initialisation ) task
●
Idle task - uses up CPU idle cycles when there is nothing
else to do
●
Logging task - logs system messages
●Exception-handling task
Finite State Machine Approach to Task Scheduling
●
A common pattern when designing schedulers is to specify a number
of possible states any given task can be in.
●
A simple pattern, typical of many pre-emptive scheduling kernels
envisions the following states
●
Ready state - task eligible to run, but cannot run because a higher
priority task is executing
●
Blocked state - occurs where the task
–
Has requested a resource that is not immediately available
–Has requested to wait until some event occurs
–
Has voluntarily delayed itself for some duration
●
Running state - the task is currently the one with the highest
Priority Levels
●
Where a kernel supports more than one task per priority level
●The kernel maintains a task-ready list
–
One per priority level or
–A single combined list
●
Without blocked states lower priority tasks could not run.
●
A task can only move to the blocked state by making a blocking
call - requesting that some blocking condition be met.
●
If higher priority tasks are not designed to block then CPU
starvation may occur
●
A blocked task remains blocked until the blocking condition is
Priority Levels
●
Common blocking conditions include
●
A semaphore token for which a task is waiting being
released
●
A message on which a task is waiting arriving in a message
queue
●
A time delay imposed on the task expiring
●When a task becomes unblocked
●
It will move from the blocked state to the ready state if it is
not the highest priority task
●
If it is the highest priority task it moves directly to the running
state and preempts the currently running task
●
The preempted task is itself moved into the appropriate
Task Creation and Task Management Services
● Typically, a kernel will provide a task-management services API for manipulating
tasks with methods for
● Creating and deleting tasks ● Controlling task scheduling
● Obtaining information about tasks ● Some kernels allow a program to
● First create the task ● Then start the task
● Some kernels provide user-configurable hooks which
● Make it possible to run programmer-supplied functions when some specific
kernel event occurs
● The programmer registers the function with the kernel - by passing in the
corresponding function pointer as one of the arguments to the relevant kernel API function
● Possible kernel events
● When a task is first created
● When a task is suspended and a context switch occurs ● When a task is deleted
Task Deletion
● Deletion of tasks in embedded applications needs to be thought about. ● An executing task
● Can acquire memory
● Access resources using other kernel objects
● If a task is not deleted correctly its resources may not be released correctly and this
may result in unfortunate system behaviour e.g.
● A task obtains a semaphore to obtain exclusive access to a shared data
structure
● The task is deleted while operating on this data structure ● The abrupt deletion of the task may result in
– A corrupt data structure - because the write operation did not complete
– An unreleased semaphore - which means that other tasks will not be able to
acquire the resource
– A data structure that is inaccessible - because of the unreleased semaphore
● i.e. the premature deletion of a task can result in memory or resource
Task Scheduling API Functionality
●
Many kernels provide an API that permits manual scheduling.
●
A typical set of operations supported by such an API might include the
following :
●
Suspend - to suspend a task
●Resume - to resume a task
●Delay - to delay a task
●
Restart - to restart a task
●
GetPriority - to get the priority of the current task
●
SetPriority - to dynamically set the priority of some task
●
Preemption lock - to lock out higher priority tasks from preempting
the current task
Obtaining Task Information
●
Kernel API Functionality for Obtaining Task Information is useful
e.g. when monitoring or debugging an application
●
Information typically retrieved includes details such as
●The IDs of the installed tasks
●
The Task Control Block of the current task
●The resources owned by the task
–
The state of the resource
Varieties of Tasks
●
A common classification of tasks involves classifying them
as either
●
Run to completion tasks, or
Run-to-completion tasks
●
The pseudocode demonstrating a run-to-completion task might look
as follows
●
In this example of a run-to-completion task
●A high priority task
–
Initialises and starts up up a collection of tasks associated with
the application and then
–
Ends itself.
ARunToCompletionTask()
{
InitialisationApplication;
Create a number of infnite loop tasks;
Create required kernel objects/resources;
Delete/Suspend this task
Infinite Loop Tasks
●
Infinite loop tasks must include blocking calls if other tasks are to have a
chance of running.
●
The pseudocode for an infinite loop task might look as follows
●
The important point to bear in mind with the above examples is that they are
based on a co-operative multi-tasking model
●
At the highest priority level a task runs to completion or till it voluntarily
makes a blocking call.
InfniteLoopTask()
{
Initialisation steps
Loop Forever
{
body of loop code - includes one or more blocking calls
}
Intertask Communication
●
Typical kernel objects for interprocess communication and
synchronisation include
●
Semaphores
●
Message queues
●
Signals
Semaphores
● Formally - A semaphore is a kernel object that one or more threads of execution
(tasks) can acquire or release for the purposes of synchronisation or mutual exclusion.
● When a semaphore object is created the kernel assigns a data structure - a
semaphore control block (SCB) to it. The kernel also assigns a unique ID, a count value and a task-waiting list.
Semaphores
●
Semaphore
●
A semaphore can be thought of as a key which a task must
have in order to access some resource
●
If the task can acquire the semaphore then it can access the
resource
●
If a task cannot acquire the semaphore it must wait till some
other task releases it.
●
There are two main kinds of semaphore
●Binary
Binary Semaphores
● A binary semaphore can have only two possible values 0 and 1. ● When a semaphore is not held by any task it has the value 1. ● When a task acquires a semaphore its value is set to 0.
● No other task can acquire the semaphore whilst its value is 0.
● A binary semaphore is a global resource - shared by all tasks that need it
● Hence, it is possible for a task other than the task that initially acquired the
semaphore to release the semaphore
● It is the choreography of semaphore usage amongst tasks that makes
Counting Semaphores
● A counting semaphore can be acquired or released multiple times. It does this via a
counter.
● If the count value is 0 the counting semaphore is in the unavailable state. If the
count is greater than 1 then the semaphore is available.
● When a counting semaphore is created its initial count can be specified.
● In some operating systems the maximum count value can also be specified - i.e. the
counting semaphore is a bounded semaphore ( the count is bounded ). In other operating systems the count may be unbounded.
● Acquiring a counting semaphore reduces the counter value by 1, and releasing the
Mutual Exclusion Semaphores (Mutexes)
● A mutex is a binary semaphore - that may, in some implementations, have additional
features such as
● Ownership
● Recursive locking ● Task deletion safety
● Priority inversion avoidance protocol
● The mutex can be in one of two states locked (0), or unlocked (1).
● A mutex is initially created in the unlocked state - and can be acquired by a task. ● When a mutex is acquired its state becomes the locked state.
● When a mutex is released (by the task that owns it) its state becomes the
unlocked state.
● [ In some implementations the verbs lock and unlock are used in place of acquire
and release ]
● Ownership
● When a task acquires a mutex it is said to acquire ownership
Recursive Locking
●
Recursive locking means that a task that owns a mutex can acquire it
multiple times in the locked state.
●
Such a mutex is called a recursive mutex.
●
In some implementation recursive locking may be automatically built into the
mutex , in others it may be a property that can be explicitly enabled.
●
A recursive mutex permits nesting of attempts to lock the mutex. A
function that acquires the mutex can call another function that can
acquire the mutex again, and can release the mutex just before it
returns.
●
Task Deletion Safety
●
Premature task deletion can be avoided by using a task deletion lock when a
task locks and unlocks the mutex.
●
The task cannot be deleted while it owns the mutex. The mutex is said to
Priority Inversion Avoidance
● Priority inversion occurs when
● A higher priority task is blocked because it is waiting for a lower priority task to
release a needed mutex, and where
● The lower priority task has itself been preempted by an intermediate level priority
task.
● Effectively the priority of the high priority task has been inverted to that of the low
priority task.
● Protocols associated with mutex implementation have been devised to avoid priority
inversions
● Priority inheritance protocol - the priority of the lower priority task that has
acquired the mutex is raised to the priority level of the task that requested the mutex when inversion happens.
– Once the low priority task releases the mutex its priority reverts to the low
level.
● Ceiling priority protocol - the priority level of the task that acquires the mutex is
automatically set to the highest priority of all possible tasks the might request that mutex when the task acquires the mutex, and
Typical Semaphore Operations
●
Typical semaphore operations are
●
Creating and deleting a semaphore
●Acquiring and releasing a semaphore
●
Clearing the task-waiting list associated with a semaphore
●Obtaining information about a semaphore
●
Calls for implementing different kinds of semaphores (where a kernel
supports multiple semaphores) may provide extra functionality e.g.
●
Binary semaphore - specify initial semaphore state and
task-waiting order
●
Counting semaphore - specify initial semaphore value, bound on
the count and the task-waiting order
●
Mutex - specify the task-waiting order, enable task deletion safety,
Semaphore Operations
●
Deleting a Semaphore
●
When a semaphore is deleted
–
Any blocked tasks in the task-waiting list are unblocked and moved
into the ready state ( or into the running state if they are the highest
priority tasks )
–
Any task attempting to acquire a deleted semaphore returns with an
error message
●
N.B. - a semaphore should not be deleted if it is acquired.
●Acquiring and Releasing Semaphores
●
Typical ways of acquiring semaphore
–
Wait forever - task remains blocked until it is able to acquire the
semaphore
–
Wait with a timeout - task remains blocked until it can acquire the
semaphore, or till the time-out period specified expires ( in which
case the task is placed on the ready list )
–
Do not wait - if a semaphore token is not available then the task does
not block - it simply returns ( usually returning some value / setting
some variable ) indicating it was not able to acquire the semaphore.
Semaphore Operations - ctd.
●
Interrupt Service Routines (ISRs) and Semaphores
●
ISRs may release binary and counting semaphores.
●
It is not (in general) meaningful to acquire a binary or counting
semaphore inside an ISR
●
Locking and unlocking of mutexes inside an ISR is also not meaningful
( and most kernels do not support such an action )
●
Binary and Counting Semaphores and Mutexes
●
Any task can release a binary or counting semaphore
●A mutex can only be release by the task that acquired it
●Clearing Semaphore Task Waiting Lists
●
Some kernels provide a flush operation for clearing all tasks on a
semaphore task-waiting list.
●
The flush operation can be used as a mechanism for broadcast ( more
Thread Rendezvous
●
A possible use of the flush mechanism
–
A set of tasks need to finish their particular operations,
and then to block by trying to acquire a common
semaphore that is made unavailable
–
When the last task has finished its particular operations it
can execute a semaphore flush operation on the common
semaphore
●
This will free all the tasks waiting on the semaphore's
Getting Semaphore Information
●
Needed in monitoring and debugging situations
●Typical semaphore information operations are
●
Show information - provides general information about the
semaphore
●
Show blocked tasks - gets a list of IDs of tasks blocked on
Semaphore Usage Patterns
●
Typical Semaphore Usage Patterns
●Wait and signal synchronisation
●
Multiple task wait and signal synchronisation
●Credit tracking synchronisation
●
Single shared resource access synchronisation
●
Recursive shared resource access synchronisation
●Multiple shared resource access synchronisation
Wait and Signal Synchronisation
●
Here two tasks communicate for the purpose of synchronisation
without the exchange of information.
●
Wait and Signal Synchronisation can be accomplished using a
binary semaphore
–
Initially the binary semaphore is unavailable ( value = 0 )
–The higher priority task (the wait task) runs first
●
At some point it makes a request for the semaphore
●But is blocked because the semaphore is not available
●This gives the lower priority task ( the signal task) an
opportunity to run
–
At some point the lower priority task releases the semaphore
thus
●
Unblocking the higher priority task which
Multiple Task Wait and Signal Synchronisation
●
In this pattern there are several higher priority tasks ( the wait
tasks )
●
Initially the binary semaphore is unavailable ( value = 0 )
●The higher priority tasks (the wait tasks) run first
–
At some point each makes a request for the semaphore
●
But is blocked because the semaphore is not
available
●
This gives the lower priority task ( the signal task) an
opportunity to run
●
At some point the lower priority task flushes the semaphore
thus
–
Unblocking the higher priority tasks which then pre-empt
Credit Tracking Synchronisation
● In this pattern - the signaling task executes at a higher rate than the signaled task
( the wait task ), and the signaling task runs at a higher priority than the signaled task
● Some mechanism is needed to count signalling occurrences ● This can be done using a counting semaphore
– When a signaling task signals the semaphore count is ( atomically )
incremented ( a release operation is performed on it )
– When the signaled task is able to run it tries to acquire the counting
semaphore - and either blocks ( if the semaphore counter is at zero ) or succeeds and decrements the semaphore count ( atomically ) by one
– The underlying assumption is that the signaling task may signal in bursts -
and the signaled task has a chance to "catch up" in between bursts
● Example scenario:
● An Interrupt Service Routine (ISR)
– Executes at high priority when the interrupt is triggered
– Offloads remaining processing to a lower priority task waiting on a
Credit Tracking Synchronisation - ctd.
●
Pseudo code for credit tracking synchronisation
tWaitTask ()
{
..
Acquire counting semaphore
..
}
tSignalTask ()
{
..
Release counting semaphore
..
Synchronising Access to a Shared Resource
●
A semaphore can be used to Synchronise Access to a Shared
Resource
●
The objective is to ensure that only one task / concurrent thread of
execution at a time can access the shared resource.
●
The strategy is as follows:
●
Create a binary semaphore - initially in the available state
●
In order to access the shared resource a task must first acquire
the semaphore
●
Any other task attempting to acquire this semaphore will now
block
●
When a task has finished with the shared resource it releases the
semaphore
●
A task that was blocked in its attempt to acquire the semaphore is
now unblocked and can proceed to make use of the shared
resource
Synchronising Access to a Shared Resource
● Pseudo code for shared resource access synchronisation
● Questions:
● What if a task accidentally releases the semaphore - e.g. a task that did not
acquire the semaphore in the first place ?
● What if there is more than one task blocking on the semaphore when the
semaphore is released ?
● How might the use of a mutex semaphore ( a semaphore that supports the
concept of ownership wherein only the task that successfully acquired the mutex can release it ) help ?
tAccessTask () {
..
Acquire binary semaphore
Make use of the shared resource ( e.g. read or write to it ) Release the binary semaphore
.. }
Synchronising Recursive Access to a Shared Resource
● The scenario is as follows
● A task, tAccessTask, calls routine A which in turn calls routine B and all three
need access to the same shared resource
● The pseudo code for such a scenario might be
tAccessTask() { .. Acquire mutex
Access shared resource Call Routine A
Release mutex .. }
Routine A() { .. Acquire mutex
Access shared resource Call Routine B
Release mutex .. }
Routine B() { .. Acquire mutex
Access shared resource Release mutex ..
}
In this scenario , using a regular mutex ,
- Routine A would end up blocking on a semaphore that the task it is running in already owns.
One solution to such a problem would be to use a
recursive mutex - here once a task has acquired a mutex then additional attempts from the task itself, or from
routines called by that task to acquire the mutex will succeed
Controlling Access to Multiple Equivalent Shared Resources
●
The strategy here is to use a counting semaphore
●
Initialise the count of the semaphore to the number of
equivalent shared resources
–
When a task requests the semaphore
●
The semaphore count will be decremented by one if it
is greater than zero
●
The task will block if the semaphore count is zero
●Problems can arise if a task releases a semaphore it did not
Message Queues
●
A message queue can be used by tasks and ISRs to communicate and to
synchronise with data - it
●
Utilises a buffer like object - a pipeline that, temporarily, holds messages
from a sender(s) till the target receiver is able to read them
●
Decouples sending and receiving tasks
●
When a message queue is created, then, typically,
●
An associated Queue Control Block (QCB) is assigned to it
●A message queue name and a unique ID are assigned to it
●
Memory buffers are assigned to it - based on properties such as queue
length and maximum message length
●
One or more task waiting lists may be associated with it - typically
–
A sending task - blocks when the message queue is full
–A reading task - blocks when the message queue is empty
Message Queues - ctd.
● Messages are read from the head of the message queue and added to the tail of the
Message Queue - State Behaviour
● When a message queue is created its initial state will be the empty state
● If a task attempts to receive messages from an empty message queue it will block ● if the task wishes to it is held in the task waiting list ( in e.g. a FIFO or priority-based
order )
● If a message is sent to a task blocking on an empty message queue then it will be
delivered directly to the blocked task
● The task will be removed from the task-waiting list and moved to either the ready
state or to the run state
● The message queue remains in the empty state
● If a message is sent to an empty message queue and there is no blocked task then
the message queue is in the non-empty state
● Additional messages will be queued up till the number of messages reaches the
maximum number the queue can hold ( the queue length )
● When the queue is in the full state then any tasking sending a message to that
queue will fail
– Failure may be indicated ( depending on kernel implementation details ) e.g.
by
● Getting the sending function to return an error code to that task
Message Sending Mechanisms
●
The details are dependent on
●
The implementation of the kernel, and also, on whether
●Virtual memory management is in operatio and
–
The various tasks are running in their own virtual address spaces.
●
Here are some possibilities:
●
The message is copied twice
–
Firstly from the task's memory area to the message queue's memory
area
–
Secondly from the message queue's memory area to the receiving
task's memory area, however,
●
If there is a receiving task already blocked on the message
queue then ( in some kernel implementations ) the message may
be copied directly from the sending task's memory are to the
receiving task's memory area
●
In real time embedded systems the cost in terms of CPU cycles and memory
Memory Allocation for Message Queues
●
Memory locations for message queues are also kernel implementation
dependent
●
The kernel may provide a system memory pool
–
All queues share this common memory area
–
Helps reduce memory requirements if it is reasonable to assume that
it will be rare for all message queues to be full to capacity at the
same time
–
Problems may arise in the case of message queues needing to hold
large messages which use up ( on occasions ) a large share of the
available system memory
●
Thus causing other message queues to fail because there is not
enough available memory
●
When a message queue is created a private buffer is allocated to it
●
Uses up more memory
●
More reliable - as ensures there is adequate memory available for all
Operations on Message Queues
● Create message queue ● Delete message queue
● Send message to message queue ● Blocking policy
● Non-blocking - ISRs and tasks ● Blocking with timeout - tasks only ● Blocking for ever - tasks only
● Queueing discipline
● FIFO
● LIFO
● Broadcast a message ● Multicast a message
● Query message queue for status information
● Get information about the message queue ( e.g. queue ID, queuing order ( e.g. FIFO
or priority based ), number of queued messages )
Operations on Message Queues - ctd.
● Receive message from message queue
● Blocking policy - on an empty message queue - blocked receiving tasks fill the task
list
● Blocking with timeout ● Blocking for ever
● Message reading policy
● Destructive read - message permanently removed from message queue's
storage buffer
● Mon-destructive read - receiving task "peeks" at message at head of queue but
does not remove it
● Queuing order
● FIFO
Message Queue - Use Patterns
●
Typical Patterns for Using Message Queues
●
Non-interlocked , one-way data communication
●Interlocked , one-way data communication
●
Interlocked , two-way data communication
●Broadcast communication
●
What other patterns of message queue usage have you come
across ?
●
What happens when message queueing is to be attempted on
Loosely Coupled Data Communication
●
Non-Interlocked, One-Way ( Loosely Coupled ) Data Communication
involves
●
A sending task - which sources messages
●
A message queue
●
A receiving task - which acts as a sink for messages
●
Details of behaviour will depend on the relative priorities of the sending and
receiving tasks
●
Where the receiving task has higher priority it will run first until it blocks
on an empty message queue
–
When the sending task sends a message the receiving task will
unblock and run again
●
Where the receiving task has the lower priority then the sending task will
fill the message queue with messages till it blocks ( or suspends itself )
–
The receiving task will "wake up" and start processing the received
ISRs and Message Queues
●
Interrupt Service Routines (ISRs) typically use the
non-interlocked, one-way data communication pattern
●
The receiving task runs and waits on a message queue
●When triggered, the ISR places one or more message on
the message queue
●
ISRs must send messages in a non-blocking way
●
If the message queue if full then messages may be lost or
overwritten
Interlocked, One-Way Data Communication
●
In this pattern the sending task sends a message and waits to see if the
message has been received
●
If the message is, for some reason, not received correctly then it can be
re-transmitted
●
The pattern can be used as means of closing a synchronisation loop so
that the sending and receiving tasks operate in lockstep with one another
●
possible implementation of lockstep - involving a sending task, a message
queue ( with a length of 1 ) , a receiving task and a binary semaphore
●
The initial value of the binary semaphore is 0
●
The sending task sends a message to the message queue and blocks
on the binary semaphore
●
The receiving task receives the message and increments the binary
semaphore
●
The sending task unblocks and sends the next message
●
The semaphore is, in effect, acting as a simple acknowledgement to the
One-Way Data Communication example
● Pseudocode for Interlocked, One-Way Data Communication example
tSenderTask() {
...
Send message to message queue Acquire binary semaphore
... }
tReceiverTask() {
...
Receive message from message queue Release binary semaphore
... }
Interlocked, Two-Way Data Communication
● Interlocked, Two-Way Data Communication involves two tasks and two message
queues
● Synchronisation details depend on the kind of data that needs to be exchanged
– For two way exchange of data two message queues are required – For simple acknowledgement a semaphore can be used
tClientTask() {
...
Send message to server's requests message queue Acquire binary semaphore
... }
tServerTask() {
...
Receive message from server's responses message queue Release binary semaphore
... }
Broadcast Communication
● A broadcast communication pattern involves
● A broadcast sending task, a message queue, a number of receiving tasks ● The broadcast sender sends a message to the message queue
● Each of the receiving tasks receives the message from the message queue ● Questions:
● How might a kernel implementation support broadcast communication involving
a message queue
● How would a receiver task associate itself with a message queue?
● Would it make sense for receiver tasks to block on an empty message queue? ● What if a response was expected from 0 or 1 receiver tasks only - and a
response from two or more receiver tasks was an error ?
● How would the above scenarios be modified / extended if sender and receiver
tasks were running on different machines / processors?
● Suppose it was necessary to distinguish between ordinary messages and urgent
PIPES
● Pipes are kernel objects provided by operating systems such as Unix/Linux and
Windows
● A pipe
● Supports unidirectional stream oriented data exchange
● Is made up of two descriptors - that are returned when an instance of the pipe is
created
– One for the reading end - data is read from the read descriptor – One for the writing end - data is written to the write descriptor
● Data is held ( buffered ) in the pipe as an unstructured byte stream ● Data is read from the pipe in fifo order
● The reader process blocks when the pipe is empty ● The writer process blocks when the pipe is full
● Unlike a message queue a pipe
● Cannot store multiple messages
● The data in the pipe is not structured
● The data in the pipe cannot be prioritised
● Via the select operation a task can block an wait for a specified condition to
PIPE - Control Block - Creation
●
Pipe Creation and Pipe Control Blocks
●
Pipes can be created and destroyed dynamically
●
When a pipe instance is created the kernel creates a pipe control block
●A pipe control block contains pipe specific information ( maintained by
the kernel ) e.g.
–
Size of pipe buffer - specified at creation time and then remains fixed
–Current data byte count
–
Input and output position indicators
–
Two descriptors in file i/o space - which will be unique
●
These descriptors are returned to the task creating the pipe
●Details vary from kernel to kernel
●
There are two task waiting lists associated with a pipe
–
List of tasks waiting to write to the pipe ( blocked when pipe buffer is
full )
–
List of tasks waiting to read from the pipe ( blocked when the pipe
PIPE Operations
● Create pipe ● Destroy pipe ● Read from pipe
● Returns data to calling task
● Calling task specifies how much data to read
● Task may opt to block - waiting for remaining data to arrive - if amount of data
requested is greater than what is available in the pipe
● Pipe reads are destructive - data IS REMOVED FROM THE PIPE ● Once data has been read it is not available to other readers
● Write to pipe
● Appends new data to existing byte stream in the pipe
● Task may choose to block - waiting for additional buffer space to become
available - if the amount of data to be written is greater than the space available currently in the pipe buffer
● Because there are no message boundaries in a pipe it is not possible to
determine the original producer of the data bytes
● Data written to a pipe CANNOT BE PRIORITISED - DO NOT USE A PIPE IF
PIPE Operations - ctd.
● Pipe Control, Flush and Select Operations ( Typical )
● Control - the fcntl operation - used to alter the behaviour / attributes of the pipe
e.g. - whether a task should block if a read operation is attempted on an empty pipe or a write operation is attempted on a full pipe
● Flush
– This command removes all data from the pipe and clears all conditions in the
pipe so that the pipe reverts to the state it had when it was created
– Useful for e.g. removing data that has been kept in the pipe for too long and
is now out of date
● Select - probably the main advantage of a pipe over a message queue
– Used to permit a task to block for some specified condition to occur in one or
more pipes e.g.
● Waiting for data to become available in a pipe(s) ● Waiting for data to be emptied from a pipe(s)
● Example - using select a task may be waiting to read from one of two
pipes and write to a third
– Select returns when data becomes available in one or other of the two pipes
being read from, or when space becomes available in the pipe being written to
PIPE - Example
●
Example - using pipes for inter-task synchronisation
●Two tasks ( A and B ) and two pipes
●
One pipe opened so that it is written to by A and read from by B
●Other pipe opened so that it is read from by A and written to by B
●A and B both issue a select operation on the pipes
–
Task A can issue a non-blocking call to write to the pipe and perform
other operations until the pipe becomes writeable ( because task B
has read some data from the pipe ( i.e. task A can wait
asynchronously for the data pipe to become writeable )
–
Task A can wait asynchronously for arrival of a transfer
acknowledgement from task B
–
Task B can wait asynchronously for the arrival of a transfer
acknowledgement from task A
–
Task B can wait for the other pipe to become writeable before
Event Registers
● An event register
● Is associated with a task
● Details are hardware and kernel specific ● Is a collection or binary event flags
● Typically 8, 16 or 32 bits
– Each bit in the event register is associated with some specific event – Each bit can either be set or cleared
● Used by a task to check for the occurrence / non-occurrence of a particular event ● Can be used by an ISR to inform a task that a particular event has occurred
● A task performs conditional checks specified by a combination ( using ANDs and
ORs ) of the event register bit flags
● Event checking strategies can be ● No wait
● Wait indefinitely ● Wait with timeout
Event Registers - ctd.
● The event register control block is (typically) part of the task control block and
(typically) contains the following fields
● Wanted events register
● Events the task wishes to receive ● Received events
– Events that have "arrived" are recorded here
● Timeout value
– Set by task to indicate how long it wishes to wait for the arrival of the events
it is "interested in"
● Notification conditions
– Specify (to the kernel) the conditions (occurring as a result of event arrivals)
under which the task wishes to be woken up
● The main event register operations ( typically ) are ● Send - an event is sent to a task
● Receive - used by a calling task to receive events from external sources
– A task can specify whether or not it wishes to wait, and,
– If waiting, whether it wishes to wait indefinitely or only for a specified timeout
Using Event Registers
●
No data is associated with an event when an event is sent through an
event register
●
Pending events in the event register do not change the execution
state of the receiving task
●
There is no mechanism for identifying the source of an event
●
Identifying protocols need to be agreed upon between senders
and receivers - e.g.
–
A particular task sends a particular event by setting a particular
bit in the event register )
●
Events in the event register are not queued
●
An event register cannot count the number of occurrences of an
Signals
●
A signal is a software interrupt
●
Generated in response to some event occurring
●
Triggers the asynchronous execution ( in the target task ) of the
associated signal handler routine ( as specified by the task for dealing
with that signal )
●
The numbers and types of available signals are system and operating
system dependent
●
Typical types of events associated with signals
●
An attempt to perform an illegal operation during program execution
–
e.g. a divide by zero
●
Notification from a task to another task that it is about to terminate
●Some user gesture - e.g. typing in a combination of characters
corresponding to a "quit" command
●
A signal is identified by a signal number ( vector number ) - which is its
Signal Control Block
● The signal control block is ( typically ) part of the task control block ● Maintains a list of signals the task is prepared to handle ( catch )
● A task can specify a signal handler for each signal it wishes to process, or,
simply rely on a default handler
● A signal interrupt is often referred to as "raising a signal in the interrupted task"
– If a signal arrives while a task is processing another signal
● The additional signal is placed in a signals pending list
– As soon as a task finishes processing a signal the next signal in the signals
pending list can "raise a signal" on that task
● It is possible for a task to partially process a signal and then to pass the signal on
for further processing by the default handler
● It is possible for a task to block the delivery of a signal during certain "critical"
stages in that task's processing
– The task tells the kernel to block certain signals by setting the appropriate
values in the "blocked signals set"
● The kernel will not deliver a signal listed in the "blocked signals set" until