Descriptor passing between processes: sendmsg() and recvmsg()
Passing an open descriptor between jobs allows one process (typically a server) to do everything that is required to obtain the descriptor, such as opening a file, establishing a connection, and waiting for the accept() API to complete. It also allows another process (typically a worker) to handle all the data transfer operations as soon as the descriptor is open.
The ability to pass an open descriptor between jobs can lead to a new way of designing client/server applications. This design results in simpler logic for both the server and the worker jobs. This design also allows different types of worker jobs to be easily supported. The server can make a simple check to determine which type of worker should receive the descriptor.
Sockets provide three sets of APIs that can pass descriptors between server jobs:
- spawn() Note: spawn() is not a socket API. It is supplied as part of the IBM® i Process-Related APIs.
- givedescriptor() and takedescriptor()
- sendmsg() and recvmsg()
The spawn() API starts a new server job (often called a "child job") and gives certain descriptors to that child job. If the child job is already active, then the givedescriptor() and takedescriptor() or the sendmsg() and recvmsg() APIs need to be used.
However, the sendmsg() and recvmsg() APIs offer many advantages over spawn() and givedescriptor() and takedescriptor():
- Portability
- The givedescriptor() and takedescriptor() APIs are nonstandard and unique to the IBM i operating system. If the portability of an application between the IBM i operating system and UNIX is an issue, you might want to use the sendmsg() and recvmsg() APIs instead.
- Communication of control information
- Often the worker job needs to know additional information when
it receives a descriptor, such as:
- What type of descriptor is it?
- What should the worker job do with it?
The sendmsg() and recvmsg() APIs allow you to transfer data, which might be control information, along with the descriptor; the givedescriptor() and takedescriptor() APIs do not.
- Performance
- Applications that use the sendmsg() and recvmsg() APIs
tend to perform slightly better than those that use the givedescriptor() and takedescriptor() APIs
in three areas:
- Elapsed time
- CPU utilization
- Scalability
- Pool of worker jobs
- You might want to set up a pool of worker jobs so that a server can pass a descriptor and only one of the jobs in the pool becomes active and receives the descriptor. The sendmsg() and recvmsg() APIs can be used to accomplish this by having all of the worker jobs wait on a shared descriptor. When the server calls sendmsg(), only one of the worker jobs receives the descriptor.
- Unknown worker job ID
- The givedescriptor() API requires the server job to know the job identifier of the worker job. Typically the worker job obtains the job identifier and transfers it over to the server job with a data queue. The sendmsg() and recvmsg() do not require the extra overhead to create and manage this data queue.
- Adaptive server design
- When a server is designed using the givedescriptor() and takedescriptor(),
a data queue is typically used to transfer the job identifiers from
worker jobs over to the server. The server then does a socket(), bind(), listen(),
and an accept(). When the accept() API
is completed, the server pulls off the next available job ID from
the data queue. It then passes the inbound connection to that worker
job. Problems arise when many incoming connection requests occur at
once and there are not enough worker jobs available. If the data queue
that contains the worker job identifiers is empty, the server blocks
waiting for a worker job to become available, or the server creates
additional worker jobs. In many environments, neither of these alternatives
are what you want because additional incoming requests might fill
the listen backlog.
Servers that use sendmsg() and recvmsg() APIs to pass descriptors remain unhindered during heavy activity because they do not need to know which worker job is going to handle each incoming connection. When a server calls sendmsg(), the descriptor for the incoming connection and any control data are put into an internal queue for the AF_UNIX socket. When a worker job becomes available, it calls recvmsg() and receives the first descriptor and the control data that was in the queue.
- Inactive worker job
- The givedescriptor() API requires the worker
job to be active while the sendmsg() API does not.
The job that calls sendmsg() does not require any
information about the worker job. The sendmsg() API
requires only that an AF_UNIX socket connection has been set up.
An example of how the sendmsg() API can be used to pass a descriptor to a job that does not exist follows:
A server can use the socketpair() API to create a pair of AF_UNIX sockets, use the sendmsg() API to send a descriptor over one of the AF_UNIX sockets created by socketpair(), and then call spawn() to create a child job that inherits the other end of the socket pair. The child job calls recvmsg() to receive the descriptor that the server passed. The child job was not active when the server called sendmsg().
- Pass more than one descriptor at a time
- The givedescriptor() and takedescriptor() APIs allow only one descriptor to be passed at a time. The sendmsg() and recvmsg() APIs can be used to pass an array of descriptors.