文章目录
目录
文章目录
一、五种IO模型
1.阻塞IO:
2.非阻塞IO
3.信号驱动IO
4.IO多路转接
5.异步IO
二、高级IO的一些重要概念
1.同步通信和异步通信
2.阻塞和非阻塞
三、其他高级IO
四、非阻塞IO
1.fctl函数
2.实现setNoBlock函数,将文件描述符设置为非阻塞
3.轮询方式读取标准输入
五、IO多路转接之select
1.初始select
2.select函数原型
3.理解select执行过程
4.select的特点
5.select的缺点
6.select使用示例:检测标准输入输出
7.select使用实例
六、IO多路转接之poll
1.poll函数接口
2.参数说明
3.返回结果
4.poll的优点
5.poll的缺点
6.poll使用实例:使用poll监控标准输入
总结
一、五种IO模型
1.阻塞IO:
在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。阻塞是最常见的IO模型。
2.非阻塞IO
如果内核还未将数据准备好,系统调用仍然会直接返回,并返回EWOULDBLOCK错误码。非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对cpu来说是较大的浪费,一般只有特定的场景下才使用。
3.信号驱动IO
内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
4.IO多路转接
虽然从图上看和阻塞IO类似,实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。
5.异步IO
由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)
在任何IO过程中,都包含两个步骤,第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都高于拷贝的时间,让IO高效,最核心的办法就是让等待的时间尽量减少。
二、高级IO的一些重要概念
1.同步通信和异步通信
同步和异步关注的是消息通信机制
- 同步,就是在发出一个调用的时候,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到了返回值。换句话说,就是由调用者主动等待这个调用的结果。
- 异步则相反,调用在发出后,这个调用结果就直接返回,所以没有返回结果。换句话说,当一个异步调用发出后,调用者不会立刻得到返回结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或者通过回调函数处理这个调用。
在多进程多线程的时候,也有提到同步和互斥。这里的是完全不同的概念。
进程/线程同步也是进程/线程之间直接的制约关系,是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系,尤其是在访问临界资源的时候。
2.阻塞和非阻塞
阻塞和非阻塞关注的是等待调用结果(消息,返回值)时的状态
- 阻塞调用的指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。
- 非阻塞调用是指在不能立刻得到结果之前,该调用者不会阻塞当前线程。
三、其他高级IO
非阻塞io,纪录锁,系统V流机制,io多路转接(io多路复用),readv和writev函数以及存储映射IO(MMAP),这些统称为高级IO
本文重点讨论IO多路转接
四、非阻塞IO
1.fctl函数
fcntl 一个文件描述符,默认都是阻塞IO
#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd,int cmd, .../*args */);
传入的cmd不同,后面追加的参数也不同
fcntl函数有5种功能:
- 复制一个现有的描述符(cmd = F_DUPFD)
- 获得/设置文件描述符标记(cmd = F_GETFD或F_SETFD)
- 获得/设置文件状态标记(cmd = F_GETFL或F_SETFL)
- 获得/设置异步io所有权(cmd = F_GETOWN或F_SETOWN)
- 获得/设置记录锁(cmd = F_GETLK,F_SETLK或F_SETLKW)
此处使用第三个功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞
2.实现setNoBlock函数,将文件描述符设置为非阻塞
基于fcntl实现一个SetNoBlock函数
void SetNoBlock(int fd)
{
int fl = fcntl(fd,F_GETFL);
if(fl <0)
{
perror("fcntl");
return;
}
fcntl(fd,F_SETFL,fl|O_NONBLOCK);
}
使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.
3.轮询方式读取标准输入
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
void SetNoBlock(int fd)
{
int fl = fcntl(fd,F_GETFL);
if(fl <0)
{
perror("fcntl");
return;
}
fcntl(fd,F_SETFL,fl|O_NONBLOCK);
}
int main()
{
SetNoBlock(0);
while(1)
{
char buf[1024] = {0};
ssize_t read_size = read(0,buf,sizeof(buf)-1);
if(read_size <0)
{
sleep(1);
continue;
}
printf("input:%s\n",buf);
return 0;
}
五、IO多路转接之select
1.初始select
系统提供select函数来实现多路复用输入/输出模型
- select系统调用是用来让我们的程序监视多个fd的状态变化的
- 程序会停在select这里等待,直到被监视的fd有一个或者多个发生了状态改变
2.select函数原型
#include<sys/select.h>
int select(int nfds,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);
//nfd 是需要监视的最大文件描述符值+1
//rdset,wrset,exset分别对应需要检测的可读文件描述符的集合,可写文件描述符的集合,以及异常文件描述符的集合
//timeout为结构timeval,用来设置select的等待时间
参数timeout取值:
- NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生
- 特定的结构值:如果在指定的时间段内没有事件发生,select将超时返回。
fd_set结构
这个结构就是一个整数数组,更严格的说,是一个位图,使用位图中对应的位来表示要监视的文件描述符,有一组fd_set接口,来比较方便操作位图
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
关于timeval结构
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0
strcut timeval
{
__time_t tv_sec; //seconds
__suseconds_t tv_usec; //microseconds
};
函数返回值:
- 执行成功则返回文件描述词状态已改变的个数
- 如果返回0代表在描述词状态改变前已经超过timeout时间,没有返回
- 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测
错误值可能为:
- EBADF 文件描述词为无效的或者该文件已关闭
- EINTR 此调用被信号中断
- EINVAL 参数n为负值
- ENOMEN 核心内存不足
常见使用场景:
fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){...}
3.理解select执行过程
理解select模型的关键在于理解ds_set,这里取fd_set长度为1字节,fd_set中的每一个bit可以对应一个文件描述符fd,则1字节长的fd_set最大可以对应8个fd
- 执行fd_set set; FD_ZERO(&set); 则set用位表示是0000 0000
- 若fd = 5 执行FD_SET(fd,&set);后变为0001 0000
- 若再加入fd = 2,fd = 1,则set变成 0001 0011
- 执行select(6,&set,0,0,0); 阻塞等待
- 若fd = 1,fd = 2 上都发生可读事件,则select返回,此时set变为0000 0011
- 注意,没有事件发生的fd = 5被清空
4.select的特点
- 可监控的文件描述符个数取决与sizeof(fd_set)的值,比如sizeof(fd_set)=512,每bit表示一个文件描述符,则服务器上支持的最大文件描述符是512*8 = 4096
- 将fd加入select监控集的同时,还要再使用一个数据结构array进行FD_ISSET判断
- select返回后会把以前加入但是没有发生事件的fd清空,则每次开始select前都要重新从array取得fd逐一加入,扫描array的同时取得fd的最大maxfd,用于select的第一个参数
5.select的缺点
- 每次调用select,都需要手动设置fd集合,从接口使用角度也非常不便
- 每次调用select,都需要把fd从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传入的所有fd,fd很多的时候开销很大
- select支持的文件描述符数量太少
6.select使用示例:检测标准输入输出
#include<stdio.h>
#include<unistd.h>
#include<sys/select.h>
int main()
{
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(0,&read_fds);
for(;;)
{
printf("> ");
fflush(stdout);
int ret = select(1,&read_fds,NULL,NULL,NULL);
if(ret <0)
{
perror("select");
continue;
}
if(FD_ISSET(0,&read_fds))
{
char buf[1024] = {0};
read(0,buf,sizeof(buf)-1);
printf("input:%s",buf);
}
else
{
printf("invalid fd");
continue;
}
FD_ZERO(&read_fds);
FD_SET(0,&read_fds);
}
return 0;
}
7.select使用实例
参照gitee,实现select字典服务器
六、IO多路转接之poll
1.poll函数接口
#include<poll.h>
int poll(struct pollfd * fds,nfds_t nfds, int timeout);
//pollfd结构
struct pollfd{
int fd;
short events; //requested events
short revents; // returned events
};
2.参数说明
- fds是一个poll函数监听的结构列表,每一个元素中包含3部分内容:fd,监听的事件集合,返回的事件集合
- nfds表示fds数组的长度
- timeout表示poll函数的超时时间,单位是ms
3.返回结果
返回值小于0,表示出错
返回值等于0,表示poll函数等待超时
返回值大于0,表示poll由于监听的fd就绪而返回
4.poll的优点
不同与select使用三个位图来表示fdset的方式,poll使用一个pollfd指针实现
- pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式,接口使用比select更方便
- poll没有max数量限制(但是数量过大后性能也是会下降)
5.poll的缺点
poll中监听的文件fd增多时
- 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的fd
- 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中
- 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的fd数量增加,效率也会线性下降
6.poll使用实例:使用poll监控标准输入
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
int main() {
struct pollfd poll_fd;
poll_fd.fd = 0;
poll_fd.events = POLLIN;
for (;;) {
int ret = poll(&poll_fd, 1, 1000);
if (ret < 0) {
perror("poll");
continue;
}
if (ret == 0) {
printf("poll timeout\n");
continue;
}
if (poll_fd.revents == POLLIN) {
char buf[1024] = {0};
read(0, buf, sizeof(buf) - 1);
printf("stdin:%s", buf);
}
}
}
总结
本文主要介绍了select和poll,下一篇文章详解epoll