IO模型
前言
网上关于IO模型的博文已经很多了,我认为这篇博文(戳进去)讲的很到位,大家可以参考一下。建议大家把参考的博文研究一下在阅读此博文。下面我就从代码级的角度再次剖析一下这几个IO模型的区别。
虽然这里参看的代码是内核驱动的代码(ARM6410开发板GPIO驱动),但是我会尽量讲的清楚点。
阻塞式IO
关于阻塞式IO的介绍就不再啰嗦了。下面我们就分别从内核角度和用户角度来进行分析。
1.用户角度
1 #include <stdio.h> 2 #include <string.h> 3 #include <sys/types.h> 4 #include <sys/stat.h> 5 #include <fcntl.h> 6 #include <unistd.h> 7 #include <signal.h> 8 9 int main(int argc,char **argv) 10 { 11 int fd = open("/dev/cmnin", O_RDONLY); 12 char key_val[5]; 13 if(fd < 0){ 14 printf("open/dev/cmnin error!\n"); 15 return 0; 16 } 17 int i; 18 while(1){ 19 sleep(3); 20 read(fd, key_val, 5); 21 for(i=0; i<5; i++) 22 printf("The port %d is %c\n", i, key_val[i]); 23 printf("\n"); 24 } 25 return 0; 26 }
该代码使用的是linux C编写的。在代码中,我们用open函数打开个文件(这是一个设备文件),然后不停的对这个文件进行读操作,并把读到内容打印出来。因为我们在调用open时,没有设置IO方式,所以内核默认为我们是以阻塞的方式进行IO操作的。当文件中没有内容可读时,while循环会直接阻塞在read函数上,直到有数据据时才返回。用户代码就表示上图中的左边部分。
2.内核角度
1 #include <linux/module.h> 2 #include <linux/kernel.h> 3 #include <linux/fs.h> 4 #include <linux/init.h> 5 #include <linux/delay.h> 6 #include <linux/sched.h> 7 #include <linux/poll.h> 8 #include <linux/irq.h> 9 #include <asm/irq.h> 10 #include <asm/io.h> 11 #include <linux/interrupt.h> 12 #include <asm/uaccess.h> 13 #include <mach/hardware.h> 14 #include <linux/platform_device.h> 15 #include <linux/cdev.h> 16 #include <linux/miscdevice.h> 17 18 #include <mach/map.h> 19 #include <mach/gpio.h> 20 #include <mach/regs-clock.h> 21 #include <mach/regs-gpio.h> 22 23 24 #define DEVICE_NAME "cmnin" 25 26 struct cmnin_desc { 27 int gpio; //端口号 28 int number; //编号 29 char *name; //中断名称 30 struct timer_list timer; //定时器 31 }; 32 33 static struct cmnin_desc cmnins[] = { 34 { S5PV210_GPH2(2), 0, "KP_COL2" }, 35 { S5PV210_GPH2(3), 1, "KP_COL3" }, 36 { S5PV210_GPH3(0), 2, "JJK" }, 37 { S5PV210_GPH3(3), 3, "DFU3" }, 38 { S5PV210_GPH3(2), 4, "PTT" }, 39 }; 40 41 static volatile char cmnin_values[] = { 42 '0', '0', '0', '0', '0' 43 }; 44 45 46 //等待队列 47 static DECLARE_WAIT_QUEUE_HEAD(cmnin_waitq); 48 49 //表明key_values数组中是否有数据,0表示无数据可读,1表示有数据可读 50 static volatile int ev_press = 0; 51 52 53 //定时器处理函数,定时器一到时间就会调用该函数 54 // 55 static void mini210_cmnins_timer(unsigned long _data) 56 { 57 struct cmnin_desc *bdata = (struct cmnin_desc *)_data; 58 int down; 59 int number; 60 unsigned int tmp; 61 62 //获取端口状态 63 tmp = gpio_get_value(bdata->gpio); 64 65 /* active low */ 66 down = !tmp; 67 printk("KEY %d: %08x\n", bdata->number, down); 68 69 number = bdata->number; 70 if (down != (cmnin_values[number] & 1)) { 71 cmnin_values[number] = '0' + down; 72 73 ev_press = 1; 74 wake_up_interruptible(&cmnin_waitq); 75 } 76 } 77 78 /**************************cmnin_interrupt()*****************************/ 79 //此中断服务程序,每次中断都重新设置定时器 80 static irqreturn_t cmnin_interrupt(int irq, void *dev_id) 81 { 82 83 struct cmnin_desc *bdata = (struct cmnin_desc *)dev_id; 84 85 mod_timer(&bdata->timer, jiffies + msecs_to_jiffies(40)); 86 printk("<0>""interrupt"); 87 return IRQ_HANDLED; 88 } 89 90 static int mini210_cmnin_open(struct inode *inode, struct file *file) 91 { 92 93 int irq; 94 int i; //循环变量,五个输入 95 int err = 0; //中断注册函数的返回值 96 97 for (i = 0; i < ARRAY_SIZE(cmnins); i++) { 98 99 //检测设定的端口是否有效 100 if (!cmnins[i].gpio) 101 continue; 102 103 //定时器初始化 104 //第三个参数是给回调函数传入的参数 105 setup_timer(&cmnins[i].timer, mini210_cmnins_timer, 106 (unsigned long)&cmnins[i]); 107 108 //将端口号转换为相应的IRQ值,并赋值给变量irp 109 irq = gpio_to_irq(cmnins[i].gpio); 110 111 //注册中断服务,上升沿触发中断 112 err = request_irq(irq, cmnin_interrupt, IRQ_TYPE_EDGE_RISING, 113 cmnins[i].name, (void *)&cmnins[i]); 114 if (err) 115 break; 116 } 117 118 //一旦发现有一个中断申请失败,则放弃中断申请,并将已申请的中断释放 119 if (err) { 120 i--; 121 for (; i >= 0; i--) { 122 if (!cmnins[i].gpio) 123 continue; 124 125 irq = gpio_to_irq(cmnins[i].gpio); 126 disable_irq(irq); 127 free_irq(irq, (void *)&cmnins[i]); 128 129 del_timer_sync(&cmnins[i].timer); 130 } 131 132 return -EBUSY; 133 } 134 135 ev_press = 1; 136 return 0; 137 } 138 139 140 static int mini210_cmnin_close(struct inode *inode, struct file *file) 141 { 142 int irq, i; 143 144 for (i = 0; i < ARRAY_SIZE(cmnins); i++) { 145 if (!cmnins[i].gpio) 146 continue; 147 148 irq = gpio_to_irq(cmnins[i].gpio); 149 free_irq(irq, (void *)&cmnins[i]); 150 151 del_timer_sync(&cmnins[i].timer); 152 } 153 154 return 0; 155 } 156 157 static int mini210_cmnin_read(struct file *filp, char __user *buff, 158 size_t count, loff_t *offp) 159 { 160 unsigned long err; 161 162 if (!ev_press) { 163 if (filp->f_flags & O_NONBLOCK) 164 return -EAGAIN; 165 else 166 wait_event_interruptible(cmnin_waitq, ev_press); 167 } 168 169 ev_press = 0; 170 171 //将数据copy到用户空间 172 err = copy_to_user((void *)buff, (const void *)(&cmnin_values), 173 min(sizeof(cmnin_values), count)); 174 175 return err ? -EFAULT : min(sizeof(cmnin_values), count); 176 } 177 178 static struct file_operations dev_fops = { 179 .owner = THIS_MODULE, 180 .open = mini210_cmnin_open, 181 .release = mini210_cmnin_close, 182 .read = mini210_cmnin_read, 183 }; 184 185 static struct miscdevice misc = { 186 .minor = MISC_DYNAMIC_MINOR, //动态分配设备号 187 .name = DEVICE_NAME, 188 .fops = &dev_fops, 189 }; 190 191 static int __init cmnin_dev_init(void) 192 { 193 int ret; 194 195 //注册设备 196 ret = misc_register(&misc); 197 if(ret < 0) 198 { 199 printk(DEVICE_NAME " can't register cmnin device\n"); 200 return ret; 201 } 202 printk(DEVICE_NAME"\tinitialized\n"); 203 return ret; 204 } 205 206 static void __exit cmnin_dev_exit(void) 207 { 208 misc_deregister(&misc); 209 } 210 211 module_init(cmnin_dev_init); 212 module_exit(cmnin_dev_exit); 213 214 MODULE_LICENSE("GPL"); 215 MODULE_AUTHOR("FriendlyARM Inc.");
先说明一下该代码的作用,一个器件(arm6410开发板)上有五个端口,这五个端口的要么的是低电平要么是高电平,在逻辑电路中就是用1(高)和0(低)表示的。有一个用户程序需要知道这几个端口是1还是0。一般情况下,用户程序是不能直接操作硬件设备的,而这部分工作是驱动来实现。该代码的作用就是查看端口的值,并返回给用户程序。
代码的其它部分就不做介绍了,我们只关心IO问题,所以就看看read是怎么实现的。用户层调用了read函数,而read函数执行者是内核,而内核是通过驱动来管理硬件的。所以最终read函数是由驱动中的mini210_cmnin_read来实现的。
1 static int mini210_cmnin_read(struct file *filp, char __user *buff, 2 size_t count, loff_t *offp) 3 { 4 unsigned long err; 5 6 if (!ev_press) { 7 if (filp->f_flags & O_NONBLOCK) 8 return -EAGAIN; 9 else 10 wait_event_interruptible(cmnin_waitq, ev_press); 11 } 12 13 ev_press = 0; 14 15 //将数据copy到用户空间 16 err = copy_to_user((void *)buff, (const void *)(&cmnin_values), 17 min(sizeof(cmnin_values), count)); 18 19 return err ? -EFAULT : min(sizeof(cmnin_values), count); 20 }
ev_press是一个标志位,当为0的时候表示没有数据(即五个端口的状态没有发生变化,一旦只要有一个发生变化我们就认为产生了数据)。一开始五个端口全为1,ev_press初始值为0。这时候用户调用了read操作,驱动中代码开始执行。en_press=0表示没有,if为真,执行代码 if (filp->f_flags & O_NONBLOCK) 该代码的作用就是查看用户使用的是阻塞式IO还是非阻塞式IO。因为我们使用的阻塞方式,所以执行 wait_event_interruptible(cmnin_waitq, ev_press); 代码。该代码就是在等待一个中断事件的发生,如果事件没发生就一直会处于等待,相当于图中的wait for data部分。当某一时刻,端口状态终于发生了改变,说明有了数据。这时 wait_event_interruptible(cmnin_waitq, ev_press); 终于可以返回了。驱动代码继续执行, copy_to_user 就是将数据copy到用户空间,相当与内核的第二个阶段。return返回是copy用户空间的数量量(单位:字节)。
3.总结
从上面的分析中,我们可以看到阻塞式IO其实是阻塞在内核中的某个事件上,等待这个事件反生后,函数才能从内核中返回,而这是数据其实也到了用户空间了。
2.非阻塞式IO
非阻塞方式是不管是否有数据,read函数都会直接返回。
1、用户角度
#include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <signal.h> int main(int argc,char **argv) { int fd = open("/dev/cmnin", O_RDONLY|O_NONBLOCK); char key_val[5]; if(fd < 0){ printf("open/dev/cmnin error!\n"); return 0; } int i; while(1){ sleep(3); if(read(fd, key_val, 5) >0){ for(i=0; i<5; i++) printf("The port %d is %c\n", i, key_val[i]); printf("\n"); } } return 0; }
代码中,我们调用open函数时,加了O_NONBLOCK关键字,该关键字就是我们要使用非阻塞方式进行IO操作。这时候read函数返回-1表示没有数据,返回>0的数表示读到的数据个数。整个程序会不断的进行死循环操作,对CPU消耗比较大。
2、内核角度
内核角度不再进行详细概述, if (filp->f_flags & O_NONBLOCK) 会为true,所以内核中的read操作,直接会返回-EAGAIN。直到有数据才会执行和阻塞模型相同的操纵。
3、总结
非阻塞就是就是但内核中没有数据时,直接返回给用户。
3.多路复用技术
多路复用技术需要内核的支持,linux C中的多路复用技术就是select和poll,其实select的底层也是用poll实现的。
1、用户角度
1 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <stdio.h> 4 #include <fcntl.h> 5 #include <sys/time.h> 6 #include <sys/types.h> 7 #include <unistd.h> 8 9 10 void main() 11 { 12 int fd,num; 13 fd_set rfds; 14 struct timeval tv; 15 16 fd = open("/dev/chardev",O_RDWR,S_IRUSR | S_IWUSR); 17 if(fd != -1) 18 { 19 while(1) 20 { 21 /*查看chardev是否有输入*/ 22 FD_ZERO(&rfds); //用来清除一个文件描述符集 23 FD_SET(fd,&rfds); //将一个文件描述符加入到文件描述符集中 24 /*设置超时时间为5s*/ 25 tv.tv_sec = 5; 26 tv.tv_usec = 0; 27 28 //fd应该为所有文件描述符值最大的那个 29 //这里只有一个所以省略了比较步骤 30 select(fd+1,&rfds,NULL,NULL,&tv); 31 32 if(FD_ISSET(fd,&rfds)) //判断文件描述符是否被置位了 33 { 34 read(fd,&num,sizeof(int)); 35 printf("The chardev is %d\n",num); 36 37 38 if(num == 0) 39 { 40 close(fd); 41 break; 42 } 43 } 44 else 45 printf("NO data within 5 seconds.\n"); 46 } 47 } 48 else 49 printf("Open file failure.\n"); 50 }
代码中使用了select实现多路复用,select的函数的设计比较坑,在我看来就是体验差。不过其原理就是图中所示的那样。用户在调用select是会发生阻塞,而一旦select返回就表示至少有一个文件描述符可以进行读写操作了。但是它不会告诉你具体那个能用,而只是将能用的文件描述符置位。所以我们还得遍历一边所有的文件描述符,看那个被置位了(上面的代码中只有一个文件描述符,所以没用进行遍历)。
2.内核实现
1 static int mini210_buttons_read(struct file *filp, char __user *buff, 2 size_t count, loff_t *offp) 3 { 4 unsigned long err; 5 6 if (!ev_press) { 7 if (filp->f_flags & O_NONBLOCK) 8 return -EAGAIN; 9 else 10 wait_event_interruptible(button_waitq, ev_press); 11 } 12 13 ev_press = 0; 14 15 err = copy_to_user((void *)buff, (const void *)(&key_values), 16 min(sizeof(key_values), count)); 17 18 return err ? -EFAULT : min(sizeof(key_values), count); 19 } 20 21 static unsigned int mini210_buttons_poll( struct file *file, 22 struct poll_table_struct *wait) 23 { 24 unsigned int mask = 0; 25 26 poll_wait(file, &button_waitq, wait); 27 if (ev_press) 28 mask |= POLLIN | POLLRDNORM; 29 30 return mask; 31 }
这里驱动代码只显示一部分,但足够我们分析了。当用户调用select或poll系统调用时,内核会先调用poll_initwait(&table),然后调用驱动程序中mini210_buttons_poll,由此看来select/poll真正的执行者是驱动中的poll函数,同时也说明,如果某个硬件驱动中没有实现该函数,那么即使想使用多路复用技术也是枉然。我们看到poll的实现很简单,就是先等待(poll_wait)一段时间,如果发现数据可用就设置掩码并返回该掩码。这也是为啥用户空间需要不断去检查是否置位的原因。至于poll_wait的具体实现这里就不深究了。
3.总结
多路复用技术在服务器程序中使用比较广泛,该技术应该被掌握。
4.信号驱动IO
信号驱动IO和异步IO很像,但是由于信号不好控,信号的个数有限(一般操作系统会给用户预留3个左右),所以在实际中使用很少。
1.用户实现
1 #include <stdio.h> 2 #include <string.h> 3 #include <sys/types.h> 4 #include <sys/stat.h> 5 #include <fcntl.h> 6 #include <poll.h> 7 #include <unistd.h> 8 #include <signal.h> 9 10 int fd = 0; 11 /* 信号处理函数当驱动发信号给应用程序时 会来执行 */ 12 void my_signal_fun(int signum) 13 { 14 unsigned char key_val; 15 read(fd, &key_val, 1); 16 printf("in signal_funkey_val: 0x%x\n", key_val); 17 } 18 19 20 int main(int argc,char **argv) 21 { 22 int Oflags = 0; 23 if (argc != 1) 24 { 25 printf("Usage:%s \n",argv[0]); 26 return 0; 27 } 28 29 30 fd =open("/dev/cmnin",O_RDONLY); 31 if (fd < 0) 32 { 33 printf("open/dev/cmnin error!\n"); 34 return 0; 35 } 36 37 signal(SIGIO,my_signal_fun); 38 39 fcntl(fd, F_SETOWN,getpid()); // 告诉内核,发给谁 40 41 Oflags = fcntl(fd,F_GETFL); // 获得 flag 42 fcntl(fd, F_SETFL, Oflags |FASYNC); //设置flag 异步通知 43 44 while(1) 45 { 46 sleep(1000); 47 } 48 return 0; 49 }
使用信号驱动IO时,首先要将回调函数和一个信号进行绑定,这个信号是操作系统预留给用户的信号。然后向内核注册该信号,告诉内核将来这个信号发给谁,最后将该信号设置为异步通知的方式。到此为之,信号IO的设置完毕,我们的程序就可以继续往下执行干别的事情。一旦内核将我们的数据准备好之后,my_signal_fun会自动被调用去读数据。
2.内核实现
static void mini210_cmnins_timer(unsigned long _data) { struct cmnin_desc *bdata = (struct cmnin_desc *)_data; unsigned int tmp; //获取端口状态 tmp = gpio_get_value(bdata->gpio); if(tmp){ key_value = bdata->val & 0x1F; } else{ key_value = bdata->val & 0x0F; } kill_fasync(&cmninsync_queue, SIGIO, POLL_IN); } static int mini210_cmnin_read(struct file *filp, char __user *buff, size_t count, loff_t *offp) { unsigned long err; if (count != 1) { return -EAGAIN; } //将数据copy到用户空间 err = copy_to_user((void *)buff, (const void *)(&key_value), 1); return err ? -EFAULT : 1; } static int cmnin_fasync(int fd, struct file *filp, int on) { /* 通过fasync_helper();cmninsync_queue这个结构体使得 * kill_fasync();能够把信号发送到应用程序的pid */ return fasync_helper(fd,filp, on, &cmninsync_queue); }
mini210_cmnins_timer 会每隔一段时间去查看是否有数据,如果数据就直接放在一个缓冲区中。并且通过 kill_fasync 向用户发信号,当用户收到该信号时会直接实行read操作,这时候 mini210_cmnin_read 就会将缓冲区中的数据copy到用户缓冲区中。
3.总结
该技术使用并不多,可以作为了解。
5.总结
关于IO这块,不同的操作系统有不同的实现方式。建议大家参考一下以下博文。
http://blog.csdn.net/historyasamirror/article/details/5778378
http://www.cnblogs.com/fanzhidongyzby/p/4098546.html