Linux高级IO
五种IO模型
- 阻塞IO:内核在准备好数据之前,系统调用会一直等待,直到内核准备好数据,再将数据报从内核拷贝到用户空间,系统调用 才会成功返回。所有的套接字默认都是阻塞方式,如:recvfrom /sendto
- 非阻塞IO:若内核未准备好数据,系统调用会直接返回,并返回EWOULDBLOCK错误码。这就需要程序员反复尝试读写文件描述符,也称这种方式为“轮询”,不足之处正在于浪费cpu,因为大多数时间是无数据可读的,但仍花费时间不断执行read进行系统调用,多任务系统中应该避免使用。
- 信号驱动IO:用信号处理函数sigaction对SIGIO信号进行处理,一旦内核准备好了数据,SIGIO信号会通知应用进程进行IO操作。这种方式一旦信号被触发,进程中的所有线程都要被挂起,对于服务器进程来说不友好,当信号到来,所有处理客户端请求的线程都要被挂起,都在等待数据从内核拷贝到用户空间,显然不可以。
- IO多路转接:先构造一张有关文件描述符的列表,然后调用一个函数,当这些文件描述符中有一个处于就绪状态,该函数就返回,返回时告诉进程哪些文件描述符准备好了。其本质是它可以等待多个文件描述符的就绪状态。这种方式现已被广泛使用。是一种有效提高时间利用率的手段(类似医生看病)。select,poll,epoll这三个函数能够实现IO多路复用。IO多路复用适用于非常多的客户端来访问服务器,但只有几个处于活跃状态(QQ服务器)
- 异步IO:内核拷贝完数据后,才会通知应用进程,
任何IO过程都包括两个步骤:1.等待;2.拷贝数据;一般来说,等待的时间都会远远大于拷贝数据的时间,要优化IO时间,提高性能,就要从数据等待的时间入手。凡是提到给一个程序进行性能优化,应该先寻找性能瓶颈。这里我们要优化等待时间,可将系统调用函数设置成非阻塞,若无数据直接返回,省去了等待数据的时间,两次调用之间程序可以做其他事情,时间并没有被浪费。信号驱动IO也是如此,在数据未准备好时,可以一直做其他事情。直到数据就绪,才会触发信号进行IO操作。
同步和异步:描述的是调用者和被调用者之间的行为
- 同步是指由调用者主动等待调用的结果。当调用者发起一个调用,未得到结果之前调用一直不返回,直到得到返回值,调用才会返回。就好比我们去吃饭,点餐后一直在前台等待,直到饭做好后我们才端走去吃。
- 异步是指调用者发出调用之后未收到返回结果就直接返回了,它的返回结果是被调用者通过状态、通知来将结果反馈给调用者。再如我们出去吃饭,点餐后我们就找个地坐下,当饭好了之后,服务员会为我们端过来。
下面的同步与上面的同步完全不是一个概念
同步和互斥:
同步是指为完成某个任务而建立两个或多个线程或进程,他们的运行必须严格按照某种先后次序来进行,比如A任务的运行依赖于B任务产生的数据。
互斥是指散布在不同任务之间的若干程序片段,当某个任务运行其中一个程序片段时,其他任务就不能运行他们之中的任何一个程序片段,只能等该任务运行完这个程序片段后才可以运行。比如,一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
-
阻塞与非阻塞:他们关注的是程序在等待调用结果时的状态。
- 阻塞调用是指调用结果返回之前,当前线程会被挂起,只有在得到结果之后才会返回
- 非阻塞调用是指未得到结果就直接返回,不会阻塞当前线程
其他高级IO还有:非阻塞IO,记录锁,系统V流机制,IO多路转接,readv和writev函数及存储映射IO(mmap)。
实现非阻塞IO
对一个给定的文件描述符,我们有两种方式将其指定为非阻塞IO:
(1).若调用open获得文件描述符,则可指定O_NONBLOCK标志。
(2).对一个已打开的文件描述符,可调用fcntl函数,由该函数打开O_NONBLOCK文件状态标识。
fcntl函数
返回值:成功依赖cmd,出错则返回-1。
该函数可改变已打开文件的的性质
传入cmd的值不同,后面追加的参数也不同,第三个参数总是一个整数。
它有五种功能:
- 复制一个现有的文件描述符(cmd=F_DUPFD)
- 获得/设置文件描述符标记(cmd=FGETFD/FSETFD)
- 获得/设置文件描状态标记(cmd=FGETFL/FSETFL)
- 获得/设置异步IO所有权(cmd=FGETOWN/FSETOWN)
- 获得/设置记录锁(cmd=FGETLK/FSETLK)
我们用到的是第三种功能获得/设置文件描状态标记(cmd=FGETFL/FSETFL),就可以将一个文件描述符设置为非阻塞。
通过FGETFL获得的文件状态标识如下表:
FSETFL是将文件状态标志设置为第三个参数的值,除前三种状态之外,其他的 状态标志都可更改。
轮询方式读取标准输入:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
//通过这个函数将文件描述符设置为非阻塞
int SetNoBlock(int fd)
{
int flag=fcntl(fd,F_GETFL);
if(flag<0)
{
perror("fcntl");
return -1;
}
int ret=fcntl(fd,F_SETFL,flag|O_NONBLOCK);
if(ret<0)
{
perror("fcntl");
return -1;
}
return 0;
}
int main()
{
SetNoBlock(0);
//从标准输入尝试读数据
while(1)
{
usleep(100000);
char buf[1024]={0};
printf(">");
fflush(stdout);
ssize_t read_size=read(0,buf,sizeof(buf)-1);
if(read_size<0)
{
perror("read");
continue;
}
if(read_size==0)
{
printf("read done!\n");
return 0;
}
buf[read_size]='\0';
printf("buf=%s\n",buf);
}
return 0;
}
dup和dup2函数
返回值:若成功返回新的文件描述符,失败返回-1.
这两个函数在man手册中是这样进行描述的:
由dup返回的新文件描述符一定是当前可用文件描述符中的最小值。
而dup2可以指定新文件描述符的数值。若newfd已经打开,则先将其关闭。若newfd和oldfd相等,则不关闭直接返回newfd。
我们还可以用上面提到的fcntl函数进行文件描述符的复制,利用fcntl的第一种功能:
dup(oldfd)
就相当于fcntl(oldfd,F_DUPFD,0)
dup2(oldfd,newfd)
相当于close(newfd);fcntl(oldfd,F_DUPFD,newfd);
在第二种情况中,dup2并不完全等价于close加fcntl,是因为:
1.dup2是一个原子操作,而close和fcntl是两个函数调用,当执行完close后,有可能某些信号被触发,进程去执行信号处理函数,这时就有可能对文件描述符进行修改。
2.dup2和fcntl有某些不同的error
使用dup将标准输出重定向到文件中
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd=open("./1.txt",O_CREAT|O_RDWR);
if(fd<0)
{
perror("open");
return 1;
}
close(1);
int new_fd=dup(fd);
if(new_fd!=1)
{
perror("dup");
return 1;
}
printf("new_fd %d\n",new_fd);
close(fd);
while(1)
{
char buf[1024]={0};
ssize_t read_size=read(0,buf,sizeof(buf)-1);
if(read_size<0)
{
perror("read");
continue;
}
printf("%s\n",buf);
fflush(stdout);
}
close(new_fd);
return 0;
}
使用dup2将标准输出重定向到文件中
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd=open("./2.txt",O_CREAT|O_RDWR,0644);
if(fd<0)
{
perror("open");
return 1;
}
close(1);
int new_fd=dup2(fd,1);
if(new_fd<0)
{
perror("dup2");
return 1;
}
printf("new_fd %d\n",new_fd);
while(1)
{
char buf[1024]={0};
ssize_t read_size=read(0,buf,sizeof(buf)-1);
if(read_size<0)
{
perror("read");
continue;
}
buf[read_size]='\0';
printf("%s\n",buf);
fflush(stdout);
}
close(new_fd);
return 0;
}