You are on page 1of 16

Device Drivers In Linux

Gregory Kesden 2000


15-412 Fall 2000

Device Types
Block devices
Random access devices (buffering) For example a file system
can only be mounted on a block device, because it requires
random access.
For example, disks are commonly implemented as block
devices.
Character devices
Sequential access (no buffering). Examples might include
printers, scanners, sound boards, &c.
The same device may have both block and character oriented
interfaces (In other UNIX this would actually require separate
drivers. A block device driver and a raw device driver.)

Major and Minor Numbers

Major number
Each device driver is identified by a unique major number.
This number is assigned by the Linux Device Registrar (A
human currently Peter.Anvin@linux.org). This number is the
index into an array that contains the information about the
driver (the device_struct)

Minor number
This uniquely identifies a particular instance of a device. For
example, a system may have multiple IDE hard disks each
would have a major number of 3, but a different minor number.

mknod /dev/name type major minor


Tin UNIX, the interface to devices is via the file system. To create a
file representing a device, you use mknod and supply the filename
and the devices type (c for character or b for block) and the
devices major and minor number.

The file_operation structure

The file operation structure for a device contains pointers to all of


the functions for a device. This is what maps the functions in the
generic interface into the specific devices behaviors..

The mappings are maintained in two arrays:


static struct device_struct chrdevs[MAX_CHRDEV]
static struct device_struct blkdevs[MAX_BLKDEV]

The same device can have both character and block interfaces, so
it can have mappings in both arrays.

struct file_operations {
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *);
int (*fasync) (int, struct file *, int);
int (*check_media_change) (kdev_t dev);
int (*revalidate) (kdev_t dev);
int (*lock) (struct file *, int, struct file_lock *);
};

Setup Function - Overview


Introduction
Setup functions are not necessary. They allow parameters to a driver to
be supplied as boot parameters. They should do nothing more than set
global variable the init() function should do all of the real work.
Boot parameters
When the kernel boots, it may be initialized with parameters destined for a
particular device driver. The paramaters passed to the kernel generally
take the form:
name=param1,parms2,parm3,,parmn name=parm1,parmn
parseoption()
The parseoption() function is called by the kernel to break the command
line into separate strings for each driver:
name=parm1,parm2,parm3,
checksetup()
This function checks the name field of the parameter to find the matching
device driver. If it finds a match, it invokes the specified setup function,
passing it the parameters as an integer array. It was once a simple
function using a static mapping table it now uses ELF black magic.

Setup Function -- Example


static int __init BusLogic_Setup(char *str)

int ints[3];
(void)get_options(str, ARRAY_SIZE(ints), ints);
if (ints[0] != 0) {
BusLogic_Error("BusLogic: Obsolete Command Line Entry
"Format Ignored\n", NULL);
return 0;
}
if (str == NULL || *str == '\0')
return 0;
return BusLogic_ParseDriverOptions(str);
}
/* This is black magic checksetup() plays in ELF land! */
__setup("BusLogic=", BusLogic_Setup);

The Init() Function

The call to the init() function for a driver is hard-coded into the
kernels bootstrap code. For example, a SCSI devices intialization
function could be called within scsi_dev_init() in
drivers/scsi/scsi.c

The init() function should ensure that the device exists.

The init function must register the drivers character and/or block
interface. This installs the mapping between the generic API and
the drivers specific behaviors. Recall that these mappings are
maintained in arrays and were defined by the drivers
file_operation struct.

There is an array of these file_operation structs for character


(chrdevs[devices and another for block devices
register_chrdev (major_num, name, &file_operation_struct)
register_blkdev (major_num, name, *file_operation_struct)

It can also perform any other initialization.

Unregister()

If necessary device drivers can be unregistered. Therse functions


NULL out the entries in the chrdevs[] or blkdevs[] arrays.
unregister_chrdev (major_number, name);
unregister_blkdev (major_number, name);

Nomenclature

In order to avoid name-space collisions with other definitions,


functions or global variables, &c, anything that could pollute the
namespace everything should begin with a unique and descriptive
prefix.

For example, consider the nomenclature used in the Vino Video


System:
struct vino_device { }
static struct vino_device vino[2];
static void vino_setup(struct vino_device *v) { }
static int vino_init(void) { }
static void vino_dma_stop(void) { }
&c

Polling Mode

Polling Mode is supported in Linux although it is favorable for


almost nothing. An older version of Linux (2.0) contained code for
a polled mode parallel port driver. Its main work loop looked like
this:
do {
status = LP_S(minor);
count++; /* count is used as a time-out */
if (need_resched) schedule();
} while(!LP_READY(minor,status) && count <
LP_CHAR(minor));

Interrupt Mode
Linux supports three different flavors of Interrupt Mode I/O:

Slow IRQs
These are the most common type of ISR. Slow interrupts can be
interrupted by other interrupts. Before returning to an interrupted
ISR(), the ret_from_sys_call() is used this macro restores the
registers, &c to their original values.

Fast IRQs
This mode is used for very brief ISRs. When fast mode is used,
other interrupts are not allowed. This reduces the overhead
required to invoke the ISR, because less state needs to be
preserved.

Interrupt Sharing
Interrupt sharing is the ability to have one IRQ service multiple
devices. Basically, a chain of ISRs, each for a different device, is
constructed for the IRQ. Each ISR is executed if the interrupt
occurs. The ISRs should be able to interrogate the device to
determine if, in fact, there is any work for them to do.

Installing an Interrupt Handler

An ISR must be installed before it can be used. This is done using


the request_irq() call:
int request_irq (unsigned int irq,
void (*handler)(int, struct pt_regs *),
unsigned long
irqflags,
const char *devname,
void *dev_id)

irq is the IRQ for which the handler is installed

handler is the handler function

irq_flags might include SA_INTERRUPT to specify fast IRQs or SA_SHIRQ to


specify a shared IRQ (or an ORed combination).

devname should be the name of the device driver (used in /proc for accounting)

dev_id is meaningless to the kernel, but is passed unchanged to the handler

Interrupt Handler Shell


static void dev_interrupt (int irq, void *dev_id, struct
pt_regs *regs)
{
/*
irq contains the number of the IRQ that caused
this handler to be invoked the same handler
can be
installed for multiple IRQs.
*/
/*
dev_id contains whatever it was passed into
request_irq() when this handler was installed.
*/
/*
*/
}

pt_regs contains the registers from the processes that


was interrupted by this handler

Bottom Half Support

Linux recognizes the fact that interrupt handlers should only take
care of actions that require immediate attention, so it has explicit
support for routines that compose the bottom half of a device
driver.

Up to 32 bottom half routines can be defined.

Bottom half routines are atomic with respect to each other.

Bottom halve routines that have work to do are said to be active.


After every slow interrupt completes, if there are no interrupted or
pending slow interrupts, the list of bottom halves is scanned and
active bottom halves are executed. (This is done as part of
ret_from_syscall)

In a typical case, the ISR will take care of time-critical business


and activate a bottom half to finish the processing.

Using Bottom Halves


Three data structures are used to implement bottom halfs
bh_base A 32 entry array. It holds the pointers to the bottom half
routines
bh_mask a 32 bit mask. A bit set in this mask indicates that a the
corresponding bottom half is initialized and usable.
bh_active a 32 bit mask. A bit set in this mask indicates that the
corresponding bottom half is active (has work to do).

void init_bh (int nr, void (*routine)(void))


This installs routine as the handler for bottom half number nr.

void disable_bh (int nr)


This disables a bottom half. It turns off the mask bit for the installed
bottom half nr.

void enable_bh (int nr)


This sets the mask bit for the installed bottom half nr.

void mark_bh (int nr)


This actives a bottom half, indicating that there is work for it to do.

You might also like