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