linux IO模型
IO模型:IO 是英文Input和Output的首字母代表了输入和输出1 用户空间的应用程序向内核发起IO调用请求(系统调用) 2 内核操作系统准备数据把IO设备的数据加载到内核缓冲区 3 操作系统拷贝数据把内核缓冲区的数据拷贝到用户进程缓冲硬件IO 设备的数据(硬盘、U 盘、屏幕、传感器) → 先传给内核→ 内核把它存到自己的缓冲区IO 模型根据实现的功能可以划分为为阻塞IO、非阻塞IO、信号驱动IOIO多路复用和异 步IO。根据等待IO的执行结果进行划分前四个IO模型又被称为同步IO等待队列:阻塞进程可以使用等待队列来实现超级大白话总结等待队列 内核的休息室没数据 → 去睡觉不占 CPU数据来了 → 被叫醒醒了继续运行等待队列用法API#includelinux/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);//中断打破不了一直等待比如ctrlc 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 flag0; 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) { flag1;//将条件置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;//在试一次 flag0; } }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);#includestdio.h #includesys/types.h #includesys/stat.h #includefcntl.h #includeunistd.h #includestdlib.h #includepoll.h #includefcntl.h #includesignal.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{ .ownerTHIS_MODULE, .fasynccdev_test_fasync, //将fasync字段指向cdev_test_fasync(...)函数 }; //当驱动程序准备好了发信号上面是通用模板不用变 kill_fasync(fasync,SIGIO,POLLIN);lseek函数进行读写位置的调整#includesys/types.h #includeunistd.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对应打开的文件指针westatic 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 offsetfile-f_pos 为当前的偏移值 break; case SEEK_END: if (BUFSIZE offset 0) { return -EINVAL; break; } new_offset BUFSIZE offset; // 如果 whence 参数为 SEEK_END则新偏移值为 BUFSIZE offsetBUFSIZE 为最大偏移量 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函数就可以了他就是命令函数//头文件 #includesys/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内核定时器:基于未来不是周期记录的时间是以节拍树为单位宏HZ1000就是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 类型定时3stimer_test.expiresjiffiesmsecs_to_jiffies(3000jiffies为32为CPUjiffies为64位CPU1初始化定时器/* param1:创建的定时器 param2超时处理函数 */ DEFINE_TIMER(timer_test,function_test);//定义一个定时器2向内核注册定时器timer_test.expiresjiffiesmsecs_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,jiffiesmsecs_to_jiffies(5000));//使用mod_timer函数将定时时间设置为五秒后 }4删除定时器del_timer(timer_test);//删除一个定时器封装API函数和编写驱动文档比如先封装一些函数然后直接调用#includestdio.h #includetimerlib.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; }#includestdio.h #includesys/types.h #includesys/stat.h #includefcntl.h #includeunistd.h #includesys/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...) 函数 作用造成系统死机并输出打印信息用于处理无法恢复的致命错误。