Saturday 3 December 2016

Character Device Driver


Device files
       In Linux kernel, most of the devices are presented to the user space applications through two different abstractions
       Character device
       Block device
       Internally the kernel identifies each device by a triplet  of information
       Type (character or block)
       Major number (typically the category of devices)
       Minor number (typically the identifier of the device)

Types of devices
       Block devices
      A device composed of fixed-sized blocks, that can be read and write to store data.
      Used for hard disks, SD cards etc.
       Character devices
      An infinite stream of bytes, with no beginning, no end, no size. For e.g. serial port.
      Used for serial ports, terminals etc.
      Most of the devices that are not block devices are represented by linux kernel as character device.

Devices: Everything is a file
       A very important Unix design decision was to represent most of the “system objects” as “files”
       It allows applications to manipulate all “system objects” with the normal file API (open, read, write, close, etc.)
       So, devices had to be represented as “files” to the applications
       This is done through a special artefact called a device file
       It a special type of file, that associates a file name visible to user space applications to the triplet (type, major, minor) that the kernel understands All device files are by convention stored in the /dev directory
       Device files examples
$ ls -l /dev/ttyS0 /dev/tty1 /dev/sda1
brw-rw---- 1 root disk    8,  1 2012-02-13 18:49 /dev/sda1
crw------- 1 root root    4,  1 2012-02-27 15:58 /dev/tty1
crw-rw---- 1 root dialout 4, 64 2012-02-13 18:49 /dev/ttyS0
Example C code that uses the usual file API to write data to a serial port
int fd;
fd = open(“/dev/ttyS0”, O_RDWR);
write(fd, “Hello”, 5);
close(fd);

Creating device files
       On a basic Linux system, the device files have to be created
manually using the mknod command
       mknod /dev/<device> [c|b] major minor
       Needs root privileges
        Coherency between device files and devices handled by the kernel is left to the system developer
       On more elaborate Linux systems, mechanisms can be added to
create/remove them automatically when devices appear and
disappear
        devtmpfs virtual filesystem, since kernel 2.6.32
        udev daemon, solution used by desktop and server Linux systems
        mdev program, a lighter solution than udev

Anatomy of device driver
       A device driver has three sides
      One side talks to the rest of the kernel
      One talks to the hardware and
      One talks to the user


Kernel interface to the device driver
       In order to talk to the kernel, the driver registers with subsystems to respond to events. Such an event might be the opening of a file, closing a file, a page fault, the plugging in of a new USB device, etc.


Character drivers
       User-space needs
      The name of a device file in /dev to interact with the device driver through regular file operations (open, read,  write, close...)
       The kernel needs
      To know which driver is in charge of device files with a given major / minor number pair
For a given driver, to have handlers (“file operations”) to execute when user- space opens, reads, writes or closes the device file.


Implementing a character driver
       Four major steps
       Implement operations corresponding to the system calls an application can apply to a file: file operations.
       Define a “file_operations” structure containing function pointers to system call functions in your driver.
       Reserve a set of major and minors for your driver
       Tell the kernel to associate the reserved major and minor to your file operations
       This is a very common design scheme in the Linux kernel
       A common kernel infrastructure defines a set of operations to be implemented by a driver and functions to register your driver
       Your driver only needs to implement this set of well-defined operations

File operations
       Before registering character devices, you have to define file_operations (called fops) for the device files.
       The file_operations structure is generic to all files handled by the Linux kernel. It contains many operations that aren't needed for character drivers.
       Here are the most important operations for a character driver. All of them are optional. (include/linux/fs.h)
struct file_operations {
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
[...]
};
       open: for opening the device(allocating resources)
       release: for closing the device (releasing resources)
       write: for writing data to the device
       read : for reading data from the device
       ioctl: for query the device statistics and passing configuration parameters to device
       mmap: for potentially faster but more complex direct access to the device

Open() and release()
       int open(struct inode *i, struct file *f)
      Called when user-space opens the device file.
      inode is a structure that uniquely represent a file in the system (be it a regular file, a directory, a symbolic link, a character or block device)
      file is a structure created every time a file is opened. Several file structures can point to the same inode structure.
       Contains information like the current position, the opening mode, etc.
       Has a void *private_data pointer that one can freely use.
       A pointer to the file structure is passed to all other operations
      The open method is provided by the driver to do any initialization in preparation to later operations such as allocating memory resources.
       int release(struct inode *i, struct file *f)
      Called when user-space closes the file.
      The role of release is reverse of open(). It performs all the operation to undo the tasks done in open() such as de-allocating the memory resources allocated at time of open().

Read()
       ssize_t read (struct file *file, __user char *buf, size_t sz, loff_t *off)
      Called when user-space uses the read() system call on the device.
      Must read data from the device, write at most sz bytes in the user-space buffer buf, and update the current position in the file off. “f ile “ is a pointer to the same file structure that was passed in the open() operation
      Must return the number of bytes read.
      On UNIX/Linux, read() operations typically block when there isn't enough data to read from the device

Write()
       ssize_t foo_write(struct file *file, __user const char *buf, size_t sz ,loff_t *off)
      Called when user-space uses the write() system call on the device
      The opposite of read, must read at most sz bytes from buf, write it to the device, update off and return the number of bytes written.

ioctl
       static long ioctl(struct file *file, unsigned int cmd,  unsigned long arg)
      Associated with the ioctl system call.
      Allows to extend drivers capabilities beyond read/write API.
      For example: changing the speed of a serial port, setting video output format, querying a device serial number.
      cmd is a number identifying the operation to perform
      arg is the optional argument passed as third argument of the ioctl() system call. Can be an integer, an address, etc.
      The semantic of cmd and arg is driver-specific.

Kernel representation of device numbers
       The kernel data type dev_t represent a malor/ minor number pair
      Also called a device number.
      Defined in <linux/kdev_t.h>
Linux 2.6: 32 bit size (major: 12 bits, minor: 20 bits)
      Macro to compose the device number:
MKDEV(int major, int minor);
      Macro to extract the minor and major numbers:
MAJOR(dev_t dev);
MINOR(dev_t dev);
Registering device numbers
       #include <linux/fs.h>
int register_chrdev_region(
                dev_t from,                        /* Starting device number */
                unsigned count,               /* Number of device numbers */
                const char *name);         /* Registered name */
Returns 0 if the allocation was successful.
       If you don't have fixed device numbers assigned to your driver
      Better not to choose arbitrary ones. There could be conflicts with other drivers.
      The kernel API offers an alloc_chrdev_region function to have the kernel allocate free ones for you. You can find the allocated major number in /proc/devices.

Information of registered devices
       Registered devices are visible in /proc/devices:
       Character devices:
        1 mem
        4 /dev/vc/0
        4 tty
        4 ttyS
        5 /dev/tty
        5 /dev/console
        5 /dev/ptmx
        6 lp
       Block devices:
        1 ramdisk
      259 blkext
        7 loop
        8 sd
        9 md
       11 sr
       65 sd    
       66 sd
Major  number Registered name

Character device registration
       The kernel represents character drivers with a cdev structure
       Declare this structure globally (within your module):
#include <linux/cdev.h>
static struct cdev char_cdev;
       In the init function, initialize the structure
       void cdev_init(struct cdev *cdev, struct file_operations *fops);
cdev_init(&char_cdev, &fops);
       Then, now that your structure is ready, add it to the system:
int cdev_add(
                struct cdev *p,  /* Character device structure */
                dev_t dev,          /* Starting device major / minor number */
                unsigned count);              /* Number of devices */
If (cdev_add(&char_cdev, dev_no, device_count))
                printk(“Char device registration failed\n”);
       After this function call, the kernel knows the association between
the major/minor numbers and the file operations. Your device is
ready to be used!.

Character device unregistration
       First delete your character device:
void cdev_del(struct cdev *p);
       Then, and only then, free the device number:
void unregister_chrdev_region(dev_t from, unsigned count);
       Example :
cdev_del(&char_cdev);
unregister_chrdev_region(char_dev, count);

Exchanging data with user space
       Kernel code isn't allowed to directly access user-space memory, using memcpy or direct pointer dereferencing
      Doing so does not work on some architectures
      If the address passed by the application was invalid, the application would segfault.
       To keep the kernel code portable and have proper error handling, your driver must use special  kernel functions to exchange data with user-space.
       A single value
      get_user(v, p);
The kernel variable v gets the value pointed by the user-space pointer p
      put_user(v, p);
The value pointed by the user-space pointer p is  set to the contents of the kernel variable v.
       A buffer
      unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
      unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
       The return value must be checked. Zero on success, non-zero on failure. If non-zero, the convention is to return -EFAULT.

Char driver example
       Read operation example
      Drivers/char/lp.c             
       Ioctl operation example
      drivers/char/lp.c
       Write operation example
      Drivers/char/lp.c

Linux error code
       The kernel convention for error management is
       Return 0 on success
       return 0;
       Return a negative error code on failure
       return -EFAULT;
       Error codes
       include/asm-generic/errno-base.h
       include/asm-generic/errno.h

General purpose kernel APIs
       Memory/string utilities
       In <linux/string.h>
      Memory-related: memset, memcpy, memmove, memscan, memcmp, memchr
      String-related: strcpy, strcat, strcmp, strchr, strrchr, strlen and variants
      Allocate and copy a string:           kstrdup, kstrndup
      Allocate and copy a memory area: kmemdup
       In <linux/kernel.h>
      String to int conversion: simple_strtoul, simple_strtol, simple_strtoull, simple_strtoll
       Other string functions: sprintf, sscanf
       Convenient linked-list facility in <linux/list.h>
      Used in thousands of places in the kernel
       Add a struct list_head member to the structure whose instances will be part of the linked list. It is usually named node when each instance needs to only be part of a single list.
       Define the list with the LIST_HEAD macro for a global list, or define a struct list_head element and initialize it with INIT_LIST_HEAD for lists embedded in a structure.
       Then use the list_*() API to manipulate the list
      Add elements: list_add(), list_add_tail()
      Remove, move or replace elements: list_del(), list_move(), list_move_tail(), list_replace()
      Test the list: list_empty()
      Iterate over the list: list_for_each_*() family of macros

Practical lab
       Writing a simple character driver implementing the basic calls open, release, read, write
       Write simple character driver implementing the 4 calls and ioctl call.
       Write simple charater driver with open, release, read, write, ioctl with parameter passing passing.
       Practicing with the character device driver API.
       Exchanging data between user space and kernel space.
       Using kernel standard error codes.
       Using the general purpose APIs


No comments:

Post a Comment