This page documents an IPC facility for object icon, based on three Unix IPC facilities, namely shared memory, semaphores and message queues. This facility enables communication between different processes, either within the same or a different programs.
The RTL/C portion of the library is contained in a shared library objecticonipclib.so
, which is built in the lib/native
subdirectory of the distribution.
Creating a sub-process is easy, and can be done with the System.fork()
function, or alternatively using the util.Process
class, which is just a wrapper around System.fork()
and some other common commands. Here is a simple use of the Process
class.
import
io(write),
util(Process)
procedure run()
write("In child")
end
procedure main()
local c
write("In parent")
c := Process{run()}
c.start()
c.close()
write("Finished")
end
This program when compiled prints
In parent
In child
Finished
The child process body is given (as a co-expression) in the Process
constructor. In the parent process in the main
procedure, invoking the start()
method of the child begins the process, whilst the close()
method waits for the child to finish.
This is very straightforward, but for things become more complicated when the child and parent process wish to communicate with one another. Consider the following expanded version of the above program.
import
io(write),
util(Process)
global var
procedure run()
write("In child, setting var")
var := 2
end
procedure main()
local c
var := 1
write("In parent var=", var)
c := Process{run()}
c.start()
c.close()
write("Finished, var=", var)
end
The output of this program is :-
In parent var=1
In child, setting var
Finished, var=1
This is not what you would expect at first glance; the child’s attempt to set the variable has had no effect on the parent. This is because at the point the child process is created, it is given a complete copy of the memory space of the parent process, and so all assignments affect the copy, not the original.
To solve this problem, shared memory can be used. Here is the same program, but this time with the variable var
replaced by an instance of the Shm
shared memory class.
import
io(write),
ipc(Shm),
util(Process)
global var
procedure run()
write("In child, setting var")
var.set_value(2)
end
procedure main()
local c
# Create a Shm instance with initial value 1
var := Shm.create_private(1)
write("In parent var=", var.get_value())
c := Process{run()}
c.start()
c.close()
write("Finished, var=", var.get_value())
var.close()
end
Now the output is :-
In parent var=1
In child, setting var
Finished, var=2
Here, var is an instance of the Shm
class, and is created with the “factory” method create_private()
. The parameter 1
is just the initial value. Then get_value()
and set_value()
are used to get/set the value of the object, and finally close()
cleans up the operating system resources used by the instance (if this is not done, it will be done automatically when the program ends, and a warning message output). These various calls delegate their work to the C functions in the shared library.
More or less anything can be used as the parameter to set_value()
, but note that the value is copied into and out of the shared variable, ie reference semantics do not apply. So, for example :-
var := Shm.create_private([1,2,3])
put(var.get_value(), 4) # No effect; just adds 4 to the copy returned by get_value
every write(!var.get_value()) # 1,2,3
var.set_value(put(var.get_value(), 4)) # OK, sets the value again with the 4 added
every write(!var.get_value()) # 1,2,3,4
As another example, here we set an instance of a class into the shared memory :-
import
io(write),
ipc(Shm)
class MyClass()
private x
public thing()
write("In thing, x=", x)
end
public new(x)
self.x := x
return
end
end
procedure main()
local var, c, d
var := Shm.create_private()
c := MyClass(10)
var.set_value(c)
d := var.get_value()
d.thing()
write("c=", image(c), " d=", image(d))
var.close()
end
The output is :-
In thing, x=10
c=object MyClass#1(1) d=object MyClass#2(1)
Note how c
and d
are different instances.
There is one restriction regarding the value of the shared variable, and that is that it must be encodable by the lang.encode()
procedure, which converts an arbitrary object into a string. Some things are by their nature not encodeable, such as a FileStream
instance.
The Sem
class provides the classic semaphore mechanism, based on the underlying Unix implementation, which can be used for inter-process co-ordination, critical regions, and so on.
Here is an example in which a child process creates some information and puts it into a shared memory variable, whilst the parent retrieves that information. The processes are co-ordinated by two semaphores to make sure they remain in step.
import
io(write),
ipc(Sem, Shm),
util(Process)
global var, sem1, sem2
procedure run()
local i
every i := 1 to 10 do {
# Wait till okay to set value
sem1.wait()
var.set_value(i)
# Signal value set
sem2.signal()
# Sleep for 1/2 second
delay(500)
}
end
procedure main()
local i, c
var := Shm.create_private()
# Create two private semaphores with initial value 0.
sem1 := Sem.create_private(0)
sem2 := Sem.create_private(0)
c := Process{run()}
c.start()
repeat {
# Signal okay to set value
sem1.signal()
# Wait till value set
sem2.wait()
i := var.get_value()
write(i)
if i = 10 then
break
}
c.close()
var.close()
delay(1000)
write(sem1.get_value())
write(sem2.get_value())
sem1.close()
sem2.close()
write("Finished okay")
end
Note that the semaphores are created and destroyed in a similar way to shared memory.
Other than the standard wait and signal methods, the Sem class also has methods to get and set the semaphore value, and to poll it for a value without suspending.
The third IPC facility is message queues. One process adds messages to the queue and another one reads the messages. A message can be any Icon value, subject to the same rules as for shared memory, described above. Here is the same example as just shown above, but using a message queue instead.
import
io(write),
ipc(Msg),
util(Process)
global mq
procedure run()
local i
every i := 1 to 10 do {
# Send the message
mq.send(i)
# Sleep for 1/2 second
delay(500)
}
end
procedure main()
local i, c
mq := Msg.create_private()
c := Process{run()}
c.start()
repeat {
i := mq.receive()
write(i)
if i = 10 then
break
}
c.close()
mq.close()
write("Finished")
end
As you can see, this particular task is much simpler using message queues.
All of the examples above used “private” resources; for example the message queue was created with
mq := Msg.create_private() | stop("Couldn't create:", &why)
But it is possible to provide a pre-arranged key in order to create a “public” resource instead. For example
mq := Msg.create_public(999) | stop("Couldn't create:", &why)
After this succeeds a message queue will exist with key 999. If the call fails, then this usually means a queue with id 999 already exists.
This message queue may then be opened by another process, possibly in another program.
mq := Msg.open_public(999) | stop("Couldn't open resource:", &why)
Similar factory procedures exist for semaphores and shared memory.
There is one difference between “private” and “public” resources: the “public” variety aren’t destroyed automatically at the end of the creating process; they have to be destroyed explicitly either by calling the close()
method, or ultimately using the Unix commands ipcs
and ipcrm
. Also, bear in mind that the close
method will completely remove the resource in question, so clients that have opened a resource with one of the open_public()
methods will usually not call close()
, leaving it available for further clients.