在介绍运行库的过程中,强调过运行库是具体语言实现的程序和操作系统之间的抽象层。经验表明,任何系统级的软件工程,IO功能的封装历来是最具有挑战性的。以下以Windows下MSVC CRT运行库中封装的文件读取函数fread()的实现思路为demo,演示运行库的功能封装。
#define size_t unsigned int
size_t fread(
void * buffer,
size_t elementSize,
size_t count,
FILE *stream
)
//fread函数功能是从文件流stream中读取count个大小为elementSize个字节的数据,存储在buffer,返回值为实际读取的字节数
Windows API的ReadFILE()
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberofBytesToRead,
LPDWORD lpNumberofBytesRead,
LPOVERLAPPED lpOverlapped
);
//hFile为要读取的文件句柄,对应的是fread函数中stream参数
//lpBuffer是存储缓冲区的其实地址,对应fread函数中的buffer
//nNumberofBytesToRead代表要读取字节总数,等于fread函数中count * elementSize
//lpNumberOfBytesRead代表一个指向DWORD类型的指针,用来表示读取了多少个字节
//lpOverlapped没用
/*
fread函数的调用顺序
fread -> fread_s -> _fread_nolock_s -> _read
MSVC的fread函数定义在crt/fread.c
*/
size_t __cdecl fread(
void *buffer,
size_t elementSize,
size_t count,
FILE *stream
)
{
/* assumes there is enough space in the destination buffer */
return fread_s(buffer, SIZE_MAX, elementSize, count, stream);
}
/* define locking/unlocking version */
//fread_s中的s表示safe,相比于fread函数多了bufferSize形参,即用于指定参数buffer的大小
//在fread_s中通过指定bufferSize来防止越界
size_t __cdecl fread_s(
void *buffer,
size_t bufferSize,
size_t elementSize,
size_t count,
FILE *stream
)
{
size_t retval = 0;
if (elementSize == 0 || count == 0)
{
return 0;
}
// only checking for stream == NULL here for _lock_str()
// the rest of the validation is in _fread_nolock_s()
if (stream == NULL )
{
if (bufferSize != SIZE_MAX)
{
memset(buffer, _BUFFER_FILL_PATTERN, bufferSize);
//作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法,函数原型void* memset(void* buffer, int c, size_t size)
}
_VALIDATE_RETURN((stream != NULL), EINVAL, 0);
}
_lock_str(stream); //使用_lock_str()对文件进行加锁
__try
{
/* do the read; _fread_nolock_s will make sure we do not buffer overrun */
retval = _fread_nolock_s(buffer, bufferSize, elementSize, count, stream);
}
__finally
{
_unlock_str(stream);//对文件所加的锁进行解锁
}
return retval;
}
typedef struct _iobuf{
char *_ptr;//指向缓冲的第一个未使用的字节
int _cnt;//记录文件缓冲区的剩余未读字节数,一般为了提高效率,减少I/O次数,会
//为文件对象配备提前的文件缓存,这些缓存都是提前存储了数据对象的
char *_base;//指向一个字符数组,即这个文件对应的缓冲
int _flag;//记录FILE结构所代表的打开文件的一些属性
/*
这一位的标志总共有3个标志
#define _IOYOURBUF 0x0100 //代表是用户通过setbuf手动为该FILE绑定的buffer
#define _IOMYBUF 0x0008 //代表这个文件使用内部的缓冲
#define _IONBF 0x0004 //代表文件使用一个单字节的缓冲,在这种情况下,
将不启用base指向的字符数组,而直接用下面_charbuf作为单字节
*/
int _file;
int _charbuf;
int _bufsiz;//记录着这个文件缓存的大小
char *_tmpfname;
}FILE;
/* define the normal version */
//_fread_nolock_s函数的内容实现,是fread读写的实际操作函数
size_t __cdecl _fread_nolock_s(
void *buffer,
size_t bufferSize,
size_t elementSize,
size_t num,
FILE *stream
)
{
char *data; /* point inside the destination buffer to where we need to copy the read chars 目标存储buffer下一个写入的地方指针*/
size_t dataSize; /* space left in the destionation buffer (in bytes) 目标存储buffer当前还剩下的可用字节数*/
size_t total; /* total bytes to read 想要读取的内容的字节总数*/
size_t count; /* num bytes left to read 当前还剩下没读取的字节数*/
unsigned streambufsize; /* size of stream buffer记录文件缓冲区的大小 */
unsigned nbytes; /* how much to read now 单次要读取的字节数*/
unsigned nread; /* how much we did read 记录单次读取到的字节数*/
int c; /* a temp char */
/* initialize local vars */
data = buffer;
dataSize = bufferSize;
if (elementSize == 0 || num == 0)
{
return 0;
}
/* validation */
_VALIDATE_RETURN((buffer != NULL), EINVAL, 0);
if (stream == NULL || num > (SIZE_MAX / elementSize))
{
if (bufferSize != SIZE_MAX)
{
memset(buffer, _BUFFER_FILL_PATTERN, bufferSize);
}
_VALIDATE_RETURN((stream != NULL), EINVAL, 0);
_VALIDATE_RETURN(num <= (SIZE_MAX / elementSize), EINVAL, 0);
}
count = total = elementSize * num;
if (anybuf(stream)) //这里的anybuf是定义在file2.h中的宏
//#define anybuf(s) ( (s)->_flag & (_IOMYBUF|_IONBF|_IO_YOURBUF)
//即判断FILE * stream中是否已经被分配了缓冲
{
/* already has buffer, use its size */
streambufsize = stream->_bufsiz;
}
else
{
/* assume will get _INTERNAL_BUFSIZ buffer */
streambufsize = _INTERNAL_BUFSIZ;//FILE *stream没有使用缓冲,则启动默认缓冲,这个
//缓冲的大小为_INTERNAL_BUFSIZ,即4096字节
}
/* 参数初始化完成,进入循环读取的步骤 */
while (count != 0) {
/* if the buffer exists and has characters, copy them to user
buffer */
if (anybuf(stream) && stream->_cnt != 0)//FILE* stream启用了缓冲,并且缓冲区剩余
//未读字节数总数不为0,则可以先把缓存中数据读取出来,然后再去继续调用磁盘I/O
{
if(stream->_cnt < 0)//如果stream缓冲区可用字节数为负数,则通过断言退出
{
_ASSERTE(("Inconsistent Stream Count. Flush between consecutive read and write", stream->_cnt >= 0));
stream->_flag |= _IOERR;
return (total - count) / elementSize;
}
/* how much do we want? */
nbytes = (count < (size_t)stream->_cnt) ? (unsigned)count : stream->_cnt;
if (nbytes > dataSize)
{
if (bufferSize != SIZE_MAX)
{
memset(buffer, _BUFFER_FILL_PATTERN, bufferSize);
}
_VALIDATE_RETURN(("buffer too small", 0), ERANGE, 0)
}
memcpy_s(data, dataSize, stream->_ptr, nbytes);//将文件stream绑定的磁盘缓冲内
//批量读入本次fread指定的读缓冲中
/* update stream and amt of data read */
count -= nbytes; //更新剩余要读字节总数
stream->_cnt -= nbytes; //磁盘
stream->_ptr += nbytes;
data += nbytes; //缓冲区可使用区域起始地址右移
dataSize -= nbytes;//缓冲区可用区域字节总数减少
}
//如果文件对应的缓存已经读取完毕,仍没有满足读取长度要求,则进入调用_read()进行磁盘I/O
//剩余要读取的字节总数大于文件配备的缓存大小
else if (count >= streambufsize)
{
//尽可能多地一次性读取时文件缓存尺寸整数倍的数据,直接通过_read()输入到最终目标buffer
nbytes = ( streambufsize ? (unsigned) (count -count % bufsize) : (unsigned)count );
nread = _read(_fileno(stream), data, nbytes);
//_read()用于直接从文件中读取数据,跳过文件缓存,直接向目标buffer拷贝整数倍缓存块
//大小的字节数
if (nread == 0) {
/* end of file -- out of here */
stream->_flag |= _IOEOF; //到了文件的末尾
return (total - count) / elementSize; //返回读取的元素个数
}
else if (nread == (unsigned)-1) {
/* error -- out of here */
stream->_flag |= _IOERR;
return (total - count) / elementSize;
}
/* update count and data to reflect read */
count -= nread; //要读取的剩余字节总数更新
data += nread; //目标存储buffer中下一个空白字节的位置右移
dataSize -= nread;//目标存储buffer可用剩余字节数更新
}
else //如果要读的字节总数不大于文件配备的缓存大小,则先填充stream的缓存,然后从缓存中读取要用的数据
{
/* less than streambufsize chars to read, so call _filbuf to
fill buffer */
if ((c = _filbuf(stream)) == EOF) {
/*文件缓存填充_filbuf(stream)的核心过程还是调用_read():
stream->_cnt = _read(_fileno(stream), stream->_base, stream->_bufsiz);
#define _fileno(stream) ( stream -> _file) 即返回该FILE指针对应的文件在打 开文件列表中的的下标_file
*/
/* error or eof, stream flags set by _filbuf */
return (total - count) / elementSize;
}
/* _filbuf returned a char -- store it */
if (dataSize == 0)
{
if (bufferSize != SIZE_MAX)
{
memset(buffer, _BUFFER_FILL_PATTERN, bufferSize);
}
_VALIDATE_RETURN(("buffer too small", 0), ERANGE, 0)
}
*data++ = (char) c;
--count;
--dataSize;
/* update buffer size */
streambufsize = stream->_bufsiz;
}
}
/* 如果读取成功,正常执行,则返回读取元素个数 */
return num;
}
/*_read()函数位于crt/src/read.c
大致内容如下*/
int __cdecl _read(int fh, void *buf, unsigned cnt )
{
int bytes_read; //已经拷贝的字节数
char *buffer; //迁移拷贝数据的目标区域指针
int os_read; //bytes read on OS call调用Windows API ReadFile获取的字节数
char *p, *q; //pointers into buffer
char peekchr; //peek-ahead character为了判断CR LF组合,预读取的字符存储位置
ULONG filepos; //file position after seek文件指针,代表在整个文档里,下一个未被读取的字节位置
ULONG dosretval; //o.s. return value Windows操作系统返回的错误值,_read()要将这些错误值转换成crt运行库的错误版本
bytes_read = 0;
buffer = buf;
//处理文件配备的单字节缓冲情况
/*根据文件下标fh从打开文件列表中找出该文件对应的ioinfo信息,是句柄osfhnd、属性osfile、管道单字符缓冲pipech,该判断语句使得该部分功能只对设备FDEV和管道文件FPIPE有效。
#define _osfile(i) ( _pioinfo(i) -> osfile)
#define _pipech(fh) ( _pioinfo(i) -> pipech)
ioinfo结构提供了衣蛾单字节缓冲pipech用于处理一些特殊情况
pipech = LF即代表\n,代表此时单字符管道缓冲无效,这样设计的原因,在于文件对象是管道文件或是设备数据时的用途下,永远不会被赋值为LF
*/
if ((_osfile(fh) & (FPIPE|FDEV)) && _pipech(fh) != LF)
{
*buffer++ = _pipech(fh);
++bytes_read;
--cnt;
_pipech(fh) = LF;
}
//处理完单字符通道缓冲问题,正式进入调用Windows API ReadFile()的步骤
if ( !ReadFile( (HANDLE)_osfhnd(fh), buffer, cnt, (LPWORD)&os_read, NULL) )
{
if ( (dosretval = GetLastError()) == ERROR_ACCESS_DENIED )
{
errno = EBADF;
_doserrno = dosretval;
return -1;
}
else if ( dosretval == ERROR_BROKEN_PIPE )
{
return 0;
}
else
{
_dosmaperr(dosretval);
return -1;
}
}
/*文件换行操作--兼容性调整
Windows系统下的文本文件回车的存储方式是0x0D 0x0A (CR LF),即字符串表达形式\r\n
Linux/Unix系统回车用\n表示,
Mac OS系统回车用\r表示
但是在C语言中,回车始终是用\n来表示的,故而需要在读取文件时,将ReadFile返回的buffer中内容遍历一下,将回车符都统一转换成\n
*/
if (_osfile(fh) & FTEXT) //检查文件是否是以文本模式打开的,如果不是,就什么都不需要处理
{
//当本次读文件的第一个字符是一个LF,意味着一个\r\n可能正好被两次读给分割了
//这时需要在_osfile(fh)的osfile字段中添加FCRLF,用于表示这种可能的情况
if ( (os_read != 0) && (*(char *)buf == LF) )
_osfile(fh) |= FCRLF; //FCRLF(0x04)在文本模式中,行缓冲已遇到回车符
else
_osfile(fh) &= ~FCRLF;
/*处理p当前指向的字符,p和q后移
1.*p是CRTL-Z:表明文本已经结束,退出循环
2.*p是CR(\r)之外的字符:把p指向的字符赋值到q指向的位置,p和q各自后移一个字节(*q++ = *p++).
3.*p是CR(\r),且*(p+1)不是LF(\n),则依旧同2
4.*p是CR(\r)且*(p+1)是LF(\):则p后移2个字节,将q指向的位置写成LR(\n),q后移一个字节(p += 2; *q++ = '\n';)
*/
p = q = buf;
while (p < (char *)buf + bytes_read)
{
if (*p == CTRLZ) {
if ( !(_osfile(fh) & FDEV) ) //非设备文件,检查到CTRLZ,在osfile字段表明一下,文件已经到达末尾;若是设备文件,则直接退出
_osfile(fh) |= FEOFLAG;//FEOFLAG(0x02)已到达文件末尾
break;
}
else if (*p != CR) //没有遇到CR,直接复制
*q++ = *p++;
else{
//遇到CR,检查下一个字符是否是LF
if (p < (char *)buf + bytes_read - 1){
//CR不处于缓冲区域的末尾,即缓冲区后面至少还有一个字符
if (*(p+1) == LF){
p += 2;
*q++ = LF;
}
else
*q++ = *p++;
}
/*CR已经处于缓冲区的末尾了,如果想查看有没有CR LF组合的可能性
需要再从文件对象中读取一个字符,这时就要分为两种情况了,终于涉及了ioinfo结构中那个神秘的字段pipech
普通的磁盘文件一种(存在那里可以随时调用,反复回看,只要回调文件指针1个字节就可以了,所以额外读取的这个字符没必要专门利用pipech存起来),另一种是设备文件和管道文件(这类文件是不能回退的,所以一旦你要额外读取一个字符,这个字符显然不能丢了,所以这个字符会被存放在pipech字段)*/
else {
++p;
dosretval = 0;
if ( !ReadFile( (HANDLE)_osfhnd(fh), &peekchr, 1, (LPDWORD)&os_read, NULL ) )
dosretval = GetLastError();
if (dosretval != 0 || os_read == 0){ //这次额外的读取出现错误或者本次读取并没有读到内容,则默认为CR LF组合是不会出现了,保留CR
*q++ = CR;
}
else {
if (_osfile(fh) & (FDEV|FPIPE)){
//管道或设备文件
if (peekchr == LF)
*q++ = LF;
else {
//如果预读的字符不是LF,则使用pipech存储字符
*q++ = CR;
_pipech(fh) = peekchr;
}
}
else {
//普通文件
if (q == buf && peekchr == LF) {
*q++ = LF;
}
else {
//如果预读的字符不是LF,则使用seek回退文件指针
filepos = _lseek_lk(fh, -1, FILE_CURRENT);
if (peekchr != LF)
*q++ = CR;
}
}
}
}
}
}
bytes_read = (int)(q- (char *)buf);
}
}