-
We need to examine how and why
scull
performs memory allocation before looking at the
read
and
write
methods.
-
The
Scull_Dev
structure is defined first:
typedef struct Scull_Dev
{
void **data;
struct Scull_Dev *next; /* Pointer to next device struct. */
int quantum; /* Quantum size */
int qset; /* Array size */
unsigned long size;
unsigned int access_key; /* Use by sculluid and scullpriv. */
unsigned int usage; /* Lock on device during use. */
} Scull_Dev;
-
A linked list of these structures is created for each device as its memory requirements grow.
-
The fields
next
,
data
,
quantum
,
qset
and
size
are used to track the memory allocation.
-
An initial one byte allocation allocates
8K
of memory, a quantum set + one quanta.
-
scull_trim
is in charge of the deallocating this data structure.
int scull_trim(Scull_Dev *dev)
{
Scull_Dev *next, *dptr;
int qset = dev->qset;
int i;
if (dev->usage)
return -EBUSY; /* scull_open ignores this error. */
for (dptr = dev; dptr; dptr = next)
{
if (dptr->data)
{
for (i = 0; i < qset; i++)
if (dptr->data[i])
kfree(dptr->data[i]);
kfree(dptr->data);
dptr->data = NULL;
}
next = dptr->next; /* Goto next quantum set. */
if (dptr != dev)
kfree(dptr);
}
dev->size = 0;
dev->quantum = scull_quantum;
dev->qset = scull_qset;
dev->next = NULL;
return 0;
}
-
This routine is called in
scull_open
when the file is opened for writing.
-
Read and Write Methods:
-
Read and write methods transfer data between the user address space and the kernel address space.
-
You cannot use the libc routines, e.g.,
memcpy
, because the data buffer pointers operate in user (virtual) address space, and not in kernel space.
-
Cross copies of data between user and kernel space is performed by functions in
<asm/uaccess.h>
(for kernels >2.0) and in
<asm/segment.h>
otherwise.
-
Two of the older functions are called
memcpy_fromfs
and
memcpy_tofs
while the newer functions are:
unsigned long copy_from_user(unsigned long to,
unsigned long from, unsigned long len);
unsigned long copy_to_user(unsigned long to,
unsigned long from, unsigned long len);
-
Once again, the code that uses these functions must be
reentrant
since a page fault will put the calling process to sleep.
-
Calls to the
read
and
write
methods request a transfer of a specific number of bytes.
-
However, the driver is free to transfer less data, as we will see.
-
Both
read
and
write
return a negative value if an error occurs while a positive value tells the calling program how many bytes were actually transferred.
-
The return value from
read
is interpreted by the calling program as follows:
-
When
return value == count
, the transfer succeeded.
-
If
return value > 0 && return value < count
, only a partial transfer occurred.
-
The caller is free to retry (which occurs in the
fread
library function).
-
If
return value == 0
, end-of-file is reached.
-
If
return value < 0
, an error occurred.
-
The caller can use this value to look up the error in
<linux/errno.h>
-
The only condition not tested for is "
there is no data, but it may arrive later
".
-
In this case, the
read
system call should block.
-
This will be covered later.
-
The
read
method of scull returns a maximum of size
quantum
.
-
If more data is requested, the caller must iterate the call.
-
If the current read position is greater than the device size, read return 0.
-
This occurs if process A reads and process B opens for writing.
ssize_t scull_read (struct file *filp, char *buf,
size_t count)
{
Scull_Dev *dev = filp->private_data;
Scull_Dev *dptr;
int quantum = dev->quantum;
int qset = dev->qset;
int quantum_set_size = quantum * qset;
unsigned long f_pos = (unsigned long)(filp->f_pos);
int qset_index, qset_offset;
int quantum_index, quantum_offset;
/* Write open truncated data. */
if (f_pos > dev->size)
return 0;
/* If current position plus request # bytes is greater than the # bytes
available, reset count. */
if (f_pos + count > dev->size)
count = dev->size - f_pos;
/* Compute the quantum to read the data from. */
qset_index = f_pos/quantum_set_size;
qset_offset = f_pos%quantum_set_size;
quantum_index = qset_offset/quantum;
quantum_offset = qset_offset%quantum;
/* Simply follow the pointers in the list qset_index times. */
dptr = scull_follow(dev, qset_index);
/* Check for end-of-file in the above call, NULL quantum_set. */
if (!dptr->data)
return 0;
/* Check for end-of-file, NULL quantum. */
if (!dptr->data[quantum_index])
return 0;
/* If the amount of data requested is greater than what is available in the
rest of the quantum, read only up to the end of the quantum. */
if (count > quantum - quantum_offset)
count = quantum - quantum_offset;
/* The following call may sleep. */
dev->usage++;
copy_to_user(buf,
dptr->data[quantum_index]+quantum_offset,
count);
dev->usage--;
/* Update the pointer to the read position. Note that this satisfies
re-entrancy requirements. */
filp->f_pos += count;
return count;
}
-
The following semantics are implemented for the
write
method:
-
If
return value == count
, the transfer succeeded.
-
If
return value > 0 && return value < count
, only a partial transfer occurred.
-
The caller is free to retry, which is what
cp
will do for you.
-
If
return value == 0
, nothing was written.
-
This is
not
an error and the standard library should retry the write.
-
This is significant for blocking write, covered later.
-
If
return value < 0,
an error occurred.
-
The caller can use this value to look up the error in
<linux/errno.h>
-
As with
scull_read
,
scull_write
deals only with a quantum at a time.
-
Also note that like
scull_read
, I've left out the forth argument passed by the kernel to the
scull_write
method.
ssize_t scull_write (struct file *filp,
const char *buf, size_t count)
{
Scull_Dev *dev = filp->private_data;
Scull_Dev *dptr;
int quantum = dev->quantum;
int qset = dev->qset;
int quantum_set_size = quantum * qset;
unsigned long f_pos = (unsigned long)(filp->f_pos);
int qset_index, qset_offset;
int quantum_index, quantum_offset;
/* Compute the quantum to write the data to. */
qset_index = f_pos/quantum_set_size;
qset_offset = f_pos%quantum_set_size;
quantum_index = qset_offset/quantum;
quantum_offset = qset_offset%quantum;
/* Simply follow the pointers in the list qset_index times. */
dptr = scull_follow(dev, qset_index);
/* If the quantum set is NOT found, then allocate a quantum set and
initialize the space to 0. */
if (!dptr->data)
{
dptr->data =
kmalloc(qset * sizeof(char *), GFP_KERNEL);
if (!dptr->data)
return -ENOMEM;
memset(dptr->data, 0, qset * sizeof(char *));
}
/* If the quantum is NOT present, then allocate a quantum. */
if (!dptr->data[quantum_index])
{
dptr->data[quantum_index] =
kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[quantum_index])
return -ENOMEM;
}
/* Check that the write request is less than the quantum size. */
if (count > quantum - quantum_offset)
count = quantum - quantum_offset;
/* Once again, the write may sleep. */
dev->usage++;
copy_from_user(
dptr->data[quantum_index]+quantum_offset,buf,
count);
dev->usage--;
/* Update the memory size of the device. */
if (dev->size < f_pos + count)
dev->size = f_pos + count;
/* Update the pointer to the write position. */
filp->f_pos += count;
return count;
}
-
You can now compile and test the driver with these four methods.
-
The device acts like a data buffer whose length is limited only by the available RAM on the system.
-
Try using
cp
,
dd
and
input/output redirection
to test out the driver, e.g.,
-
You can also add
printk
statements in the appropriate places in the driver code to watch variables.
-
Or you can use the
strace
utility to monitor system calls issued by a program.