Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Sauermann J.Realtime operating systems.Concepts and implementation of microkernels for embedded systems.1997.pdf
Скачиваний:
27
Добавлен:
23.08.2013
Размер:
1.32 Mб
Скачать

54

3.6 Interprocess Communication

 

 

3.6Interprocess Communication

So far, we have considered different tasks as being independent of each other. Most often, however, some of the tasks in an embedded system have to exchange information. The simplest way for the tasks to enable this exchange is to share memory. One task updates a variable in the memory while another task reads that variable. Although shared memory is considered as the fastest way of exchanging information, this is only true for the information exchange as such. In addition to exchanging the information, the tasks have to coordinate when the information is valid (i.e. when it is provided by the sending task) and how long it is processed by the receiving task. This coordination could be implemented as a valid flag, which is initially set to invalid. After a task has provided information, it sets the flag to valid. The receiving task then processes the information and sets the flag back to invalid, so that the memory can be used again. Obviously, this procedure means busy wait for both tasks involved and is thus inefficient.

A much better way is to use queues containing messages for exchanging information. To avoid busy waiting at either end, both put and get semaphores are used. If the queue is full, the sending task is blocked until the receiving task has removed items. For small information quantities, such as characters or integers, the information can be stored in the message itself; for larger quantities, pointers to the information are used. This way, the performance of shared memory for the information exchange as such can be maintained. Using pointers is tricky in detail, since it needs to be defined whether the receiver or the sender must release the memory. For example, the receiver must release the memory if the memory is allocated with the new operator. The sender has to release the memory, e.g. if the memory is allocated on the senders stack; in this case, the sender needs to know when the receiver has finished processing of the message. If the memory is released by the sender, then the receiver typically sends an acknowledgment back to the sender to indicate that the memory is no longer needed. As a consequence, the receiver needs to know which task has sent the message.

Rather than defining a specific queue for each particular purpose, it is convenient to have the same data structure for messages in the whole system, as defined in Message.hh (see also Appendix A.9).

1 // Message.hh

...

5class Message

6{

7public:

8

Message()

: Type(0), Body(0),

Sender(0) {};

9

Message(int t, void * b) : Type(t), Body(b),

Sender(0) {};

10

int

Type;

 

 

11void * Body;

12const Task * Sender;

13};

3. Kernel Implementation

55

 

 

This data structure contains a type that indicates the kind of message, a body that is optionally used for pointers to larger data structures, and a task pointer identifying the sender of the task.

Communication between tasks being so common, every task is provided with a message queue:

// Task.hh

25class Task

26{

...

 

 

138

Queue_Gsem_Psem<Message>

msgQ;

139

};

 

The size of the message queue can be specified individually for each task in order to meet the task’s communication requirements.

1

// Task.cc

...

 

33

Task::Task(void (*main)(),

...

 

35

unsigned short qsz,

...

 

38)

39: US_size(usz),

...

44 msgQ(qsz),

As we know by now, every task executing code must be the current task. Thus a message sent is always sent by CurrentTask. Since Message itself is a very small data structure, we can copy the Type, Body and Sender members without loosing much of the performance. This copy is made by the Put() function for queues. The code for sending a message becomes so short that it makes sense to have it inline.

 

// Task.hh

96

void

SendMessage(Message & msg)

97

{

msg.Sender = currTask; msgQ.Put(msg); };

Note that SendMessage() is a non-static member function of class task. That is, the instance of the class for which SendMessage() is called is the receiver of the message, not the sender. In the simplest case, only a message type is sent, e.g. to indicate that an event has occurred:

void informReceiver(Task * Receiver, int Event)

{

Message msg(Event, 0); Receiver->SendMessage(msg);

}

The sender may return from informReceiver() before the receiver has received the message, since the message is copied into the message queue. It is also safe to

56

3.6 Interprocess Communication

 

 

send pointers to the .TEXT section of the program to the receiver (unless this is not prevented by hardware memory management):

void sayHello(Task * Receiver)

{

Message msg(0, "Hello"); Receiver->SendMessage(msg);

}

This ??? structure/function/code ??? is valid since “Hello” has infinite lifetime. It is illegal, however, to send dangling pointers to the receiver; as it is illegal to use dangling pointers in general:

void DONT_DO_THIS(Task * Receiver)

{

char hello[6] = "Hello"; Message msg(0, hello);

Receiver->SendMessage(msg); // DON’T DO THIS !!!

}

After the above function has returned, the pointer sent to the receiver points to the stack of the sender which is not well defined when the receiver gets the message.

The receiving task may call GetMessage() in order to get the next message it has been sent. This function is even shorter, so it is declared inline as well:

// Task.hh

56static void GetMessage(Message & msg)

57{ currTask->msgQ.Get(msg); };

The receiver uses GetMessage() as follows:

void waitForMessage()

{

Message msg(); Task::GetMessage(msg); switch(msg.Type)

{

...

}

}

This usage pattern of the Message class explains its two constructors: the constructor with Type and Body arguments is used by the sender, while the receiver uses the default constructor without any arguments that is updated by GetMessage() later on. A scenario where the sender allocates memory which is released by the receiver could be as follows: the sender sends integers 0, 1 and 2 to the receiver. The memory is allocated by new, rather than ??? pointing ???

on the stack like in the bad example above.

void sendData(Task * Receiver)

{

3. Kernel Implementation

57

 

 

int * data =

new int[3];

 

data[0] = 0;

data[1] = 1;

data[2] = 2;

Message msg(0, data);

 

Receiver->SendMessage(msg);

 

}

The receiver would then release the memory after having received the message:

void receiveData()

{

Message msg(); Task::GetMessage(msg);

...

delete [] (int *)(msg.Body);

}

If a system uses hardware memory management (which is rarely the case for embedded systems today, but may be used more frequently in the future), the data transmitted must of course be accessible by both tasks.

The last scenario using new/delete is safe and provides sufficient flexibility for large data structures. Unfortunately, using new/delete is a bad idea for embedded systems in general. While resetting a PC twice a day is not uncommon, resets cannot be accepted for a robot on the mars. The safest but least flexible way of allocating memory is by means of static variables. Automatic allocation on the stack is a bit more risky, because the stack might overflow; but this solution is much more flexible. The ultimate flexibility is provided by new/delete, but it is rather difficult to determine the memory requirements beforehand, which is partly due to the fragmentation of the memory. The problem in the bad example above was the lifetime of the variable hello, which was controlled by the sender. This problem can be fixed by using a semaphore that is unlocked by the receiver after having processed the message:

class DataSemaphore

{

public:

DataSemaphore() : sem(0) {}; // resource not available int data[3];

Semaphore sem;

}

void sendMessageAndWait(Task * Receiver)

{

DataSemaphore ds; Message msg(0, ds);

ds.data[0] = 0; ds.data[1] = 1; ds.data[2] = 2; Receiver->SendMessage(msg);

ds.sem.P();

}

58

3.6 Interprocess Communication

 

 

The sender is blocked as soon as it has sent the message, since the semaphore was initialized with its counter set to 0, indicating that the resource (i.e. the data) is not available. The receiver processes the message and unlocks it, which causes the sender to proceed:

void receiveDataAndUnlock()

{

Message msg(); Task::GetMessage(msg);

...

((DataSemaphore *)msg.Body).V();

}

Unfortunately, blocking the sender is a disadvantage of this otherwise perfect method. The sender may, however, proceed its operation as long as it does not return from the function. This is also one of the very few examples where a semaphore is not static. It does work here because both sender and receiver cooperate in the right way. Although we have not shown any perfect solution for any situation of interprocess communication, we have at least seen a set of different options with different characteristics. Chances are good that one of them will suit the particular requirements of your application.