文章目录
一、 阻塞IO
(一)阻塞IO模式
写操作阻塞:
写操作有阻塞的,当缓冲区满时,写操作就会阻塞
直到缓冲区中腾出足够的空间容纳的下本次写操作时,写操作解除阻塞。
读操作阻塞:
当程序执行到读操作时,如果缓冲区有内容,就读走继续向下执行。
如果缓冲区中没有内容,程序就会进入休眠态,直到缓冲区中有内容了,
由内核唤醒当前进程,读走内容继续向下执行。
(二)示例
以有名管道通信为例:同时开启一个读端和三个写端,三个写端分别向各自的管道文件写数据,读端使用循环分别读出管道的内容并打印
read.c
#include <my_head.h>
int main(int argc, char const *argv[])
{
int fd1 = 0, fd2=0, fd3=0;
if(-1 == (fd1 = open("fifo1",O_RDONLY))){
ERR_LOG("open error");
}
if(-1 == (fd2 = open("fifo2",O_RDONLY))){
ERR_LOG("open error");
}
if(-1 == (fd3 = open("fifo3",O_RDONLY))){
ERR_LOG("open error");
}
char buff[128];
int ret=0;
while(1){
if(0 < (ret = read(fd1,buff,sizeof(buff)))){
printf("fifo1:[%s];ret = %d\n",buff,ret);
memset(buff,0,sizeof(buff));
}
if(0 < (ret = read(fd2,buff,sizeof(buff)))){
printf("fifo2:[%s];ret = %d\n",buff,ret);
memset(buff,0,sizeof(buff));
}
if(0 < (ret =read(fd3,buff,sizeof(buff)))){
printf("fifo3:[%s];ret = %d\n",buff,ret);
memset(buff,0,sizeof(buff));
}
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
write.c(三个写端除了打开的管道文件不同,其余相同)
#include <my_head.h>
int main(int argc, char const *argv[])
{
int fd = open("fifo1",O_WRONLY);
char buff[128];
while(1){
scanf("%s",buff);
if(-1 == write(fd,buff,sizeof(buff))){
ERR_LOG("write error");
}
}
close(fd);
return 0;
}
- 注:
- 阻塞IO会相互影响:以上例说明,在写入fifo1并输出后,就会在读fifo2文件内容时阻塞等待,此时如果fifo1或者fifo3中有内容输入也不会处理。
- 此时read进程处于休眠状态,等待用户输入内容,不会占用CPU
二、非阻塞IO
(一)特点
读操作为例:
当程序执行到读操作时,如果缓冲区有内容,就读走继续向下执行。
如果缓冲区中没有内容,程序会立即返回一个错误,而不会休眠。
这种操作一般需要一个循环来不停地测试是否有数据可读,
这种操作十分浪费CPU资源 一般不推荐使用。
有一部分函数 是自带非阻塞标志位的
如:recv和recvfrom的 MSG_DONTWAIT
O_NONBLOCK
、waitpid的 W_NOHANG
(二)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 就是非阻塞的标志位
...:可变参
返回值:
F_GETFL 成功 返回文件状态信息 失败 -1 重置错误码
(三)示例
将文件设置为非阻塞。
read.c
#include <my_head.h>
int main(int argc, char const *argv[])
{
int fd1 = 0, fd2=0, fd3=0;
int flag=0;
if(-1 == (fd1 = open("fifo1",O_RDONLY))){
ERR_LOG("open error");
}
flag=fcntl(fd1,F_GETFL);
flag |= O_NONBLOCK;
if(-1 == fcntl(fd1,F_SETFL,flag)){
ERR_LOG("fcntl error");
}
if(-1 == (fd2 = open("fifo2",O_RDONLY))){
ERR_LOG("open error");
}
flag=fcntl(fd2,F_GETFL);
flag |= O_NONBLOCK;
if(-1 == fcntl(fd2,F_SETFL,flag)){
ERR_LOG("fcntl error");
}
if(-1 == (fd3 = open("fifo3",O_RDONLY))){
ERR_LOG("open error");
}
flag=fcntl(fd3,F_GETFL);
flag |= O_NONBLOCK;
if(-1 == fcntl(fd3,F_SETFL,flag)){
ERR_LOG("fcntl error");
}
char buff[128];
int ret=0;
while(1){
if(0 < (ret = read(fd1,buff,sizeof(buff)))){
printf("fifo1:[%s];ret = %d\n",buff,ret);
memset(buff,0,sizeof(buff));
}
if(0 < (ret = read(fd2,buff,sizeof(buff)))){
printf("fifo2:[%s];ret = %d\n",buff,ret);
memset(buff,0,sizeof(buff));
}
if(0 < (ret =read(fd3,buff,sizeof(buff)))){
printf("fifo3:[%s];ret = %d\n",buff,ret);
memset(buff,0,sizeof(buff));
}
sleep(1);
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
- 注:
- 非阻塞IO不会相互影响:以上例说明,不会再阻塞等待用户输入,在哪个管道读到数据,就直接输出。
- 此时read进程处于运行状态,不会阻塞等待用户输入内容,会一直占用CPU资源进行遍历
三、IO多路复用
(一)实现原理
构造一个关于文件描述符的表,将所有要监视的文件描述符都放在这个表中
将这个表交给一个函数:select / poll / epoll
这个函数默认也是阻塞的,直到监视的文件文件描述符中有一个或多个准备就绪
这个函数会返回,返回时,会告诉调用者哪些文件描述符已经就绪,
调用者对已经就绪的文件描述符操作,就不会阻塞了。
(二)select函数
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:
实现多路IO复用
参数:
nfds:要监视的最大文件描述符+1
readfds:要监视的读文件描述符集合(监视它是否可读) 不关心可以传NULL
writefds:要监视的写文件描述符集合(监视它是否可写) 不关心可以传NULL
exceptfds:要监视的异常文件描述符集合 不关心可以传NULL
timeout:超时时间 传 NULL 表示永久阻塞 直到有就绪的文件描述符为止
返回值:
成功 返回就绪的文件描述符的个数
void FD_CLR(int fd, fd_set *set);//将文件描述符在集合中删除
int FD_ISSET(int fd, fd_set *set);//测试文件描述符是否还在集合中
//在 返回真 不在 返回假
void FD_SET(int fd, fd_set *set);//将文件描述符添加到集合中
void FD_ZERO(fd_set *set);//清空集合
- 注:
- select只能监视小于FD_SETSIZE(1024)的文件描述符
- 我们一般只关心读文件描述符集合 其他两个传NULL即可
- select每次返回时会将没有就绪的文件描述符在集合中擦除
所以在循环中使用select时每次需要重置集合
(三)使用示例
#include <my_head.h>
int main(int argc, char const *argv[])
{
int fd1 = 0, fd2=0, fd3=0;
if(-1 == (fd1 = open("fifo1",O_RDONLY))){
ERR_LOG("open error");
}
if(-1 == (fd2 = open("fifo2",O_RDONLY))){
ERR_LOG("open error");
}
if(-1 == (fd3 = open("fifo3",O_RDONLY))){
ERR_LOG("open error");
}
//创建
fd_set readfds;//用来保存需要监视的文件描述符的母本
fd_set tempfds;
int max_fd=0;
FD_ZERO(&readfds);
FD_ZERO(&tempfds);
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];
int num = 0;
while(1){
tempfds = readfds;
num = select(max_fd+1,&tempfds,NULL,NULL,NULL);
for(int i=0; i<max_fd+1 && num; i++){
if(FD_ISSET(i,&tempfds)){
memset(buff,0,sizeof(buff));
if(0 < read(i,buff,sizeof(buff))){
printf("fd%d:[%s]\n",i,buff);
}
num--;
}
}
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
(四)关于select函数的理解
- fd_set 实际是一个长度为1024的bit数组,
typedef struct{
long ss[16]; //8*8*16=1024
}fd_set;
但是C语言最小单位是字节,因此才用这种方式表示;
- select的集合是基于数组实现的,最多监视1024个文件
- 本质是一个数组,包一个结构体是为了复制方便
- 四个宏定义的本质就是封装好的方便用户操作集合的位运算