One of the strenghts of Linux is its ability to serve both as engine for powerful number-crunchers and as effective support for minimal computer systems. The PLIP implementation is an outstanding resource in the latter realm, and this article is going to show its internals at the software level. The information herein refers to the 2.0 implementation of the PLIP driver.
PLIP means ``Parallel Line Internet Protocol''. It is a protocol to bring IP traffic over a parallel cable; it works with any parallel interface and is able to transfer about 40kB per second. With PLIP you can connect any two computers at virtually no cost. Although nowadays ISA network cards are readily found and installed, you still will enjoy PLIP as soon as you get a laptop, unless you can afford a PCMCIA network card.
PLIP allows to connect your computer to the Internet wherever there is a networked Linux box with a parallel port available, as long as you are root on both systems (only root can load a module or configure an interface).
I find the internal design of PLIP quite interesting at threee levels: it shows how to use simple I/O instructions, it lets you look at how network interfaces fit in the overall kernel, and it shows in practice how kernel software uses the task queues.
Before showing any PLIP code, I'd like to describe the workings of the standard parallel port, so you'll be able to understand how the actual data trasfer takes place.
The parallel port is a simple device: its external connector exposes 12 output bits and 5 input bits. Software has direct access to the bits by means of three 8-bit ports: two ports can be written to and the other one can be read from. Moreover, one of the input signals can trigger an interrupt; this ability is enabled by setting a bit in one of the output ports. Figure 1 shows how the three ports are mapped to the 25-pin connector.
Figure 1: Pinout of the parallel port
The image is available as PostScript here
The ``base_addr
'' of a parallel port (the address of its data
port) is usually 0x378, 0x278 or 0x3bc. The vast majority of the
parallel ports uses 0x378.
Physical access to the ports is achieved by calling two C-language functions, defined in the kernel headers:
#include "<"linux/io.h">" unsigned char inb(unsigned short port); void outb(unsigned char value, unsigned short port);
The ``b'' in the names means ``byte''. Linux also offers inw
(word, 16-bit), inl (long, 32-bit) and their output
counterparts, but they are not needed to use the parallel port. The
functions just shown are in fact macros, and they expand to a single
machine instruction on most Linux plaftorms. Their definition relies
on extern inline
functions; this means you must turn
optimization on when compiling any code using them. The reason behind
this is somehow technical, and it is well explained in the gcc
man page.
You don't need to be in kernel space to call inb and outb. If you want to access I/O ports from a shell script you can compile inp.c and outp.c and play games with your devices (and even destroy the computer). As you may imagine, only root can access ports.
Based on the description of the parallel port I provided in the previous section, it should be clear that two parties that communicate using PLIP must exchange five bits at a time, at most. The PLIP cable must be specially wired in order to connect five of the outputs of one side to the five iputs at the other side, and vice-versa. The exact pinout of the PLIP cable is described in the source file drivers/net/plip.c and in several places elsewhere, so I won't repeat the information here.
One of the deficiencies of the parallel port is the unavailability of any timing resource in hardware (as opposed to the serial port, which includes its own clock). The communication protocol, therefore, can't exploit any hardware functionality, and any handshake must be performed in software. The chosen protocol uses one of the bits as a strobe signal to assert availability of four data bits; the receiving party must acknowledge reception of such bits by toggling its own strobe line. This approach to data transmission turns out to be very CPU-intensive; the processor must be polling the strobe signal to be able to send out its data, and system performance severely degrades during PLIP data transfers.
The time line of a PLIP transmission is depicted in figure 2, which details the steps involved in the transmission of a single byte of information.
Figure 2: transmission of a byte of data using PLIP
The image is available as PostScript here
As far as the kernel proper is concerned, the PLIP device is just like any other network device. More specifically, it is like any other ethernet interface even though its name is plipx instead of ethx.
When a datagram must be transmitted through a network interface, it is
passed to the transmission function of the device driver. The driver
receives a ``socket buffer'' argument (a struct sk_buff
) and a
pointer to itself (a pointer to struct device
).
With PLIP, transmission occurs by encapsulating the IP datagram into an ``hardware header'' for delivery, not unlikely what happens for any other transmission medium. The difference with PLIP is that although it receives a data packet which already includes an Ethernet header, the driver adds its own haeader. A packet encapsulated by PLIP, as it travels over the parallel cable, is made up of the following fields:
Whenever a packet is transmitted, all of the bytes are sent through the cable using the 5-bit protocol described earlier. This is quite straightforward and works flawlessly, unless something goes hairy during transmission.
The interesting part of a PLIP communication channel is in how asynchronous operations is handled. Transmission and reception of data packets must fit with other system operations, and must be fault-tolerant as much as possible. This involves several kernel resources, and is quite interesting for anyone who's interested in kernel internals.
There are three problems to face in order to achieve reliable PLIP transmission. Outgoing packets must be transmitted asynchronously with the rest of the system (even if transmission is CPU-bound, it should happen outside of the normal computational flow). Incoming packets must too be received asynchronously, and they must be able to notify the PLIP device driver about their arrival. The last problem is fault-tolerance: if one of the parties locks up transmission for any reason, we don't want the peer host to freeze while it waits for a strobe signal.
Asynchronous operation is achieved in PLIP by using the kernel task queues (which have been introduced in the June 1996 issue of Linux Journal). Fault-tolerance and timeouts are implemented using a state-machine implementation, which allows to interleave PLIP transmission/reception with other computational activities without loosing track of the internal status of the transmitter.
Let's imagine to look at a PLIP cable connecting Tanino and Romeo. %% Tanino = Tx, Romeo = Rx. It should be self-explanatory. The following paragraphs explain what happens when Tanino sends a packet to Romeo.
Tanino sends the signal which interrupts Romeo, disables interrupt reporting in its own computer and goes to the transmission loop by registering plip_bh in the immediate task queue and returning. When plip_bh runs, it knows that the interface is sending data and calls plip_send_packet.
Romeo, when interrupted (in plip_interrupt), registers plip_bh in the immediate task queue. The plip_bh function dispatches computation to plip_receive_packet, which disables interrupt reporting in the interface and starts receiving bytes.
Tanino's loop is built on plip_send (which transmits a single
byte) while Romeo's loop in build on plip_receive (which
receives a single byte). These two functions are ready to detect a
timeout condition, and in this case they return the TIMEOUT
macro to the calling function, which returns TIMEOUT
to
plip_bh.
The plip_bh function, when the callee aborts the loop by
returning TIMEOUT
just registers a function in the timer task
queue, so that the loop will be entered again at the next clock
tick. If timeout continues after a few clock ticks, transmission or
reception of this packet is aborted, and an error is registered in the
enet_statistics
structure; these erros are shown by the
ifconfig command.
If the timeout condition doesn't persist at the next clock tick, data
exchange goes on flawlessly. The state machine implemented in the
interface is responsible of restarting communication at exactly the
place when the timeout occurred.
As you see, the PLIP interface is pretty symmetrical.
As far as a network driver is concerned, being able to transmit and receive data is not all of its job. The driver needs to interface with the rest of the kernel in order to fit with the rest of the system. The PLIP device driver devotes more ore less one quarter of its source code to interface issues, and I feel it worth introducing here.
Basically, a network interface needs to be able to send and receive packets. Network drivers are organized in a set of ``methods'', like char drivers are (see the KK article published by LJ in April 1996). Sending a packet is easy: one of the methods is concerned with packet transmission, and the driver just implements the right function to transmit data to the network.
Receiving a packet is somehow more hairy, as the packet arrives through an interrupt, and the driver must actively manage received data. Packet reception for any network interface is managed exploting the so-called ``bottom halves'', which deserve an introduction.
In Linux, interrupt handling is split into two halves: the top-half is the hardware interrupt, which is triggered by an hardware event and is executed immediately; and the bottom-half: a software routine that gets scheduled by the kernel when it can run without interfering with normal system operation. Bottom halves are run whenever a process returns from a system call and when ``slow'' interrupt handlers return. When a slow handler runs, all of the processor registers are saved and hardware interrupt are not disabled; therefore, it's safe to run the pending bottom halves when such handlers return. It's interesting to note that new kernels in the 2.1 hierarchy don't differentiate any more between fast and slow interrupt handlers.
A bottom-half handler must be ``marked''; this consists in setting a
bit in a bitmask register, so that the kernel will easily check
whether any bottom half is pending or not. The immediate task queue,
used by the PLIP driver is implemented as a bottom-half: when a task
is enqueued, the caller must mark_bh(IMMEDIATE_BH)
, and the
queue will be run as soon as a process is done with a system call, or
a slow handler returns.
Back to network interfaces: when a driver receives a network datagram,
it must call netif_rx(struct sk_buff *skb)
, where skb
is
the buffer hosting the received packet; PLIP calls netif_rx from
plip_receive_packet. The netif_rx function enqueues the
packet for later processing and calls mark_bh(NET_BH)
. When
bottom halves are run, then, the packet will be processed.
In practice, something more is needed to fit a newtwork interface in the Linux kernel: the module must register its own interface and initialize them. Moreover, an interface must export a few house-keeping functions that the kernel will call. All of this is performed in a few short functions, listed below:
plip_init
: the function is in charge of initializing
the network device; it is called when init_module registers
its devices. The function check if the hardware is installed
in the system and assigns fields in the struct device
that describes the interface.
plip_open
: whenever an interface is brought up, its
open function is called by the kernel. The function must
prepare to become operative (similar to what the open
method does for char devices).
plip_close
: the reverse of plip_open
plip_get_stats
: the function is called whenever
statistical information is needed. For example, the printout
of ifconfig shows values returned by this function.
plip_config
: if a program changes the hardware
configuration of the device, this function gets called. PLIP
allows to specify the interrupt line at run time, because
probing can't be safely performed when a module is
loaded. Most of the parallel ports are configured to use the
default interrupt line.
plip_ioctl
: any interface that needs to implement
device-specific ioctl commands must have an ioctl
method. PLIP allows changing its timeout values, although I
never needed to play with these numbers. The plipconfig
program is the one that allows changing the timeouts.
plip_rebuild_header
: the function is used to build
an ethernet header in front of the IP data. Ethernet interfaces
that use ARP don't need to implement this function, as the default
one for ethernet interface does all of the work.
init_module
. As you should already know, this is
the entry point to the modularized driver. When a network interface
is loaded to a running system, its init_module should call
register_netdev, passing a pointer to struct device
.
Such a structure should be partly initialized and should include
a pointer to a init function which completes initialization
of the structure. For PLIP, such a function is plip_init.
These functions (and hw_start_xmit, the one responsible for actual packet transmission) are all that's needed to run a network interface within Linux. Although I admit the there's more to know in order to write a real driver, I hope the real sources can prove interesting to fill the holes.
My choice to discuss PLIP is motivated by the easy availability of
such network connection, and the ``do it yourself'' approach that
might convince someone to build their own infrared ethernet link. If
you really are going to peek in the sources to learn how a network
interface works, I'd suggest starting with loopback.c
, which
implements the lo
interface, and skeleton.c
, which is
quite detailed about the problems you'll encounter when building a
network driver.
If you are more keen to using PLIP than to write device drivers, you
can refer to the PLIP-HOWTO in any LDP mirror, and to /usr/doc/HOWTO
in most Linux installations.
Verbatim copying and distribution of this entire article is permitted in any medium, provided this notice is preserved
Reprinted with permission of Linux Journal