-
We'll develop a device driver,
scull
, which treats an area of memory as a device.
-
There are several types of
scull
devices:
-
scull[0-3]
-
This type has four members,
scull0
,
scull1
,
scull2
and
scull3
.
-
Each encapsulates a memory area that is
global
and
persistent
.
-
Global means that all opens of these devices share the same data.
-
Persistent means that data isn't lost across closes and reopens.
-
Command such as
cp
,
cat
and
shell I/O redirection
can be used to access these devices.
-
scullpipe[0-3]
-
Four devices that act like pipes between a reader and writer process.
-
Blocking
and
non-blocking
reads and writes are illustrated here.
-
scullsingle, scullpriv, sculluid and scullwuid
-
Devices similar to scull0 with certain limitations.
-
Major and Minor Numbers:
-
Char devices are accessed through names (or nodes) in the filesystem, usually in
/dev
.
-
Device files are special files and are identified with a "c" for character and a "b" for block in the first column of the output of
ls -l
:
-
crw-rw---- 1 root daemon 6, 0 May 5 1998 lp0
-
crw-rw---- 1 root daemon 6, 1 May 5 1998 lp1
-
brw-rw---- 1 root disk 3, 1 May 5 1998 hda1
-
The major and minor numbers are given by integers before the date.
-
The
major
number identifies
the driver
associated with the device.
-
The
minor
number is used ONLY by the device driver, and allow the driver to manage
more than one device
.
-
You must assign a new number to a new driver at driver (module) initialization time using the function defined in
<linux/fs.h>
:
int register_chrdev(unsigned int major,
const char *name, struct file_operations *fops);
-
A negative return value indicates an error, 0 or positive indicates success.
-
major
: the major number being requested (a number < 64 or 128).
-
name
: the name of the device (which appears in
/proc/devices
).
-
fops
: a pointer to a jump table used to invoke driver functions.
-
How does a program get access to the driver?
-
Through a device node in
/dev
or course.
-
To create a char device node with major 127 and minor 0, use:
mknod /dev/scull0 c 127 0
-
Minor numbers should be in the range of 0 to 255.
-
Some major numbers are statically assigned to the most common devices (see
Documentation/devices.txt
in the linux source tree).
-
You can choose a major number dynamically by setting the
major
argument to
0
in the call to
register_chrdev
-
The problem with this method is that you can't create the device nodes in advance, since the major number may change each time.
-
Solution is to read it from
/proc/devices
and create the nodes using a simple script:
#!/bin/sh
module="scull"
device="scull"
group="wheel"
mode="664"
/sbin/insmod -f $module $* || exit 1
#Remove old nodes.
rm -f /dev/${device}[0-3]
major=`cat /proc/devices | awk "\\$2==\"$module\" {print \\$1}"`
mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3
chgrp $group /dev/${device}?
chmod $mode /dev/${device}?
-
A sample from my /proc/devices looks like:
Character devices:
1 mem
2 pty
Block devices:
1 ramdisk
-
This script can be called at boot time from
/etc/rc.d/rc.local
, invoked manually or using
kerneld
.
-
You MUST release the major number when the module is unloaded in
cleanup_module
using:
int unregister_chrdev(unsigned int major,
const char *name);
-
Here, the kernel compares
name
with the registered name for the major number, and if they differ, return -EINVAL.
-
Failing to unregister the device when you unload the driver results in an unrecoverable (reboot) problem !
-
You should also remove the nodes created by the script given earlier, when you unload the device.
-
Otherwise, another device may be loaded using the major number !
-
When the kernel calls the driver, it tells the driver what device is being acted upon using a combined major/minor number pairing.
-
This number is saved in the
inode
field
i_rdev
, which every driver function receives a pointer to.
-
The data type is
dev_t
, declared in
<sys/types.h>
-
The kernel uses a different type internally called kdev_t in <linux/kdev_t.h>
-
Use the following in your headers for portability:
#if LINUX_VERSION_CODE < VERSION_CODE(1, 3, 28)
# define kdev_t dev_t
# define kdev_t_to_nr(dev) (dev)
# define to_kdev_t(dev) (dev)
#endif
-
The following macros can be used to extract/convert the numbers:
MAJOR(kdev_t dev);
MINOR(kdev_t dev);
MKDEV(int major, int minor);
kdev_t_to_nr(kdev_t dev); /* Convert to dev_t */
to_kdev_t(int dev); /* And back again */
-
File Operations
-
A device is identified internal to the kernel through a
file
structure.
-
The
file
structure contains a
file_operations
structure (table of function pointers defined in <
linux/fs.h
>) to allow the kernel to call the driver's functions.
-
The
fops
pointer, passed as an arg to
register_chrdev
, is a pointer to the table.
-
It contains function pointers to
open
,
read
, etc. and NULL pointers for operations that are not supported.
-
The functions in
kernel 2.2.12
struct file_operations:
loff_t (*llseek) (struct file *, loff_t, int);
-
A method used to change the current read/write positon in a file.
ssize_t (*read) (struct file *, char *, size_t,
loff_t *);
-
A method used to retrieve data from the device.
-
-EINVAL is returned if the function is NULL.
-
A non-negative return value indicates the # of bytes read.
ssize_t (*write) (struct file *, const char *,
size_t, loff_t *);
-
Sends data to the device, otherwise the same as read.
int (*readdir) (struct file *, void *, filldir_t);
-
NULL for device nodes -- used only for directories.
-
struct
file_operations
(cont):
unsigned int (*poll) (struct file *,
struct poll_table_struct *);
-
Replaces
select
function in kernels <
2.1
.
-
It should perform 2 tasks:
-
1) It queues the process in any wait queue that may awaken it in the future.
-
2) Build a bitmask to describe the status of the device and return it to the caller (see <
linux/poll.h
> for bit values. )
int (*ioctl) (struct inode *, struct file *,
unsigned int, unsigned long);
-
A method that allows device-specific commands to be issued (e.g., formatting a track of a floppy, which is neither reading nor writing).
-
If this function is NULL, the kernel returns -EINVAL for any request that isn't predefined.
-
struct
file_operations
(cont):
int (*mmap) (struct file *, struct vm_area_struct *);
-
Used to request a mapping of device memory to a process's memory.
int (*open) (struct inode *, struct file *);
-
The first operation performed on the device node.
-
If NULL, opening always succeeds.
int (*flush) (struct file *);
-
Not covered in the text -- a new method for kernels >=
2.2
int (*release) (struct inode *, struct file *);
-
Called when a node is closed.
int (*fsync) (struct file *, struct dentry *);
-
Flush the device.
-
struct
file_operations
(cont):
int (*fasync) (int, struct file *, int);
-
Used for asynchronous notification to notify the device of a change in its FASYNC flag.
int (*check_media_change) (kdev_t dev);
-
Used with block devices, e.g. floppies, by the kernel to determine if the floppy was changed.
int (*revalidate) (kdev_t dev);
-
Used with block devices and is related to buffer cache management.
int (*lock) (struct file *, int, struct file_lock *);
-
The following methods are defined for scull:
struct file_operations scull_fops = {
scull_lseek,
scull_read,
scull_write,
NULL, /* scull_readdir */
NULL, /* scull_poll */
scull_ioctl,
NULL, /* scull_mmap */
scull_open,
NULL, /* scull_flush */
scull_release,
/* Nothing more, fill with NULLs */
};
-
The
file
structure:
-
struct file appears in <
linux/fs.h
>
-
It represents an "open file" which is created by the kernel on
open
() and is passed to any function that operates on file.
-
Note that this is different from a "disk file" which is represented by an inode.
-
Some of the more important fields with
struct file
{ ...
struct dentry *f_dentry;
-
Provides a means of getting at the i_rdev field in inode using:
minor = MINOR(file->f_dentry->d_inode->i_rdev);
-
See the
f_inode
field below.
struct file_operations *f_op;
-
A pointer to the operations as discussed above.
-
The kernel assigns the pointer at open time and reads it when it needs to dispatch any operations.
mode_t f_mode;
-
The mode bits FMODE_READ and FMODE_WRITE indicate whether or not the device can be read or written to.
-
They may be consulted in the
ioctl
() function (since calls to the
read
and
write
functions are checked by the kernel.)
-
The
file
struct (cont):
loff_t f_pos;
-
The current reading or writing position.
-
loff_t
is a 64-bit value (long long in gcc).
-
The lseek, read and write functions need to keep this up-to-date.
unsigned int ..., f_flags;
-
These are the file flags, such as O_RDONLY, O_NONBLOCK and O_SYNC.
-
A driver needs to check the flag for nonblocking operation.
-
The read/write permission should be checked through f_mode.
...
struct inode *f_inode;
-
This is missing in my structure now and is no longer passed as the first arg to many of the methods (except
ioctl
,
open
and
release
).
-
Gives you access to the
inode
structure.
-
The
file
struct (cont):
...
void *private_data;
-
The open system call sets this pointer to NULL before calling the open method of the driver.
-
The driver is free to define it, e.g. point it to allocated data for use in preserving data across system calls.
-
The allocated data must be freed in the release method.
-
The Open Method:
-
Checks for device-specific error, e.g. device not ready.
-
Initializes the device (if opened for the first time).
-
Identifies the minor number and updates the
f_op
pointer in
struct file
, if necessary.
-
Allocates and fills any data structure to be put in
filp->private_data
.
-
Increments the usage count for the device.
-
The minor number of the device is required for most of these:
unsigned int minor = MINOR(inode->i_rdev);
-
Note that different minor numbers can be used to access different physical devices, OR to open the same device in a different way.
-
For example:
crw------- 1 root tty 4, 64 May 5 1998 /dev/ttyS0
crw------- 1 root tty 4, 65 May 5 1998 /dev/ttyS1
-
refer to
different physical devices
while
crw------- 1 root root 5, 64 May 5 1998 /dev/cua0
crw------- 1 root root 5, 65 May 5 1998 /dev/cua1
-
are the same physical devices but behavior differently.
-
The cuax devices are "callout" devices (not ttys), and don't get all the software support needed for terminals.
-
Since device names are not used by the driver (only the number), aliases can be created (symbolic links in this case) for the same device:
-
The scull driver uses the minor number like this:
-
The most significant nibble (4 bits) identifies the type of the devices.
-
The least significant nibble distinguishes between devices of the same type.
-
For example,
scull0
is different from
scullpipe0
in the top nibble, while
scull0
and
scull1
differ in the bottom.
-
Each device defines its own
file_operations
structure, which is substituted into filp->f_op in the
open
method.
#define TYPE(dev) (MINOR(dev) >> 4) /* High nibble */
#define NUM(dev) (MINOR(dev) & 0xF) /* Low nibble */
struct file_operations *scull_fop_array[] = {
&scull_fops, /* Type 0 */
&scull_priv_fops, /* Type 1 */
&scull_pipe_fops, /* Type 2 */
&scull_sngl_fops, /* Type 3 */
&scull_user_fops, /* Type 4 */
&scull_wusr_fops }; /* Type 5 */
#define SCULL_MAX_TYPE 5
-
/* ========================== */
int scull_open (struct inode *inode,
struct file *filp)
{
int type = TYPE(inode->i_rdev);
int num = NUM(inode->i_rdev);
Scull_Dev *dev;
-
/* For device types 1 through 5 */
if (type)
{
if (type > SCULL_MAX_TYPE)
return -ENODEV;
-
/* Set filp->f_op to point to the appropriate list of methods and
-
call the open method. */
filp->f_op = scull_fop_array[type];
return filp->f_op->open(inode, filp);
-
}
-
-
/* Type 0 devices make it this far. Check num against global var for
-
number of type 0 devices. */
if ( num >= scull_nr_devs)
return -ENODEV;
/* Scull_Dev is a data struct used to hold a region of memory. */
dev = &scull_devices[num];
-
/* Trim to 0 the length of the device if open was write-only. */
if ((filp->f_flags & O_ACCMODE) == O_WRONLY )
scull_trim(dev);
filp->private_data = dev;
-
/* Increment the usage count. */
MOD_INC_USE_COUNT;
return 0;
}
-
There isn't alot of initialization that is needed for
scull0-3
devices, such as "initializing the device on first open".
-
Note that opening the device for writing truncates the data, analogous to file opens.
-
This can be a problem if, for example, the memory is in use by another process that is sleeping in the read or write method.
-
Scull deals with this by not releasing the memory if it is in use (as shown later).
-
Release is responsible for:
-
Decrementing the usage count.
-
Deallocating memory allocated in open pointed to by filp->private_data.
-
Shut down the device on last close.
void scull_release( struct inode *inode,
struct file *filp)
{
MOD_DEC_USE_COUNT;
}