![]() |
|
||||
Writing Device Drivers for LynxOS |
Network Device Drivers
A network driver is defined as a link-level device driver that interfaces with the LynxOS TCP/IP module. Unlike other drivers, a network driver does not interface directly with user applications. It interfaces instead with the LynxOS TCP/IP module. This interface is defined by a set of driver entry points and data structures, described in the following sections.
Kernel threads play an important role in LynxOS networking software, not only within the drivers, but also as part of the TCP/IP module.
The example code below illustrates these points for an Ethernet device. These examples can easily be adapted to other technologies.
Kernel Data Structures
A network driver must make use of a number of kernel data structures. Each of these structures is briefly described here and its use is further illustrated.
A driver must include the following header files, which define these structures and various symbolic constants used in the rest of this chapter:
struct ether_header
The Ethernet header must be prefixed to every outgoing packet. It specifies the destination and source Ethernet addresses and a packet type. The symbolic constants ETHERTYPE_IP, ETHERTYPE_ARP and ETHERTYPE_RARP can be used for the packet type.
struct arpcom
The arpcom structure is used for communication between the TCP/IP module and the network interface driver. It contains the ifnet structure (described below) and the interface's Ethernet and Internet addresses. This structure must be the first element in the statics structure.
struct sockaddr
This is a generic structure for specifying socket addresses, containing an address family field and up to 14 bytes of protocol-specific address data.
struct sockaddr_in
A structure used for specifying socket addresses for the Internet protocol family
struct in_addr
Structure specifying a 32 bit host Internet address
struct ifnet
This is the principle data structure used to communicate between the driver and the TCP/IP module. struct ifnet is defined in /usr/include/bsd/if_var.h. It provides the TCP/IP software with a generic hardware-independent interface to the network drivers. It specifies a number of entry points that the TCP/IP module can call in the driver, a flag variable indicating general characteristics and current state of the interface, a queue for outgoing packets, and a number of statistics counters.
The symbolic constants IFF_UP, IFF_RUNNING, and IFF_BROADCAST can be used to set bits in the if_flags field.
Looking at the arpcom structure, notice that the first member is an ifnet structure. A driver should declare a struct arpcom as part of the statics structure and use the ifnet structure within this. There is an important reason for this, explained in "ioctl Entry Point".
struct mbuf
Data packets are passed between the TCP/IP module and a network interface driver using mbuf structures. This structure is designed to allow the efficient encapsulation and decapsulation of protocol packets without copying data. A number of functions and macros are defined in mbuf.h for using mbuf structures.
Adding or Removing Data in an mbuf
The position and size of data currently in an mbuf are identified by a pointer and a length. By changing these values, data can be added or deleted at the beginning or end of the mbuf. A pointer to the start of the data in the mbuf can be obtained using the mtod macro. The pointer is cast as an arbitrary data type, specified as an argument to the macro. For example:
The macro dtom takes a pointer to data placed anywhere within the data portion of the mbuf and returns a pointer to the mbuf structure itself. For example, if we know that cp points within the data area of an mbuf, the sequence will be:
Data is added to the head of an mbuf by decrementing the m_data pointer, incrementing the m_len value and copying the data using a function such as bcopy. Data is added to the tail of an mbuf in a similar manner by incrementing the m_len value. The ability to add data to the tail of an mbuf is useful for implementing trailer protocols; LynxOS does not currently support such protocols.
Data is removed from the head or tail of an mbuf by simply incrementing the m_data pointer or decrementing m_len.
Allocating mbufs
The above examples did not discuss what to do when sufficient space is not available in an mbuf to add data. In this case, a new mbuf can be allocated using the function m_get. The new mbuf is linked onto the existing mbuf chain using its m_next field. m_get can be replaced with MGET, which is a macro rather than a function call. MGET produces faster code whereas m_get results in smaller code. The example to add data to the beginning of a packet now becomes:
The second argument to m_get or MGET specifies whether the function should block or return an error when no mbufs are available. A driver should use M_DONTWAIT, which causes the mbuf pointer to be set to 0 if no mbufs are free.
The third argument to m_get or MGET specifies how the mbuf will be used. This is for statistical purposes only and is used, for example, by the command netstat m. The types used by a network driver are MT_HEADER for protocol headers and MT_DATA for data packets.
mbuf Clusters
When receiving packets from a network interface, a driver must allocate mbufs to store the data. If the data packets are large enough, a structure known as an mbuf cluster can be used. A cluster can hold more data than a regular mbuf; MCLBYTES bytes as opposed to MLEN. As a rule, there is benefit to be gained from using a cluster if the packet is larger than MCLBYTES/2.
Freeing mbufs
Because there is a limited number of mbufs in the system, the driver must take care to free mbufs at appropriate points. These are listed below:
- Interface is down
- Address family not supported
- No mbufs for Ethernet header
- if_snd queue is full
- After packet has been transferred to interface
The sections "Packet Input" and "Packet Output" show appropriate code examples for each of the above situations. Failure to free them will eventually lead to the system running out of mbufs.
Statics Structure
In keeping with the general design philosophy of LynxOS drivers, network drivers should define a statics structure for all device-specific information. However, the TCP/IP software has no knowledge of this structure, which is specific to each network interface, and does not pass it as an argument to the driver entry points.
The solution to this situation is for the ifnet structure to be contained within the statics structure. The user-defined field, p, in the ifnet structure, is initialized to contain the address of the statics structure.
Given the address of the ifnet structure passed to the entry point from the TCP/IP software, the driver can obtain the address of the statics structure as follows:
The arpcom structure must be the first element in the statics structure. In the code examples below, the arpcom structure is named ac.
Packet Queues
A number of queues are used for transferring data between the interface and the TCP/IP software. There is an output queue for each interface, contained in the ifnet structure. There are two input queues used by all network interfaces. One for IP packets and another for ARP/RARP packets. All queues are accessed using the macros IF_ENQUEUE, IF_DEQUEUE, IF_QFULL, or IF_DROP.
Driver Entry Points
A network driver contains the following entry points:
By convention, the entry point names are prefixed with the driver name.
install Entry Point
In addition to the usual things done in the install routine (allocation and initialization of the statics structure, declaration of interrupt handler, etc.), the driver must also fill in the fields of the ifnet structure and make the interface known to the TCP/IP software. Note also that hardware initialization is normally done in the ioctl routine rather than in the install routine.
Finding the Interface Name
The install routine must initialize the if_name field in the ifnet structure. This is the name by which the interface is known to the TCP/IP software. It is used, for example, as an argument to the ifconfig and netstat utilities.
The interface name is a user-defined field specified in the driver's device configuration file (drvr.cfg) in the /sys/lynx.os directory. The usual technique used by the driver to find this field is to search the ucdevsw table for an entry with a matching device information structure address. ucdevsw is a kernel table containing entries for all the character devices declared in the CONFIG.TBL file. The kernel variable nucdevsw gives the size of this table.
Initializing the Ethernet Address
The ac_enaddr field in the arpcom structure is used to hold the interface's Ethernet address, which must be included in the Ethernet header added to all outgoing packets. The install routine should initialize this field by reading the Ethernet address from the hardware.
In the above example, get_ether_addr would be a user written function that reads the Ethernet address from the hardware.
Initializing the ifnet Structure
The various fields in the ifnet structure should be initialized to appropriate values. Unused fields should be set to zero or NULL. Once the structure has been initialized, the network interface is made known to the TCP/IP module by calling the appropriate network interface attach routine, for example ether_ifattach() for Ethernet drivers.
Note that the if_output handle in the ifnet structure should point to the ether_output routine in the TCP/IP module. Previously it pointed to the driver specific local routine. In BSD 4.4, most of the hardware-independent output code has been moved to the ether_output routine. After the ether_output routine has packaged the data for output, it calls a start routine specified by if_start, a member of the interface ifnet structure. For example:
Packet Output
The processing of outgoing packets is divided into two parts. The first part concerns the TCP/IP module, which is responsible for queueing the packet on the interface's output queue. The actual transmission of packets to the hardware is handled by the driver start routine and the kernel thread. The driver is responsible in all cases for freeing the mbufs holding the data once the packet has been transmitted or when an error is encountered.
ether_output Function
A number of tasks previously performed by the driver output routine are now done in TCP/IP module by ether_output routine. Thus the if_output field of the interface ifnet structure is initialized to the address of the ether_output routine in the driver install routine:
This causes ether_output routine to be called indirectly when the TCP/IP module has a packet to transmit. After enqueuing the packet to transmit, ether_output calls a device-specific function indirectly through the if_start pointer in the ifnet structure. For example, if ifp points to an ifnet structure,
the if_start field is also initialized by the driver install routine. The driver start routine starts output on the interface if resources are available. Before removing packets from the output queue for transmission, the code normally has to test whether the transmitter is idle and ready to accept a new packet. It typically dequeues a packet (which is enqueued by ether_output) and transmits it.
One important point to consider is that the start routine can now be called by the TCP/IP module (by way of ether_output) and the driver stream task upon receiving an interrupt. Thus, the start routine must protect code and data in the critical area. For example, it could check a pending flag, which is set before starting to transmit, and cleared when a transmit done interrupt is received. If the transmit start routine is not reentrant, it could signal a semaphore in order to notify the driver's kernel thread that packets are now available on the output queue. The routine should then return 0 to indicate success. For example:
Also note that the total data available in an mbuf can be obtained from the mbuf packet header. For example:
Kernel Thread Processing
The kernel thread must perform the following activities relating to packet output.
Starting Transmission
As explained above, the kernel thread also calls the driver start routine to start transmission. The transmit start routine dequeues a packet from the interface send queue and transmits it.
Statistics Counters
The counters relating to packet output are the if_opackets, if_oerrors, and if_collisions fields in the ifnet structure. The if_opackets counter should be incremented for each packet that is successfully transmitted by the interface without error. If the interface indicates that an error occurred during transmission, the if_oerrors counter should be incremented. The driver should also interrogate the interface to determine how many collisions, if any, occurred during transmission. A collision is not necessarily an error condition. An interface normally makes a number of attempts to send a packet before raising an error condition.
Packet Input
When packets are received by a network interface they must be copied from the device to mbufs and passed to the TCP/IP software. Because this can take a significant amount of time, the bulk of the processing of incoming packets should be done by the driver's kernel thread so that it does not impact the system's real-time performance. The interrupt handler routine should do the minimum necessary to ensure that the interface continues to function correctly.
To maintain bounded system response times, the interrupt handler should also disable further interrupts from the interface. These will be re-enabled by the driver's kernel thread. The processing of input packets involves the following activities:
- Determining packet type
- Copying data from interface into mbufs
- Stripping off Ethernet header
- Enqueueing packet on input queue
- Re-enabling receiver interrupts
- Maintaining statistics counters
Determining Packet Type
The packet type is specified in the Ethernet header and is used by the driver to determine where to send the packet received. In the following code, the ptr variable is assumed to be a pointer to the start of the received Ethernet frame. The use of the ntohs function ensures the portability of code across different CPU architectures.
Copying Data to mbufs
Most network devices have local RAM, which is visible to the device driver. On packet reception, the driver must allocate sufficient mbufs to hold the received packet, copy the data to the mbufs, then pass the mbuf chain to the TCP/IP software. The ifnet structure is added to the start of the packet so that the upper layers can easily identify the originating interface. The Ethernet header must be stripped from the received packet. This can be achieved simply by not copying it into the mbuf(s). If the entire packet can not be copied, any allocated mbufs must be freed. The following code outlines how a packet is copied from the hardware to mbufs using the m_devget routine. m_devget is called with the address and the size of the buffer that contains the received packet. It creates a new mbuf chain and returns the pointer to the chain.
ifp is the device interface pointer. The variable buf points to the received data. This is usually an address in the interface's local RAM.
By default, m_devget uses bcopy, which copies data one byte at a time.
A driver can provide a different algorithm for more efficiency and pass its address to the m_devget routine.
Enqueueing Packet
The packet read routine finally calls a TCP/IP module called ether_input to enqueue the received packet on one of the TCP/IP software's input queues for further processing.
Statistics Counters
The counters relating to packet input are the if_ipackets and if_ierrors fields in the ifnet structure. The if_ipackets counter should be incremented for each packet that is successfully transferred from the interface and enqueued on the TCP/IP input queue. Receive errors are normally indicated by the interface in a status register. In this case the if_ierrors counter should be incremented.
ioctl Entry Point
The ioctl entry point is called by the TCP/IP software in the following syntax:
The ioctl function must support the two commands SIOCSIFADDR and SIOCSIFFLAGS.
SIOCSIFADDR
This command is used to set the network interface's IP address. Currently, the only address family supported is Internet. Typically this ioctl gets called by the ifconfig utility. The driver should set the IFF_UP bit in the if_flags and call the drvr_init function to initialize the interface. The argument passed to the ioctl routine is cast to a pointer to an ifaddr structure, which is then used to initialize the interface's Internet address in the arpcom structure. The driver should also call arpwhohas to broadcast its Internet address on the network. This allows other nodes to add an entry for this interface in their ARP tables.
SIOCSIFFLAGS
This command is used to bring the interface up or down and is called, for example, by the command ifconfig name up. The TCP/IP software sets or resets the IFF_UP bit in the if_flags field before calling the driver's ioctl entry point to indicate the action to be taken. An interface that is down cannot transmit packets.
When the interface is brought up, the driver should call the drvr_init function to initialize the interface. When the interface is brought down, the interface should be reset by calling drvr_reset. In both cases, the statistics counters in the ifnet structure should be zeroed.
The driver normally defines a flag in the statics structure that it uses to keep track of the current state of the interface (s>ds_flags in the example code below).
if ((ifp->if_flags & IFF_UP) == 0 && s->ds_flags & DSF_RUNNING) {
drvr_reset (s); /* interface going down */
watchdog Entry Point
The watchdog entry point can be used to implement a function that periodically monitors the operation of the interface, checking for conditions such as a hung transmitter. The function can then take corrective action if necessary. If the driver does not have a watchdog function, the corresponding field in the ifnet structure should be set to NULL before calling if_attach.
The watchdog function is used in conjunction with the if_timer field in the ifnet structure. This field specifies a timeout interval in seconds. At the expiration of this interval, the TCP/IP module calls the watchdog entry point in the driver, passing it the p field from the ifnet structure as an argument. The p field is normally used to contain the address of the statics structure.
Note that the timeout interval specified by if_timer is a one-shot function. The driver must reset it to a non-zero value to cause the watchdog function to be called again. Setting the if_timer value to 0 disables the watchdog function.
reset Entry Point
This entry point is called by the kernel during a reboot sequence, passing it the p field from the ifnet structure, which is normally the address of the statics structure. This function may also be called internally from the driver's ioctl entry point. The function should reset the hardware, putting it into an inactive state.
Kernel Thread
The kernel thread receives events from two sources, the interrupt handler (indicating completion of a packet transmission or reception) and the driver output routine (indicating the availability of packets on the if_snd queue). A single event synchronization semaphore is used for both purposes. The thread should handle interrupts first and then the packets on the output queue. The general structure of the thread looks something like:
The precise details of the thread code depend on the hardware architecture. The function for processing interrupts contains the packet input code discussed above. It also maintains the various statistics counters. Also, receiver interrupts, if disabled by the interrupt handler, are re-enabled at this point. The output function performs the tasks discussed above in the "Packet Output" section.
Priority Tracking
Whenever the set of user tasks using the TCP/IP software changes or the priority of one of these tasks changes, the setprio entry point in the driver is invoked to allow the driver to properly implement priority tracking on its kernel thread. The entry point is passed two parameters, the address of the ifnet structure and the priority that the kernel thread should be set to. For example:
Driver Configuration File
The driver configuration file drvr.cfg in the /sys/lynx.os directory needs to declare only the install (and uninstall) entry points. The other entry points are declared to the TCP/IP module dynamically using the if_attach function. A typical configuration file looks something like:
IP Multicasting Support
ether_multi Structure
For each Ethernet interface there is a list of Ethernet multicast address ranges to be received by the hardware. This list defines the multicast filtering to be implemented by the device. Each address range is stored in an ether_multi structure. For example:
The entire list of ether_multi is attached to the interface's arpcom structure.
- If the interface supports IP multicasting, the install routine should set the IFF_MULTICAST flag. For example:
- Two new ioctls need to be added. These are SIOCADDMULTI to add the multicast address to the reception list and SIOCDELMULTI to delete the multicast address from the reception list. For example:
- The driver reset routine must program the controller filter registers from the filter mask calculated from the multicast list associated with this interface. This list is available in the arpcom structure and there are macros available to access the list. For example:
![]() LynuxWorks, Inc. 855 Branham Lane East San Jose, CA 95138 http://www.lynuxworks.com 1.800.255.5969 |
![]() |
![]() |
![]() |
![]() |