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