IO模型:

IO 是英文Input和Output的首字母,代表了输入和输出

(1) 用户空间的应用程序向内核发起IO调用请求(系统调用) (2) 内核操作系统准备数据,把IO设备的数据加载到内核缓冲区 (3) 操作系统拷贝数据,把内核缓冲区的数据拷贝到用户进程缓冲

硬件(IO 设备)的数据(硬盘、U 盘、屏幕、传感器) → 先传给 内核 → 内核把它存到自己的缓冲区

IO 模型根据实现的功能可以划分为为阻塞IO、非阻塞IO、信号驱动IO,IO多路复用和异 步IO。根据等待IO的执行结果进行划分,前四个IO模型又被称为同步IO

等待队列:

阻塞进程可以使用等待队列来实现

超级大白话总结

等待队列 = 内核的休息室

  • 没数据 → 去睡觉(不占 CPU)
  • 数据来了 → 被叫醒
  • 醒了继续运行

等待队列用法API

#include<linux/io.h>
#include <linux/wait.h>

1. 定义等待队列头
wait_queue_head_t wq;

2. 初始化
init_waitqueue_head(&wq);

//DECLARE_WAIT_QUEUE_HEAD(head);

3. 进程在这睡觉,等条件满足
wait_event(wq, condition);//中断打破不了,一直等待,比如ctrl+c
wait_event_interruptible(wq, condition);//可被中断打破不等待

4. 唤醒睡觉的进程
wake_up(&wq);
wake_up_interruptible(&wq);唤醒可中断的等待队列

比如:

读函数需要等待队列,写函数有数据了,就把条件置1,并且唤醒等待队列

1. 定义等待队列头
wait_queue_head_t wq;

2. 初始化
init_waitqueue_head(&wq);

int flag=0;

static ssize_t cdev_test_read(struct file *file,char__user* buf,size_t size,loff_t *off)
{
    //到这一步,先检查flag为真继续执行,为假睡觉
    wait_event_interruptible(wq,flag);//可中断的阻塞等待,使进程进入休眠态
}

static ssize_t cdev_test_write(struct file *file,const char__user *buf,size_t size,loff_t *off)
{

    flag=1;//将条件置1
    wake_up_interruptible(&read_wq);//并使用wake_up_interruptible唤醒等待队列中的休眠进程
    return0;
}

非阻塞IO:(读写不到数据立刻返回,不等待)

操作如下,打开方式非阻塞,自己实现read,write函数的非阻塞

file->f_flags对应打开的文件标志

fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */

write函数置标志位为1,表示有数据,这样都可以读,不返回

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    struct device_test *test_dev = (struct device_test *)file->private_data;

    flag = 1; // 将条件置1,并使用wake_up_interruptible唤醒等待队列中的休眠进程

    return 0;
}
int flag;

static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    struct device_test *test_dev = (struct device_test *)file->private_data;

    // 判断是否为非阻塞模式
    if (file->f_flags & O_NONBLOCK) {
        // 如果设备未就绪,直接返回错误,不阻塞
        if (flag != 1)
            return -EAGAIN;//在试一次
            flag=0;
    }
}

IO多路复用(epoll,select,poll)

用户代码:

int main(int argc, char *argv[])
{
    int fd;             // 要监视的文件描述符
    char buf1[32] = {0};
    char buf2[32] = {0};
    struct pollfd fds[1];
    int ret;

    fd = open("/dev/test", O_RDWR);  // 打开/dev/test设备,阻塞式访问
    if (fd < 0)
    {
        perror("open error\n");
        return fd;
    }

    // 构造结构体
    fds[0].fd = fd;
    fds[0].events = POLLIN;  // 监视数据是否可以读取

    printf("read before\n");

    while (1)
    {
        ret = poll(fds, 1, 3000);  // 轮询文件是否可操作,超时3000ms

        if (!ret)  // 超时
        {
            printf("timeout!!\n");
        }
        else if (fds[0].revents == POLLIN)  // 如果返回事件是有数据可读取
        {
            read(fd, buf1, sizeof(buf1));  // 从/dev/test文件读取数据
            printf("buf is %s\n", buf1);    // 打印读取的数据
            sleep(1);
        }
    }

    printf("read after\n");
    close(fd);  // 关闭文件
    return 0;
}

驱动代码:
 

DECLARE_WAIT_QUEUE_HEAD(read_wq);//定义并初始化等待队列

static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{

    flag = 1;          // 标记数据就绪
    return 0;
}

static __poll_t cdev_test_poll(struct file *file, struct poll_table_struct *p)
{

    __poll_t mask = 0;

    poll_wait(file, &read_wq, p); // 将进程加入等待队列,不会阻塞

    // 如果设备就绪(有数据可读)
    if (flag == 1)
    {
        mask |= POLLIN; // 返回可读事件
    }

    return mask; // 返回给应用层判断
}

/* 设备操作函数 */
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE,        // 将 owner 字段指向本模块,可以避免在模块的操作正在被使用时
    .poll = cdev_test_poll,      // 将 poll 字段指向 chrdev_poll(...) 函数
};

信号驱动IO(发信号)

应用程序通过 fcntl 开启异步通知(FASYNC 标志),触发驱动的 fasync 方法。

驱动程序fasync 方法中调用 fasync_helper,维护一个异步监听进程列表。当数据就绪时,

驱动调用 kill_fasync应用进程发信号(如 SIGIO),应用即可立即处理。

用户程序:

步骤 1:当应用程序开启信号驱动 IO 时,会触发驱动中的 fasync 函数。所以首先在 file_operations 结构体中实现 fasync 函数。函数原型如
int (*fasync) (int fd, struct file *filp, int on);

步骤 2:在驱动中的 fasync 函数中调用 fasync_helper 函数来操作 fasync_struct 结构体,fasync_helper 函数原型如下:
fasync_helper 函数原型:
int fasync_helper(int fd, struct file *filp, int on, struct fasync_struct **fapp);

步骤 3:当设备准备好的时候,驱动程序需要调用kill_fasync函数通知应用程序,此时应用程序的SIGIO信号处理函数就会被执行。kill_fasync负责发送指定的信号。函数原型如下:
void kill_fasync(struct fasync_struct **fp, int sig, int band);
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<poll.h>
#include<fcntl.h>
#include<signal.h>

int fd;
char buf1[32] = {0};

// SIGIO信号的信号处理函数
static void func(int signum)
{
    read(fd, buf1, 32);
    printf("buf is %s\n", buf1);
}

int main(int argc, char *argv[])
{
    int ret;
    int flags;

    fd = open("/dev/test", O_RDWR); // 打开led驱动
    if (fd < 0)
    {
        perror("open error\n");
        return fd;
    }

    signal(SIGIO, func); // 步骤一:使用signal函数注册SIGIO信号的信号处理函数

    // 步骤二:设置能接收这个信号的进程
    // fcntl函数用来操作文件描述符,
    // F_SETOWN设置当前接收的SIGIO的进程ID
    fcntl(fd, F_SETOWN, getpid());

    flags = fcntl(fd, F_GETFD); // 获取文件描述符标志

    // 步骤三开启信号驱动IO使用fcntl函数的F_SETFL命令打开FASYNC标志
    fcntl(fd, F_SETFL, flags | FASYNC);

    while (1);

    close(fd); // 关闭文件
    return 0;
}

 为啥  fcntl(fd, F_SETOWN, getpid());

驱动函数:

struct fasync_struct* fasync;

static int cdev_test_fasync(int fd, struct file *file, int on)
{
    return fasync_helper(fd, file, on, &fasync);
}

struct file_operations cdev_test_fops={
.owner=THIS_MODULE,
.fasync=cdev_test_fasync, //将fasync字段指向cdev_test_fasync(...)函数
};

//当驱动程序准备好了,发信号,上面是通用模板不用变
kill_fasync(&fasync,SIGIO,POLLIN);

lseek函数,进行读写位置的调整

#include<sys/types.h>
#include<unistd.h>
/*
fd:文件描述符;
off_toffset:偏移量,单位是字节的数量,可以正负,如果是负值表示向前移动;如果是正
值,表示向后移动。
whence:当前位置的基点,可以使用以下三组值。
SEEK_SET:相对于文件开头
SEEK_CUR:相对于当前的文件读写指针位置
SEEK_END:相对于文件末尾
函数返回值:成功返回当前位移大小,失败返回-
*/
off_t lseek(int fd,off_t offset,int whence)

比如:

把文件位置指针设置为 5:
lseek(fd, 5, SEEK_SET);

把文件位置设置成文件末尾:
lseek(fd, 0, SEEK_END);

确定当前的文件位置:
lseek(fd, 0, SEEK_CUR);

lseek驱动函数如下:返回新的文件指针位置

file->f_pos,对应打开的文件指针we

static loff_t cdev_test_llseek(struct file *file, loff_t offset, int whence)
{
    loff_t new_offset; // 定义 loff_t 类型的新的偏移值

    switch (whence) // 对 lseek 函数传递的 whence 参数进行判断
    {
        case SEEK_SET:
            if (offset < 0) {
                return -EINVAL;
                break;
            }
            if (offset > BUFSIZE) {
                return -EINVAL;
                break;
            }
            new_offset = offset; // 如果 whence 参数为 SEEK_SET,则新偏移值为 offset
            break;

        case SEEK_CUR:
            if (file->f_pos + offset > BUFSIZE) {
                return -EINVAL;
                break;
            }
            if (file->f_pos + offset < 0) {
                return -EINVAL;
                break;
            }
            new_offset = file->f_pos + offset; // 如果 whence 参数为 SEEK_CUR,则新偏移值为 file->f_pos + offset,file->f_pos 为当前的偏移值
            break;

        case SEEK_END:
            if (BUFSIZE + offset < 0) {
                return -EINVAL;
                break;
            }
            new_offset = BUFSIZE + offset; // 如果 whence 参数为 SEEK_END,则新偏移值为 BUFSIZE + offset,BUFSIZE 为最大偏移量
            break;

        default:
            return -EINVAL;
    }

    file->f_pos = new_offset; // 更新 file->f_pos 偏移值
    return new_offset;
}

新的read函数如下:

static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    loff_t p = *off; // 将读取数据的偏移量赋值给 loff_t 类型变量 p
    int i;
    size_t count = size;

    if (p > BUFSIZE) { // 如果当前偏移值比最大偏移量大则返回错误
        return -1;
    }

    if (count > BUFSIZE - p) {
        count = BUFSIZE - p; // 如果要读取的偏移值超出剩余的空间,则读取到最后位置
    }

    if (copy_to_user(buf, mem + p, count)) { // 将 mem 中的值写入 buf,并传递到用户空间
        printk("copy_to_user error\n");
        return -1;
    }

    for (i = 0; i < 20; i++) {
        printk("buf[%d] is %c\n", i, mem[i]); // 将 mem 中的值打印出来
    }

    printk("mem is %s, p is %llu, count is %d\n", mem + p, p, count);
    *off = *off + count; // 更新偏移值

    return count;
}

新的write函数如下:

static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    loff_t p = *off; // 将写入数据的偏移量赋值给 loff_t 类型变量 p
    size_t count = size;

    if (p > BUFSIZE) { // 如果当前偏移值比最大偏移量大则返回错误
        return 0;
    }

    if (count > BUFSIZE - p) {
        count = BUFSIZE - p; // 如果要写入的偏移值超出剩余的空间,则写入到最后位置
    }

    if (copy_from_user(mem + p, buf, count)) { // 将 buf 中的值,从用户空间传递到内核空间
        printk("copy_from_user error\n");
        return -1;
    }

    printk("mem is %s, p is %llu\n", mem + p, p); // 打印写入的值
    *off = *off + count; // 更新偏移值

    return count;
}

ioctl:控制命令

应用程序通过向内核空间写入1和0从而控制GPIO引脚 的亮灭,但是读写操作主要是数据流对数据进行操作,而一些复杂的控制通常需要非数据操作,用ioctl函数就可以了,他就是命令函数

//头文件:
#include<sys/ioctl.h>

//函数原型:
/*
fd:是用户程序打开设备时返回的文件描述符
cmd:是用户程序对设备的控制命令,
args:应用程序向驱动程序下发的参数,如果传递的参数为指针类型,则可以接收驱动向
用户空间传递的数据(在下面的实验中会进行使用)
*/
int ioctl(int fd,unsigned int cmd,unsigned long args);

命令格式如下,有API可以用

_IO(type,nr)        //:合成没有数据传递的命令

//size args 的类型,比如int ,char
_IOR(type,nr,size)  //:合成从驱动中读取数据的命令
_IOW(type,nr,size)  //:合成向驱动中写数据的命令
_IOWR(type,nr,size) //:合成先写入数据再读取数据的命令

比如:用户区域代码,注意,第一个参数和第二个不能完全相同

//三条命令的解释如下
#define CMD_TEST0 _IO('L',0) //表示无数据交换
#define CMD_TEST1 _IOW('L',1,int)//表示写数据到内核
#define CMD_TEST2 _IOR('L',2,int)//表示从内核读数据

主函数代码:

int main(int argc, char *argv[])
{
    int fd;    // 定义 int 类型的文件描述符 fd
    int val;   // 定义 int 类型的传递参数 val

    fd = open("/dev/test", O_RDWR);  // 打开 test 设备节点
    if (fd < 0) {
        printf("file open fail\n");
        return -1;
    }

    if (!strcmp(argv[1], "write")) {
        ioctl(fd, CMD_TEST1, 1);  // 如果第二个参数为 write,向内核空间写入 1
    }
    else if (!strcmp(argv[1], "read")) {
        ioctl(fd, CMD_TEST2, &val); // 如果第二个参数为 read,则读取内核空间传递向用户空间传递的值
        printf("val is %d\n", val);
    }

    close(fd);
    return 0;
}

驱动函数代码:注意命令要定义的一样,就是那些#define命令

static long cdev_test_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    int val; // 定义 int 类型向应用空间传递的变量 val
    int usr_val; // 用于接收应用空间数据

    switch(cmd) {
        case CMD_TEST0:
            printk("this is CMD_TEST0\n");
            break;

        case CMD_TEST1:
            printk("this is CMD_TEST1\n");
            // 正确:从用户空间安全复制数据
            if (copy_from_user(&usr_val, (int *)arg, sizeof(usr_val))) {
                printk("copy_from_user error\n");
                return -EFAULT;
            }
            printk("arg is %d\n", usr_val);
            break;

        case CMD_TEST2:
            val = 1;
            printk("this is CMD_TEST2\n");
            if(copy_to_user((int *)arg, &val, sizeof(val)) != 0) {
                printk("copy_to_user error\n");
                return -EFAULT;
            }
            break;

        default:
            break;
    }

    return 0;
}

如果我们想传递多个数据,就传递地址就好了

比如;用户代码

struct args{
int a;
int b;
int c;
};
struct args test;
ioctl(fd,CMD_TEST0,&test);//使用ioctl函数传递结构体变量test地

驱动代码


struct args test;
if(copy_from_user(&test,(struct args *)arg,sizeof(test)) != 0){//将用户空间传递来的 arg 赋值给 test
printk("copy_from_user error\n");
}

printk("a = %d\n",test.a);//对传递的值进行打印验证
printk("b = %d\n",test.b);
printk("c = %d\n",test.c);

linux内核定时器:基于未来,不是周期

记录的时间是以节拍树为单位,宏HZ=1000,就是1s1000个节拍

通过全局变量jiffies来记录自系统启动以来产生节拍的总数。启动时,为0,此后,每次时钟中断处理程序都会增加该变量的值,定义在/include/linux/jiffies.h文件

int jiffies_to_msecs(const unsigned long j)
将 jiffies 类型的参数 j 转换为对应的毫秒


long msecs_to_jiffies(const unsigned int m)
将毫秒转换为 jiffies 类型

long usecs_to_jiffies(const unsigned int u)
将微秒转换为 jiffies 类型

unsigned long nsecs_to_jiffies(u64 n)
将纳秒转换为 jiffies 类型

定时3s:timer_test.expires=jiffies+msecs_to_jiffies(3000);

jiffies为32为CPU,jiffies为64位CPU

1:初始化定时器

/*
param1:创建的定时器
param2:超时处理函数

*/
DEFINE_TIMER(timer_test,function_test);//定义一个定时器

2:向内核注册定时器

timer_test.expires=jiffies+msecs_to_jiffies(5000);//将定时时间设置为五秒
add_timer(&timer_test);//添加一个定时器

3:周期定时器,在回调函数里面再次更改定时器未来时间

static void function_test(struct timer_list*t)
{
    printk("this is function test\n");
    mod_timer(&timer_test,jiffies+msecs_to_jiffies(5000));//使用mod_timer函数将定时时间设置为五秒后
}

4:删除定时器

del_timer(&timer_test);//删除一个定时器

封装API函数和编写驱动文档

比如先封装一些函数,然后直接调用

#include<stdio.h>
#include"timerlib.h"

int timer_set(int fd, int arg)
{
    int ret;
    ret = ioctl(fd, TIMER_SET, arg);
    if(ret < 0){
        printf("ioctl error\n");
        return -1;
    }
    return ret;
}
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/ioctl.h>

#define TIME_OPEN _IO('L',0)
#define TIME_CLOSE _IO('L',1)
#define TIME_SET _IOW('L',2,int)

int main(int argc, char *argv[])
{
    int fd;
    fd = dev_open();
    timer_set(fd, 1000);
    timer_open(fd);
    sleep(3);
    timer_set(fd, 3000);
    sleep(7);
    timer_close(fd);
    close(fd);
    return 0;
}

优化驱动效率和稳定性

就是对错误进行处理,比如传递参数可以先判断

if(_IOC_TYPE(cmd) != 'L'){
printk("cmd type error \n");
return-1;
}

这个检查指针是否合法

/*
函数作用:检查用户空间内存块是否可用
addr :用户空间的指针变量,其指向一个要检查的内存块开始处。
size :要检查内存块的大小。
成功返回1,失败返回0
*/

access_ok(addr, size);

likely /unlikely 函数

作用:用于优化 if-else 分支的执行效率,告诉编译器哪条分支更可能执行,从而优化指令预取与流水线。

  • if (likely(value)):暗示 value 大概率为真,编译器会把 if 分支代码放在更靠前的位置。
  • if (unlikely(value)):暗示 value 大概率为假,编译器会把 else 分支代码放在更靠前的位置。

原理:现代 CPU 会预取后续指令,如果分支跳转和预取方向不一致,就会浪费流水线周期。用这两个宏可以让编译器把高概率执行的代码放在顺序执行路径上,减少跳转开销。

等价性

  • if (likely(value)) 等价于 if (value),只是给编译器的优化提示。
  • if (unlikely(value)) 等价于 if (value),同样是优化提示,不改变逻辑。

调试

dump_stack() 函数
作用:打印内核调用堆栈,并打印函数的调用关系。

WARN(condition, fmt...) 和 WARN_ON(condition) 函数
作用:打印函数的调用关系,用于发出警告信息(不会导致系统崩溃)。

BUG() 和 BUG_ON(condition) 函数
作用:触发内核的 OOPS 错误,输出调试信息,通常用于检查内核代码中的严重异常。

panic(fmt...) 函数
作用:造成系统死机并输出打印信息,用于处理无法恢复的致命错误。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐