第三章 文件操作
3.1 linux文件结构
与UNIX一样,linux环境中的文件具有特别重要的意义,因为它们为操作系统服务和设备提供了一个简单而一致的接口。在linux中,一切都是文件。
这意味着,通常程序可以像使用文件那样使用磁盘文件、串行口、打印机等等。
目录也是文件,但它是一种特殊类型的文件。在现代UNIX(包括linux)版本中,即使是超级用户可能也不再被允许直接对目录进行写左操作了。所有用户通常都使用上层的opendir/readdir接口来读取目录,而无需了解特定系统中目录实现的具体细节。
3.1.1 目录
文件,除了本身包含的内容以为,它还有一个名字和一些属性,即”管理信息“,包括我呢见的创建/修改日期和它的访问权限。这些属性被保存在文件爱你的inode(节点)中,它是文件系统中的一个特殊的数据块,它同时还包含文件的长度和文件在磁盘上的存放位置。系统使用的是文件的inode编号,目录结构为文件命名仅仅是为了便于人们使用。
目录是用于保存其他文件的节点号和名字的文件。目录文件中的每个数据项都是指向某个文件节点的链接,删除文件名就等于删除与之对应的链接。文件的节点号通过ln -i命令查看。
删除一个文件时,实质上是删除了该文件对应的目录项,同时指向该文件的链接数减1.该文件中的数据可能仍然能够通过其他指向同一个文件的链接访问到。如果指向某个文件的链接数变为0,就表示该节点以及其指向的数据不再被使用,磁盘上的相应位置就被标记为可用空间。
根目录中通常包含用于存放系统程序(二进制可执行文件)的/bin,用于存放系统配置文件的/etc和用于存放系统函数库的/lib。代表物理设备并未这些设备提供接口的文件通常会存放在/dev。
3.1.2 文件和设备
硬件设备在linux中通常也被表示(映射)为文件。
UNIX和linux中比较重要的设备文件有3个:/dev/console,/dev/tty和/dev/null。
1./dev/console
系统控制台,错误信息和诊断信息通常被发送这个设备。
2./dev/tty
如果一个进程有控制终端的话,那么特殊文件/dev/tty就是这个控制终端的别名。
虽然/dev/console设备只有一个,但是通过/dev/tty却能够访问许多不同的物理设备。
3./dev/null
/dev/null文件是空设备。所有写向这个文件的输出都将被丢弃,而读这个设备会立刻返回一个文件尾标志,所以在cp命令里面可以把它用作复制空文件的源文件。
创建空文件的另一个方法是使用touch <filename>命令,该命令的作用是改变文件的修改时间,如果指定的文件不存在,就创建它,但该命令不会把有内容的文件变成空文件。
3.2 系统调用和设备驱动程序
只需少量的函数就可以对文件和设备进行访问和控制,这些函数被称为系统调用,由UNIX直接提供,它们也是通向操作系统本身的接口。
操作系统的核心部分,即内核,是一组设备驱动程序。它们是一组对系统硬件进行控制的底层接口。例如,磁带机就有一个与之对应的设备驱动程序,它知道如果启动磁带,如果和对它前后回绕,如何对它读写等,因为磁带是一个顺序存取设备,所以驱动程序并不能直接访问磁带上的数据块,而是必须把它回绕到正确的位置。
为了向用户提供一个一致的接口,设备驱动程序封装了所有与硬件相关的特性,硬件的特有功能通常可通过ioctl系统来系统。
下面是用于访问设备驱动程序的底层函数(系统调用)
open 打开文件或设备
read 从打开的文件或设备中读取数据
write 向文件或设备写数据
close 关闭文件或设备
ioctl 把控制信息传递给设备驱动程序
3.3 库函数
针对输入输出操作直接使用底层系统调用的一个问题是它们的效率非常地:
1.使用系统调用影响系统的性能。与函数调用相比,系统调用的开销要大,因为在执行系统调用时,linux必须从运行用户代码切换到执行内核代码,然后返回用户代码。减少这种开销的一个方法是,在程序中尽量减少系统调用的次数,并且让每次系统调用完成尽可能多的工作。
2.硬件会限制对底层系统调用使用一次所读写的数据块的大小。
使用库函数,可以高效的写任意长度的数据块,库函数则在数据满足数据块长度要求时安排执行底层调用,这样就极大降低了系统调用的开销。
3.4 底层文件访问
每个运行中的程序被称为进程process,它有一些与之关联的文件描述符。这是一些小值整数,可以通过它们访问打开文件或设备。当一个程序开始运行时,它一般有3个已经打开的文件描述符:
0:标准输入
1:标准输出
2:标准错误
3.4.1 write系统调用
系统调用write的作用是把缓冲区buf的前nbytes个字节写入与文件描述符fildes关联的文件中。下面是write系统调用的原型:
#include <unistd.h>
size_t write(int fildes, const void *buf, size_t nbytes);
返回实际写入的字节数(正常的话)
返回0表示未写入任何数据
返回-1表示write调用中出现了错误
编写程序simple_write.c
3.4.2 read系统调用
系统调用read的作用是:从与文件描述符fildes相关联的文件里读取nbytes个字节的数据,并把它们放到数据区buf中。
#include <unistd.h>
size_t read(int fildes, void *buf, size_t nbytes);
返回实际读入的字节数(正常的话)
返回0表示未读入任何数据,已经到达了文件尾。
返回-1表示read调用出现了错误。
编写程序simple_read.c
echo hello there | ./simple_read.exe
./simple_read.exe < draft1.txt
第一次运行程序时,使用echo通过管道为程序提供输入。
第二次运行程序时,通过文件重定向输入。
3.4.3 open系统调用
为了创建一个新的文件描述符,需要使用系统调用open。
#include <fcnt1.h>
#include <sys/types.h>
#include <sys/stat.h>
int open(const char *path, int oflags);
int open(const char *path, int oflags, mode_t mode);
简单地说,open建立了一条到文件或设备的访问路径。如果调用成功,它将返回一个可以被read,write和其他系统调用使用的文件描述符。准备打开的文件或设备的名字作为参数path传递给函数,oflags参数用于指定打开文件所采取的动作。
open ("myfile", O_CREAT, S_IRUSR|S_IXOTH_);
它的作用是创建一个名为myfile的文件,文件爱你属主拥有读权限,其他用户拥有执行权限。
3.4.4 close系统调用
使用close调用终止文件描述符fildes与其对应文件之间的关联。文件描述符被释放并能够重新使用。close调用成功时返回0,出错时返回-1
#include <unistd.h>
int close(int fildes);
3.4.5 ioctl系统调用
ioctl调用有点像个大杂烩,它提供了一个用于控制设备及其描述符行为和配置底层服务的接口,终端,文件描述符,套接字甚至是磁带机都可以有为它们定义的ioctl。
#include <unistd.h>
int ioctl(int fildes, int cmd,...);
编写一个底层程序copy_system.c,用来逐个字符地把一个文件复制到另外一个文件。
然后再编写一个改进的程序copy_block.c,通过复制大一些的数据块来改善效率。
3.4.6 其余与文件管理的系统调用
1.lseek系统调用
lseek系统调用对文件描述符fildes的读写指针进行设置。
2.fstat,stat和lstat系统调用
3.dup和dup2系统调用
3.5标准I/O库
标准I/O库及其头文件stdio.h为底层I/O系统提供了一个通用的接口。使用标准I/O库的方式和使用底层文件描述符一样,需要先打开一个文件建立一个访问路径。这个操作的返回值将作为其他I/O库函数的参数。在标准I/O库中,与底层文件描述符相对应的是流,它被实现为指向结构FILE的指针。
3.5.1 fopen函数
fopen函数类似于底层的open系统调用,它主要用于文件和终端的输入输出。如果需要对设备进行明确的控制,最好使用底层系统调用,因为这样可以避免用库函数带来的一些潜在问题,如输入输出缓冲。
#include <stdio.h>
FILE* fopen(const char *filename, const char *mode);
fopen打开由filename参数指定的文件,并把它与一个文件流关联起来。mode参数指定文件的打开方式,它取下列字符串中的值:
r只读;w写方式,并把文件长度截短为零;a写方式,新内容追加在文件尾,r+更新方式打开(读和写);w+更新方式打开,并把文件长度截短为零;a+以更新方式打开,新内容追加在文件尾。
fopen在成功时返回一个非空的FILE*指针,失败时返回NULL值,NULL值在头文件stdio.h里定义。
3.5.2 fread函数
fread函数用于从一个文件流里读取数据。数据从文件流stream读到由ptr指向的数据缓冲区里。fread和fwrite都是对数据激励进行操作,size参数指定每个数据记录的长度,计数器nitems给出要传输的记录个数。它的返回值是成功读到数据缓冲区的记录个数。
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nitems, FILE* stream);
3.5.3 fwrite函数
fwrite函数从指定的数据缓冲区里取出数据记录,并把它们写道输出流中。
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE* stream);
3.5.4 fclose函数
fcloase函数关闭指定的文件流stream,使所有尚未写出的数据都写出。
#include <stdio.h>
int fclose(FILE *stream);
3.5.5 fflush函数
fflush函数的作用是把文件流里的所有未写出数据立刻写出。
#include <stdio.h>
int fflush(FILE* stream)
3.5.6 fseek函数
fseek函数与lseek系统调用对应的文件流函数。它在文件流里为下一次读写操作指定位置。offset和whence参数的汉所以与lseek系统调用一样。
#include <stdio.h>
int fseek(FILE* stream, long int offset, int whence);
3.5.7 fgetc、getc和getchar函数
fgetc函数从文件流取出下一个字符并把它作为一个字符返回。当它到达文件尾或者出现错误时,它返回EOF。
#include <stdio.h>
int fgetc(FILE* stream);
int getc(FILE* stream);
int getchar();
getc函数作用和fgetc一样,getchar函数的作用相当于getc(stdin),它从标准输入里读取下一个字符。
3.5.8 fputc、putc和putchar函数
fputc函数把一个字符写到一个输出流中,它返回写入的值,如果失败,则返回EOF
#include <stdio.h>
int fputc(int c, FILE* stream);
int putc(int c, FILE* stream);
int putchar(int c);
putchar函数相当于putc(c, stdout),它把单个字符写到标准输出。
3.5.9 fgets和gets函数
fgets函数从输入文件流stream里读取一个字符串
#include <stdio.h>
char *fgetc(char* s, int n, FILE *stream);
char *gets(char* s);
fgets把读到的字符写到s指向的字符串里,直到出现下面的某种情况:
1.遇到换行符
2.已经传输了n-1个字符
3.到达文件尾
它会把遇到的换行符也传递到接收字符串里,再加上一个表示结尾的空字节\0。一次调用最多传输n-1个字符,因为它必须把空字节加上以结束字符串。
当成功完成时,fgets返回一个指向字符串s的指针。
如果已经到达文件尾,fgets会设置这个文件流的EOF标识并返回一个空指针。
gets函数类似于fgets,只不过它从标准输入读取数据并丢弃遇到的换行符。它在接收字符串的尾部加上一个null字节。
注意:gets对传输字符的个数并没有限制,所以它可能会溢出自己的传输缓冲区。因此,应该避免使用gets并且用fgets来替代。