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.");
View Code

  先说明一下该代码的作用,一个器件(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

 

posted @ 2016-04-18 23:16  被罚站的树  阅读(220)  评论(0编辑  收藏  举报