C 标准I/O库粗略实现

正文同时上在
https://github.com/zhangyachen/zhangyachen.github.io/issues/123

形容一下fopen/getc/putc等C库的简约实现,参考了K&R,但是生几乎沾根据自己明白的粗改变,下面又具体说一样下^_^

描绘就篇稿子要是帮助协调理解下标准I/O库大体是怎么工作的。

fopen与open之间的涉嫌

操作系统提供的接口就为系统调用。而C语言为了为用户更加惠及的编程,自己包裹了一部分函数,组成了C库。而且不同之操作系统对同一个功能提供的体系调用可能两样,在不同的操作系统及C库对用户屏蔽了这些不同,所谓一破编译处处运行。这里open为系统调用,fopen为C库提供的调用。

图片 1

C库对之读写操作封装了一个缓冲区。试想如果用户频繁之指向文本读写少量字符,会频繁之进展系统调用(read函数),而网调用比较耗时。C库自己包裹了一个缓冲区,每次读取特定数据量到缓冲区,读取时先由缓冲区读取,当缓冲区内容为读光后才开展系统调用将缓冲区再次填满。

图片 2

FILE结构体

方我们视一个结构体,里面有5独参数,分别记录了:缓冲区剩余的字符数cnt、下一个字符的职ptr、缓冲区的岗位base、文件访问模式flag、文件讲述符fd。
其间文件讲述称就是系统调用open返回的文书讲述符fd,是int类型。ptr与base上面图被早就亮了。cnt是缓冲区剩余字符数,当cnt为0时,会系统调用read来填满缓冲区。flag为文件访问模式,记录了文件打开方式、是否到文件结尾等。

结构体的切实定义如下,对许调用fopen返回的文书指针FILE *fp = fopen(xxx,r)

typedef struct _iobuf{
    int cnt;                //缓冲区剩余字节数
    char *base;     //缓冲区地址
    char *ptr;      //缓冲区下一个字符地址
    int fd;         //文件描述符
    int flag;             //访问模式
} FILE;     //别名,与标准库一致

结构体中起flag字段,flag字段可以是以下几种的并集:

enum _flags {
    _READ = 1,      
    _WRITE = 2, 
    _UNBUF = 4,  //不进行缓冲
    _EOF = 8,       
    _ERR = 16
};

俺们注意到其中起一个字段,标识不开展缓冲,说明这个种植情形下各个一样破读取和出口都调用系统函数。一个例就是是明媒正娶错误流stderr
: 当stderr连接的是终点设备时,写副一个字符就立即以终端设备显示。
假设stdin和stdout都是带缓冲的,明确的乃是行缓冲。本文不考虑行缓冲,默认都是全缓冲,即缓冲区满了才刷新缓冲区。(详细可参见《UNIX环境高级编程》标准I/O库章节)。

当今咱们可以初始化stdin、stdout与stderr:

FILE _iob[OPEN_MAX] = {
    {0,NULL,NULL,_READ,0},
    {0,NULL,NULL,_WRITE,1},
    {0,NULL,NULL,_WRITE|_UNBUF,2}
};

_ferror/_feof/_fileno

//判断文件流中是否有错误发生
int _ferror(FILE *f){
    return f-> flag & _ERR;
}
//判断文件流是否到达文件尾
int _feof(FILE *f){
    return f-> flag & _EOF;
}
//返回文件句柄,即open函数的返回值
int _fileno(FILE *f){
    return f->fd;
}

_fopen

FILE *_fopen(char *file,char *mode){

    int fd;
    FILE *fp;   

    if(*mode != 'r' && *mode != 'w' && *mode != 'a') {
        return NULL;
    }   

    for(fp = _iob; fp < _iob + OPEN_MAX; fp++) {   //寻找一个空闲位置
        if (fp->flag == 0){ 
            break;
        }
    }
    if(fp >= _iob + OPEN_MAX){
        return NULL;
    }

    if(*mode == 'w'){
        fd = creat(file,PERMS);
    }else if(*mode == 'r'){
        fd = open(file,O_RDONLY,0);
    }else{      //a模式
        if((fd = open(file,O_WRONLY,0)) == -1){
            fd = creat(file,PERMS);
        }
        lseek(fd,0L,2);     //文件指针指向末尾
    }
    if(fd == -1){
        return NULL;
    }

    fp->fd = fd;
    fp->cnt = 0;        //fopen不分配缓存空间
    fp->base = NULL;
    fp->ptr = NULL;
    fp->flag = *mode == 'r' ? _READ : _WRITE;

    return fp;
}

fopen的处理过程:

  • 看清打开模式的合法性。
  • 在_iob中找找一个悠然位置,找不至的讲话说明程序打开的文件数已经到的极其深价值,不能够重复打开新的文件。
  • 设若是w模式,创建一个新文件。如果是r模式,以单读方式打开文件。如果是a模式,首先打开文件,如果打开失败则创造文件,否则通过系统调用lseek将文件指针置到结尾。
  • 本着FILE结构体进行初始化,注意fopen不见面分配缓冲区。

_getc getc的用意是打文本中归下一个字符,参数是文本指针,即FILE:

int _getc(FILE *f){
    return --f->cnt >= 0 ? *f->ptr++ : _fillbuf(f);
}

对比点的图示:当缓冲区中还有剩余字符待读取时,读取该字符并返回,并将缓冲区指针向后移动一个char单位,否则就调用_fillbuf函数填满缓冲区,_fillbuf的回值就是是要读取的字符。

这边产生一个题目:当读取到最终一个字符时,cnt为0,但是ptr已经越界了,如下图:
图片 3

这种气象则是不法的,但是C语言中管:数组末尾之后的首先只要素(即&arr[n],或者arr

  • n)的指针算术运算可以对执行。下面的例证吗是上述的一律种植使场景:

    int a[10] = {1,2,3,4,5,6,7,8,9,10};

    for(int *x = a;x < a + 10; x++){

      printf("%d\n",*x);
    

    }

当for循环到最后一步时,x也针对了a[10],虽然是地下的,但是C语言保证可以对执行,只要非出新如下情况就算ok:

int a[10] = {1,2,3,4,5,6,7,8,9,10};

int *x;
for(x = a;x < a + 10; x++){
    printf("%d\n",*x);
}

*x = 11;     //越界进行值访问

_fillbuf 我们看下_getc中的_fillbuf的实现,_fillbuf是当缓冲区没有得以读取的字符时,通过网调用read读取一定字节的多寡填满缓冲区,供之后采用:

int _fillbuf(FILE *f){

    int bufsize;

        if((f->flag & (_READ | _EOF | _ERR)) != _READ){     //判断文件是否可读
        return EOF;
    }

    bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;

    if(f->base == NULL){            //没有分配过缓冲区
        if((f->base = (char *)malloc(bufsize)) == NULL){
            return EOF;
        }
    }

    f->ptr = f->base;
        int n = read(f->fd,f->ptr,BUFSIZ);      //系统调用read
        if(n == 0){         //到达文件结尾
        f->base = NULL;
        f->cnt = 0;
        f-> flag |= _EOF;
        return EOF;
        }else if(n == -1){      //出错
                f->cnt= 0;
        f->flag |= _ERR;
                return EOF;
    }else{
        f->cnt = --n;
        return *f->ptr++;   
    }
}

_fillbuf的处理过程:

  • 判定文件是否只是读
  • 判断调用read函数时应当读取的字节数,当文件设置了任缓冲时,读取1单字节,否则读取BUFSIZ个字节,BUFSIZ以中定义,定义了当该操作系统条件下缓冲区的特等大小。
  • 判定是否分配了缓冲区(fopen不会见分配缓冲区,会更第一坏调动用getc时分配)。
  • 调用系统函数read。
  • 看清read返回值,分为到达文件结尾、出错和健康读取三种植状况。
  • 健康状态下返回缓冲区第一只字符给getc函数,并以cnt减1。

此注意,到达文件结尾和失误都是回去EOF,区别是前者会将flag的_EOF位置1,后者会以flag的_ERR位置1,上游可以由此feofferror函数进行判定(这片单函数在方已落实了了)。

The character read is returned as an int value.
If the End-of-File is reached or a reading error happens, the function returns EOF and the corresponding error or eof indicator is set. You can use either ferror or feof to determine whether an error happened or the End-Of-File was reached.

_putc

int _putc(int x,FILE *f){
    return --f->cnt >= 0 ? *f->ptr++ = x : _flushbuf(x,f);
}

与_getc的落实相似,将写副的字符放到ptr指向的职,并将ptr向后动一位。当缓冲区满时,调用_flushbuf将缓冲区内容刷新到文件中。

_flushbuf

int _flushbuf(int x,FILE *f){

    if((f->flag & (_WRITE | _EOF | _ERR)) != _WRITE){     //判断文件是否可写
        return EOF;
    }

    int n;
    int bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;
    if(f->base != NULL){
        n = write(f->fd,f->base,f->ptr - f->base);      //判断需要写入多少字节
        if(n != f->ptr - f->base){
            f->flag |= _ERR;
            return EOF;
        }
    }else{
        if((f->base = (char *)malloc(bufsize)) == NULL){
            f->flag |= _ERR;
            return EOF;
        }
    }

    if(x != EOF){
        f->cnt = bufsize - 1;
        f->ptr = f->base;
        *f->ptr++ = x;
    }else{          //当写入EOF时,代表强制刷新缓冲区内容到文件中
        f->cnt = bufsize;
        f->ptr = f->base;
    }
    return x;
}

_flushbuf的处理过程:

  • 判断文件是否可写。
  • 当都分配了缓冲区时,将缓冲区的内容通过网调用write写副文件被。
  • 当没有分配了缓冲区时,分配缓冲区。
  • 认清当写副的字符为EOF时,说明调用此函数的目的吧强制刷新缓冲区,不写副字符。将cnt赋值为BUFSIZ,ptr赋值为缓冲区首地址base。
  • 当写副字符不为EOF时,说明缓冲区已满,需要拿缓冲区刷新到文件中。cnt为BUFSIZE –
    1,将写副的字符x放到到缓冲区之第一格,然后拿ptr向后倒一个char单位。

留神,调用write函数时,写副的字节数不能够写很吗1或者BUFSIZ:

n = write(f->fd,f->base,f->ptr - f->base);      //判断需要写入多少字节

图片 4

要达到图,我们得写入base至ptr之间的数额,而不是BUFSIZ,因为我们恐怕会见强制刷新缓冲区而无是等及缓冲区满了才刷新缓冲区。

还有一些:当我们纪念要强制刷新缓冲区时,第一个参数x该传入什么吧?K&R传递的是字符0,但是本人以为这样见面招缓冲区,所以我之落实是传播一个特殊字符EOF,根据EOF来开不同之拍卖:

if(x != EOF){
    f->cnt = bufsize - 1;
    f->ptr = f->base;
    *f->ptr++ = x;
}else{          //当写入EOF时,代表强制刷新缓冲区内容到文件中
    f->cnt = bufsize;
    f->ptr = f->base;
}

当缓冲区满时,刷新缓冲区后缓冲区的变现:
图片 5
当强制刷新缓冲区时,缓冲区之显现:
图片 6
但论K&R的道来强制刷新缓冲区时,缓冲区底变现:
图片 7

这么见面传染缓冲区,所以我之兑现是传EOF来强制刷新缓冲区。

_fflush _fflush(FILE *f)的来意是将缓冲区内容写副文件。当参数为空时,会刷新有文件:

int _fflush(FILE *f){

        int res = 0;
    if(f == NULL){
        for(int i = 0; i < OPEN_MAX; i++){          //当参数为NULL时,刷新所有的文件流
                       if((f->flag & _WRITE) && (_fflush(&_iob[i]) == -1)){  //有一个出错即返回-1
                           res = EOF;
                       }
        }
        }else{
            if(f->flag & _WRITE){
                    _flushbuf(EOF,f);
            }else{
                res = EOF;
            }
        }

    if(f->flag & _ERR){     //出错
                res = EOF;
    }
        return res;
}

_fflush的处理过程:

  • 认清参数是否也空,如果为空的讲话,遍历_iob数组,将拥有文件流都强制刷新。
  • 要是参数不为空,判断文件是否可写,再调用_flushbuf进行刷新,注意此处传递让_flushbuf的参数是EOF。还有一些便是判_flushbuf是否出错不是判定返回值是否为-1,因为参数为EOF时之返回值也为-1,所以这里用flag
    & _ERR判断是否出错。

顾,这里我们只是针对可写的文书流进行操作,忽略了不过念之公文流:

If the stream was open for reading, the behavior depends on the specific implementation. In some implementations this causes the input buffer to be cleared.

对只念之文书流,不同系统处理的章程不均等,有的系统会清空缓冲区。

_fclose

int _fclose(FILE *f){
    int ret;
    if((ret = _fflush(f)) != EOF){
        free(f->base);
        f->base = NULL;
        f->ptr = NULL;
        f->fd = 0;
        f->flag = 0;        
                f->cnt=0;
    }

    return 0;
}

fclose调用fflush函数,保证在文书关闭前将缓冲区中之情节刷到文件被,并且释放掉缓冲区的内存空间。

_fseek 关于fseek的介绍请圈fseek

int _fseek(FILE *f,long offset,int origin){

    int rc;

    if(f->flag & _READ) {
        if(origin == 1) {
            offset -= f->cnt;
        }
        rc = lseek(f->fd,offset,origin);
                f->cnt = 0;             //将缓冲区剩余字符数清0
    }else if(f->flag & _WRITE) {
                rc = _fflush(f);       //强制刷新缓冲区
        if(rc != EOF) {
            rc = lseek(f->fd,offset,origin);
        }
    }

    return rc == -1 ? EOF : 0;
}

当文件流为可读常,见下图:
图片 8

鉴于起缓冲区的在,我们直觉上的文件指针位置与真实性的文本指针位置是殊之,差了cnt个单位长度。所以当我们安移动offset独长时,真实的公文指针需要活动offset-cnt单单位长度(offset为正数或者负数)。
日后咱们得将cnt置为0,以便下次读取时用缓冲区的多少更新。
当origin为0或者2常,直接调动lseek即可。

如当文件流为可写时,见下图:
图片 9

忠实的文书指针位置及我们直觉上之公文指针位置差了ptr –
base个单位长度,即我们新写副缓冲区的情长度,所以我们直接调用_fflush即可。(K&R中一直调用的write,但是自己当这样没重置ptr指针的位置与cnt,这样的话base与ptr之间的始末会为刷入到文件被少不成)。

当文件是为a模式打开时,fseek无效:

a+     Open for reading and appending (writing at end of file).  The file is created if it does not exist.  The initial file 
       position for  reading is at the beginning of the file, but output is always appended to the end of the file.

_getchar

int _getchar(){
  return _getc(stdin);
}

俺们好发现,_getchar调用的虽是_getc,只不过_getc可以传任意的公文指针,而对_getchar来说,_getc传入的凡stdin,也就是{0,NULL,NULL,_READ,0}

图片 10

  • 当调用getchar时,首先去stdin结构体中之休养存取数据,如果缓存为空,会以_fillbuf中的int n = read(f->fd,f->ptr,BUFSIZ); //系统调用read处阻塞住,等待用户输入字符。
  • 当正规输入(stdin)连接的是极限时,终端I/O会采用专业模式输入处理:对于极端输入以实施吧单位进行处理,对于每个读请求,终端设备输入队列会回去一行(用户输入的字符会缓存在终极输入队列中,直到用户输入一个行定界符,输入队列中的数据会返回给read函数)。
  • 即一行数会缓存在标准I/O缓冲区中,下次调用getchar时见面回来缓冲区第一独字符。当缓冲区数据给读光时,重复上述过程。

_putchar

int _putchar(int x){
    return _putc(x,stdout);
}

我们可以窥见,_putchar调用的即是_putc,只不过_putc可以传任意的文件指针,而针对性_putchar来说,_putc传入的凡stdout,也就算是{0,NULL,NULL,_WRITE,1}

图片 11

  • 调用putchar时,数据会缓存在stdout中之休养冲着。
  • 当stdout的缓冲被装满时,会调用write将数据写入到stdout中,stdout将数据写入到终极设备出口队列中,输出队列将数据写入到终端。

完整代码

#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#define EOF -1
#define BUFSIZ 1024
#define OPEN_MAX 20      //打开的最大文件数
#define PERMS 0666

typedef struct _iobuf{
    int cnt;        //缓冲区剩余字节数
    char *base;     //缓冲区地址
    char *ptr;      //缓冲区下一个字符地址
    int flag;       //访问模式
        int fd;         //文件描述符
} FILE;     //别名,与标准库一致

extern FILE _iob[OPEN_MAX];

//八进制
enum _flags {
    _READ = 01,     
    _WRITE = 02,    
    _UNBUF = 04,     //不进行缓冲
    _EOF = 010,     
    _ERR = 020  
};

FILE _iob[OPEN_MAX] = {
        {0,NULL,NULL,_READ,STDIN_FILENO},
        {0,NULL,NULL,_WRITE,STDOUT_FILENO},
        {0,NULL,NULL,_WRITE|_UNBUF,STDERR_FILENO}
};

#define stdin (&_iob[0])
#define stdout (&_iob[1])
#define stderr (&_iob[2])

int _ferror(FILE *f){
    return f-> flag & _ERR;
}

int _feof(FILE *f){
    return f-> flag & _EOF;
}

int _fileno(FILE *f){
    return f->fd;
}

//返回第一个字符
int _fillbuf(FILE *f){

    int bufsize;

    if((f->flag & (_READ | _EOF | _ERR)) != _READ){     //判断文件是否可读
        return EOF;
    }

    bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;

    if(f->base == NULL){            //没有分配过缓冲区
        if((f->base = (char *)malloc(bufsize)) == NULL){
            return EOF;
        }
    }

    f->ptr = f->base;
    int n = read(f->fd,f->ptr,BUFSIZ);      //系统调用read
    if(n == 0){         //到达文件结尾
        f->base = NULL;
        f->cnt = 0;
        f-> flag |= _EOF;
        return EOF;
    }else if(n == -1){      //出错
        f->cnt= 0;
        f->flag |= _ERR;
        return EOF;
    }else{
        f->cnt = --n;
        return *f->ptr++;   
    }
}

int _flushbuf(int x,FILE *f){

    if((f->flag & (_WRITE | _EOF | _ERR)) != _WRITE){
        return EOF;
    }

    int n;
    int bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;
    if(f->base != NULL){
        n = write(f->fd,f->base,f->ptr - f->base);      //判断需要写入多少字节
        if(n != f->ptr - f->base){
            f->flag |= _ERR;
            return EOF;
        }
    }else{
        if((f->base = (char *)malloc(bufsize)) == NULL){
            f->flag |= _ERR;
            return EOF;
        }
    }

    if(x != EOF){
        f->cnt = bufsize - 1;
        f->ptr = f->base;
        *f->ptr++ = x;
    }else{          //当写入EOF时,代表强制刷新缓冲区内容到文件中
        f->cnt = bufsize;
        f->ptr = f->base;
    }
    return x;
}

/**
 * @brief _fflush
 * @param f
 * @return
 */
int _fflush(FILE *f){

        int res = 0;
    if(f == NULL){
                for(int i = 0; i < OPEN_MAX; i++){          //当参数为NULL时,刷新所有的文件流
                       if((f->flag & _WRITE) && (_fflush(&_iob[i]) == -1)){  //有一个出错即返回-1
                           res = EOF;
                       }
        }
        }else{
            if(f->flag & _WRITE){
                    _flushbuf(EOF,f);
            }else{
                res = EOF;
            }
        }

        if(f->flag & _ERR){     //出错
                res = EOF;
    }
        return res;
}

int _fclose(FILE *f){
    int ret;
    if((ret = _fflush(f)) != EOF){
        free(f->base);
        f->base = NULL;
        f->ptr = NULL;
        f->fd = 0;
        f->flag = 0;     //@TODO
    }

    return 0;
}

int _fseek(FILE *f,long offset,int origin){

    int rc;

    if(f->flag & _READ) {
        if(origin == 1) {
            offset -= f->cnt;
        }
        rc = lseek(f->fd,offset,origin);
                f->cnt = 0;             //将缓冲区剩余字符数清0
    }else if(f->flag & _WRITE) {
                rc = _fflush(f);       //强制刷新缓冲区
        if(rc != EOF) {
            rc = lseek(f->fd,offset,origin);
        }
    }

    return rc == -1 ? EOF : 0;
}


int _getc(FILE *f){
    return --f->cnt >= 0 ? *f->ptr++ : _fillbuf(f);
}

int _putc(int x,FILE *f){
    return --f->cnt >= 0 ? *f->ptr++ = x : _flushbuf(x,f);
}

int _getchar(){
  return _getc(stdin);
}

int _putchar(int x){
    return _putc(x,stdout);
}

FILE *_fopen(char *file,char *mode){

    int fd;
    FILE *fp;   

    if(*mode != 'r' && *mode != 'w' && *mode != 'a') {
        return NULL;
    }   

        for(fp = _iob; fp < _iob + OPEN_MAX; fp++) {   //寻找一个空闲位置
                if (fp->flag == 0){
            break;
        }
    }
    if(fp >= _iob + OPEN_MAX){
        return NULL;
    }

    if(*mode == 'w'){
        fd = creat(file,PERMS);
    }else if(*mode == 'r'){
        fd = open(file,O_RDONLY,0);
    }else{      //a模式
        if((fd = open(file,O_WRONLY,0)) == -1){
            fd = creat(file,PERMS);
        }
        lseek(fd,0L,2);     //文件指针指向末尾
    }
    if(fd == -1){
        return NULL;
    }

    fp->fd = fd;
    fp->cnt = 0;        //fopen不分配缓存空间
    fp->base = NULL;
    fp->ptr = NULL;
    fp->flag = *mode == 'r' ? _READ : _WRITE;

    return fp;
}

int main(int argc,char *argv[]){


    FILE *f = _fopen("zyc.txt","a");    
    /*char c;
      for(int i = 0; i < 10; i++){
      c = _getc(f);
      }*/

    /*for(int i = 0; i < 9; i++){
      _putc('6',f);
      }

      _fseek(f,-5,1);

      for(int i = 0; i < 9; i++){
      _putc('8',f);
      }

      _fclose(f);*/

        int c;
        while((c = _getchar()) != '\n'){
            _putchar(c);            
        }

        _fclose(stdout);

    return 0;
}

上面提到的有些函数在Answer to Exercise 8-3, page
179遭遇起再次详实的实现。

参考资料:

  • C程序设计语言(第2本•新版)第8章
    UNIX系统接口
  • Macros vs
    Functions
  • 自”read”看系统调用的耗时
  • stdin/stdout/stderr的缓冲方式问题
  • Answer to Exercise 8-3, page
    179
  • 缓存与IO
  • UNIX环境高级编程(第3版本)
    第18章 终端I/O