El driver actúa como puente entre la capa de usuario y el kernel. No es posible intercambiar datos directamente entre ambos espacios; por ello, este artículo explica cómo un driver permite que la programación en espacio de usuario interactúe con el kernel para transferir información.
Conceptos previos
- El nodo de dispositivo es el punto de interacción entre la capa de aplicación (usuario) y el kernel.
- Se usan estructuras predefinidas, por ejemplo, la función del sistema
opencorresponde al punteroopende la estructurastruct file_operations. - La estructura de operaciones de archivo se rellana con los punteros que se necesitan; los demás pueden omitirse sin provocar fallos (retornan 0).
Este artículo se centra en cómo escribir datos desde la capa de usuario hacia el kernel y cómo devolver datos desde el kernel al usuario mediante read.
Preparación de la plantilla del driver
Copiamos el driver anterior testFileOpts y lo renombramos:
cd ~/work/drive/
ls
cp -arf 003_testFileOpts 004_testReadWrite
cd 004_testReadWrite/
make clean
ls
mv testFileOpts.c testReadWrite.c
vi Makefile
ls
Modificamos el Makefile para cambiar el nombre del módulo (obj-m). Luego editamos el archivo testReadWrite.c para registrar un dispositivo misceláneo:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
int my_open(struct inode *pInode, struct file *pFile)
{
printk("my_open ejecutado\n");
return 0;
}
int my_release(struct inode *pInode, struct file *pFile)
{
printk("my_release ejecutado\n");
return 0;
}
ssize_t my_read(struct file *pFile, char __user *pUser, size_t size, loff_t *pLofft)
{
printk("my_read ejecutado\n");
return 0;
}
ssize_t my_write(struct file *pFile, const char __user *pUser, size_t size, loff_t *pLofft)
{
printk("my_write ejecutado\n");
return 0;
}
struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
struct miscdevice my_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "mi_dispositivo_rw",
.fops = &my_fops,
};
static int __init mi_init(void)
{
int res;
printk("Inicio del driver de prueba\n");
res = misc_register(&my_misc);
if (res < 0) {
printk("Error al registrar dispositivo\n");
return -1;
}
return 0;
}
static void __exit mi_exit(void)
{
misc_deregister(&my_misc);
printk("Driver eliminado\n");
}
MODULE_LICENSE("GPL");
module_init(mi_init);
module_exit(mi_exit);
Funciones del kernel para transferencia de datos
El kernel y el espacio de usuario no pueden intercambiar datos directamente; se usan copy_to_user y copy_from_user (archivo de cabecera <linux/uaccess.h>).
copy_from_user(void *to, const void __user *from, unsigned long n): copia datos desde el espacio de usuario al kernel.copy_to_user(void __user *to, const void *from, unsigned long n): copia datos desde el kernel al espacio de usuario.
Ejemplo completo con transferencia de datos
Añadimos un búfer interno y las llamadas a las funciones de copia.
1. Incluir cabecera y definir buffer estático
#include <linux/uaccess.h>
static char internal_buf[256] = {0};
2. Inicializar el buffer en open
int my_open(struct inode *pInode, struct file *pFile)
{
printk("my_open: inicializando buffer\n");
memcpy(internal_buf, "buffer inicializado", sizeof("buffer inicializado"));
return 0;
}
3. Implementar read usando copy_to_user
ssize_t my_read(struct file *pFile, char __user *pUser, size_t size, loff_t *pLofft)
{
size_t len = strlen(internal_buf);
printk("my_read: copiando al usuario %zu bytes\n", len);
if (copy_to_user(pUser, internal_buf, len)) {
printk("Error en copy_to_user\n");
return -EFAULT;
}
return len;
}
4. Implementar write usando copy_from_user
ssize_t my_write(struct file *pFile, const char __user *pUser, size_t size, loff_t *pLofft)
{
printk("my_write: recibiendo %zu bytes desde usuario\n", size);
if (copy_from_user(internal_buf, pUser, size)) {
printk("Error en copy_from_user\n");
return -EFAULT;
}
return size;
}
5. Programa de prueba en espacio de usuario
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(void)
{
int fd = -1;
char buf[64] = {0};
int ret;
fd = open("/dev/mi_dispositivo_rw", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
printf("Dispositivo abierto\n");
// Leer
ret = read(fd, buf, sizeof(buf)-1);
if (ret < 0) {
perror("read");
close(fd);
return 1;
}
printf("Leido: %s\n", buf);
// Escribir nuevo contenido
strcpy(buf, "Saludos desde el usuario");
ret = write(fd, buf, strlen(buf)+1);
if (ret < 0) {
perror("write");
close(fd);
return 1;
}
printf("Escrito: %s\n", buf);
// Volver a leer
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf)-1);
if (ret < 0) {
perror("read");
close(fd);
return 1;
}
printf("Despues de escribir: %s\n", buf);
close(fd);
return 0;
}
6. Compilar el driver y cargarlo
make
sudo insmod testReadWrite.ko
7. Compilar el programa de prueba y ejecutar
gcc prueba.c -o prueba
sudo ./prueba
La salida esperada muestra que la lectura inicial devuelve el texto de inicialización, luego se escribe un nuevo mensaje y la segunda lectura lo confirma.
Problemas comunes
Lectura no coincide con lo esperado
Posibles causas: no se copió la longitud correcta (usar strlen o el tamaño exacto del buffer), o el buffer no se inicializó adecuadamente. Verificar que copy_to_user reciba el tamaño apropiado.