IO模型
分类
在UNIX/Linux下主要有4种I/O 模型:
阻塞I/O:
最常用、最简单、效率最低
非阻塞I/O:
可防止进程阻塞在I/O操作上,需要轮询
I/O 多路复用:
允许同时对多个I/O进行控制
信号驱动I/O:
一种异步通信模型
阻塞IO
阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。
缺省情况下,套接字建立后所处于的模式就是阻塞I/O 模式。
前面学习的很多读写函数在调用过程中会发生阻塞。
读操作中的read、recv、recvfrom
写操作中的write、send
其他操作:accept、connect
写操作也是有阻塞的:当缓冲区满的时候,写操作就会阻塞,
直到缓冲区中有足够的空间能容纳下本次的写操作时,
写操作的阻塞就会解除,只不过一般情况下,我们一般不考虑写阻塞的问题。
以读阻塞为例:
当程序执行到读操作时,如果缓冲区中有内容,则直接读走继续向下执行。
如果缓冲区中没有内容,进程就会进入休眠态(阻塞),直到缓冲区中有内容了
内核唤醒该进程,去缓冲区中把内容读走,继续向下执行。
以管道为例,演示读阻塞
读端:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int fd1 = open("fifo1", O_RDONLY);
int fd2 = open("fifo2", O_RDONLY);
int fd3 = open("fifo3", O_RDONLY);
char buff[128] = {
0};
while(1){
memset(buff, 0, 128);
read(fd1, buff, 128);
printf("fd1:[%s]\n", buff);
memset(buff, 0, 128);
read(fd2, buff, 128);
printf("fd2:[%s]\n", buff);
memset(buff, 0, 128);
read(fd3, buff, 128);
printf("fd3:[%s]\n", buff);
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
三个一样的写端:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int fd = open("fifo1", O_WRONLY);
char buff[128] = {
0};
while(1){
memset(buff, 0, 128);
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';
write(fd, buff, 128);
}
close(fd);
return 0;
}
非阻塞IO
以读操作为例:
当程序执行到读操作时,如果缓冲区中有内容,则直接读走继续向下执行。
如果缓冲区中没有内容,进程不会进入休眠态,而是立即返回一个错误。但是这种操作,需要轮询,来不停的测试是否有数据可读,这种轮询的操作,是十分占用CPU的,一般不推荐使用。有一部分函数,是自带非阻塞标志位的
如:waitpid的WNOHANG recv和recvfrom的MSG_DONTWAIT O_NONBLOCK
但是大部分的函数是没有非阻塞标志位的,这时可以使用 fcntl 函数来设置非阻塞的属性
fcntl 函数说明
功能:
控制文件描述符的状态
头文件:
#include <unistd.h>
#include <fcntl.h>
函数原型:
int fcntl(int fd, int cmd, ... /* arg */ );
参数:
fd:文件描述符
cmd:指令
F_GETFL 获取文件描述符的状态 arg被忽略
F_SETFL 设置文件描述符的状态 arg是一个int类型的参数
文件描述符的状态中 O_NONBLOCK 表示非阻塞
...:可变参
返回值:
cmd是F_GETFL 成功 返回文件描述符的状态 失败 返回 -1 重置错误码
cmd是F_SETFL 成功 0 失败 -1 重置错误码
使用 fcntl 设置非阻塞
int value = fcntl(fd, F_GETFL);//先获取原来的状态信息
value |= O_NONBLOCK;//添加非阻塞属性
fcntl(fd, F_SETFL, value);//再设置回去
读端:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int fd1 = open("fifo1", O_RDONLY);
int fd2 = open("fifo2", O_RDONLY);
int fd3 = open("fifo3", O_RDONLY);
//设置非阻塞
int flag = fcntl(fd1, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd1, F_SETFL, flag);
flag = fcntl(fd2, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd2, F_SETFL, flag);
flag = fcntl(fd3, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd3, F_SETFL, flag);
char buff[128] = {
0};
while(1){
memset(buff, 0, 128);
read(fd1, buff, 128);
printf("fd1:[%s]\n", buff);
memset(buff, 0, 128);
read(fd2, buff, 128);
printf("fd2:[%s]\n", buff);
memset(buff, 0, 128);
read(fd3, buff, 128);
printf("fd3:[%s]\n", buff);
//sleep(1);//此处的sleep是为了演示现象时防止刷屏的
//把sleep去掉 可以看到 CPU基本被 读端进程占满了
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
三个一样的写端:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int fd = open("fifo1", O_WRONLY);
char buff[128] = {
0};
while(1){
memset(buff, 0, 128);
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';
write(fd, buff, 128);
}
close(fd);
return 0;
}
多路IO复用
基本思路:
构造一个关于文件描述符的表,将所有要监视的文件描述符都添加到这个表里
将这个表传给一个函数 (select poll epoll),这个函数默认也是阻塞的。当监视的文件描述符中有一个或多个准备就绪时,该函数就会返回。并且会告诉我们,哪些文件描述符准备就绪了,我们已经知道哪些文件描述符就绪了,再去执行对应的IO操作,就不会再阻塞了
select 函数说明
功能:
实现多路IO复用
头文件:
#include <sys/select.h>
函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数:
nfds: 要监视的最大的文件描述符+1
readfds: 要监视的 读 文件描述符集合 不关心可以传 NULL
writefds: 要监视的 写 文件描述符集合 不关心可以传 NULL
exceptfds: 要监视的 异常 文件描述符集合 不关心可以传 NULL
timeout: 超时时间 后面在说 传 NULL 表示一直阻塞 直到有文件描述符就绪
返回值:
成功 就绪的文件描述符的个数
失败 -1 重置错误码
超时 0
注意事项:
1.select只能监视小于 FD_SETSIZE(1024) 的文件描述符
2.select返回时会将集合修改,所以循环中调用select时
每次需要重置要监视的文件描述符集合
3.我们一般只关心读文件描述符结合 写和异常传NULL 即可
操作集合的四个宏
void FD_CLR(int fd, fd_set *set); //将文件描述符在集合中删除
int FD_ISSET(int fd, fd_set *set); //判断 文件描述符是否还在集合中
//在 返回非0 不在 返回0
void FD_SET(int fd, fd_set *set); //将文件描述符添加到集合中
void FD_ZERO(fd_set *set); //清空文件描述符集合
读端代码:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
int fd1 = open("fifo1", O_RDONLY);
int fd2 = open("fifo2", O_RDONLY);
int fd3 = open("fifo3", O_RDONLY);
//构建要监视的文件描述符集合
fd_set readfds;//母本
FD_ZERO(&readfds);//先清空
fd_set readfds_temp;//用来给select修改用
FD_ZERO(&readfds_temp);//先清空
int max_fd = 0;//保存最大文件描述符
int ret = 0;
//将要监视的文件描述符添加到集合中
FD_SET(fd1, &readfds);
max_fd = max_fd>fd1?max_fd:fd1;//更新最大文件描述符
FD_SET(fd2, &readfds);
max_fd = max_fd>fd2?max_fd:fd2;//更新最大文件描述符
FD_SET(fd3, &readfds);
max_fd = max_fd>fd3?max_fd:fd3;//更新最大文件描述符
char buff[128] = {
0};
int i = 0;
while(1){
readfds_temp = readfds;//select每次返回会将没有就绪的文件描述符在集合中擦除
//所以每次需要重置集合
if(-1 == (ret = select(max_fd+1, &readfds_temp, NULL, NULL, NULL))){
perror("select error");
exit(-1);
}
//遍历文件描述符集合 确定哪些就绪
for(i = 3; i < max_fd+1 && ret!=0 ; i++){
if(FD_ISSET(i, &readfds_temp)){
memset(buff, 0, 128);
read(i, buff, 128);
printf("buff:[%s]\n", buff);
ret--;//此处的ret--是为了减少遍历次数 提高效率的
}
}
//select返回时 集合中剩余的就是就绪的
//使用上面的方式处理更好些
#if 0
if(FD_ISSET(fd1, &readfds_temp)){
memset(buff, 0, 128);
read(fd1, buff, 128);
printf("fd1:[%s]\n", buff);
}
if(FD_ISSET(fd2, &readfds_temp)){
memset(buff, 0, 128);
read(fd2, buff, 128);
printf("fd2:[%s]\n", buff);
}
if(FD_ISSET(fd3, &readfds_temp)){
memset(buff, 0, 128);
read(fd3, buff, 128);
printf("fd3:[%s]\n", buff);
}
#endif
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
三个一样的写端:
#include <stdio.h>
#include <sys/stat.h>