Linux从0到1——基础IO(上)
- 1. 预备知识
- 2. 复习一下常见的C语言文件接口
- 3. 系统调用接口
- 3.1 函数传参小技巧——标志位
- 3.2 使用系统调用接口
- 3.2.1 open
- 3.2.2 write
- 3.2.3 read
- 4. 文件描述符fd
- 4.1 fd的本质
- 4.2 理解struct file结构体
- 4.3 fd的分配规则
- 5. 重定向
- 5.1 引入
- 5.2 一般的重定向写法——配合函数dup2
- 5.3 stderr的意义
- 6. 缓冲区
- 6.1 预备知识
- 6.2 看一个样例
- 6.3 用户缓冲区VS内核缓冲区
- 6.4 验证缓冲区的存在
1. 预备知识
1. 文件 = 内容 + 属性:
- 所有对文件的操作都可以分为两种:a. 对内容操作 b. 对属性操作;
- 内容是数据,属性其实也是数据。存储文件,必须即存储内容又存储数据。这里指的文件默认就是在磁盘中的文件;
- 进程要访问一个文件时,都是要先把这个文件打开的:
- 打开前:这个文件就是普通的磁盘文件;
- 打开后:就是将磁盘文件加载到内存。
2. 一个进程可以打开多个文件吗?多个进程可以打开多个文件吗?
- 一个进程可以打开多个文件,多个进程可以打开多个文件。所以加载到内存中,被打开的文件可能会存在多个。
- 既然操作系统在运行时,可能会打开多个文件,那么操作系统一定要对这些文件进行管理——先描述,再组织。
- 我们大胆猜测一下,一个文件要被打开,一定要先在内核中形成被打开的文件对象(结构体),这些对象又可以通过一定的方式链接起来(链表)。
3. 文件按照是否被打开,分为:被打开的文件、没有被打开的文件
- 被打开的文件,存在于内存中;
- 没有被打开的文件,存在于磁盘中。
4. 本次研究文件操作的本质是:研究进程和被打开文件之间的关系。
2. 复习一下常见的C语言文件接口
1. fopen:
2. fputs:
3. 代码实践:
#include<stdio.h>int main()
{// "w": 按照写方式打开,如果文件不存在就创建它,并且每次打开都会清空文件内容// "r": 按照只读的方式打开,文件不存在直接报错// "a": 按照追加方式打开,如果文件不存在就创建它,每次打开不会清空文件内容,会在文件结尾处写入FILE *fp = fopen("log.txt", "w");if (NULL == fp){perror("fopen");return 1;}const char *msg = "hello Linux file\n";fputs(msg, fp); // 像文件中写入字符串fclose(fp);return 0;
}
3. 系统调用接口
- 进程打开文件的说法是不准确的,准确的说法应该是,进程通过操作系统打开文件。所以上层的
fopen
,fread
等函数在底层一定封装了系统调用接口。
3.1 函数传参小技巧——标志位
1. 先写一段代码:
#include<stdio.h>#define Print1 1 // 0001
#define Print2 (1<<1) // 0010
#define Print3 (1<<2) // 0100
#define Print4 (1<<3) // 1000void Print(int flags)
{if (flags&Print1) printf("hello 1 ");if (flags&Print2) printf("hello 2 ");if (flags&Print3) printf("hello 3 ");if (flags&Print4) printf("hello 4 ");printf("\n");
}int main()
{Print(Print1);Print(Print1|Print2);Print(Print1|Print2|Print3);Print(Print3|Print4);Print(Print4);return 0;
}
2. 编译并运行:
3. 解释:
- 对于
Print
函数来说,它只有一个参数flags
,这个参数是标记位; flags
一共有32个比特位,这里我们只使用四个比特位,也就是只有四个选项;- 定义了四个宏
Print*
,也是四个选项,他们对应的二进制信息已在代码中写出,通过|
的方式,将这些选项组合起来,传给Print
函数; - 在
Print
函数内部,通过if (flags&Print*)
的方式,可以判断对应的选项是否传入(对应比特位是否是1),如果传入了,就执行该条if
后的代码; - 上面说的选项,也叫标志位。
flags
就是各种标记位的组合。
3.2 使用系统调用接口
3.2.1 open
1. 查看man手册:
flags
参数就是各种标志位的组合;pathname
就是文件路径;- 返回值是
int
类型的数据,是打开文件的文件描述符fd
,关于这个文件描述符,我们后面再详细讲,这里只需要知道,文件描述符的使用方式和C语言接口中的FILE*
文件指针一样即可; - 当
open
失败时,会返回-1,同时错误码被设置; - 可以看到
open
接口有两个,第二个还有一个参数mode
,需要我们以8进制形式传入权限。
2. flags对应选项:
O_WRONLY
:以只写方式打开;O_CREAT
:以只写方式打开时,如果文件不存在,就创建它;O_TRUNC
:以只写方式打开时,清空文件内容;O_APPEND
:以只写方式打开时,不清空文件,在文件末尾追加内容。
3. 代码实例:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{// 打开已经存在的文件,不需要带权限// 打开不存在的文件,需要带权限;如果打开不存在的文件,不带权限,那么这个新文件的权限是乱码// 权限以8进制方案传入int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}// close也是一个系统调用,用于关闭文件,头文件是unistd.h// 参数是fdclose(fd); return 0;
}
- 编译并运行:
- 问题:为什么我们设置的权限是666,可是创建的
log.txt
权限却是664?
4. 关于权限:
- 如果我们打开一个不存在的文件,还不传权限,那么它的文件描述符是乱码。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT); // 不带权限if (fd < 0){perror("open");return 1;}close(fd); return 0;
}
- 关于3中的问题,答案是有系统默认的权限掩码存在,默认是
0002
,将对应的权限过滤掉了。我们可以通过umask
函数来重新设置权限掩码:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{umask(0); // 重新设置权限掩码int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}// close也是一个系统调用,用于关闭文件,头文件是unistd.h// 参数是fdclose(fd); return 0;
}
- 注意:不建议使用上面这种方式重新设置权限掩码,尽量和系统默认权限掩码保持一致。
5. close接口:
- 关闭哪个文件,就传对应文件的文件描述符即可。
3.2.2 write
1. 查看man手册:
2. 代码实例:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}const char *msg = "hello file system call\n";// 操作文件write(fd, msg, strlen(msg)); // 要不要传 strlen(msg) + 1,将'\0'也传进去?// 第三个参数不要传 strlen(msg) + 1,'\0'只是C语言层面的概念,不是文件层面的概念// '\0'传进文件,会出现乱码close(fd); return 0;
}
- 以
O_WRONLY | O_CREAT
的方式打开文件,write
默认是覆盖式写入。比如文件中原本有内容aaaa
,如果再向文件中写入bb
,文件内容就会变为bbaa
。
3. fopen以w方式打开文件的底层:
int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0){perror("open");return 1;}close(fd); return 0;
}
4. fopen以a方式打开文件的底层:
int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd < 0){perror("open");return 1;}close(fd); return 0;
}
3.2.3 read
1. 查看man手册:
fd
:要读取文件的文件描述符;buf
:用户自定义的一块空间(缓冲区);count
:缓冲区总大小;
2. 代码实例:
int main()
{int fd = open("log.txt", O_RDONLY);if (fd < 0){perror("open");return 1;}char buffer[1024];read(fd, buffer, 1024);printf("%s\n", buffer);close(fd);return 0;
}
4. 文件描述符fd
4.1 fd的本质
1. 文件描述符fd的本质,就是数组下标:
- 操作系统会为打开的文件创建一个结构体
struct file
来描述它,然后通过链表的方式将多个打开的文件组织起来; - 进程PCB中会有一个
struct files_struct *files
指针,指向该进程管理打开文件的结构体struct files_struct
。其中有一个成员为struct file *fd_array[]
指针数组,每一个位置对应一个打开文件的结构体对象struct file
; - 文件描述符
fd
,其实就是struct file *fd_array[]
数组的下标,所以只要拿到对应的文件描述符(数组下标),就可以找到对应的文件; - C/C++程序在运行时,会默认打开三个文件,标准输入流
stdin
,标准输出流stdout
,和标准错误流stderr
。这三个文件分别对应的硬件设备为,键盘、显示器、显示器。文件描述符数组的0, 1, 2
位置,分别对应这几个文件。
2. FILE*到底是什么?
stdin
,stdout
,stderr
的都是FILE*
类型的指针。
FILE
其实就是C语言中提供的一个结构体类型,我们可以大胆猜测一下,FILE
的内部必定封装了文件描述符。
关于
FILE
现在没有办法讲太多。
3. 代码验证:
int main()
{int fda = open("loga.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);printf("stdin->fd: %d\n", stdin->_fileno); // 这个_fileno成员就是文件描述符printf("stdout->fd: %d\n", stdout->_fileno);printf("stderr->fd:%d \n", stderr->_fileno);printf("fda: %d\n", fda);printf("fdb: %d\n", fdb);printf("fdc: %d\n", fdc);printf("fdd: %d\n", fdd);close(fda);close(fdb);close(fdc);close(fdd);return 0;
}
- 可以发现,文件描述符的分配是有一定规律的。
4. 如何理解一切皆文件?
- 底层的很多硬件,大多都有两个基本的功能,输入和输出。但是它们的输入和输出方法是不同的;
- 但是在上层,我们想通过一切皆文件的方式去管理底层不同的硬件,是如何做到的?
- 比如此时打开一个磁盘文件,OS在上层就会为磁盘文件创建一个
struct file
对象。里面有两个很重要的内容就是读方法和写方法的指针,指向磁盘文件具体的读写方法; - 从此往后,我们再调用磁盘的读写方法时,不用关心底层是如何实现的,直接调用
struct file
对象中的方法即可,一切皆文件; - 这种封装的方式,可以让我们自然联想到C++中的继承和多态。
4.2 理解struct file结构体
struct file
结构体中,必定要存储两个信息:a. 文件的属性 b. 文件的内容。
对文件的操作无非就分为两种,一种是读,一种是写。但是无论读写,都需要先将磁盘中的文件数据加载到文件缓冲区中。
我们在应用层进行的数据读写,本质是将内核缓冲区中的数据进行来回拷贝。
4.3 fd的分配规则
1. 进程默认已经打开了fd为0,1,2的三个文件,我们可以通过0,1,2直接访问:
- 0,2可以直接使用,从侧面验证了上述结论。
int main()
{char buffer[1024]; // 用户自己定义的缓冲区ssize_t s = read(0, buffer, 1024); // 从键盘读取if (s > 0){write(1, buffer, strlen(buffer)); // 向显示器写入}return 0;
}
2. 文件描述符的分配规则是:从上往下扫描struct file *fd_array[]
数组,寻找最小的,没有被使用的位置对应的下标,分配给打开的文件。
- 关闭0或2,再打开文件,查看分配给新打开文件的
fd
(先不要关闭1):
int main()
{close(0);//close(2);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);if (fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}
- 发现显示器输出结果是:
fd: 0
或者fd: 2
; - 可见,文件描述符的分配规则:在
struct file *fd_array[]
数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
如果关闭1号文件(显示器),则无法在显示器中看到输出结果。
5. 重定向
5.1 引入
1. 先看代码,观察现象:
int main()
{close(1); // 先关1int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);if (fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);printf("stdout->fd: %d\n", stdout->_fileno);// C语言提供的缓冲区问题,先待定!fflush(stdout); // 在close前,刷新缓冲区close(fd);return 0;
}
printf
明明是向显示器打印的,怎么打印到了文件log.txt
里?- 这种现象叫输出重定向
>
,常见的重定向有:- 输出重定向:
>
; - 追加重定向:
>>
; - 输入重定向:
<
。
- 输出重定向:
2. 解释:
- 首先,根据
fd
的分配规则,关闭1后,再打开新文件log.txt
,新文件的fd
就是1。文件描述符数组的1号位置,不再指向显示器,而是文件log.txt
; printf
只认文件描述符1,默认向struct file *fd_array[]
数组下标为1的位置所指向的文件打印,所以本该打印到显示器上的内容,打印到了文件log.txt
中;- 一定要在
close
前刷新缓冲区,因为printf
会先将数据放到C语言提供的缓冲区中,刷新缓冲区,才能让缓冲区中的数据换入到文件中;
这里只是粗力度的解释一下为什么要刷新缓冲区,关于缓冲区更多的细节,我们在后面讲解。
3. 输入重定向(一般不这样写):
int main()
{close(0); int fd = open("log.txt", O_RDONLY); // fd == 0if (fd < 0){perror("open");return 1;}char buffer[1024]; // 用户自己定义的缓冲区,先将数据读到bufferfread(buffer, 1, sizeof(buffer), stdin); // stdin->fd: 0printf("%s\n", buffer);close(fd);return 0;
}
4. 追加重定向:
- 只需要将1中代码中,
open
的O_TRUNC
选项改为O_APPEND
即可。 - 不过一般也不这样写。
int main()
{close(1); // 先关1int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);if (fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);printf("stdout->fd: %d\n", stdout->_fileno);// C语言提供的缓冲区问题,先待定!fflush(stdout); // 在close前,刷新缓冲区close(fd);return 0;
}
5. 重定向的本质:
- 上层
fd
不变,底层fd
所指向的内容在改变。
5.2 一般的重定向写法——配合函数dup2
1. dup2函数:
- 将
oldfd
下标指向的内容拷贝给newfd
下标指向的内容。
2. 输出重定向:
int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0){perror("open");return 1;}dup2(fd, 1);// 如下内容将打印到文件 log.txt 中printf("hello printf\n");fprintf(stdout, "hello fprintf\n");close(fd);return 0;
}
3. 追加重定向:
int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd < 0){perror("open");return 1;}dup2(fd, 1);// 如下内容将追加到文件 log.txt 中printf("hello printf\n");fprintf(stdout, "hello fprintf\n");close(fd);return 0;
}
4. 输入重定向:
int main()
{int fd = open("log.txt", O_RDONLY);if (fd < 0){perror("open");return 1;}dup2(fd, 0);char buffer[1024];fread(buffer, 1, 1024, stdin);printf("%s\n", buffer);close(fd);return 0;
}
5. 引用计数f_count:
- 通过重定向的学习,我们知道了,一个打开的文件,可以被多个
struct file*
指针指向。如图,log.txt
就被两个struct file*
指针指向。
- 我们在调用
close
接口关闭一个文件时,如果这个文件还被其他的指针指向,该怎么办,这样不是会互相影响吗? - 所以
struct file
结构体内还设计了一个f_count
字段,它是引用计数,记录有多少个指针指向自己。如果f_count
不为0,则执行一次close
,f_count
就减一。直到f_count
等于0时,才释放log.txt
的资源。
5.3 stderr的意义
1. 看代码,观察现象:
int main()
{fprintf(stdout, "hello stdout\n");fprintf(stderr, "hello stderr\n");return 0;
}
- 这个问题现在很好解释,因为重定向只是将1位置的指针改成指向
log.txt
了,但是stderr
对应的2位置的指针,还是指向显示器文件,所以第二条fprintf
语句还是向显示器打印了。
- 想让这两条
fprintf
语句都往log.txt
打印,需要将2位置也重定向了。在命令行中可以直接执行指令./myfile > log.txt 2>&1
,其中2>&1
表示让1位置的指针覆盖2位置的指针,这样1位置和2位置都指向log.txt
了。 - 所以
./myfile > log.txt
的完整写法应该是./myfile 1>log.txt
。
2. stderr的实际运用:
- 比如在一个日志系统中,我们希望将正常信息和错误信息进行分流,将他们放在不同的文件中。这时候就可以使用输出重定向,将1位置和2位置重定向到不同的文件,将错误信息单独储存起来。
6. 缓冲区
6.1 预备知识
1. 我们理解的缓冲区:
- 缓冲区其实就是一部分内存,重要的是搞清楚这一部分内存由谁提供。
- 用户缓冲区:用户自己提供,用户在程序中自己定义的缓冲区,如
char buffer[1024]
; - C语言缓冲区:由C语言提供的,定义在C库中;
- 内核缓冲区:由操作系统提供的,内核级别的缓冲区。
- 用户缓冲区:用户自己提供,用户在程序中自己定义的缓冲区,如
2. 缓冲区存在的意义:
- 任何缓冲区存在的目的只有一个,就是提高效率。
- 举一个生活中的例子:
- 假如你住在云南,你要给你远在北京的朋友送一个键盘。在快递还没有出现的时候,你需要先自己坐火车跑到北京,把键盘送到朋友手中,然后自己再跑回来,一来一回花了一个月时间,效率低下。
- 后来快递出现了,你可以先把键盘给楼下的菜鸟驿站,然后由菜鸟驿站完成送键盘的任务,等键盘到了北京,你的朋友再去自己楼下的菜鸟驿站把键盘拿到手。
- 整个过程中,键盘从云南到送到北京这个时间成本是不可避免的,这个时间成本由菜鸟驿站承担了。但是你就轻松了很多,在你把键盘送到菜鸟驿站的那一刻,你就可以认为你把键盘送出去了,然后你就可以干自己的事情了。
- 所以菜鸟驿站的存在,节省了使用者的时间。
- 菜鸟驿站就像缓冲区,进程先将数据放入缓冲区,然后由缓冲区执行向特定位置传输数据的操作,解放进程。所以,缓冲区的主要作用是——提高使用者的效率。
3. 缓冲区的刷新策略:
-
菜鸟驿站在送快递时,肯定也有自己的配送方式。
- 比如某一个客户要求紧急配送,驿站就派专机专门送这个快递;
- 不紧急的快递,派专机送成本太高了,会积累到一定的量后,统一配送。
-
缓冲区的刷新也有自己的策略:
- 无缓冲(立即刷新);
- 行缓冲(行刷新);
- 全缓冲(缓冲区满了,再刷新)。
-
上面说的都是缓冲区刷新的一般策略,除此之外还有一些特殊情况:
- 因为某些场景需要,需要强制刷新缓冲区;
- 进程退出时,一定要进行缓冲区刷新。
4. 磁盘和显示器的刷新策略:
- 一般对于显示器文件,会进行行刷新;
- 对于磁盘文件,采取全缓冲策略。
6.2 看一个样例
1. 观察现象:
int main()
{fprintf(stdout, "C: hello fprintf\n");printf("C: hello printf\n");fputs("C: hello fputs\n", stdout);const char *str = "system call: hello write\n";write(1, str, strlen(str));fork();return 0;
}
- 向显示器打印:
- 重定向,向
log.txt
文件中打印:
2. 理解样例:
- 当我们直接向显示器打印时,显示器的刷新方式是行刷新,并且我们写的所有打印语句后都有
\n
(\n
是一种行刷新策略)。在fork
函数执行之前,缓冲区中的数据已经全部刷新,缓冲区为空。(包括系统调用接口write
,系统内核级别的缓冲区也为空,这个后面说) - 重定向到
log.txt
文件的本质,是向磁盘文件写入,系统对数据的刷新方式就变成了全缓冲。 - 全缓冲,意味着实际写入的简单数据,不足以把缓冲区写满,无法达到刷新条件。
fork
函数执行的时候,数据依旧在缓冲区中。 - 由于
write
对应的打印内容正常打印了,所以我们可以得出一个结论:我们目前所谈的“缓冲区”和操作系统没有关系,只和C语言本身有关。 - C/C++提供的缓冲区,里面保存的一定是用户的数据,属于当前进程在运行时自己的数据。当进程将数据交给操作系统后,这个数据就不属于当前进程了,而是属于操作系统。
- 当进程退出时,一般要强制刷新缓冲区。缓冲区的刷新,本质上也是一种对当前进程一个变量的清空或“写入”操作。
fork
后,任意一个进程退出的时候,会强制刷新缓冲区(修改变量),此时就会发生写时拷贝,所以我们看到缓冲区中打印的内容多出一份。write
是系统调用,没有使用C语言的缓冲区,它会将数据直接写入操作系统。所以这部分数据不属于进程,也就不会发生写时拷贝。
6.3 用户缓冲区VS内核缓冲区
1. 用户缓冲区:
- 我们日常接触的最多的是C/C++提供的语言级别的缓冲区;
- 我们通常说的刷新,指的是将C语言缓冲区中的数据刷新到操作系统中。
2. 内核缓冲区:
- C语言缓冲区刷新到操作系统中后,实际上是传给了内核缓冲区,内核缓冲区最后还要将数据刷新到磁盘文件上;
- 不同的操作系统,内核缓冲区的刷新策略不同。
3. 解释printf(“hello printf\n”);这段代码从执行,到在显示器打印的全过程:
- 首先,
hello printf\n
这段数据会先通过printf
函数写入到C语言缓冲区中,然后printf
函数返回,至此它的任务就完成了; - 接着,C语言缓冲区中的数据会根据一定的刷新策略,通过
write
接口,写入操作系统; - 写入操作系统,实际上是先写入了
stdout
对应的内核缓冲区,然后操作系统再根据自己的刷新策略,将内核缓冲区中的数据刷新到磁盘中。
6.4 验证缓冲区的存在
任何情况下,我们调用C语言文件接口的时候,都要有一个FILE*
指针。我们都知道FILE
结构体中封装了文件描述符fd
,其实FILE
结构体中也封装了缓冲区。
在在/usr/include/stdio.h
中,有:
typedef struct _IO_FILE FILE;
在/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 0int _blksize;
#elseint _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
};
验证了缓冲区的存在。