UNXI系统级的IO函数,在某些情况下,传送的字节数比用户要求的少,会出现不足值(short count)主要原因为:
- ①读取时遇到EOF:文件的大小不足以填充read需要读取的字节数,那么返回不足值0表示提前遇到EOF
- ②从终端读取文本行:read函数每次传送一个文本行,返回的不足值表示文本行的大小
- ③读写网络SOCKET:由于网络延迟等原因,对Linux套接字执行read可能返回不足值(数据还没有接受完)
一种策略是反复调用read函数,直到将所需要的字节全部读取完毕。另一种思路采取缓冲区方式:
带缓冲区输入函数:允许应用程序高效的从文件中读取文本和二进制数据,避免频繁读取和以及解决不足问题。
RIO robust IO健壮IO读取包,其通过实现一个用户级的缓冲区来处理不足值,以及减少频繁的系统级IO的调用。
【rio_t】
一个用户级的内存缓冲区与一个打开的文件描述符关联:
#define RIO_BUFFERSIZE 8192
typedef struct {
int rio_fd; //关联的描述符fd
int rio_cnt; //剩余可读字节数
char* rio_bufptr; //当前读开始位置
char rio_buf[RIO_BUFFERSIZE];
}rio_t;
//初始化缓冲区 关联一个文件描述符fd
void rio_readinitb(rio_t* rp,int fd) //初始化一个rio_t结构的读缓冲区
{
rp->rio_bufptr=rp->rio_buf;//初始化读指针为 buf起点
rp->rio_cnt=0; //未读字节数
rp->rio_fd=fd;
}
【rio_readn rio_writen】
实现无缓冲区的读写,主要应用于写入文件:
ssize_t rio_readn(int fd,void* usrbuf,size_t n)
{
size_t nleft=n;
ssize_t nread;
char* bufp=(char*)usrbuf;
while(nleft>0)
{
if((nread=read(fd,bufp,nleft))<0)
{
if(errno==EINTR)
nread=0;
else
return -1;
}
else if(nread==0)
break;
nleft-=nread; //将fd的数据直接传送到用户区
bufp+=nread;
}
}
ssize_t rio_writen(int fd,void* usrbuf,size_t n)
{
size_t nleft=n;
ssize_t nwritten;
char* bufp=(char*)usrbuf;
while(nleft>0)
{
if((nwritten=write(fd,bufp,nleft))<=0)
{
if(errno==EINTR)
nwritten=0;
else
return -1;
}
nleft-=nwritten;
bufp+=nwritten; //指针右移
}
}
核心函数rio_read
ssize_t rio_read(rio_t* rp,char* usrbuf,size_t n)
- rp:需要读取的缓冲区(与一个打开文件描述符关联)
- usrbuf:读取到的用户内存位置
- n:读取的字节数
其基本实现思路为:
static ssize_t rio_read(rio_t* rp,char* usrbuf,size_t n)
{
int cnt;
while(rp->rio_cnt<=0)
{
//当缓冲区可读数量为0时,调用read读取到缓冲区
rp->rio_cnt=read(rp->rio_fd,rp->rio_buf,sizeof(rp->rio_buf));
if(rp->rio_cnt<0)
{
if(errno!=EINTR)
return -1; //读取发生错误
}
else if(rp->rio_cnt==0)
return 0;
else
rp->rio_bufptr=rp->rio_buf;//缓冲区填充成功 重置起始指针
}
cnt =n;
if(rp->rio_cnt<n)
{
cnt=rp->rio_cnt;//可读的字节数 取较小值
}
memcpy(usrbuf,rp->rio_bufptr,cnt);
rp->rio_bufptr+=cnt;
rp->rio_cnt-=cnt;//可读字节数减少
return cnt;//返回读取成功的字节数
}
由此可见RIO_READ和Linux系统级的Read函数具有类似的含义。出错时,返回-1.在遇到EOF时,返回不足值0 。
基于rio_read的带缓冲区实现,可以进一步实现:
ssize_t rio_readlineb(rio_t *rp,void* usrbuf,sszie_t maxlen)
该函数读取一行的数据,它会调研rio_read函数从rio缓冲区一个个读取字符,可能发生两种情况:
- 提前遇到\n 换行符,那么返回已经读取字节数的串
- 读取到maxlen-1字节的字符,末尾添加NULL 返回这一行
//带缓冲区版本的readline实现
ssize_t rio_readlineb(rio_t* rp,void* usrbuf,size_t maxlen)
{
int n;
int rc;
char c;
char *bufp=(char*)usrbuf;
for(n=1;n<maxlen;n++) //填充maxlen-1个字符
{
if((rc=rio_read(rp,&c,1))==1) //每次从缓冲区中读取一个字符
{
*bufp++=c;
if(c=='\n')
{
//遇到换行符 提前结束
n++;
break;
}
}
else if(rc==0)
{
if(n==1)
return 0;
else
break;
} else
return -1;
}
*bufp=0; //最后一个填充NULL
return n-1;
}
而借助rio_read实现的rio_readnb实现缓冲区的读取n个字符:
ssize_t rio_readnb(rio_t* rp,void* usrbuf,size_t n)
{
ssize_t nleft=n;
ssize_t nread;
char* bufp=(char*) usrbuf;
while(nleft>0)
{
if((nread=rio_read(rp,bufp,nleft))<0)
{
return -1;
}
else if(nread==0)
break;
nleft-=nread;
bufp+=nread;
}
return (n-nleft);
}
【实验与演示】
int main()
{
int n;
rio_t rio;
char buf[1024];
int file_fd=open("rio_test.txt",O_RDWR|O_CREAT,S_IWOTH);
rio_readinitb(&rio,STDIN_FILENO);//初始化rio缓冲区 与 STDIN关联
while((n=rio_readlineb(&rio,buf,1024))!=0)
rio_writen(file_fd,buf,n);//无缓冲区 写入到STDOUT
}
通过关联标准的输入到用户级缓冲区RIO
不断的读取行,并通过直接复制的方式IO输出到 fd打开的文件中:
g++ rio_readlineb.cpp -o rio_readlineb
打开相应的文本文件(权限设定):
chmod 777 rio_test.txt
rio_test.txt 的内容:
RIO是带有用户缓冲区的封装实现,相较于直接调用read函数的优势有以下几点:
①:当缓冲区有可读字节时,直接返回相应大小的字节,无需执行系统调用Read ,避免陷入内核
②:当缓冲区没有可读字节时,执行一次Read,并且Read一次申请读取缓冲区大小的字节,提高了存取效率。