Project 6

1)      compile driver.c using the Makefile

2)      Create ‘rw.c’ (tests the driver)

3)      Compile test program

a.       gcc rw.c –o test_driver

4)      start with a fresh copy of uml

a.       cd /class/cs153/cs153_04fal/<login_name>

b.      rm –rf linux-2.4.23

c.       rm –rf /tmp/<login>/*

d.      tar-xvf linux-2.4.23-uml.tar

e.       cd linux-2.4.23

5)      Start uml, and login

6)      Mount homedir

a.       Mount none –o /home –t hostfs /home

7)      Copy your module ‘driver.o’ and test program executable ‘test_driver’ to uml

a.       cp /home/csmajs/<login>/cs153/proj6/driver.o .

b.      cp /home/csmajs/<login>/cs153/proj6/test_driver .

8)      Install your module

a.       insmod driver.o

b.      Note the major number assigned ‘m’ for step 9

9)      Inform kernel of your device

a.       mknod /dev/chardev c ‘m’ 1

10)  Test for existence of your driver

a.       cat /dev/chardev

b.      ls /dev/chardev

11)  run your test program

a.       ./test_driver

b.      “try@”

c.       “art@” should be the outputs

 

We have used put_user(kernel_buf,  userbuf) to copy data from kernel space to user space (The buffers character data in (Kernel) space)

And copy_from_user(*kernel_buf, userbuf, size)

(The test_driver program is a user mode program and obtains data in user space only when copy_from_user is executed)

 

Chapter 10 Device Driver

FIFO character device driver

 

This is a Kernel Module (Recollect from Project 2)

 

Turnin fifo.c

       Makefile

       Project_patch.diff (if it’s less than 100KB)

       Testfile

 

How to APPROACH the problem

 

1) Include the header files

 

#include <linux/module.h>

 

#include <linux/kernel.h>

#include <linux/sched.h>

#include <linux/fs.h>         /* file modes and device registration */

#include <linux/poll.h>       /* for poll */

#include <linux/wrapper.h>    /* mem_map_reserve,mem_map_unreserve */

#include <linux/proc_fs.h>

#include <linux/sysctl.h>

#include <linux/init.h>

#include <asm/io.h>

2)

Declare Version info such as major and minor version no

#define DEV_FIFO        10

#define DEV_FIFO_ENTRY        1

#define FIFO_MAJOR            42

#define FIFO_INC        1024

 

3)

skeleton for the driver operations

static ssize_t fifo_read(struct file *file, char *buf, size_t count, loff_t *offset);

static ssize_t fifo_write(struct file *file, const char *buf, size_t count, loff_t *offset);

static int fifo_open(struct inode *inode, struct file *file);

static int fifo_release(struct inode *inode, struct file *file);

4)

populated structure

static struct file_operations fifo_fops = {

      NULL,       /* llseek */

      fifo_read,

      fifo_write,

      NULL,       /* readdir */

      NULL,

      NULL,

      NULL,

      fifo_open,

      NULL,       /* flush */

      fifo_release,

      NULL,       /* fsync */

      NULL,       /* fasync */

      NULL        /* lock */

};

5) FIFO read

static ssize_t fifo_read(struct file *file, char *buf, size_t count,  loff_t *offset)

{

 

     

      while (count > fifo_pool) {

           

            if (file->f_flags & O_NONBLOCK) {

                  if (fifo_pool > 0) {

                        file->f_pos += fifo_pool;

                        fifo_data_read += file->f_pos;

                        copy_to_user(buf, fifo_buffer, fifo_pool);

                        count = fifo_pool;

                        fifo_pool = 0;

                        return count;

                  }

            }

                 

      }

      copy_to_user(buf, fifo_buffer, count);s

           

 

      fifo_pool -= count;

      fifo_data_read += count;

      file->f_pos += count;

      return count;

}

 

6) FIFO Write

static ssize_t fifo_write(struct file *file, const char *buf, size_t count,

                     loff_t *offset)

{

      fifo_pool += count;

      fifo_data_written += count;

      file->f_pos += count;

      copy_from_user(fifo_buffer, buf, count);

           

     

      return count;

}

Suggested LINKS:

http://www.cs.swarthmore.edu/~newhall/cs45/proj5.html#details

http://www.tldp.org/LDP/lkmpg/2.4/html/index.html

http://www.tldp.org/LDP/khg/HyperNews/get/devices/devices.html

http://www.redhat.com/mirrors/LDP/LDP/khg/HyperNews/get/devices/basics.html

 

Ten: Device Drivers

Exercise Goal: You will focus on Linux device drivers, learning how all device drivers are organized. Then you will write your own device driver for a virtual device, a FIFO queue (pipe).

Introduction

Part 1, Section 6. introduced the device manager strategy for the Linux kernel. In summary, each physical peripheral device -- keyboard, display, mouse, disk, serial port, parallel port, network adapter, and so on -- has a software device driver specifically designed to control that device. The device driver software encapsulates these details of how to control the device and exports a canonical set of operations through a specific interface (see Figure 2.11). The kernel uses the canonical device interfaces (block and character interfaces) to export the device operations to user-space programs via the file system interface.

Each device driver may use a polling strategy or an interrupt strategy.

Figure 2.11 - Interface, Driver, and Device

Figure 2.11 shows a family of device drivers for block devices and another for character devices. In traditional UNIX systems, the interfaces for block devices differ substantially from those for character devices; this distinction is not as significant in Linux. I/O operations for block devices transfer multiple bytes to/from the primary memory cache on each hardware operation, whereas character devices transfer information byte-by-byte without using caching. As suggested by the figure, a disk is a block device and a serial port is a character device.

All UNIX systems are designed so that device drivers can be defined when the system is booted. At that time, the device drivers register their specific device interface implementation with the kernel. Linux differs from other versions of

UNIX in that it also allows device drivers to be implemented as loadable kernel modules (see Exercise 4). Thus the driver's implementation of the interface can be registered when the module is loaded, rather than just at boot time.

Driver Organization

A device driver is a collection of functions and data structures whose purpose is to implement a simple interface for managing a device. The kernel uses the interface to request the driver to manipulate its device for I/O operations. Alternatively, you can view a device driver as an abstract data type that creates a common function interface to every hardware device in the computer.

As mentioned in Part 1, Section 6, the kernel references a device driver by a major number and a minor number. The major number of a device normally identifies the class of devices that the driver can manage. The Linux community has agreed on a set of major numbers for device classes that are typically supported. For example, floppy disks have a major number of 2, IDE hard disks have a major number of 3, and parallel ports have a major number of 6. The file include/linux/major.h provides the full list of major numbers for the release of Linux on which you are working.

The minor number is an 8-bit number that references a specific device of a particular class (major number). Thus two floppy disks on a machine would have a major number of 2, with the first disk having a minor number of 0 and the second a minor number of 1.(1)

The kernel must be informed of the device's existence. When booted, the kernel creates a special file for each device in the system. A special file is an entry in /dev for the device that will be used to identify the device driver for that device. This can be done with the mknod command:

mknod /dev/<dev_name> <type> <major_number> <minor_number>

The <dev_name> parameter is the special file's name (you can view the list of special files in the /dev directory). The <type> parameter is c for a character device and b for a block device. The <major_number> and <minor_number> are the device's major number and minor numbers, respectively.

The interface to a device is intended to look the same as an interface to the file system. Recall from Part 1, Section 7 that each file system defines a fixed set of operations (functions) that can be applied to any of its files and directories. It does this by specifying a struct file_operations definition (see include/linux/fs.h). These same functions can be defined for any device driver, as follows.

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 fileJock *);
};

A device driver needs to define only functions that make sense for itself. For example, an input-only device probably does not have a write() function, and an output-only device might not have a read() function. The device driver designer decides which functions on the interface are required to operate the device, implements the desired functions, and then creates an instance of struct file_operations, with the appropriate entry points defined.

A conventional device driver for a device of type foo should also define an initialization function, foo_init(). This function should be called when the kernel is booted (loadable modules do not use the init() function, but init_module() can be used for initialization code). For example, the tty_init() function is for serial ports and the hd_init() function is for hard disks. Next, you must modify the kernel initialization code so that it calls the new foo_init() function. If the device is a character device, then you must modify the function chr_dev_init() in drivers/char/mem.c. Similarly, a block device is initialized in blk_dev_init(), which is stored in drivers/block/ll_rw_bl.c. Drivers for SCSI and network devices have their own initialization routines.

foo_init() allows the device driver to set up required data structures when the driver is installed (for example, when the machine is booted). Also, the initialization code should register the device driver interface -- struct file_operations -- with the kernel by using the register_chrdev() function for character devices or the register_blkdev() function for block devices. For example, if the imaginary foo device were a character device, then its driver would contain a function like the following.

foo_init()
{
  ...
  struct file_operations foo_fops = {
  NULL;       /* llseek-default*/
  foo_read;   /* read */
  foo_write;  /* write */
  NULL;       /* readdir */
  NULL;       /* select */
  foo_ioctl;  /* ioctl */
  NULL;       /* mmap */
  foo_open;   /* open */
  NULL;       /* release */
};
...
register_chrdev(FOO_MAJOR, "foo", &foo_fops);
 
/* Other initialization ... */
...
}

Once the driver has been initialized, the kernel can route a system call such as

open(/dev/foo, 0_RDONLY);

to the foo_open() function in the driver. (You should inspect sys_open() in fs/open.c to get an idea of how the kernel can determine the entry point to call.)

In an interrupt-style device driver, each entry point is a part of the driver that performs an I/O operation. Once the device has been started, the calling process is changed to the TASK_UNINTERRUPTIBLE or TASK_INTERRUPTIBLE state. Then it is suspended (by a call to one of the two forms of sleep_on()) while it awaits the interrupt from the device. Therefore, when you write an entry point function that uses an interrupt, you also must write a separate device handler function. When the IRQ occurs, your device handler should be called. How does the kernel know which device handler to call? You register the device handler with the kernel by associating it with a particular IRQ using the kernel function request_irq().

The interrupt may have bottom halves that it can mark for execution (or tasks that it can queue). To use a bottom half to finish any interrupt processing, you need to initialize the bottom half (see init_bh()) and then mark it for execution by using the mark_bh() kernel function.

Loadable Kernel Module Drivers

Loadable kernel modules (see Exercise 4) can be used to implement device drivers (see Pomerantz [1999] for a comprehensive discussion of modules). A driver implemented as a module can be loaded or unloaded at any time while the machine is executing (as opposed to being loaded only at boot time). If the device driver is implemented as a module, mknod must still have been used to create the special file in /dev. This differs from conventional drivers in that the driver can be registered when the module is installed, by using the /sbin/insmod command. The /sbin/rmmod command calls the cleanup_module(), which contains a kernel call to reverse the registration process. That is, it deletes the information that /sbin/insmod placed in the linkage tables of the driver entry point.

The driver module's init_module() function must be written to register the device driver entry points. This serves the same purpose as the foo_init() function in the previous subsection's example. A module may export one of two styles of interface: a /proc or a general /dev. The module example in Exercise 4 uses the /proc-style interface.

Here is a code fragment for the foo device driver for registering the driver.

/* Module device driver code fragment */
...
struct file_operations foo_fops = {
  NULL;        /* llseek-default*/
  foo_read;    /* read */
  foo_write;   /* write */
  NULL;        /* readdir */
  NULL;        /* select */
  foo_ioctl;   /* ioctl */
  NULL;        /* mmap */
  foo_open;    /* open */
  NULL;        /* release */
};
...
int init_module()
{
  int majorNumber;
  ...
  majorNumber = module_register_chrdev(0, "foo", &foo_fops);
  if(majorNumber < 0) {
  /* Registration failed */
  ...
  /* Other initialization */
  ...
return 0;
}

As you can see, the init_module() code performs the same function as the init() code in the conventional UNIX driver case, except that the code is called by /sbin/insmod rather than by the kernel initialization code.

The module unregisters the driver when /sbin/rmmod calls cleanup_module(), as follows.

void cleanup_module()
{
  int flag;
  ...
  flag = module_inregister_chrdev(majorNumber, "foo");
  if(flag < 0) {
    /* Unregistration failed */
    ...
  }
  /* Other cleanup */
  ...
}

Example: A Disk Driver

The drivers/block directory contains various device drivers for disks. This directory contains files named floppy.c, ide_floppy.c, and hd.c, all device drivers for disks. The relative sizes of these three files for Version 2.0.36 are f1oppy.c= 109,094 bytes, ide_floppy.c = 45,171 bytes, and hd.c= 29,009 bytes. The extra code for the two floppy disk drivers is for addressing various special cases that are unique to floppies, such as starting and stopping the drive motors. So even though you might think that the floppy disk driver should be simpler than the hard disk driver, it is not. Therefore this manual examines the code for the hard disk driver that is found in hd.c, given shortly. However, you are strongly encouraged to study the other device drivers on your own.

As noted previously, blk_dev_init() (in drivers/block/ll_rw_bl.c) makes a call to hd_init() (in drivers/block/hd.c), so the device driver for the hd device must incorporate such a function, as follows.

static struct file_operations hd_fops = {
  NULL, /* lseek - default */
  block_read, /* read - general block-dev read */
  block_write, /* write - general block-dev write */
  NULL, /* readdir - bad */
  NULL, /* select */
  hd_ioctl, /* ioctl */
  NULL, /* mmap */
  hd_open, /* open */
  hd_release, /* release */
  block_fsync /* fsync */
};
 
int hd_init(void)
{
  if (register_blkdev(MAJOR_NR,"hd",&hd_fops)) {
    printk("hd: unable to get major %d for harddisk\n", MAJOR_NR);
    return -1;
  }
  blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
  read_ahead[MAJOR_NR] = 8; /* 8 sector (4kB) read-ahead */
  hd_gendisk.next = gendisk_head;
  gendisk_head = &hd_gendisk;
  timer_table[HD_TIMER].fn = hd_times_out;
  return 0;
}

Notice in the hd.c file that the following routines are defined.

static int hd_ioctl(struct inode * inode, struct file * file, unsigned int cmd, unsigned long arg)
{
  ...
}
 
static int hd_open(struct inode * inode, struct file * filp)
{
  int target;
  target = DEVICE_NR(inode->i_rdev);
  if (target >=NR_HD)
    return -ENODEV;
  while (busy[target])
    sleep_on(&busy_wait);
  access_count[target]++;
  return 0;
}
 
static void hd_release(struct inode * inode, struct file * file)
{
  ...
}

The function hd_open() is called when a user-space program makes an open() system call on the kernel. This happens because hd_open() is registered with the kernel as the open function for the special file /dev/hdi (where i is a particular hard disk). hd_open() does the following:

Notice that the hd.c file does not contain functions for block_read(), block_write(), and block_fsync(). These functions are defined in fs/block_dev.c, a file in the file system code. Recall that the reads and writes (and the sync operations) must read and write data to/from buffers rather than performing I/O on each system call. The device driver does not have enough information to know how a file is assembled -- that information is kept in the file system. As a result, if blocks are going to be read and written automatically according to the amount of buffer space available, then the information needed to perform the I/O is in the file system rather than the device driver. Thus the block_read() and block_write() functions are part of the file system code, even though they ultimately will cause sector I/O in the device driver. Once the buffer management code determines which blocks to read or write -- the process is complex -- it calls the device driver ll_rw_block() function (in drivers/block/ll_rw_blk.c) to perform the operation.

Problem Statement

Part A

Design and implement a character device driver for a virtual FIFO device. Recall that a FIFO is equivalent to a pipe: One process can write into the FIFO, and another can read from it. The first character written to the FIFO is the first character returned by a read operation. Your driver should implement N FIFOs queues (N = 4) by implementing 2N minor device numbers. Even-numbered devices are write-only devices, and odd-numbered are read-only devices. Characters written to device i are readable from device i + 1.

Your driver should be a loadable device driver, that is, it should be implemented as a module. Use a major device number, K, assigned to you by your instructor. Use mknod to create a special file with the name /dev/fifoLK, where L is a number in the range [100, 255].

Reading from an empty FIFO device should return an EOF if the other end is not currently open. Otherwise, the process calling the read operation must be blocked. Write operations (and a close on a write device) must wake up a blocked reader, if one exists.

FIFO device drivers must cope with writes that are too large by using a variant of sleep() and wakeup(). A write operation might be too large for the remaining space or for the statically allocated space. Alternatively, you may implement a FIFO device of unbounded size that dynamically obtains kernel space.

Part B

Demonstrate that your device driver operates properly. Be sure that your test program exercises the boundary conditions for the driver.

Attacking the Problem

The driver in this exercise is easier to design than, for example, a disk driver, because it does not have to deal with buffers and because no actual hardware device is associated with the driver. To further simplify your driver, design it as a polling driver rather than an interrupt driver.

Design your virtual hardware -- the FIFO data structure -- and the interface to register with the kernel. Your module should have the following general form.

#include 
#include 
...
/* Driver entry points */
...
/* Registration information */
struct file_operations fifo_fops = {
  ...;   /* Iseek */
  ...;   /* read */
  ...;   /* write */
  ...;   /* readdir */
  ...;   /* select */
  ...;   /* iocti */
  ...;   /* mmap */
  ...;  /* open */
  ...;   /* release */
};
...
int init_module()
{
  ...
  ... = module_register_chrdev(0, "fifo", &fifo_fops);
  /* Other initialization */
  ...
  return 0;
}
 
void cleanup_module()
{
  ...
  flag = module_inregister_chrdev(..., "fifo");
  /* Other cleanup */
  ...
}

(1) As discussed in Beck, et al. [1998], major devices 4 and 5 have minor number assignments to accommodate virtual consoles and pseudoterminals.