General Tables

Some table structures are not suitable for the simple table approach, due to the failure of one or more of the assumptions listed earlier. Perhaps they are indexed by something other than a single integer (such as a 4-octet IP address), or the maximum index is not easily determinable (such as the interfaces table), or not all indices are valid (running software), or the necessary data is not directly accessible (interfaces again).
In such circumstances, a more general approach is needed. In contrast with the two styles already covered, this style of module will commonly combine the two functions of request validation and data retrieval. Note that mib2c will assume the simple table case, and this will need to be corrected.

General table algorithm

The basic algorithm is as follows:
Perform any necessary initialization, then walk through the underlying instances, retrieving the data for each one, until the desired instance is found. If no valid entry is found, return failure.
For an exact match (GET and similar), identifying the desired instance is trivial - construct the OID (from the 'vp' variable parameter and the index value or values), and see whether it matches the requested OID.
For GETNEXT, the situation is not quite so simple. Depending on the underlying representation of the data, the entries may be returned in the same order as they should appear in the table (i.e. lexically increasing by index). However, this is not guaranteed, and the natural way of retrieving the data may be in some "random" order. In this case, then the whole table needs to be traversed for each request. in order to determine the appropriate successor.
This random order is the worst case, and dictates the structure of the code used in most currently implemented tables. The ordered case can be regarded as a simplification of this more general one.

The algorithm outlined above can now be expanded into the following pseudo-code:

	Init_{Name}_Entry();	// Perform any necessary initialisation

	while (( index = Get_Next_{Name}_Entry() ) != EndMarker ) {
			// This steps through the underlying table,
			//   returning the current index,
			//   or some suitable end-marker when all
			//   the entries have been examined.
			// Note that this routine should also return the
			//   data for this entry, either via a parameter,
			//   or some external location

	    construct OID from vp->name and index
	    compare new OID and request
	    if valid {
		save current data
		if finished	// exact match, or ordered table
		    break;	//  so don't look at any more entries


		//  Otherwise, we need to loop round, and examine
		//    the next entry in the table.  Either because
		//    the entry wasn't valid for this request,
		//    or the entry was a possible "next" candidate,
		//    but we don't know that there isn't there's a
		//    better one later in the table.

	if no saved data	// Nothing matched
	   return failure

		// Otherwise, go on to the switch handling
		//  we've already covered in the earlier styles.
This is now very close to the actual code used in many current implementations (such as the the routine header_ifEntry in mibII/interfaces.c). Notice that the pseudo-code fragment if valid expands in practise to
	if ((exact && (result == 0))  ||
			// GET request, and identical OIDs
	    (!exact && (result < 0)) )
			// GETNEXT, and candidate OID is later
			//  than requested OID.
This is a very common expression, that can be seen in most of the table implementations.

Notice also that the interfaces table returns immediately the first valid entry is found, even for GETNEXT requests. This is because entries are returned in lexical order, so the first succeeding entry will be the one that's required.
(As an aside, this also means that the underlying data can be saved implicitly within the 'next entry' routine - not very clean, but it saves some unnecessary copying).

The more general case can be seen in the TCP and UDP tables (see mibII/tcp.c and mibII/udp.c). Here, the if valid fragment expands to:

	if ( exact && (result == 0)) {
		// save results
	else if (!exact && (result < 0)) {
	    if ( .... ) { 	// no saved OID, or this OID
				//  precedes the saved OID
		// save this OID into 'lowest'
		// save the results into Lowinpcb
		// don't break, since we still need to look
		//      at the rest of the table
The GET match handling is just as we've already seen - is this the requested OID or not. If so, save the results and move on to the switch statement. The GETNEXT case is more complicated. As well as considering whether this is a possible match (using the same test we've already seen), we also have to check whether this is a better match than anything we've already seen. This is done by comparing the current candidate (newname) with the best match found so far (lowest).
Only if this extra comparison shows that the new OID is earlier than the saved one, do we need to save both the new OID, and any associated data (such as the inpcb block, and state flag). But having found one better match, we don't know that there isn't an even better one later on. So we can't break out of the enclosing loop - we need to keep going and examine all the remaining entries of the table.

These two cases (the TCP and UDP tables) also show a more general style of indexing. Rather than simply appending a single index value to the OID prefix, these routines have to add the local four-octet IP address plus port (and the same for the remote end in the case of the TCP table). This is the purpose of the op and cp section of code that precedes the comparison.

These two are probably among the most complex cases you are likely to encounter. If you can follow the code here, then you've probably cracked the problem of understanding how the agent works.

Finally, the next part discusses how to implement a writable (or SETable) object in a MIB module.

Copyright 1999 - D.T.Shield. Not to be distributed without the explicit permission of the author.