This article is about kernel modules (hereafter, "modules"). I will, for the sake of article autonomy, repeat some of what Mani said in his overview article, but if you haven't read it, you should. I command thee:
Be Engineering Insights: Kernel Programming on the BeOS: Part 1
As an example, we will use the until-recently-fictitious "xyz5038" module, which provides an interface to the still- fictitious XYZ Systems Model 5038 Squarepusher chip, a mainstay of many popular squarepushing peripherals. It's a simple chip, and has only two hardware registers, FOO and BAR. You can find the code for the "xyz5038" module at
<ftp://ftp.be.com/pub/samples/drivers/xyz5038.zip>
Modules export an API through a structure containing pointers to the functions the module provides, and any other ancillary information:
#defineXYZ5038_MODULE_NAME
"generic/xyz5038/v1" struct xyz5038_module_info { module_infomodule
; // returns contents of FOO int32 (*read_foo
)(); // returns contents of BAR int32 (*read_bar
)(); // returns previous contents of FOO int32 (*write_foo
)(int32new_value
); // returns previous contents of BAR int32 (*write_bar
)(int32new_value
); };
In order to use these functions, all you have to do is ask the kernel for a pointer to this structure, and you're in business:
struct xyz5038_module_info *xyz5038
=NULL
; // get a pointer to the xyz5038 moduleget_module
(XYZ5038_MODULE_NAME
, (module_info **)&xyz5038
); // read the value of FOOfoo
=xyz5038
->read_foo()
;
When you've no more use for the module, simply tell the kernel so:
put_module
(XYZ5038_MODULE_NAME
);
Your practical use of modules will be dependent on the functions exported by the ones you use, but that's all you need to get started using them.
Creating your own module is a matter of extending the basic one defined in <module.h>. Note that the first field in xyz5038_module_info is a module_info:
struct module_info { const char*name
; uint32flags
; status_t (*std_ops
); }
The name
field should be the name you provide in the header file for
your module; in this case, XYZ5038_MODULE_NAME
(or "generic/xyz5038/v1").
The flags
field, surprisingly enough, is how you indicate which flags
you want to be in effect for your module. B_KEEP_LOADED
is currently the
only flag there is.
The first time someone calls get_module()
with your module's name, the
kernel loads it. With every subsequent call, a reference count associated
with your module is incremented. Every time someone calls put_module()
with your module's name, that reference count is decremented, and when it
reaches zero, your module is unloaded—unless you set B_KEEP_LOADED
.
"std_ops" is pointer to a function you provide that deals with standard
module operations. Currently, the only two things that entails are
initialization and uninitialization. std_ops()
usually looks like this:
static status_tstd_ops
(int32op
, ...) { switch(op
) { caseB_MODULE_INIT
:module_init_hijinks()
; break; caseB_MODULE_UNINIT
:module_uninit_shenanigans()
; break; default: returnB_ERROR
; } returnB_OK
; }
Exporting your module to the outside world is similar to publishing device driver hooks, but since you are the one defining the hooks, there are a few twists. You'll need to have a filled-out version of your module info struct:
static struct xyz5038_module_infoxyz5038_module
= { // module_info for the kernel {XYZ5038_MODULE_NAME
, 0,std_ops
},read_foo
,read_bar
,write_foo
,write_bar
};
When loading your module, the kernel looks for a symbol called "modules",
which contains a list of pointers to the modules you export, terminated
by a NULL
:
_EXPORT module_info *modules
[] = { (module_info *)&xyz5038_module_info
,NULL
};
Clever readers may have surmised by now that in the same process of including module_info to make your own module, APIs can be defined on top of that and then extended in other modules. As a matter of fact, this has already been done with bus managers, and it will be discussed in a future article.
The standard method for creating Ethernet device drivers is to write the entire driver and then use the protocol stack (NetServer) for testing and debugging. While there are samples and documentation to assist developers, this approach has some weaknesses:
If you use your driver on the same machine you use to retrieve mail and browse the network, it's a hassle to switch between a solid network connection with a working driver and an unstable connection with the driver you're developing.
The driver is affected by the protocol layers above, which is good in the later stages of development but adds unnecessary complications to early stages.
It's difficult to test individual driver components as they're developed.
There is virtually no way to do structured, controlled testing through the NetServer.
To address these problems, I wrote the E-Drive application ("E" for Ethernet, and "Drive" for pushing or controlling) to help create and test a fully working Ethernet driver. The code is available from <ftp://ftp.be.com/pub/samples/drivers /E-Drive.zip>. I want to add more features to this, so please see the TODO file and contact me for updates or to offer suggestions.
In development, the first real packets to go through a new driver are typically the ubiquitous "pings." Ping is often used as a simple network diagnostic tool, and the Internet Control Message Protocol (ICMP) protocol supports sequence numbers and variable payload lengths, so it was a natural choice to build into E-Drive. Of course, the driver only sees an array of bytes, but those bytes make up ICMP echo messages. E-Drive also has limited Address Resolution Protocol (ARP) support. ARP allows other workstations on the network to resolve the address of your device while you're sending out ping requests. E-Drive works with the BeOS, Linux, and FreeBSD as both the sender and receiver of ICMP echo packets.
E-Drive is written like a normal Be application, with one menu and a text view for the main window. Choose File->Open Receive to load the driver and start reading incoming packets. File-> Transmit sends out ping requests. E-Drive dumps raw packets, debug information, and statistics to the text view (instead of standard out). There is also a settings panel for user preferences and network "must haves" like source and destination IP addresses. The values are saved in an E-Drive preference file.
The guts of E-Drive application are in the I0 class. The Open method
opens the driver and gets the card's hardware address. By default, the
driver must be in the
/boot/beos/system/add-ons/kernel/drivers/bin
folder with a link in
/boot/beos/system /add-ons/kernel/drivers/dev/net
folder. As a quick check that the kernel has located and loaded the
driver, look in
/dev/net
for the driver's published name. If it's not
there, E-Drive won't be able to open the driver.
Once the driver is open, a reader thread is spawned from the IOCtrl()
method. The thread loops continuously and receives incoming packets. If
the packet is an ARP or Ping request,
the PrepareArpReply()
and
PreparePingReply()
methods build up an appropriate reply packet. Both
methods do some address swapping, set a few bytes, and calculate an IP
checksum. E-Drive then replies to the requesting host. If the packet is a
ping reply to a request sent from E-Drive,
the ProcessPingReply()
method
is called. The ProcessPingReply()
does some simple round trip
calculations and displays the results. Other packets are ignored,
although they can be viewed by the DumpPacket()
method.
For sending packets, a transmitter thread is spawned from the IOCtrl()
method when the user chooses File->Transmit from the main menu. The
thread loops continuously and calls PreparePingRequest()
at set time
intervals to build up ping request packets. Each time
PreparePingRequest()
is called, the sequence number is incremented and
set in the ICMP header. For the ICMP
identifier, E-Drive uses the
transmitter thread ID. The thread then sends the packet.
Using a test harness like E-Drive lets you focus on writing kernel code to manipulate a device without having to worry about the protocol layers above. And that lets you develop and test Ethernet device drivers with more confidence and control.
Several Gentle Readers have commented that perhaps we should call this column "Media Kit Workshop" rather than "Developer's Workshop." There's some truth in that—we have been emphasizing the glories of the R4.5 Media Kit over the last few months. However, we know that not every application needs to fling video clips around the screen. So this week I'll leave the rarefied world of real-time media handling and begin a series of articles on a more mundane—but necessary—topic: copying files.
"Bah!" you say. "Copying files is easy! Read the data, write it out again, and you're done!" Ahhh, but the Be File System (bfs) is a bit more complex than the run-of-the-mill flat Unix-style file system, and with power comes responsibility. Attribute data is important, too—some files, such as Net Positive bookmarks, consist solely of attribute data -- so a file-copying routine for bfs will be more sophisticated than one for a flat-data file system like ext2 or FAT.
You'll find a simple file-copying function and a simple program which uses at <ftp://ftp.be.com/pub/samples/storage_kit/CopyFile.zip>.
When I say "simple," I mean it. No bells, no whistles, just the ability to copy one file to another. Here's a quick look at the copy routine's salient points.
First, the prototype:
status_tCopyFile
(const entry_ref&source
, const entry_ref&dest
, void*buffer
=NULL
, size_tbufferSize
= 0);
Notice that instead of taking the source and destination files' paths as arguments, the function takes entry_refs. entry_refs are the standard BeOS representation of a file system entity (that is, a file or a directory). They're easier to manage than character string paths: no messy string manipulations or foreign character sets to deal with. Most of the Storage Kit uses entry_refs as the basic token for indicating a file.
entry_refs can be inconvenient when dealing with user input, as users
tend to think in terms of file names and paths, but the Storage Kit
provides a couple of ways to convert from a path to an entry_ref. The
first is the BEntry
class, which encapsulates
an entry_ref inside a
useful object wrapper. One of its constructors takes a path as an
argument; once it is constructed, the entry_ref struct can be extracted
via the BEntry
::GetRef()
method. The second alternative is a C function
called get_ref_for_path()
that converts from a character string path
directly to an entry_ref. The sample "copyall" command line application
in today's code illustrates both of these mechanisms.
The buffer
and bufferSize
arguments to CopyFile()
are optional; they
allow the caller to specify a buffer to be used when copying the file's
data. If they are unspecified, the function allocates its own buffer and
deallocates it when it's finished, so unless you're concerned with memory
usage you generally won't bother to supply a buffer.
We all know how to copy *data* from one file to another, right? To conserve newsletter space I'll just skip to the interesting part: copying attributes from one file to another.
The attributes of a file are accessed by name. The BNode class provides
access to a file's attributes via its GetNextAttrName()
method, which
iteratively returns the name of every attribute associated with the file.
Copying the attributes involves successively reading each attribute of
the source file and replicating it in the destination file.
There's some subtlety involved in guaranteeing that while you're reading
the list of attributes, no other program is adding new ones to the file,
or deleting old ones. To circumvent this unhappy race condition, the
BNode
class provides a locking mechanism.
While the BNode
is locked, the
lock holder has the exclusive ability to access the file. Once the
attributes are copied, of course, it's considered polite (to put it
mildly) to unlock the BNode
again.
In all, CopyFile()
is a pretty simple routine. For now, it will serve as
an introduction to file data and attribute manipulation, and give
developers a commonly needed tool. In my next article, I'll dig a little
deeper into the subject of copying files and discuss how to determine
ahead of time whether a given file will fit on the destination volume.
Estimating the amount of physical disk space required to store a file
under bfs is tricky, as we shall see....