运行库:Windows下MSVC CRT运行库封装fread()函数解析

在介绍运行库的过程中,强调过运行库是具体语言实现的程序和操作系统之间的抽象层。经验表明,任何系统级的软件工程,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);
    }
}

你可能感兴趣的:(Linux内核,运行库,windows,fread函数)