前言
思维导图
思考
试想一下三种情况:你想打游戏,但是怕爸妈回来
1.一直趴在猫眼看,但是你玩不了游戏:你为了确定爸妈是否回来,阻塞了自己打游戏的进程。
2.时不时从猫眼看一眼,你可以打游戏,但是比较累:你要在观察爸妈是否回来(看猫眼)的间隔打游戏。
3.你用耳朵去听,传来开门的声音,就停止打游戏:你能专心打游戏,有声音就不打。
其实以上三种种情况就类似接下来要说的IO模型:阻塞,非阻塞,异步;还有最后一种情况最后说。
一.阻塞式IO
特点:
1.阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。
2.效率低。进程阻塞,一直等一个任务结束,只能干一件事,效率很低。
3.不浪费CPU。当阻塞的时候,进程就会一直等待,不会浪费CPU资源。
二.非阻塞式IO
当我们设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
1.当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。
2.应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
3.这种模式使用中不普遍。
1.使用函数自带参数实现(例:消息队列):
接收端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
struct msgbuf{
long type;
int num;
char ch;
};
int main(int argc, const char *argv[])
{
key_t key;
key = ftok("2",'a');
if(key < 0){
perror("ftok err");
return -1;
}
int msgid = msgget(key,IPC_CREAT | IPC_EXCL | 0777);
if(msgid < 0){
if(errno == EEXIST)
msgid = msgget(key,0777);
else{
perror("msgget err");
return -1;
}
}
printf("msgid:%d\n",msgid);
struct msgbuf msg2;
//轮询,不是阻塞
while(1){
//本来msgrcv最后一个参数是0的时候是阻塞的,但最当后一个参数为IPC_NOWAIT就不阻塞,没收到就返回-1
printf("查看猫眼\n");
if(msgrcv(msgid,&msg2,sizeof(msg2) - sizeof(long),0,IPC_NOWAIT) >= 0){ //当返回值大于1打印接受值
printf("type:%ld num:%d ch:%c 停止打游戏\n",msg2.type,msg2.num,msg2.ch);
break;
}
else{
printf("打游戏\n");
continue;
}
sleep(1); //一秒看一次猫眼
}
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
发送端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
struct msgbuf{
long type;
int num;
char ch;
};
int main(int argc, const char *argv[])
{
key_t key;
key = ftok("2",'a');
if(key < 0){
perror("ftok err");
return -1;
}
int msgid = msgget(key,IPC_CREAT | IPC_EXCL | 0777);
if(msgid < 0){
if(errno == EEXIST)
msgid = msgget(key,0777);
else{
perror("msgget err");
return -1;
}
}
printf("msgid:%d\n",msgid);
struct msgbuf msg1;
msg1.type = 1;
msg1.num = 6;
msg1.ch = 'c';
msgsnd(msgid,&msg1,sizeof(msg1) - sizeof(long),0);
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
2.通过设置文件描述符的属性设置非阻塞
int fcntl(int fd, int cmd, ... /* arg */ ); //file control
头文件:#include <unistd.h>
#include <fcntl.h>
功能:设置文件描述符属性
参数:fd:文件描述符
cmd:设置方式 - 功能选择
F_GETFL 获取文件描述符的状态信息 第三个参数化忽略
F_SETFL 设置文件描述符的状态信息 通过第三个参数设置
O_NONBLOCK 非阻塞
O_ASYNC 异步
O_SYNC 同步
arg:设置的值 in
返回值:特殊选择返回特殊值 F_GETFL 返回的状态值(int)
其他:成功0 失败-1,更新errno
使用:0为例(输入文件标识符)
0原本:阻塞、读权限、修改或添加非阻塞
int flags=fcntl(0,F_GETFL); //1.获取文件描述符原有的属性信息
flags = flags | O_NONBLOCK; //2.修改添加模式为非阻塞
fcntl(0,F_SETFL,flags); //3.设置修改后的模式
cmd全部参数:
F_DUPFD
复制文件描述符 (指定最小值) F_DUPFD_CLOEXEC
复制文件描述符并原子性设置 FD_CLOEXEC
标志F_GETFD
获取文件描述符标志 (主要是 FD_CLOEXEC
)F_SETFD
设置文件描述符标志 F_GETFL
获取文件状态标志 ( O_RDONLY
,O_NONBLOCK
等)F_SETFL
设置文件状态标志 F_GETLK
测试是否可以获取一个锁 (不实际加锁) F_SETLK
尝试获取或释放锁 (非阻塞) F_SETLKW
尝试获取或释放锁 (阻塞等待) F_GETOWN
获取接收 SIGIO
/SIGURG
信号的进程/进程组 IDF_SETOWN
设置接收 SIGIO
/SIGURG
信号的进程/进程组 ID
练习:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, const char *argv[])
{
//int flag = fcntl(0,F_SETFL,O_NONBLOCK);
//我直接给输入文件描述符设置状态非阻塞行不行?不行!!!
//F_SETFL命令会完全覆盖文件描述符的所有现有标志位,O_NONBLOC只是众多标志位中的一个
int flag = fcntl(0,F_GETFL); //先获取全部标志位
flag |= O_NONBLOCK; //再添加标志位(非阻塞)
fcntl(0,F_SETFL,flag); //再替换原来标志位
char buf[32] = "";
while(1){
fgets(buf,32,stdin);
if(!buf[0])
printf("看猫眼\n打游戏\n"); //猫眼没人打游戏
else{
printf("不打游戏\n"); //猫眼有人不打游戏
break;
}
memset(buf,0,sizeof(buf));
sleep(2);
}
return 0;
}
三. 信号驱动IO:
定义:
信号驱动 I/O 是一种异步 I/O 模型,它允许应用程序在文件描述符就绪时(数据可读或可写)通过信号接收通知,而不是主动轮询或阻塞等待。
步骤:
//1. 设置文件描述符和进程号提交给内核驱动
//一旦fd又事件响应,则内核驱动会给进程发送一个SIGIO的信号
fcntl(fd,F_SETOWN,getpid()); //让文件描述符能接收信号
//2. 设置异步通知
int flag;
flag = fcntl(fd, F_GETFL); //获取原属性
flag |= O_ASYNC; //给flag设置异步 O_ASNC 异步
fcnl(fd,F_SETFL,flag); //将修改的属性设置进去,此时fd为异步
//3. signal捕捉SIGIO信号
//一旦内核给进程发送SIGI信号,则执行handler
signal(SIGIO,hander);
练习:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <signal.h>
int fd;
void handler(int sig){
char buf[32] = "";
read(fd,buf,sizeof(buf));
printf("%s不打游戏\n",buf);
}
int main(int argc, const char *argv[])
{
//int flag = fcntl(0,F_SETFL,O_NONBLOCK);
fd = open("/dev/input/mouse0",O_RDONLY); //打开鼠标移动文件
if(fd < 0){
perror("open err");
return -1;
}
fcntl(fd,F_SETOWN,getpid()); //让信号发送给当前进程,并用fd接收
//添加异步通知
int flag = fcntl(fd,F_GETFL); //获取fd描述符
flag |= O_ASYNC; //添加描述符
fcntl(fd,F_SETFL,flag); //设置描述符
signal(SIGIO,handler); //听门外是否有声音,不影响打游戏
while(1){
printf("打游戏\n");
sleep(2);
}
close(fd);
return 0;
}
当我鼠标移动,就代表门外有声音
三种基础IO模型比较:
阻塞IO(Blocking IO) | 非阻塞IO(Non-blocking IO) | 信号驱动IO(Signal-driven IO) | |
同步性 | 同步 | 同步 | 异步 |
描述 | 调用IO操作的线程会被阻塞,直到操作完成 | 调用IO操作时,如果不能立即完成操作,会立即返回,线程可以继续执行其他操作 | 当IO操作可以进行时,内核会发送信号通知进程 |
特点 | 最常见、效率低、不耗费cpu, | 轮询、耗费CPU,可以处理多路IO,效率高 | 异步通知方式,需要底层驱动的支持 |
适应场景 | 小规模IO操作,对性能要求不高 | 高并发网络服务器,减少线程阻塞时间 | 实时性要求高的应用,避免轮询开销 |
四.多路复用IO模型(select/poll/epoll)
最后一种情况:
你想要知道是谁回来了,如果是爸妈,停止打游戏,如果是哥哥就出门迎接:让回来的角色来告诉我是谁回来了,但是不影响自己打游戏。
分析:
1.阻塞:一直等待没时间玩。
2.非阻塞(轮询):一直跑来跑去看猫眼,太累了。
3.信号驱动IO:只能识别声音这个信号,不知道谁回来。
4.这个时候就用到多路复用IO:让爸妈或者哥哥回来的时候告诉我谁要回来。
介绍:
1.应用程序中同时处理多路输入输出流,若采用阻塞模式,得不到预期的目的;
2.若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;
3.若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂;
4.比较好的方法是使用I/O多路复用技术。其基本思想是:
先构造一张有关描述符的表(select读表最大1024),然后调用一个函数。
当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。
1.select
特点:
1.一个进程最多只能监听1024个文件描述符
2.select被唤醒之后需要重新轮询,效率相对较低
3.select每次都会清空未发生响应的文件描述符,每次拷贝都需要从用户空间到内核空间,效率低,开销大
编程步骤:
1.先构造一张关于文件描述符的表
2.清空表FD_ZERO
3.将关心的文件描述符添加到表中 FD_SET
4.调用select
5.判断哪一个或者哪些文件描述符发生了事件FD_ISSET
6.做对应的逻辑处理
结合代码更好理解。
函数接口:
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
头文件:#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>功能:实现IO的多路复用
参数:nfds:关注的最大的文件描述符+1
readfds:关注的读表
writefds:关注的写表
exceptfds:关注的异常表
timeout:超时的设置
NULL:一直阻塞,直到有文件描述符就绪或出错
时间值为0:仅仅检测文件描述符集的状态,然后立即返回
时间值不为0:在指定时间内,如果没有事件发生,则超时返回0,并清空设置的时间值
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒 = 10^-6秒 */
};
返回值:成功:时返回准备好的文件描述符的个数
0:超时检测时间到并且没有文件描述符准备好
-1 :失败
注:select返回后,关注列表中只存在准备好的文件描述符
操作表:
void FD_CLR(int fd, fd_set *set); //清除集合中的fd位
void FD_SET(int fd, fd_set *set); //将fd放入关注列表中
int FD_ISSET(int fd, fd_set *set); //判断fd是否产生操作 是:1 不是:0
void FD_ZERO(fd_set *set); //清空关注列表
练习:
基础功能实现
鼠标文件描述符就相当于爸妈回来的声音,输入文件描述符相当于哥哥回来的声音
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int fd = open("/dev/input/mouse0",O_RDONLY); //打开鼠标文件
if(fd < 0){
perror("open err");
return -1;
}
fd_set readfds; //创建读表
FD_ZERO(&readfds); //清空读表
FD_SET(fd,&readfds); //把文件描述符fd放入读表中
FD_SET(0,&readfds); //把文件描述符0方式读表中
if(select(fd+1,&readfds,NULL,NULL,NULL) < 0){ //一直阻塞直到存的文件描述符就绪或者出错
perror("select err");
return -1;
}
char buf[32] = "";
if(FD_ISSET(fd,&readfds)){
ssize_t n = read(fd,buf,sizeof(buf)-1); //读取鼠标文件内容,并预留一个位置给'\0'
buf[n] = '\0'; //最后一个位置存'\0'
printf("mouse:%s\n",buf);
}
if(FD_ISSET(0,&readfds)){ //判断终端是否有数据输入
scanf("%s",buf);
printf("keybord:%s\n",buf);
}
return 0;
}
对鼠标移动和键盘输入进行判断后做出相应反应。
如果想要循环检测,那就得在清空表前加循环,因为select表会交给内核去操作,会改变原表的值,执行完一次就要重新清空添加再监测。
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int fd = open("/dev/input/mouse0",O_RDONLY); //打开鼠标文件
if(fd < 0){
perror("open err");
return -1;
}
fd_set readfds; //创建读表
while(1){
FD_ZERO(&readfds); //清空读表
FD_SET(fd,&readfds); //把文件描述符fd放入读表中
FD_SET(0,&readfds); //把文件描述符0方式读表中
if(select(fd+1,&readfds,NULL,NULL,NULL) < 0){ //一直阻塞直到存的文件描述符就绪或者出错
perror("select err");
return -1;
}
char buf[32] = "";
if(FD_ISSET(fd,&readfds)){
ssize_t n = read(fd,buf,sizeof(buf)-1); //读取鼠标文件内容,并预留一个位置给'\0'
buf[n] = '\0'; //最后一个位置存'\0'
printf("mouse:%s\n",buf);
}
if(FD_ISSET(0,&readfds)){ //判断终端是否有数据输入
scanf("%s",buf);
printf("keybord:%s\n",buf);
}
}
close(fd);
return 0;
}
超时检测:
概念
什么是网络超时检测呢,比如某些设备的规定,发送请求数据后,如果多长时间后没有收到来自设备的回复,那么需要做出一些特殊的处理
比如: 链接wifi的时候,等了好长时间也没有连接上,此时系统会发送一个消息: 网络连接失败;
必要性:
1.避免进程在没有数据时无限制的阻塞;
2.规定时间未完成语句应有的功能,则会执行相关功能
时间结构:
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒 = 10^-6秒 */
};
设置时间,这个结构体是系统自带,直接定义变量使用就可以。
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int fd = open("/dev/input/mouse0",O_RDONLY); //打开鼠标文件
if(fd < 0){
perror("open err");
return -1;
}
fd_set readfds; //创建读表
while(1){
struct timeval tm; //设置时间
tm.tv_sec = 3; //秒
tm.tv_usec = 0; //微妙
FD_ZERO(&readfds); //清空读表
FD_SET(fd,&readfds); //把文件描述符fd放入读表中
FD_SET(0,&readfds); //把文件描述符0方式读表中
if(select(fd+1,&readfds,NULL,NULL,&tm) < 0){ //一直阻塞直到存的文件描述符就绪或者出错
perror("select err");
return -1;
}
else if(select(fd+1,&readfds,NULL,NULL,&tm) == 0){ //返回值为零,超时
printf("time over\n");
}
char buf[32] = "";
if(FD_ISSET(fd,&readfds)){
ssize_t n = read(fd,buf,sizeof(buf)-1); //读取鼠标文件内容,并预留一个位置给'\0'
buf[n] = '\0'; //最后一个位置存'\0'
printf("mouse:%s\n",buf);
}
if(FD_ISSET(0,&readfds)){ //判断终端是否有数据输入
scanf("%s",buf);
printf("keybord:%s\n",buf);
}
}
close(fd);
return 0;
}
2.poll
特点:
1.优化文件描述符的限制,文件描述符的限制取决于系统
2.poll被唤醒之后要重新轮询一遍,效率相对低
3.poll不需要重新构造表,采用结构体数组,每次都需要从用户空间拷贝到内核空间
函数接口:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
头文件:#include <poll.h>
#define _GNU_SOURCE
#include <signal.h>
#include <poll.h>功能:同select相同实现IO的多路复用
参数:fds:指向一个结构体数组的指针,用于指定测试某个给定的文件描述符的条件。
nfds:指定的第一个参数数组的元素个数。
timeout:超时设置
-1:永远等待
0:立即返回
>0:等待指定的毫秒数
struct pollfd
{
int fd; // 文件描述符
short events; // 等待的事件
short revents; // 实际发生的事件
};
struct pollfd fds[NUM];
返回值:成功:返回结构体中 revents 域不为 0 的文件描述符个数
超时:0,超时前没有任何事件发生时,返回 0
失败:-1,失败并设置 errno
编程步骤:
1.创建一个表, 也就是一个结构体数组 struct pollfd fds[100];
2.将关心的描述符条件到表里并赋予事件
3.循环更新表 while(1) {poll(); }
4.逻辑判断: if(fds[i].revents == POLLIN) {}
练习:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/time.h>
#define NUM 2
int main(int argc, const char *argv[])
{
int fd = open("/dev/input/mouse0",O_RDONLY); //打开鼠标文件
if(fd < 0){
perror("open err");
return -1;
}
struct pollfd fds[NUM]; //创建表
fds[0].fd = fd; //添加文件描述符到结构体内
fds[0].events = POLLIN; //设置模式为监听
fds[1].fd = 0;
fds[1].events = POLLIN;
int size = 2;
while(1){ //不需要清空状态
int t = poll(fds,size,2000); //一直阻塞直到存的文件描述符就绪或者出错,设置时间2000毫秒(两秒)
if(t < 0){
perror("select err");
return -1;
}
else if(t == 0){ //返回值为零,超时
printf("time over\n");
continue;
}
char buf[32] = "";
if(fds[0].revents == POLLIN){
ssize_t n = read(fd,buf,sizeof(buf)-1); //读取鼠标文件内容,并预留一个位置给'\0'
buf[n] = '\0'; //最后一个位置存'\0'
printf("mouse:%s\n",buf);
}
if(fds[1].revents == POLLIN){ //判断终端是否有数据输入
fgets(buf,sizeof(buf),stdin);
printf("keybord:%s",buf);
}
}
close(fd);
return 0;
}
超时响应:
鼠标移动:
键盘输入:
注: 因为有revents这个成员,表示实际发生的事件,所以对其他成员没有影响。调用poll后会把实际发生的是按放到revents中,判断它就可以了所以不用清空表
3.epoll
特点
1. 监听的最大的文件描述符没有个数限制
2. 异步IO,epoll当有事件产生被唤醒之后,文件描述符主动调用callback函数(回调函数)直接拿到唤醒的文件描述符,不需要轮询,效率高
3.epoll不需要重新构造文件描述符表,只需要从用户空间拷贝到内核空间一次。
4.总结
select | poll | epoll | |
监听个数 | 一个进程最多监听1024个文件描述符 | 由程序员自己决定 | 百万级 |
方式 | 每次都会被唤醒,都需要重新轮询 | 每次都会被唤醒,都需要重新轮询 | 红黑树内callback自动回调,不需要轮询 |
效率 | 文件描述符数目越多,轮询越多,效率越低 | 文件描述符数目越多,轮询越多,效率越低 | 不轮询,效率高 |
原理 | 每次使用select后,都会清空表 每次调用select,都需要拷贝用户空间的表到内核空间 内核空间负责轮询监视表内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用select,如此循环 | 不会清空结构体数组 每次调用poll,都需要拷贝用户空间的结构体到内核空间 内核空间负责轮询监视结构体数组内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用poll,如此循环 | 不会清空表 epoll中每个fd只会从用户空间到内核空间只拷贝一次(上树时) 通过epoll_ctl将文件描述符交给内核监管,一旦fd就绪,内核就会采用callback的回调机制来激活该fd,epoll_wait便可以收到通知(内核空间到用户空间的拷贝 |
特点 | 一个进程最多能监听1024个文件描述符 select每次被唤醒,都要重新轮询表,效率低 select每次都清空未发生相应的文件描述符,每次都要拷贝用户空间的表到内核空间 | 优化文件描述符的个数限制 poll每次被唤醒,都要重新轮询,效率比较低(耗费cpu) poll不需要构造文件描述符表(也不需要清空表),采用结构体数组,每次也需要从用户空间拷贝到内核空间 | 监听的文件描述符没有个数限制(取决于自己的系统) 异步IO,epoll当有事件产生被唤醒,文件描述符会主动调用callback函数拿到唤醒的文件描述符,不需要轮询,效率高 epoll不需要构造文件描述符的表,只需要从用户空间拷贝到内核空间一次。 |
结构 | 数组 | 数组 | 红黑树+就绪链表 |
开发复杂度 | 低 | 低 | 中 |
常见题目
1.标准IO和文件IO的区别是什么?
2.什么是库,静态库和动态库的区别?
3.什么是孤儿进程?什么是僵尸进程?
4.什么是守护进程?
5.进程和线程的区别?
6.线程的同步和互斥,怎么实现?什么是异步?什么是阻塞? 什么是非阻塞?
7.进程间通讯方式有哪些,分别描述一下?效率最高是哪种?