二、
UNIX
文件基础
0.1 UNIX -
输入和输出
重点:
- Linu –一切皆文件
- 文件IO和标准IO是Linux上操作文件的两套API集合。
回顾一下:我们可以把操作系统看做应用程序和硬件之间的一层软件。为应用程序提供简单一致的接口来访问底层的功能各异的硬件。操作系统将前述的计算机系统抽象为几个基本的抽象概念:其中有一个是
文件:抽象了计算机系统上的所有类型的I/O
设备。使得应用程序可以统一看待他们并采用几乎一致的编程接口访问和操作这些I/O
设备。而无需了解具体的设备操作技术。譬如最普通的读写磁盘上的文件,我们无需了解具体的磁盘操作技
术。
0.2
文件
IO
的
API
讲解:
API | 讲解点 |
#include <fcntl.h> # int open(const char *pathname, int oflag, ... /* mode_t mode */ ); int creat(const char *pathname, mode_t mode); | man 2 open, Whereis fcntl.h 讲解重点: l 调用返回的文件描述符一定是最小的未用描述符数字 l Pathname,首先是个路径,不是文件名。以及相对路径,绝对路径的概念。 l Oflag指明要对文件后继访问的方式,参考P56。针对O_APPEND引入讲解文件当前读写位置的概念,。后面练习时注意show一下。注:缺一个O_NONBLOCK l Open函数是一个变参形式。Mode就是那个变参。只有在oflag中指明了O_CREAT时才需要指定mode。选择性重点讲一下mode的含义 l creat()等价于open(pathname, O_CREAT|O_WRONLY|O_TRUNC, mode) l open()可以打开设备文件,但是不能创建设备文件,设备文件必须使用mknod()创建。 详细参考man 2 open的相关说明或者APUE的说明3.11 第一版。 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> |
#include <unistd.h> int close(int filedes); Returns: 0 if OK, 1 on error | 调用成功返回0,出错返回-1,并设置errno。 当一个进程终止时,该进程打开的所有文件都由内核自动关闭。 关闭一个文件的同时,也释放该进程加在该文件上的所有记录锁。 |
#include <unistd.h> ssize_t read(int filedes, void *buf, size_t nbytes); ssize_t write(int filedes, const void *buf, size_t nbytes); | 注意突出文件偏移量在读写中会随着改变,其改变的值==实际读写的返回量。 |
#include <unistd.h> off_t lseek(int filedes, off_t offset, int whence); Returns: new file offset if OK, 1 on error | l 每个打开的文件都有一个与其相关的“当前文件位移量”,它是一个非负整数,用以度量从文件开始处计算的字节数。 l 通常,读/写操作都从当前文件位移量处开始,在读/写调用成功后,使位移量增加所读或者所写的字节数。 l lseek()调用成功为新的文件位移量,失败返回-1,并设置errno。 l lseek()只对常规文件有效,对socket、管道、FIFO等进行lseek()操作失败。 l lseek()仅将当前文件的位移量记录在内核中,它并不引起任何I/O操作。(这个在文件偏移中可以看出来只是修改了文件表内存里的变量) 文件位移量可以大于文件的当前长度,在这种情况下,对该文件的写操作会延长文件,并形成空洞。 在ls -l(ls -lh)和du -s(du -sh)文件上得到的数据的不同 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> |
3.
标准
I/O
3.1
标准
I/O -
介绍
标准
IO
提供了两方面的封装:
1
)操作对象
– FILE*:
封装了不同
OS
对文件的实现,
2
)封装了缓存支持和优化。
3.2
标准
I/O -
流和
FILE
对象
3.2.1
流的概念
在
C
中引入了流
(stream)
的概念。它将数据的输入输出看作是数据的流入和流出,这样不管是磁盘文件或者是物理设备
(
打印机、显示器、键盘等
),
都可看作一种流的源和目的,视他们为同一种东西,而不管其具体的物理结构,即对他们的操作,就是数据的流入和流出。
这种把数据的输入输出操作对象,抽象化为一种流
,而不管它的具体结构的方法很有利于编程,而涉及流的输出操作函数可用于各种对象,与其具体的实体无关,即具有通用性。
在C
中流可分为两大类,即
文本流
(text stream)
和
二进制流
(binary stream)
。
3.2.2 FILE
定义
FILE
指针:每个被使用的文件都在内存中开辟一个区域,用来存放文件的有关信息,这些信息是保存在一个结构体类型的变量中,该结构体类型是由标准IO
库定义的,取名为FILE
。
FILE
封装了系统调用中的文件描述符的概念,所以标准
IO
中的函数不会直接操作文件描述符。同理:
FILE
屏蔽了操作系统的差别(
标准
IO
不用文件描述符也是为了屏蔽
OS
的概念,用
FILE
这个抽象的概念。而文件描述符绝对是个
UNIX/LINUX
的概念。
)
。
Show
一下:stdio.h
中typedef struct _IO_FILE FILE; // /usr/include/libio.h
_IO_FILE
定义在libio.h
(Whereis libio.h
);
简单浏览一下_IO_FILE
对象的数据结构
_flags
: _IO_LINE_BUF/_IO_UNBUFFERED/
fp->_IO_buf_end – fp->_IO_buf_base
内部分配的缓存大小的首尾地址
注意:
3.2.3
文本流和二进制流
所谓文本流是指在流中流动的数据是以字符形式出现。在文本流中,'\n'
被换成回车CR
和换行LF
的代码0DH
和0AH
。而当输出时,则0DH
和0AH
本换成'\n'
。
二进制流是指流动的是二进制数字序列,若流中有字符,则用一个字节的二进制ASCII
码表示,若是数字,则用一个字节的二进制数表示。在流入流出时,对\n
符号不进行变换。例如2001
这个数,在文本流中用其ASCII
码表示为:
'2' '0' '0' '1'
| | | |
50 48 48 49
共占4
字节。而在二进制流中则表示为:00000111 11010001
用十六进制就是07D1
。只占两字节。
由此看出,二进制流比文本流节省空间,且不用进行对\n
的转换,这样可以大大加快流的速度,提高效率。因而,对于含有大量数字信息的数字流,可以采用二进制流的方式;对于含有大量字符信息的流,则采用文本流的方式。
3.3
标准
I/O
预定义的
3
个流。
一个进程启动的时候,会打开三个流:标准输入、标准输出、标准出错。
和文件IO
中的对应关系。可以查看头文件定义
3.4
标准
I/O -
文件缓冲
首先理解为何要设置缓冲区
–
避免频繁地呼叫系统调用;其次缓冲区大小的设置在不同
OS
上是有技术的,标准库为我们做了优化选择。
标准
IO
函数是根据文件流关联的设备类型,会选择采用何种缓冲区的操作方式。
分类如下:
1)
全缓冲区:这种缓冲区要求填满整个缓冲区后才进行I/O
系统调用操作。
对于磁盘文件通常使用全缓冲区访问。
第一次执行I/O
操作时,ANSI
标准的文件管理函数通过调用malloc
函数获得需使用的缓冲区。linux
默认大小为4096
。
2)
行缓冲区:在这种情况下,当在输入和输出中遇到换行符时,标准I/O
库执行I/O
系统调用操作。
当流涉及一个终端时(例如标准输入和标准输出),使用行缓冲区
。因为标准I/O
库收集的每行的缓冲区长度是固定的,只要填满了缓冲区,即使还没有遇到换行符也将执行I/O
系统调用操作。默认行缓冲区大小为1024
字节。
3
)无缓冲区:标准I/O
库不对字符进行缓存。如果用标准I/O
函数写若干字符到不带
缓冲区的流中,则相当于用write
系统调用函数将这些字符写至相关联的打开文件。
标准出错流
stderr
通常是不带缓冲区的
,这使得出错信息能够尽快地显示出来
stdbuf.c
重点:
1
)不同类型设备导致关联的
FILE
流采用不同的缓冲类型,
2
)不同缓冲类型的缓冲大小;
3
)标准
IO
内部
malloc
的过程,实际使用时才会申请。
注意:
程序中有一个
getchar()
的作用
因为标准
IO
内部
malloc
的过程,实际使用时才会申请,所以如果没有输入的化标准输入的大小为
0
当程序调用
getchar
时
.
程序就等着用户按键
.
用户输入的
字符
被存放在键盘
缓冲区
中
.
直到用户按回车为止(回车字符也放在缓冲区中)
.
当用户键入回车之后,
getchar
才开始从
stdio
流中每次读入一个字符
.
存。
总结一下
:
l
对于磁盘文件通常使用全缓冲区访问。
l
当流涉及一个终端时(例如标准输入和标准输出),使用行缓冲区
l
标准出错流
stderr
通常是不带缓冲区的
2.5
标准
I/O
库函数
I/O模型 | 文件I/O | 标准I/O |
缓冲方式 | 非缓冲I/O | 缓冲I/O |
操作对象 | 文件描述符 | 流(FILE *) |
打开 | open() | fopen()/freopen()/fdopen() 课件P14 重点: l 三个函数的区别: (1)fopen打开路径名由pathname指示的一个文件的路径 (2)freopen常用于一个打开的流重新定向。比如stdout是标准输出,我们可以把它重定向到由path指定的一个文件。重定向的举例见example2。 (3)fdopen取一个现存的文件描述符,并使一个标准的I/O流与该描述符相结合。 l fopen时mode里为b所标识的文本和二进制的区别。可以略带讲一下。对UNIX无影响,如果你的代码会被移植到非unix系统上,主要是Windows,这个b就要注意了。 l fopen函数创建文件时不像open,没有mode参数指明在文件系统里创建的权限值,这个问题的答案是参考man,其缺省值是0666再加上umask的影响。参考课件P20, l fopen的mode参数和open函数的做一个对比 FILE *freopen(const char *restrict pathname, const char *restrict type, FILE* restrict fp) 作用:重定向输入输出流 const char *restrict pathname: 重新定向的文件或者是路径 const char *restrict type: 文件打开的方式 restrict fp 被改变的流 FILE *fdopen(int fd, const char *mode); fdopen取一个现存的文件描述符,并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信通道函数获得的描述符。因为这些特殊类型的文件不能用标准I/O fopen函数打开,首先必须先调用设备专用函数以获得一个文件描述符,然后用fdopen使一个标准I/O流与该描述符相结合。 重点:1)如果文件不存在则新建,mode为0666&~umask;2)如果文件存在则打开并清零。 |
关闭 | close() | fclose() |
读 | read() | fread()/fgetc()/fgets()…参考读写流的三种变体 |
写 | write() | fwrite()/fputc()/fputs()…参考读写流的三种变体 l 中间要穿插讲feof和ferror,clearerr,查看man fgetc/fgets/fread,看返回值的说明知道为何要有feof和ferror,注意要自己手工reset,通过调用clearerr。 |
定位 | lseek() | fseek()/ftell()/rewind(): ftell()和fseek(): 这两个函数自V7以来就存在了,但是它们都假定文件的位置可以存放在一个长整型中。 rewind() fsetpos()/fgetpos():这两个函数是新由ANSI C引入的。它们引进了一个新的抽象数据类型fpos_t,它记录文件的位置。 举例:取文件长度:<<<<< samples\2-stdIO\2.5\getfilesize.c >>>>>>>>>> 注意:ftell不会改变文件位置指针。 |
读写流的三种变体
l
每次一个字符的
I/O
。
使用fgetc()/fputc()
一次读或写一个字符,如果流是带缓存的,则标准I/O
函数处理所有缓存。
注意:
fgetc/fputc
操作的是一个
int
,
fgetc
返回的是
int
,
变体:
getc()/getchar()
Int c;
C = fgetc(stdin);
Fputc(c,stdout);
fgetc
和
getc
基本一致,除了
getc()
可能是由宏实现的,
getchar () == getc(stdin)
l
每次一行的
I/O
。
使用fgets()
和fputs()
一次读或写一行。每行都以一个新行符终止。当调用fgets()
时,应说明能处理的最大行长。
变体:
gets()/puts()
注意
1
:
char *fgets(char *s ,int size,FILE *stream)
的用法
fgets
每次至多多
size-1
个字符,每次都把都到的字符存储到
s
字符串数组内,当读到文件末尾(
EOF
)或者是换行的时候它会结束。
换行符
(‘/n’),
会被存储到字符数组内部
,
结束时会在字符数组最后一位补一个空位
(’/0’)
。
如果文件中的该行,不足
bufsize
个字符,则读完该行就结束。如若该行(包括最后一个换行符)的字符数超过
bufsize-1
,则
fgets
只返回一个不完整的行,但是,缓冲区总是以
NULL
字符结尾,对
fgets
的下一次调用会继续读该行。
如果
n
大于一行的字符串长度,那么当读到字符串末尾的换行符时,
fgets(..)
会返回。并且在
s
的最后插入字符串结束标志
'\0'
。
而
s
缓冲区剩余的位置不会再填充。
注意
2
:
char *gets(char *s,FILE *stream)
gets
不推荐使用,该函数容易造成缓冲区的溢出,造成不良的后果。
l
二进制直接
I/O
。fread()
和fwrite()
函数支持这种类型的I/O
。
每次
I/O
操作读或写某种数量的对象,而每个对象具有指定的长度
。
这两个函数常用于从二进制文件中读或写一个结构。
l
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
l
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
对
fread
和
fwrite
需要重点讲解的是:
1.
注意
size_t size, size_t nmemb
这两个参数的含义
。
读取/
写入nmemb
个元素数据,每个元素数据的大小为size
,
2.
什么情况下需要二进制读写
(
1
)两个函数的返回:读或写的对象数
(
2
)对于二进制数据我们更愿意一次读或写整个结构。
(
3
)为了使用
getc()
或
putc()
做到这一点,必须循环读取整个结构,一次读或写一个字节。
(
效率低
)
(
4
)
fputs()
在遇到
null
字节时就停止,而在结构中可能含有
null
字节,所以不能使用每次一行函数实现这种要求。如果输入数据中包含有
null
字节或换行符,则
fgets()
也不能正确工作。
(
实现限制
)
l
读写流结束以及错误判定
。EOF/feof()/ferror()/clearer()
===========================================================================
文件
I/O
和标准
I/O
区别
(
1
)文件
I/O
是低级
I/O
,支持
POSIX
规范,任何支持
POSIX
标准的操作系统都支持使用文件
I/O
。
标准
I/O
是高级
I/O
,支持
ANSI C
相关标准,只要开发环境支持标准
C
库,标准
I/O
就可以使用。
(
2
)通过文件
I/O
读取文件时,每次操作都会执行相应的系统调用。好处是直接读取实际文件,
坏处是频繁调用系统调用,增加系统开销。
标准
I/O
可以看成在文件
I/O
的基础之上封住了缓冲机制,先读写缓冲区,必要时读写真实文件,
从而减少系统调用的次数。
(
3
)文件
I/O
使用文件描述符,表示一个打开的文件,可以访问不同类型的文件,普通文件,设备文件和管道文件。
标准
I/O
中用
FILE
(流)来表示一个打开的文件,通常只用来访问普通文件。
Note:
多数人认为文件中有一个
EOF,
用于表示文件的结尾。但这个观点实际上是错误的,在文件所包含的数据中
,
并没有什么文件结束符。对
fgetc
或
getc
而
言,如果不能从文件中读取,则返回一个整数
-1
,这就是所谓的
EOF,
返回
EOF
无非是出现了两种情况,一是文件已经读完;二是文件读取出错,反正是读不下去了。