一般来说,*nix支持四种I/O结构,分别是阻塞式I/O、非阻塞式I/O、I/O复用,信号驱动I/O。
下面分别介绍一下这四种I/O结构:
一,阻塞式I/O。
这是*nix中使用最多的I/O模式,默认情况下对于设备节点的read,对于套接字的read/recvfrom都是阻塞的。
进程(线程)进入这个系统调用,一般来说在申请的条件(如read到足够的数据)被满足之前task是阻塞的,直到条件满足或者被信号中断才回返回。
一般来说,这种导致task挂起的方式浪费cpu,是不太合理的。
但是合理使用时,也能带来好处,比如说,这是一个天然的同步机制啊,不见兔子不撒鹰;
同时,搭配ioctl的某些cmd命令(当然,需要fd在驱动中支持相关cmd,如:FIONREAD)在提前获取到驱动(内核)buf信息时,在给内核指定读取长度,这样就不会造成阻塞。
二,非阻塞式I/O
一个fd的I/O是否阻塞是可以通过系统调用设置的,*nix中一般是ioctl,cmd为FIONBIO(在嵌入式设备中,对于串口之类的节点,为了提高效率,一般自己实现的驱动里就直接将fd配成非阻塞式,无需通过ioctl设置),设置为非阻塞式I/O时,如果申请条件得不到满足一般会返回一些错误信息,错误返回之后继续申请,再返回再申请,直到得到合理响应后返回成功,这就是所谓的轮训。
阻塞式I/O和非阻塞式I/O的区别,很想互斥锁和自旋锁的区别,一个是拿不到资源就在挂起直到资源继续执行,一个拿不到资源就一直尝试去拿。一个哥们说,他们像是一个30岁的男人和20岁的毛头小伙在追求姑娘,没有哪个好或者不好,关键在于你追求的对象和你们所在的环境,当然还有你的‘颜值’和车房,扯远了。
三,信号驱动式I/O
怎么理解?就是该干嘛干嘛去,等电话响了(就是信号SIGIO)就回家吃饭(妈,我想你了)。
怎么样对一个fd启动信号驱动式I/O呢?
在*nix中需要以下三个步骤:
1,信号驱动,所以必须先建立一个信号处理函数,就是先跟妈妈商量好,电话打来就回家吃饭。
2,设置fd的属主,通过fcntl的F_SETOWN命令设置。如果是ioctl就是FIOSETOWN命令咯
----fcntl(fd,F_SETOWN,getipd());
3,通过fcntl的F_SETFL命令打开O_ASYNC标志来开启该fd的信号驱动式I/O。如果是ioctl就是FIOASYNC命令咯。
----ioctl(fd,FIOASYNC,&flag)
在执行完以上三个步骤之后,进入系统调用后立即返回,task继续运行,并不阻塞也不轮询。当申请条件满足时,kernel会为该task产生一个SIGIO信号,收到该信号时,第一步的信号处理函数就可以执行相关I/O动作了。
这个模式在某种程度上来说属于并发了,如果把信号的产生和接受也当做一个任务的话,效率要高一些。
四,I/O复用
通过类似select或者poll的syscall,阻塞在syscall中,而不是阻塞在真正的I/O系统调用上。
select/poll返回后,fd即可读或可写,可以继续执行I/O动作。
实际上,如果不是这两个系统调用能一次处理多个fd,鉴于多了一个系统调用,反而是一种效率下降。
尽管这是使用最多的一种I/O线程处理方式,但是select的实现机制并不完美,遭到了很多批评,于是有了pselect,同时,poll做了改善,同样不尽如人意,这两个函数内容很多,可以单独写一篇blog了,不在这里扩散。
其实据《UNIX Network Programming》介绍,POSIX还规范了一种叫做异步I/O的I/O模型,跟信号驱动类似,不同的是信号驱动I/O是由内核通知我们何时可以启动一个I/O操作;异步I/O是kernel通知我们I/O操作何时完成。