DEVICE DRIVERS ON LINUX

DEVICE DRIVERS ON LINUX

Introduction:

Most of the Linux code is independent of the hardware it runs on. Applications are often agnostic to the internals of a hardware device they interact with. They interact with the devices as a black box using operating system defined interfaces. As far as applications are concerned, inside the black box sits a program that exercises a protocol to interact with the device completely. This program interacts with the device at a very low level and abstracts away all the oddities and peculiarities of the underlying hardware to the invoking application. Obviously every device has a different device driver. The demand for device drivers is increasing as more and more devices are being introduced and the old ones become obsolete.

In the context of Linux as an open source OS, device drivers are in great demand. There are two principal drivers behind this. Firstly, many hardware manufacturers do not ship a Linux driver so it is left for someone from the open source community to implement a driver. Second reason is the large proliferation of Linux in the embedded system market. Some believe that Linux today is number one choice for embedded system development work. Embedded devices have special devices attached to them that require specialized drivers. An example could be a microwave oven running Linux and having a special device driver to control its turntable motor.

In Linux the device driver can be linked into the kernel at compile time. This implies that the driver is now a part of the kernel and it is always loaded. The device driver can also be linked into the kernel dynamically at runtime as a pluggable module.

image

Almost every system call eventually maps to a physical device. With the exception of the processor, memory and a few other entities, all device control operations are performed by code that is specific to the device. This code as we know is called the device driver. Kernel must have device drivers for all the peripherals that are present in the system right from the keyboard to the hard disk etc.

Device classes:

Char devices:

These devices have a stream oriented nature where data is accessed as a stream of bytes example serial ports. The drivers that are written for these devices are usually called “char device drivers”. These devices are accessed using the normal file system. Usually they are mounted in the /dev directory. If ls –al command is typed on the command prompt in the /dev directory these devices appear with a ‘c’ in the first column.

Example:

crw-rw-rw- 1 root tty 2, 176 Apr 11 2002 ptya0

crw-rw-rw- 1 root tty 2, 177 Apr 11 2002 ptya1

crw-rw-rw- 1 root tty 2, 178 Apr 11 2002 ptya2

crw-rw-rw- 1 root tty 2, 179 Apr 11 2002 ptya3

Block devices:

These devices have a ‘block’ oriented nature where data is provided by the devices in blocks. The drivers that are written for these devices are usually called as block device drivers. Classic example of a block device is the hard disk. These devices are accessed using the normal file system. Usually they are mounted in the /dev directory. If a ls –al command is typed on the command prompt in the /dev directory these devices appear with a ‘b’ in the first column.

Example:

image

Network devices:

These devices handle the network interface to the system. These devices are not accessed via the file system. Usually the kernel handles these devices by providing special names to the network interfaces e.g. eth0 etc.

Note that Linux permits a lot of experimentation with regards to checking out new device drivers. One need to learn to load, unload and recompile to check out the efficacy of any newly introduced device driver. The cycle of testing is beyond the scope of discussion here.

Major/minor numbers:

Most devices are accessed through nodes in the file system. These nodes are called special files or device files or simply nodes of the file system tree. These names are usually mounted in the /dev/ directory.

If a ls –al command is issued in this directory we can see two comma separated numbers that appear where usually the file size is mentioned. The first number (from left side) is called the device major number and the second number is called the device minor number.

Example: crw-rw-rw- 1 root tty 2, 176 Apr 11 2002 ptya0 Here the major number is 2 and the minor number is 176 The major number is used by the kernel to locate the device driver for that device. It is an index into a static array of the device driver entry points (function pointers). The minor number is passed to the driver as an argument and the kernel does not bother about it. The minor number may be used by the device driver to distinguish between the different types of devices of the same type it supports. It is left to the device driver, what it does with the minor numbers. For the Linux kernel 2.4 the major and minor numbers are eight bit quantities. So at a given time you can have utmost 256 drivers of a particular type and 256 different types of devices loaded in a system. This value is likely to increase in future releases of the kernel.

Kernel 2.4 has introduced a new (optional) file system to handle device. This file system is called the device file system . In this file system the management of devices is much more simplified. Although it has lot of user visible incompatibilities with the previous file system, at present device file system is not a standard part of most Linux distributions.

In future, things might change in favour of the device file system. Here it must be mentioned that the following discussion is far from complete. There is no substitute for looking at the actual source code. The following section will mainly help the reader to know what to grep for in the source code.

We will now discuss each of the device class drivers that is block, character and network drivers in more detail.

Character Drivers:

Driver Registeration/Uregisteration:

We register a device driver with the Linux kernel by invoking a routine (<Linux/fs.h>) int register_chrdev(unsigned int major, const char * name, struct file_operations * fops); Here the major argument is the major number associated with the device. Name signifies the device driver as it will appear in the /proc/devices once it is successfully registered. The fops is a pointer to the structure containing function pointers to the devices’ functionalities. We will discuss fops in detail later.

Now the question arises: how do we assign a major number to our driver:

Assigning major numbers:

Some numbers are permanently allocated to some common devices. The reader may like to explore: /Documentation/devices.txt in the source tree. So if we are writing device drivers for these devices we simply use these major numbers.

If that is not the case then we can use major numbers that are allocated for experimental usage. Major numbers in the range 60-63, 120-127, 240-254 are for experimental usage.

But how do we know that a major number is not already used especially when we are shipping a driver to some other computer.

By far the best approach is to dynamically assign the major number. The idea is to get a free major number by looking at the present state of the system and then assigning it to our driver. If the register_chrdev function is invoked with a zero in the major number field, the function, if it registers the driver successfully, returns the major number allocated to it. What it does is that it searches the system for an unused major number, assigns it to the driver and then returns it. The story does not end here. To access our device we need to add our device to the file system tree. That is, we need to do mknod for the device into the tree. For that we need to know the major number for the driver. For a statically assigned major number that is not a problem. Just use that major number you assigned to the device. But for a dynamically assigned number how do we get the major number? The answer is: parse the /proc/devices file and find out the major number assigned to our device. A script can also be written to do the job.

Removing a driver from the system is easy. We invoke the unregister_chrdev(unsigned int major, const char * name);

Important Data Structure: The file Structure<linux/fs.h>:

Every Linux open file has a corresponding file structure associated with it. Whenever a method of the device driver is invoked the kernel will pass the associated file structure to the method. The method can then use the contents of this structure to do its job. We list down some important fields of this structure.

mode_t f_mode;

This field indicates the mode of the file i.e for read write or both etc. loff_t f_pos;

The current offset in the file. unsigned int f_flags;

This fields contains the flags for driver access for example synchronous access (blocking) or asynchronus(non blocking) access etc.

struct file_operations * fops;

This structure contains the entry points for the methods that device driver supports. This is an important structure we will look at it in more detail in the later sections.

void * private_data;

This pointer can be allocated memory by the device driver for its own personal use. Like for maintaining states of the driver across different function calls.

sruct dentry * f_dentry;

The directory entry associated with the file. Etc.

The file operations structure(fops):<linux/fs.h>

This is the most important structure as far as device driver writer are concerned. It contains pointers to the driver functions. The file structure discussed in the previous section contains a pointer to the fops structure. The file (device) is the object and fops contains the methods that act on this object. We can see here object oriented approach in the Linux Kernel.

Before we look at the members of the fops structure it will be useful if we look at taggd structure initialization:

Tagged structure initializations:

The fops structure has been expanding with every kernel release. This can lead to compatibility problems of the driver across different kernel versions.

This problem is solved by using tagged structure initialization. Tagged structure initialization is an extension of ANSI C by GNU. It allows initialization of structure by name tags rather than positional initialization as in standard C.

Example:

struct fops myfops={

……………………..

………………….

open : myopen;

close : myclose:

…………..

…………

}

The intilization can now be oblivious of the change in the structure (Provided obviously that the fields have not been removed).

Pointers to functions that are implemented by the driver are stored in the fops structure. Methods that are not implemented are made NULL.

Now we look at some of the members of the fops structure: loff_t (*llseek) (struct file *,loff_t);

/* This method can be used to change the present offset in a file. */ ssize_t (*read) (struct file*,char *,size_t,loff_t *);

/* Read data from a device.*/

ssize_t(*write) (struct file *,const char *,size_t,loff_t *);

/* Write data to the device. */

int (* readdir) (struct file *,void *,fill_dir_t);

/* Reading directories. Useful for file systems.*/

unsigned int (* poll) (struct file *,struct poll_table_struct *);

/* Used to check the state of the device. */

int (*ioctl)(struct inode *,struct file *,unsigned int,unsigned long);

/* The ioctl is used to issue device specific calls(example setting the baud rate of the serial port). */

int (*mmap) (struct file *,struct vm_area_struct *);

/* Map to primary memory */

int (* open) (struct inode *,struct file *);

/ * Open device.*/

int ( *flush) (struct file *) ;

/* flush the device*/

int (*release) (struct inode *,struct file *);

/* Release the file structure */

int(*fsync) (struct inode *,struct dentry *);

/* Flush any pending data to the device. */ Etc.

Advance Char Driver Operations:

Although most of the following discussion is valid to character as well as network and block drivers, the actual implementation of these features is explained with respect to char drivers.

Blocking and non-blocking operations:

Device drivers usually interact with hardware devices that are several orders of time slower than the processor. Typically if a modern PC processor takes a second to process a byte of data from a keyboard, the keyboard takes several thousand years to produce a single byte of data. It will be very foolish to keep the processor waiting for data to arrive from a hardware device. It could have severe impact on the overall system performance and throughput. Another cause that can lead to delays in accessing devices, which has nothing to do with the device characteristics, is the policy in accessing the device. There might be cases where device may be blocked by other drivers . For a device driver writer it is of paramount importance that the processor is freed to perform other tasks when the device is not ready.

We can achieve this by the following ways.

One way is blocking or the synchronous driver access. In this way of access we cause the invoking process to sleep till the data arrives. The CPU is then available for other processes in the system. The process is then awakened when the device is ready.

Another method is in which the driver returns immediately whether the device is ready or not allowing the application to poll the device.

Also the driver can be provided asynchronous methods for indicating to the application when the data is available.

Let us briefly look at the Linux kernel 2.4 mechanisms to achieve this.

There is a flag called O_NONBLOCK flag in filp->f_flags ( <linux/fcntl.h> ).

If this flag is set it implies that the driver is being used with non-blocking access. This flag is cleared by default. This flag is examined by the driver to implement the correct semantics.

Blocking IO:

There are several ways to cause a process to sleep in Linux 2.4. All of them will use the same basic data structure, the wait queue (wait_queue_head_t). This queue maintains a linked list of processes that are waiting for an event.

A wait queue is declared and initialized as follows: wait_queue_head_t my_queue; /* declaration */ init_waitqueue_head(&my_queue) /* initialization */

/* 2.4 kernel requires you to intialize the wait queue, although some earlier versions of the kernel did not */

The process can be made to sleep by calling any of the following: sleep_on(wait_queue_head_t * queue);

/* Puts the process to sleep on this queue. */

/* This routine puts the process into non-interruptible sleep */

/* this a dangerous sleep since the process may end up sleeping forever */ interruptible_sleep_on(wait_queue_head_t * queue)

/* same as sleep_on with the exception that the process can be awoken by a signal */ sleep_on_timeout(wait_queue_head_t * queue,long timeout)

/* same as sleep_on except that the process will be awakened when a timeout happens. The timeout parameter is measured in jiffies */ interruptible_sleep_on_timeout(wait_queue_head_t * queue,long timeout)

/* same as interruptible_sleep_on except that the process will be awakened when a timeout happens. The timeout parameter is measured in jiffies */

void wait_event(wait_queue_head_t * queue,int condition)

int wait_event_interruptible(wait_queue_head_t * queue, int condition)

/* sleep until the condition evaluates to true that is non-zero value */

/* preferred way to sleep */

If a driver puts a process to sleep there is usually some other part of the driver that awakens it, typically it is the interrupt service routine.

One more important point is that if a process is in interruptible sleep it might wake up even on a signal even if the event it was waiting on, has not occurred. The driver must in this case put a process in sleep in a loop checking for the event as a condition in the loop. The kernel routines that are available to wake up a process are as follows: wake_up(wait_queue_head_t * queue)

/* Wake proccess in the queue */ wake_up_interruptible(wait_queue_head_t * queue)

/* wake process in the queue that are sleeping on interruptible sleep in the queue rest of the procccess are left undisturbed */

wake_up_sync(wait_queue_head_t_ * queue) wake_up_interruptible_sync(wait_queue_head_t_ * queue)

/* The normal wake up calls can cause an immediate reschedule of the processor */

/* these calls will only cause the process to go into runnable state without rescheduling the CPU */

Non Blocking IO:

If O_NONBLOCK flag is set then driver does not block even if data is not available for the call to complete. The normal semantics for a non-blocking IO is to return -EAGAIN which really tells the invoking application to try again. Usually devices that are using non-blocking access to devices will use the poll system call to find out if the device is ready with the data. This is also very useful for an application that is accessing multiple devices without blocking.

Polling methods: Linux provides the applications 'poll' and 'select' system calls to check if the device is ready without blocking. (There are two system calls offering the same functionality for historical reasons. These calls were implemented in UNIX at nearly same time by two different distributions: BSD Unix (select) System 5(poll))

Both the calls have the following prototype: unsigned int (*poll)(struct file * ,poll_table *);

The poll method returns a bit mask describing what operations can be performed on the device without blocking.

Asynchronous Notification:

Linux provides a mechanism by which a drive can asynchronously notify the application if data arrives. Basically a driver can signal a process when the data arrives. User processes have to execute two steps to enable asynchronous notification from a device.

1. The process invokes the F_SETOWN command using the fcntl system call, the process ID of the process is saved in filp->f_owner. This is the step needed basically for the kernel to route the signal to the correct process.

2. The asynchronous notification is then enabled by setting the FASYNC flag in the device by means of F_SETFEL fcntl command.

After these two steps have been successfully executed the user process can request the delivery of a SIGIO signal whenever data arrives.

Interrupt Handling in LINUX 2.4

The Linux kernel has a single entry point for all the interrupts. The number of interrupt lines is platform dependent. The earlier X86 processors had just 16 interrupt lines. But

now this is no longer true. The current processors have much more than that. Moreover new hardware comes with programmable interrupt controllers that can be programmed among other things to distribute interrupts in an intelligent and a programmable way to different processors for a multi-processors system. Fortunately the device driver writer does not have to bother too much about the underlying hardware, since the Linux kernel nicely abstracts it. For the Intel x86 architecture the Linux kernel still uses only 16 lines. The Linux kernel handles all the interrupts in the same manner. On the receipt of an interrupt the kernel first acknowledges the interrupt. Then it looks for registered handlers for that interrupt. If a handler is registered, it is invoked. The device driver has to register a handler for the interrupts caused by the device.

The following API is used to register an interrupt handler.

<linux/sched.h>

int request_irq(unsigned int irq, void ( * interruptHandler ) (int, void *,struct pt_regs *), unisgned long flags, const char * dev_name, void * dev_id);

/* irq -> The interrupt number being requested */

/* interruptHandler -> function pointer to the interrupt handler */

/* flags -> bitwise orable flags one of

SA_INTERRUPT implies 'fast handler' which basically means that the interrupt handler finishes its job quickly and can be run in the interrupt context with interrupts disabled. SA_SHIRQ implies that the interrupt is shared

SA_SAMPLE_RANDOM implies that the interrupt can be used to increase the entropy of the system */

/* dev_name ->A pointer to a string which will appear in /proc/interrupts to signify the owner of the interrupt */

/* dev_id-> A unique identifier signifying which device is interrupting. Is mostly used when the interrupt line is shared. Otherwise kept NULL*/

/* the interrupt can be freeed implying that the handler associated with it can be removed

*/void free_irq(unsigned int irq,void * dev_id);

/* by calling the following function. Here the meaning of the parameters is the same as in request_irq

*/Now the question that arises: how do we know which interrupt line our device is going to use. Some device use predefined fixed interrupt lines. So they can be used. Some

devices have jumper settings on them that let you decide which interrupt line the device will use. There are devices (like device complying to the PCI standard) that can on request tell which interrupt line they are going to use. But there are devices for which we cannot tell before hand which interrupt number they are going to use. For such device we need the driver to probe the IRQ number. Basically what is done is the device is asked to interrupt and then we look at all the free interrupt lines to figure out which line got interrupted. This is not a clean method and ideally a device should itself announce which interrupt it wants to use. (Like PCI).

The kernel provides helper functions for probing of interrupts( <linux/interrupt.h> probe_irq_on , probe_irq_off) or the drive can do manual probing for interrupts.

Top Half And Bottom Half Processing:

One problem with interrupt processing is that some interrupt service routines are rather long and take a long time to process. These can then cause interrupts to be disabled for a long time degrading system responsiveness and performance. The method used in Linux (and in many other systems) to solve this problem is to split up the interrupt handler into two parts : The “top half” and the “bottom half”. The top half is what is actually invoked at the interrupt context. It will just do the minimum required processing and then wake up the bottom half. The top half is kept very short and fast. The bottom half then does the time consuming processing at a safer time.

Earlier Linux had a predefined fixed number of bottom halves (32 of them) for use by the driver. But now the (Kernel 2.3 and later) the kernel uses “tasklets” to do the bottom half processing. Tasklet is a special function that may be scheduled to run in interrupt context, at a system determined safe time. A tasklet may be scheduled to run multiple times, but it only runs once. An interesting consequence of this is that a top half may be executed several times before a bottom half gets a chance to execute. Now since only a single tasklet will be run, the tasklet should be able to handle such a situation. The top half should keep a count of the number of interrupts that have happened. The tasklet can use this count to figure out what to do.

/* Takelets are declared using the following macro */ DECLARE_TASKLET(taskLetName,Function,Data);

/* taskLetName -> Name of the tasklet */

/* the function to be run as a tasklet. The function has the following prototype */

/* void Function(usigned long ) */

/* Data is the argument to be passed to the function */

/* the tasklet can be scheduled using this function */ tasklet_schedule(&takletName)

Interprocess Communication in Linux:

Again there is considerable similarity with Unix. For example, in Linux, signals may be utilized for communication between parent and child processes. Processes may synchronize using wait instruction. Processes may communicate using pipe mechanism. Processes may use shared memory mechanism for communication.

Probably need some more points on this topic on IPC and the different mechanisms available. I found a good url “http://cne.gmu.edu/modules/ipc/map.html”.

(Show these using animation)

Let us examine how the communication is done in the networked environment. The networking features in Linux are implemented in three layers:

1. Socket interface

2. Protocol drivers

3. Network drivers.

Typically a user applications’ first I/F is the socket. The socket definition is similar to BSD 4.3 Unix which provides a general purpose interconnection framework. The protocol layer supports what is often referred to as protocol stack. The data may come from either an application or from a network driver. The protocol layer manages routing, error reporting, reliable retransmission of data For networking the most important support is the IP suite which guides in routing of packets between hosts. On top of the routing are built higher layers like UDP or TCP. The routing is actually done by IP driver. The IP driver also helps in disassembly / assembly of the packets. The routing gets done in two ways:

1. By using recent cached routing decisions

2. By using a table which acts as a persistent forwarding base

Generally the packets are stored in a buffer and have a tag to identify the protocol that need to be used. After the selection of the appropriate protocol the IP driver then hands it over to the network device driver to manage the packet movement.

As for security, the firewall management maintains several chains – with each chain having its own set of rules of filtering the packets.

Real Time Linux:

Large number of projects both open source and commercial have been dedicated to get real time functionality from the Linux kernel. Some of the projects are listed below Commercial distributions:

FSMLabs: RTLinuxPro Lineo Solutions: uLinux LynuxWorks: BlueCat RT

MontaVista Software: Real-Time Solutions for Linux Concurrent: RedHawk

REDSonic: REDICE-Linux Open source distributions: ADEOS –

ART Linux

KURT -- The KU Real-Time Linux Linux/RK QLinuxRealTimeLinux.org

RED-Linux RTAI

RTLinux

Comments

Popular posts from this blog

Input Output (IO) Management:HW/SW Interface and Management of Buffers.

Introduction to Operating Systems:Early History: The 1940s and 1950s

Input Output (IO) Management:IO Organization.