【Linux 驱动中的 I/O 模型】

Linux 驱动中的 I/O 模型

  • 一、等待队列(Wait Queue)
    • 1.1 原理与概念
    • 1.2 代码示例
    • 1.3 应用场景
  • 二、非阻塞访问(Non - blocking I/O)
    • 2.1 原理与概念
    • 2.2 代码示例
    • 2.3 应用场景
  • 三、I/O 多路复用(I/O Multiplexing)
    • 3.1 原理与概念
    • 3.2 代码示例
    • 3.3 应用场景
  • 四、信号驱动 I/O(Signal - driven I/O)
    • 4.1 原理与概念
    • 4.2 代码示例
    • 4.3 应用场景
  • 五、各 I/O 模型对比与选择

在 Linux 驱动开发中,I/O 模型的选择直接影响着设备与系统之间数据交互的效率和性能。不同的 I/O 模型适用于不同的应用场景,合理运用这些模型,能够让驱动程序更好地满足设备需求。本文将详细介绍 Linux 驱动中常见的 I/O 模型,包括等待队列、非阻塞访问、I/O 多路复用以及信号驱动 I/O,帮助开发者深入理解并灵活运用它们。

一、等待队列(Wait Queue)

1.1 原理与概念

等待队列是 Linux 内核中实现阻塞式 I/O 操作的基础机制,它允许进程在等待特定事件发生时进入睡眠状态,释放 CPU 资源。当事件发生后,内核会唤醒等待在队列上的进程,使其继续执行。等待队列由struct wait_queue_head结构体表示,进程通过struct wait_queue结构体与等待队列关联。

1.2 代码示例

以一个简单的字符设备驱动为例,展示等待队列的使用:

#include 
#include 
#include 
#include 
#include 
#include 

#define DEVICE_NAME "my_wait_queue_device"

// 定义等待队列头
static wait_queue_head_t my_wait_queue;
// 用于标识数据是否准备好的标志
static int data_ready = 0;
// 模拟数据缓冲区
static char buffer[100];

static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
    // 等待数据准备好
    wait_event_interruptible(my_wait_queue, data_ready);
    if (copy_to_user(buf, buffer, count)) {
        return -EFAULT;
    }
    data_ready = 0;
    return count;
}

static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
    if (copy_from_user(buffer, buf, count)) {
        return -EFAULT;
    }
    data_ready = 1;
    // 唤醒等待在队列上的进程
    wake_up_interruptible(&my_wait_queue);
    return count;
}

static const struct file_operations my_fops = {
   .owner = THIS_MODULE,
   .read = my_read,
   .write = my_write,
};

static int __init my_driver_init(void) {
    // 初始化等待队列头
    init_waitqueue_head(&my_wait_queue);
    if (register_chrdev(0, DEVICE_NAME, &my_fops) < 0) {
        return -1;
    }
    printk(KERN_INFO "My wait queue device driver initialized\n");
    return 0;
}

static void __exit my_driver_exit(void) {
    unregister_chrdev(0, DEVICE_NAME);
    printk(KERN_INFO "My wait queue device driver removed\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("cmy");
MODULE_DESCRIPTION("A simple driver using wait queue");

在上述代码中,my_read函数使用wait_event_interruptible等待data_ready标志变为真,若条件不满足则进程进入睡眠状态。my_write函数在数据写入后,设置data_ready为真,并调用wake_up_interruptible唤醒等待在队列上的进程。

1.3 应用场景

等待队列适用于设备数据不能立即准备好,需要等待特定条件满足的场景。例如,在读取传感器数据时,传感器可能需要一定时间进行数据采集,此时驱动程序可以使用等待队列让进程进入睡眠,直到传感器数据准备完毕后再唤醒进程进行数据读取。

二、非阻塞访问(Non - blocking I/O)

2.1 原理与概念

非阻塞访问允许进程在执行 I/O 操作时,即使数据未准备好,也不会被阻塞,而是立即返回。通过设置文件描述符的O_NONBLOCK标志,驱动程序可以实现非阻塞 I/O。在非阻塞模式下,当 I/O 操作无法立即完成时,驱动会返回特定的错误码(如-EAGAIN),告知用户空间数据尚未准备好。

2.2 代码示例

修改上述字符设备驱动,使其支持非阻塞访问:

static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
    if (filp->f_flags & O_NONBLOCK) {
        if (!data_ready) {
            return -EAGAIN;
        }
    } else {
        wait_event_interruptible(my_wait_queue, data_ready);
    }
    if (copy_to_user(buf, buffer, count)) {
        return -EFAULT;
    }
    data_ready = 0;
    return count;
}

static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
    if (filp->f_flags & O_NONBLOCK) {
        if (count > sizeof(buffer)) {
            return -EAGAIN;
        }
    }
    if (copy_from_user(buffer, buf, count)) {
        return -EFAULT;
    }
    data_ready = 1;
    wake_up_interruptible(&my_wait_queue);
    return count;
}

在my_read和my_write函数中,首先检查文件描述符是否设置了O_NONBLOCK标志。如果是,并且数据未准备好(读取时)或缓冲区空间不足(写入时),则立即返回-EAGAIN,而不是阻塞进程。

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_testNonBlockRead(JNIEnv *env, jobject thiz) {
    int fd;
    char buffer[BUFFER_SIZE];

    // 打开设备
    fd = open(DEVICE_PATH, O_RDWR | O_NONBLOCK);
    if (fd == -1) {
        LOGD("testNonBlockRead 无法打开设备");
        return 1;
    }

    // 读取数据
    if (read(fd, buffer, sizeof(buffer)) == -1) {
        LOGD("testNonBlockRead 读取失败");
        close(fd);
        return 1;
    }

    LOGD("testNonBlockRead 读取内容: %s\n", buffer);

    // 关闭设备
    close(fd);
    return 0;

}

应用层代码需要带上O_NONBLOCK标志位。

2.3 应用场景

非阻塞访问适用于对实时性要求较高,不希望在 I/O 操作上被阻塞的场景。例如,网络客户端程序需要不断尝试发送和接收数据,同时还要处理其他任务,此时使用非阻塞 I/O 可以避免在等待数据时被阻塞,提高程序的响应速度。

三、I/O 多路复用(I/O Multiplexing)

3.1 原理与概念

I/O 多路复用允许一个进程同时监控多个文件描述符,当其中任何一个文件描述符准备好进行 I/O 操作时,进程就会被唤醒并进行相应处理。常见的 I/O 多路复用机制有select、poll和epoll。在驱动层面,需要配合用户空间的 I/O 多路复用函数,通过poll方法实现对设备文件描述符的状态监控。

3.2 代码示例

在字符设备驱动中添加poll方法:


unsigned int my_poll(struct file *filp, poll_table *wait) {
    unsigned int mask = 0;
    poll_wait(filp, &my_wait_queue, wait);
    if (data_ready) {
        mask |= POLLIN | POLLRDNORM;
    }
    printk(KERN_INFO "my_poll\n");
    return mask;
}


static const struct file_operations my_fops = {
   .owner = THIS_MODULE,
   .read = my_read,
   .write = my_write,
   .poll = my_poll,
};


my_poll函数通过poll_wait将等待队列与文件描述符关联起来。然后根据数据准备情况和缓冲区空间,设置相应的事件掩码(如POLLIN表示数据可读,POLLOUT表示可以写入数据),返回给用户空间,以便用户空间的select、poll或epoll函数判断文件描述符的状态。

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_testSelect(JNIEnv *env, jobject thiz) {
    int fd, ret;
    fd_set readfds;
    char buffer[BUFFER_SIZE];
    struct timeval timeout;

    // 打开设备文件
    fd = open(DEVICE_PATH, O_RDWR);
    if (fd < 0) {
        perror("open failed");
        return -1;
    }

    while (1) {
        // 清空文件描述符集合
        FD_ZERO(&readfds);
        // 添加设备文件描述符
        FD_SET(fd, &readfds);

        // 设置超时时间(可选)
        timeout.tv_sec = 1;
        timeout.tv_usec = 0;

        // 调用select等待事件
        ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
        LOGD("testSelect ret = %d", ret);
        if (ret < 0) {
            LOGD("select failed");
            break;
        } else if (ret == 0) {
            LOGD("select timeout\n");
            continue;
        }

        // 检查是否是设备文件就绪
        if (FD_ISSET(fd, &readfds)) {
            // 读取数据
            ret = read(fd, buffer, sizeof(buffer));
            if (ret > 0) {
                LOGD("Read %d bytes: %s\n", ret, buffer);
                break;
            }
        }
    }

    close(fd);
    return 0;
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_testPoll(JNIEnv *env, jobject thiz) {
    int fd, ret;
    struct pollfd fds[1];
    char buffer[BUFFER_SIZE];

    // 打开设备文件
    fd = open(DEVICE_PATH, O_RDWR);
    if (fd < 0) {
        perror("open failed");
        return -1;
    }

    // 设置pollfd结构体
    fds[0].fd = fd;
    fds[0].events = POLLIN;  // 监视可读事件

    while (1) {
        // 调用poll等待事件
        ret = poll(fds, 1, 1000);  // 超时时间1秒
        if (ret < 0) {
            LOGD("poll failed");
            break;
        } else if (ret == 0) {
            LOGD("poll timeout\n");
            continue;
        }

        // 检查是否有事件发生
        if (fds[0].revents & POLLIN) {
            // 读取数据
            ret = read(fd, buffer, sizeof(buffer));
            if (ret > 0) {
                LOGD("Read %d bytes: %s\n", ret, buffer);
            }
        }
    }

    close(fd);
    return 0;

}

应用层读取方式。

3.3 应用场景

I/O 多路复用适用于需要同时处理多个 I/O 设备或文件描述符的场景。例如,网络服务器程序需要同时监听多个客户端连接,处理多个套接字的读写操作,使用 I/O 多路复用可以高效地管理这些文件描述符,避免为每个文件描述符创建单独的进程或线程,从而降低系统资源消耗。

四、信号驱动 I/O(Signal - driven I/O)

4.1 原理与概念

信号驱动 I/O 允许设备在 I/O 操作准备好时,向用户空间进程发送信号,通知进程进行相应处理。用户空间进程通过fcntl函数设置文件描述符为信号驱动模式,并使用sigaction函数注册信号处理函数。当设备准备好进行 I/O 操作时,内核会向进程发送SIGIO信号,进程在信号处理函数中执行 I/O 操作。

4.2 代码示例

在驱动中添加对信号驱动 I/O 的支持:

static struct fasync_struct *my_async_queue;

static void my_data_ready_notify(void) {
    if (my_async_queue) {
        kill_fasync(&my_async_queue, SIGIO, POLL_IN);
    }
}

static int my_fasync(int fd, struct file *filp, int on) {
    int ret;
    ret = fasync_helper(fd, filp, on, &my_async_queue);
    return ret;
}
// 在数据准备好的地方(如my_write函数中)调用my_data_ready_notify
static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {

    if (copy_from_user(buffer, buf, count)) {
        return -EFAULT;
    }
    data_ready = 1;
    printk(KERN_INFO "My wait queue my_write buffer = %s\n", buffer);
    my_data_ready_notify();
    wake_up_interruptible(&my_wait_queue);
    return count;
}
static const struct file_operations my_fops = {
   .owner = THIS_MODULE,
   .read = my_read,
   .write = my_write,
   .poll = my_poll,
   .fasync = my_fasync,
   .release = my_release,
};

my_fasync函数用于管理异步通知队列,my_data_ready_notify函数在数据准备好时向用户空间进程发送SIGIO信号。用户空间进程通过注册SIGIO信号处理函数,在接收到信号后执行相应的 I/O 操作。

void signal_handler(int signum) {
    char buffer[BUFFER_SIZE];
    int fd;

    fd = open(DEVICE_PATH, O_RDONLY);
    if (fd < 0) {
        perror("open failed");
        return;
    }

    int ret = read(fd, buffer, sizeof(buffer));
    if (ret > 0) {
        LOGD("signal_handler Read %d bytes: %s\n", ret, buffer);
    }

    close(fd);
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_testSignal(JNIEnv *env, jobject thiz) {

    int fd;

    // 打开设备文件
    fd = open(DEVICE_PATH, O_WRONLY);
    if (fd < 0) {
        LOGD("open failed");
        return 1;
    }

    // 设置信号处理函数
    signal(SIGIO, signal_handler);

    // 设置当前进程为文件owner
    fcntl(fd, F_SETOWN, getpid());

    // 启用异步通知
    int flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | O_ASYNC);

    LOGD("Waiting for data... (PID: %d)\n", getpid());

    sleep(1);

    // 向设备写入数据(触发信号)
    write(fd, "Hello", 5);

    // 保持程序运行以接收信号
    while (1) {
        pause();
    }

    close(fd);
    return 0;
}

4.3 应用场景

信号驱动 I/O 适用于对实时性要求极高,希望在设备数据准备好时能立即得到通知的场景。例如,在实时监控系统中,当传感器有新数据产生时,需要尽快通知应用程序进行处理,使用信号驱动 I/O 可以实现高效的实时数据交互。

五、各 I/O 模型对比与选择

I/O 模型 优点 缺点 适用场景
等待队列 实现简单,适合阻塞式 I/O 操作 进程在等待时会阻塞,浪费 CPU 资源 数据准备时间不确定,允许进程阻塞等待的场景
非阻塞访问 不会阻塞进程,提高程序响应速度 需要用户空间不断轮询检查,可能浪费 CPU 资源 对实时性要求高,不希望被阻塞的场景
I/O 多路复用 可以同时监控多个文件描述符,高效管理资源 在高并发场景下,select和poll性能会下降 需要同时处理多个 I/O 设备或文件描述符的场景
信号驱动 I/O 实时性高,无需轮询 信号处理函数编写复杂,可能存在信号丢失问题 对实时性要求极高的场景

在实际的 Linux 驱动开发中,开发者需要根据设备的特性和应用场景的需求,综合考虑各 I/O 模型的优缺点,选择最合适的 I/O 模型,以实现高效、稳定的设备与系统数据交互。同时,也可以结合多种 I/O 模型,发挥它们的优势,进一步提升驱动程序的性能。

你可能感兴趣的:(Android系统开发,linux,驱动开发,android,framework)