如何理解Linux下的文件描述符 ?以及Linux下的重定向与缓冲区这一概念???

发布于:2024-04-29 ⋅ 阅读:(48) ⋅ 点赞:(0)

目录

前言

1.文件是什么?

2.文件的相关操作是什么?

3.访问文件本质是谁在访问?

4.为什么有Linux下一切皆文件这一说法?

关于C语言的文件接口与Linux下系统文件接口

C语言的文件接口

Linux下系统文件接口

fd的理解 与FILE关系

fd的理解

fd与FILE关系

fd的底层理解

fd的分配规则

fd与重定向

C标准库缓冲区

缓冲区是什么?

为什么要有缓冲区?

缓冲区刷新策略是什么?

缓冲区是谁维护的???

缓冲区在哪里??


前言

        开始正式内容之前,会先大致描述一下我们在说些什么。所有是内容都是围绕文件描述符这一概念进行展开,但是并不会直接切入这一概念,因为这一概念并不是很难,但是因为文件描述符而涉及的周边概念是很广泛的,我会由周边涉及到的知识逐步递进,逐渐引入文件描述符。

首先我们先解答下面的疑问。

1.文件是什么?

请参考C语言文件操作 ,总结一下就是,文件 = 内容 + 属性(属性也是数据)

2.文件的相关操作是什么?

无外乎两种:a.对内容的操作 b.对属性的操作

3.访问文件本质是谁在访问?

进程。为什么?

用户访问文件,会先写代码 ---> 编译  ---> 形成可执行程序  ---> 运行 ---> 通过接口进行访问,而运行起来的程序不就是进程嘛。

文件存储在哪里?磁盘(磁盘是硬件),用户可以直接向硬件进行写入嘛?答案是肯定不可以的,只有操作系统才有权限向硬件进行写入。

如果普通用户也想向硬件中进行写入呢?比如说我们就想直接修改磁盘上的文件,怎么办???此时,就需要操作系统提供文件类的系统调用接口了。

要注意,这样的接口只有一套,因为系统只有一个。什么意思呢?Linux关于文件类的系统接口只有一套,但是C/C++/Java/Python...都有各自关于文件类的接口,为什么它们不直接使用系统接口呢?

一是因为系统接口比较难:语言上对这一套系统接口进行封装,可以让用户更好的使用。也就导致了不同的语言,有不同的语言级别的文件访问接口(都不一样),但是,底层都是一样的。
 

二是因为跨平台:如果语言不提供对文件的系统接口的封装,那么所有的访问文件的操作,都必须直接使用系统的接口,而使用语言的用户访问文件一旦使用系统接口,编写所谓的文件代码,那么就无法在其他平台中直接运行!
 

4.为什么有Linux下一切皆文件这一说法?

感性的认识:

文件:可以read , write

显示器: printf 、 cout -->  也可以理解为是一种 write

键盘 :scanf、cin ---> 也可以理解为是一种  read

站在程序的角度,读取/写入的数据需要加载至内存(input)

站在内存的角度,需要向文件/显示器写入/读取这些数据(output)

普通文件 ---> fopen/fread  ---> 进程的内部(内存 )---> fwrite  ---> 文件内部  (前面的部分是input,后面的部分是output)

那么什么叫做文件呢?

站在系统的角度,能够被input读取,或者能够output写出的设备就叫做文件!
狭义文件:普通的磁盘文件
广义上的文件:显示器,键盘,网卡,声卡,显卡,磁盘,几乎所有的外设,都可以称之为文件
结论就是所有的硬件也都是可以被看作成文件,都是可读可写的,但是在实现的时候,键盘〈只能读,不能写)显示器(只能写,不能读)
 

那么经过上面的铺垫,欢迎阅读下面的内容。

        注意,下面内容很长很长,涉及面很宽泛,欢迎恰饭。


关于C语言的文件接口与Linux下系统文件接口

C语言的文件接口

a.什么叫做当前路径???

当一个进程运行起来的时候,每一个进程都会记录自己当前所处的工作路径
 

示例1:

"w" :打开一个文件进行写入,如果不存在该文件,创建之

示例:

其实想说明的是,无论我是哪个路径,运行上述的程序,如果该文件不存在,那么就会在当前路径下创建该文件。问题是,系统是如何得知当前路径的???

Windows(Vs)下,一般源文件和可执行程序是不在一个路径的,编译链接的时候,会生成一个debug文件夹,可执行程序是放在这个文件夹的

 Linux下当前路径是根据可执行程序在改变(有些不准确)

Linux下,进程在从上往下运行的时候,当没有看到这个文件,就会去调用系统接口,进行创建


        所以我们能解释这个问题:为什么在当前路径运行可执行程序,形成的临时文件就在当前路径,在上级路径运行可执行程序,形成的临时文件就在上级路径 。 因为:进程运行也在这个路径,所以创建的临时文件当然也在这个路径 

小结①(上述所讲的是关于“当前路径”这一个知识点)


示例:

输出:

那么,问题来了,请问往文件中写入字符串的时候要不要加上'\0’(C语言中字符串结束标志是'\0',这里'\n'只是一个普通字符)

这里要不要加1?答案是不需要。因为'\0'是C语言的规定,文件不需要遵守。文件存储是在磁盘,属于系统的管理范围,不属于语言的范畴
 

恶补一个小知识点:可以利用 > 重定向来清空文件

因为C语言规定,w写入,会先清空文件再进行写入

> :重定向,先打开文件,然后清空文件,再进行重定向

>:可以理解为是一条指令,底层是C语言“w”形式实现的


示例:

小结②(上述所讲的是a.关于C语言文件操作是不需要考虑'\0'的,因为'\0'不属于系统的规定; b.利用 > 重定向可以清理文件)


函数:

  • fopen
FILE *fopen( const char *filename, const char *mode );
  • fwrite
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
  • fgets

char *fgets( char *string, int n, FILE *stream );
  • fprintf
int fprintf( FILE *stream, const char *format [, argument ]...);

示例:

说明:argc:命令行参数的个数                     argv:存放命令行参数地址的指针数组;    示例:./myfile log.txt      argv[0] = ./myfile    argv[1] = log.txt                所以这里fopen打开的就是log.txt这个临时文件,fgets每次会读取一行的数据,再由while循环,fprintf打印,就形成了一个简易的cat指令    

输出:

小结③(上述所讲的是实现一个简单的 "cat" 指令)


Linux下系统文件接口

其实C语言接口也是封装的系统接口,对应情况如下:

感觉是不是很像,但是用法嘛,系统接口还是有点难以学习的,因为C语言接口可以直接使用,系统接口需要一定的知识储备量。

  • open
NAME
       open, creat - open and possibly create a file or device  //打开或创建一个文件

SYNOPSIS
       #include <sys/types.h>
       #include <sys/stat.h>
       #include <fcntl.h>

       int open(const char *pathname, int flags);    //pathname :文件路径+文件名  flags:选项
       int open(const char *pathname, int flags, mode_t mode);  //mode :权限

       int creat(const char *pathname, mode_t mode);  //使用很少很少

//...

The argument flags must include one of the following access modes: O_RDONLY, O_WRONLY, or O_RDWR.   
These  request  opening  the  file  read-only, write-only, or read/write, respectively.  
//注意:必须包含一个读,写,读和写;可以配合其他选项

//...

 O_APPEND  //追加
              The file is opened in append mode.  Before each write(2), the file offset is positioned at the end  of  the  file,  as  if  with  lseek(2).
              O_APPEND may lead to corrupted files on NFS file systems if more than one process appends data to a file at once.  This is because NFS does
              not support appending to a file, so the client kernel has to simulate it, which can't be done without a race condition.

 O_CREAT  //如果文件不存在,进行创建
              If  the file does not exist it will be created.  The owner (user ID) of the file is set to the effective user ID of the process.  The group
              ownership (group ID) is set either to the effective group ID of the process or to the group ID of the parent directory (depending  on  file
              system type and mount options, and the mode of the parent directory, see the mount options bsdgroups and sysvgroups described in mount(8)).

 O_TRUNC  //清空文件
              If the file already exists and is a regular file and the open mode allows writing (i.e., is O_RDWR or O_WRONLY) it  will  be  truncated  to
              length 0.  If the file is a FIFO or terminal device file, the O_TRUNC flag is ignored.  Otherwise the effect of O_TRUNC is unspecified.

//...


补充一个知识点:位图

上述 open 的接口选项其实还有很多,那么是如何进行传递的呢?一次只能传一个? 那样也达不到我们的预期啊,为什么这样说,不着急,先来解释下Linux是如何实现传递多个标记位置的:

首先再我们的认知中,什么会是纯大写带下划线 ---> 宏定义 , 上面所有的选项都叫做宏定义 。注意,这里flags底层结构是位图,32个比特位记录了是或否,每一个位置表明上述一个选项, 所以flags是可以传入多个选项的

示例:

验证 --- Linux下的 宏 

小结④(上述知识涉及“位图”)


回到open这个系统文件接口,再来看一下它的返回值:  注意:成功返回的就是文件描述符

RETURN VALUE
       open() and creat() return the new file descriptor,   //成功了返回文件描述符,失败了返回-1
or -1 if an error occurred (in which case, errno is set appropriately).

注意,这里的返回值提到了“文件描述符”!好,先来看一下第一个接口如何使用:

 int open(const char *pathname, int flags);

示例:

        运行我们会发现,程序的确成功的打开了文件,并且输出了文件描述符。但是,当文件不存在的时候,它是不会进行创建的!!!为什么C语言接口会去进行创建,因为C语言是被封装过的。用户在应用层看到一个很简单的动作,在系统接口层面甚至OS层面,可能要做非常多的动作!

所以上述为什么要加入位图这一概念,就是为了增加选项,这里不仅要O_WRONLY, 还需要加上选项O_CREAT(如果文件不存在,创建该文件)

示例:

但是上述的权限是不是很奇怪?不是只有rwx?为什么会这样??因为我们没有设置权限!所以其实第一个open接口用的并不是很多,或者使用的场景一般是O_RDONLY,一般写入或者创建文件我们会使用这个接口:

int open(const char *pathname, int flags, mode_t mode); 

 示例:

示例:

为什么最后other的权限少了??因为有权限掩码的存在,想要不受权限掩码的制约,umask设置成0就可以了。参考:Linux权限

示例:

所以用户想要使用系统的文件接口需要多费劲,需要了解选项标记位、位图、权限、进制转换、权限掩码、路径、以及最终的文件描述符,这也是语言层面上不得不提供封装接口的原因。


  • close
NAME
       close - close a file descriptor //关闭文件描述符

SYNOPSIS
       #include <unistd.h>

       int close(int fd);

  • write
NAME
       write - write to a file descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);

示例:

变形1:

观察输出,可以看到,write写入并不会清空源文件之前的内容,C语言’w‘ 写入为什么会清空文件之前的内容?

因为C语言是做过封装的,这也印证了那句话:用户在应用层看到一个很简单的动作,在系统接口层面甚至OS层面,可能要做非常多的动作!

所以这里需要继续添加选项:O_TRUNC

示例:

追加字符串选项:O_APPEND

示例:

  • read
NAME
       read - read from a file descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t read(int fd, void *buf, size_t count);

示例: 

小结⑤(上述讲述知识系统文件接口 open ,close, write,read)


fd的理解 与FILE关系

fd的理解

所以上述我们兜了那么大一个圈子,又说文件,又说C语言接口,又说系统接口,究竟想要说些什么,而文件描述符与这些又有什么关系???

其实说明白文件描述符之前,还要兜最后一个圈子。

示例:

这里为什么文件描述符打出来是3,4,5?先不讨论为什么是小整数,这里的0, 1,2去哪里呢???

另外,我们需要知道,C语言会为我们默认打开三个文件流:标准输入,标准输出,标准错误,那么着之间又有什么联系呢??

其实对应关系是这样的:

那么到底是不是这样呢?验证一下

示例:往 ‘1’里面进行写入

示例:在‘0’里面进行读取

所以经过上面的验证,我们发现,的确0,1,2这三个文件描述符所代表的就是三个文件标准输入,标准输出,标准错误。

底层不做深究,因为键盘,显示器必定有属于自己的读写方法,但是上层都被FILE进行封装。

小结⑥(上述相关知识点文件描述符1,2,3对应输入,输出关系)


fd与FILE关系

    FILE *fopen(const char *path, const char *mode);

这里的FILE是什么???

文件指针
       缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
       每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及 文件当前的位置等)。
这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
例如,VS2008编译环境提供的 stdio.h 头文件中有以下的文件类型申明
struct _iobuf {
        char *_ptr;
        int   _cnt;
        char *_base;
        int   _flag;
        int   _file;
        int   _charbuf;
        int   _bufsiz;
        char *_tmpfname;
       };
typedef struct _iobuf FILE;

        所以FILE是结构体,C文件库函数内部一定要调用系统调用,那么在系统角度,是认识FILE还是fd ???   只认识 fd ! ! 总结就是,既然FILE是一个结构体,既然FILE底层一定调用系统接口,既然系统只认fd,FILE结构体里面,必定封装了fd! ! !

那么stdin,stdout,stdout内部有没有fd,必定是有的,请看:

这里的_fileno就是文件描述符

示例:


fd的底层理解

所以上述所有想要说的就是,文件描述符就是小整数!!!它有什么意义呢?请接着往下看:

进程要访问文件,必须要先打开文件!

一个进程可以打开多个文件吗?可以! 一般而言进程︰打开的文件=1 : n

文件被打开的目的是为了被访问,一个文件要被访问,前提条件是它也要被加载到内存中才能被访问

进程︰打开的文件= 1: n ---> 如果是多个进程都打开自己的文件呢?  系统中会存在大量被打开的文件!

那么操作系统要不要把如此之多的文件也管理起来呢? 怎么管理呢? --->  先描述,再组织

在内核中,如何看待打开的文件?  系统内部要为了管理每一个被打开的文件,构建一个结构体
 

struct file
{
    //包含了一个被打开的文件的几乎所有内容(属性、权限、缓冲区...)
}

创建一个struct file对象,充当一个被打开的文件(其中,每打开一个文件,就创建一个对象)

如果有很多struct file对象呢?  内核再使用链表组织起来

struct file
{
   struct file* _next;
   struct file* _prev;
}

那么进程是如何找到这些文件对象的呢???答案是通过创建数组,通过哈希映射的方式!!!

例图:

所有,我们看到的文件描述符fd ---> 0、1、2、3、4、5....  在内核当中,本质就是数组的下标

上面兜那么大的圈子,所有的一切,都是为阐述这一概念。

侧面也能理解open的本质是在做什么?

1.在内核中创建文件对象

2.在数组内部找一个没有被使用的空间,将地址填入到数组中

3.把对应的数组下标返回给用户

4.用户拿到这个数组下标,就可以调用对应的接口,根据当前进程的PCB找到数组,根据数组索引到文件对象,文件对象里面,包含了文件的所有内容

这就是文件操作(注意:上述所讲到的文件都是“被进程打开的文件”,被称为内存文件)

这里并没涉及到没有被打开的文件(磁盘文件)

例图:

        而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files,指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

小结⑦(上述所说的是文件描述符的底层到底是什么)



fd的分配规则

示例:

验证1:关闭 0 号文件

验证2:关闭 2号文件  ---> close(2);  输出:

所以通过验证可以得知,文件描述符的分配规则是:最小的,没有被占用的文件描述符(从0开始,按顺序向后遍历)

伪代码:

int i = 0;
while(1)
{
    if(fd_array[i])  
        ++i;
    else
        break;
}
return i;


fd与重定向

其实按照上面的示例,可以玩一个好玩的东西,那就是,stdout(显示器)不是1号文件吗,如果我们关闭这个文件,会发生什么?

示例1:

上述的printf应该是往显示器上打印的(标准输出stdout),但是现在打印在了自己的文件log.txt
这里发现,close不认识显示器,它只认识1
如果把close (1)显示器关闭,那么我们自己的文件地址就在(1)的位置,就打印到了我们自己的文件
 

示例2:删除临时文件log.txt,再运行程序

注:上述埋了一个坑,缓冲区那里会回来添,什么坑?示例1中,为什么要把close(fd)给注释掉?示例2中为什么要fflush(stdout)?

回到重定向,现象是什么???就是原本应该打印到显示器上面的内容,竟然打印到了临时文件log.txt里面,这个操作是什么???

把本来应该写进显示器的内容写进文件就叫输出重定向!!!

例图:

观察可以发现:重定向的本质,其实是在OS内部,更改fd对应的内容的指向!!
当我们发现了这一结论,是不是就可以整点花活?如下:

输入重定向:

输出重定向:

追加重定向:

但是上述的整活是不是太拉了?就这,每次重定向还得关闭文件?当然,上述的操作是野路子的操作,正规军是这样的:

系统提供了dup接口

NAME
       dup, dup2, dup3 - duplicate a file descriptor

SYNOPSIS
       #include <unistd.h>

       int dup(int oldfd);
       int dup2(int oldfd, int newfd);  //了解这个

//...

dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following: 
 //拷贝的是文件指针对应的值,newfd被合理关闭

      
//...

这里谁拷贝谁??? newfd vs oldfd ?描述的是 oldfd copy to newfd  ---> 意味着最终的值要和 oldfd 保持一致

  int dup2(int oldfd, int newfd); 

那么谁是newfd? 谁是oldfd?

例图:

所以这样是不是就清晰明了。

示例:

输出重定向:


C标准库缓冲区

聊这个之前先来看一个现象:

示例1:未注释dup2,重定向成功

示例2:注释dup2,重定向失败

示例3:注释dup2,fflush(stdout), 重定向成功

为什么会出现上述情况?首先,fflush是用来刷新缓冲区的,所以上述示例足以证明,其一dup2是自带刷新缓冲区的功能的。其二,为什么不刷新缓冲区,重定向就会失败???还记得重定向一开始埋的坑吗,请接着往下看:

这里需要重新提到一个概念:Linux下一切皆文件

怎样去理解呢?Linux设计哲学,体现在操作系统的软件设计层面

Linux是使用C语言写的。如何使用C语言实现面向对象,甚至是运行时多态?? ?

C语言可以定义一个struct结构体,可以定义成员变量(属性),但是可以在结构体内定义成员方法吗(能把函数实现写进去吗)??

答案是肯定不可以,那么,如何让C语言的结构体支持包含成员方法呢??? ---- >  函数指针
 

 

例图:

有了这一认识,我们再重新来聊聊缓冲区:

缓冲区是什么?

一块内存空间。

为什么要有缓冲区?

示例1:小a有一本书想要交给小b

示例2:

这里顺丰就可以理解为是“缓冲区”,为什么要存在,主要就是提高整机效率,提高用户的响应速度


缓冲区刷新策略是什么?

接上面,那么顺丰是一收到物品就立即进行发送吗?顺丰会不会将这一本书直接飞机过去???不会。 而是等待校区的其他学生:例如小c,小d,小e...它们也有发快递到武汉的需求,打包一起进行发送  (积累足够多的数据)

那么顺丰的发快递的策略是什么?a.货架满了就发送 b.发往某一地区的物品数量多就发送 c.加急立即发送 。这些都是顺丰的发送策略。

那么缓冲区的刷新策略呢???

一般情况:

  • a.立即刷新
  • b.行刷新(行缓冲)
  • c.满刷新(全缓冲)

特殊情况:

  • a.用户强制刷新(fflush)
  • b.进程退出

出于对效率的考虑,一般而言:

  • 行缓冲的设备文件--显示器
  • 全缓冲的设备文件--磁盘文件

所有的设备,永远都倾向于全缓冲!  --->  缓冲区满了,才刷新 --- > 需要更少次的 I/O 操作 --- >更少次的外设的访问 (由冯诺依曼体系结构决定的)

和外部设备I0的时候,数据量的大小不是主要矛盾,用户和外设预备I/O的过程是最耗费时间的!

其他刷新策略是,结合具体情况做的妥协!

显示器:直接给用户看的,一方面要照顾效率,一方面要照顾用户体验

极端情况:用户是可以自定义规则的(采用fflush)

缓冲区是谁维护的???

接下来看一段有意思的示例:代码执行完毕之后,创建子进程

可以看到,输出没有任何问题,当我们进行重定向试一下:

为什么同样一段代码,重定向之后,输出的结果不同?

尝试一下,不创建子进程,重定向再试一下:

没有任何问题,那么一定是fork的问题,但是为什么???

同样的一个程序,向显示器打印输出4行文本

向普通文件(磁盘上),打印的时候,变成了7行,其中:

1. C语言接口 I0接口是打印了2次的

2.系统接口,只打印一次和向显示器打印一样!

上面的测试,并不影响系统接口!

如果有所谓的缓冲区,我们之前所谈的“缓冲区”应该是由谁维护的呢?  

上述"我们所谈的缓冲区”,绝对不是由OS提供的!!

如果是OS统一提供,那么我们上面的代码,表现应该是一样的! 所以缓冲区是由C标准库维护的
 

回到上面的问题,我们是在最后调用的fork,上面的函数已经被执行完了,但是并不代表进程的数据已经被刷新了!!
 

1.如果向显示器打印,刷新策略是行刷新,那么最后执行fork的时候 --- 一定是函数执行完了&&数据已经被刷新了! fork无意义!

2.如果你对应的程序进行了重定向 -- 要向磁盘文件打印 -- 隐形的刷新策略变成了全缓冲! —— \n 变没有意义了,fork的时候 --- 一定是函数一定执行完了,但是数据还没有刷新! !  --  在当前进程对应的C标准库中的缓冲区中!

这部分数据是不是父进程的数据?是的! 那么刷新是不是写的过程呢?此时 会发生写时拷贝(fork之后,子进程”写“,拷贝父进程,系统会生成两份数据

总结:说到底还是因为刷新策略,显示器是行刷新,磁盘是满刷新,上面函数结束之后,数据留在了缓冲区

此时,如果是行刷新,子进程拿不到里面的数据,因为已经被显示出来,缓冲区为空,子进程无意义;如果是满刷新,此时里面的数据还在,子进程直接拿到了,(刷新也是写入),所以有两份(写时拷贝),而这个缓冲区是C语言标准库提供的,所以系统接口是看不到缓冲区的,所以只会写入一次,而C语言的函数会写入两次
 


所以以上我们所熟知的“缓冲区”都是C标准库给我们提供的用户级缓冲区,这也就意味着还有“内核级缓冲区”(暂时不做解释)

缓冲区在哪里??

fflush(stdout); 这里只是传入了一个stdout,那么fflush是如何知道缓冲区在哪里??

上述我们说到,FILE是一个结构体,那么一定包含了fd对应的语言层面的缓冲区结构

typedef struct _IO_FILE FILE; 在/usr/include/stdio.h
在/usr/include/libio.h
struct _IO_FILE {
 int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
 //缓冲区相关
 /* The following pointers correspond to the C++ streambuf protocol. */
 /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
 char* _IO_read_ptr; /* Current read pointer */
 char* _IO_read_end; /* End of get area. */
 char* _IO_read_base; /* Start of putback+get area. */
 char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
 char* _IO_write_end; /* End of put area. */
 char* _IO_buf_base; /* Start of reserve area. */
 char* _IO_buf_end; /* End of reserve area. */
 /* The following fields are used to support backing up and undo. */
 char *_IO_save_base; /* Pointer to start of non-current get area. */
 char *_IO_backup_base; /* Pointer to first valid character of backup area */
 char *_IO_save_end; /* Pointer to end of non-current get area. */
 struct _IO_marker *_markers;
 struct _IO_FILE *_chain;
 int _fileno; //封装的文件描述符
#if 0
 int _blksize;
#else
 int _flags2;
#endif
 _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
 /* 1+column number of pbase(); 0 is unknown. */
 unsigned short _cur_column;
 signed char _vtable_offset;
 char _shortbuf[1];
 /* char* _save_gptr; char* _save_egptr; */
 _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};


 



网站公告

今日签到

点亮在社区的每一天
去签到