linux IO模型
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...) 函数
作用:造成系统死机并输出打印信息,用于处理无法恢复的致命错误。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)