03 文件操作

3.1 linux文件结构

文件的inode是文件系统中一个特殊的数据块,保存文件的属性。目录文件中的每个数据项都是指向其它文件的链接,删除文件就等于删除与之对应的链接。如果指向文件的链接数为0,则会在磁盘上把空间空出来。

三个重要的设备文件:

  • /dev/console:错误信息和诊断信息通常会发送到这个设备

  • /dev/tty:console只有一个,而tty有很多个,能访问到不同的物理设备

  • /dev/null:空设备

3.2 系统调用与标准库函数

设备驱动程序直接与硬件打交道,系统调用是面向应用程序的接口。下面是访问设备驱动程序的一些系统调用:

  • open:打开文件(设备)

  • read:从打开的文件中读数据

  • write:向文件或设备中写数据

  • close:关闭文件

  • ioctl:把控制信息传给驱动程序。这个方法与设备关联,不具备移植性。

直接使用系统调用比使用标准库的效率低,因为:

  • 系统调用会在用户态和核心态之间切换,开销大。而标准库会帮我们减少系统调用的次数。

  • 系统调用对能读写数据块的大小有限制。

一般来说应用程序使用标准库调用系统调用函数系统调用函数再去调用设备驱动程序

3.4 底层文件访问

4.1 write系统调用

#include<unistd.h>
size_t write(int fildes, const void *buf, size_t nbytes);

write把缓冲区buf中前nbytes个字节写入与文件描述符fildes关联的文件中。返回实际写入的字节数,如果返回-1,说明出错,错误代码保存在全局变量errno中。

#include<unistd.h>
#include<stdlib.h>
int main()
{
if ((write(1, "Here is some data\n", 18)) != 18)
write(2, "A write error has occurred on file descriptor 1\n", 46);
exit(0);
}

运行上面的例子,输出

leo@ubuntu:~/c_test/cp3$ ./a.out
Here is some data

4.2 read系统调用

#include<unistd.h>
size_t read(int fildes, void *buf, size_t nbytes);

read和write正好相反,是把buf的数据写到文件中。

#include<unistd.h>
#include<stdlib.h>
int main()
{
char buffer[128];
int nread;
nread = read(0, buffer, 128);
if (nread == -1)
write(2, "A read error has occurred\n", 26);
if ((write(1, buffer, nread)) != nread)
write(2, "A write error has occurred\n", 27);
exit(0);
}

上面例子的输出。

leo@ubuntu:~/c_test$ ./read < file.txt
Hi, I am leo. # file.txt的文件内容就是Hi, I am leo.
leo@ubuntu:~/c_test$ echo hello world | ./read
hello world

4.3 open系统调用

#include<fcntl.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); // create时需要第三个参数

函数成功返回一个全局唯一的文件描述符。不同程序用open打开同一文件,会返回两个不同的文件描述符。如果都进行写操作,可能会各写各的。

oflags的三个基本可选值:

  • O_RDONLY 以只读方式打开

  • O_WRONLY 以只写方式打开

  • O_RDWR 以读写方式打开

另外还有几个值可以和上面三个值用|来搭配使用

  • O_APPEND 把写入的数据加在末尾

  • O_TRUNC 把文件长度设置为0,丢弃已有内容

  • O_CREAT 按照第三个参数mode来创建文件,

第三个参数的9个可选值就是linux中文件的9个权限位,用|来选取多个。

  • S_IRUSR

  • S_IWUSR

  • S_IXUSR

  • S_IRGRP

  • S_IWGRP

  • S_IXGRP

  • S_IROTH

  • S_IWOTH

  • S_IXOTH

4.4 close系统调用

#include<unistd.h>
int close(int fildes);

close用来终止文件描述符与其对应文件之间的关联。注意检查close的返回值很重要,成功返回0,失败返回-1。

4.5 实验 - 文件复制程序

我们用block来做缓存,每次赋值1kb。

#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
int main()
{
char block[1024];
int in, out;
int nread;
in = open("file.in", O_RDONLY);
out = open("file.out", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
while ((nread = read(in, block, sizeof(block))) > 0)
write(out, block, nread);
exit(0);
}

4.6 与文件管理相关的其他系统调用

lseek

#include<unistd.h>
#include<sys/types.h>
off_t lseek(int fildes, off_t, offset, int whence);

lseek用来对文件描述符fildes的读写指针进行设置。offset参数用来指定位置。

whence可以指定以下值:

  • SEEK_SET offset是绝对位置

  • SEEK_CUR offset是相对当前fildes的位置

  • SEEK_END offset是相对于文件尾的位置

成功返回文件头到文件指针被设置的位置的字节偏移,失败返回-1。

fstat、stat、lstat

#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
int fstat(int fildes, struct stat *buf);
int stat(const char *path, struct stat *buf);
int lstat(const char *path, struct stat *buf);

这三个函数返回文件信息。都通过stat类型指针的buf来返回。fstat是返回文件描述符相关的文件的信息。stat和lstat则是直接用路径。不同之处在于,若是符号链接,lstat则会返回符号链接本身的信息,stat则会返回符号链接指向的文件的信息。

stat结构体成员:

  • st_mode 文件权限和文件类型信息

  • st_ino inode

  • st_dev 保存文件的设备

  • st_uid

  • st_gid

  • st_atime 上一次被访问的时间

  • st_ctime

  • st_mtime

  • st_nlink 硬链接的个数

dup、dup2

这两个系统调用都用来复制文件描述符。dup直接返回一个新的文件描述符,而dup2则用参数来复制。

#include<unistd.h>
int dup(int fildes);
int dup2(int fildes, int fildes2);

3.5 标准IO库

标准IO库及其头文件stdio.h为底层IO调用提供了一个通用接口。底层文件描述符对应流(stream),它被实现为指向结构FILE的指针。

启动程序时,3个流自动打开,分别是stdin、stdout和stderr。与底层文件描述符0,1,2相对应。

fopen

FILE *fopen(const char *filename, const char *mode)

mode加b表示以二进制方式打开,而不是以文本文件方式打开。unix和linux中,所有文件都看做是二进制文件。成功返回非空FILE *指针,失败返回NULL。文件流数量和文件描述符一样,是有限的。stdio.h中的FOPEN_MAX定义了最大值。

fread

size_t fread(void *ptr, size_t size, size_t ntimes, FILE *stream);

从stream流中读到ptr指向的缓冲区。size是每个记录的长度,ntimes指定要传输的记录个数。函数返回读到ptr的记录的个数

fwrite

size_t fwrite(void *ptr, size_t size, size_t ntimes, FILE *stream);

和fread方向相反。从ptr向stream中写。返回成功写入的记录个数。

fclose

int fclose(FILE *stream);

关闭文件流,并将尚未写出的数据都写出。

fflush

int fflush(FILE *stream);

把文件流中的数据立刻写出。fclose隐含执行了一次flush操作。

fseek

int fseek(FILE *stream, long int offset, int whence);

与lseek对应,参数的意义也一样。0表示成功,-1表示失败。

fgetc getc getchar

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar();

fgetc从文件流中读取一个字节并返回。getc和fgetc一样,不过可能是用宏来实现的。getchar则从标准输入中读取下一个字符。

fputc putc putchar

int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);

这三个函数把字符c写到流里面。三者的关系同上面一样。注意fgetc和fputc的参数类型和返回值都是int而不是char,这使得返回EOF(-1)得以可能。

fgets gets

char *fgets(char *s, int n, FILE *stream);
char *gets(char *s);

两个函数从流中把字符写到s指向的内存。停止情况:遇到换行、到文件尾、已经传输了n-1个字符。字符串最后要加\0,所以只能传n-1个字符。

注意gets对输入的字符没有限制,可能溢出自己的缓冲区。所以推荐使用fgets而不是gets。

3.6 格式化输入输出

printf sprintf fprintf

int printf(const char *format, ...);
int sprintf(char *s, const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);

printf输出到标准流,sprintf输出到s,fprintf输出到指定的stream。

scanf sscanf fscanf

int scanf(const char *format, ...);
int sscanf(const char *s, const char *format, ...);
int fsacnf(FILE *stream, const char *format, ...);

分别从标准输入,s和指定流stream中读入。注意格式字符串中的空格用来忽略流中的各种空白字符(包括空格、制表符、换页符、换行符)。

int num;
scanf("Hello %d", &num);

对上面的代码,下面两个输入都能把数字写到num中。

Hello 1234
Hello1234

注意两点:

  • %c 不会跳过空格

  • %s 遇到空格就会停止。

一般推荐用fread或fgets进行输入。再用字符串函数进行数据的处理。

之前我们使用系统调用实现了复制文件的程序吗,现在我们使用标准库来实现。

#include<stdio.h>
#include<stdlib.h>
int main()
{
int c;
FILE *in, *out;
in = fopen("file.in", "r");
out = fopen("file.out", "w");
while ((c = fgetc(in)) != EOF)
fputc(c, out);
exit(0);
}

文件流错误

#include<errno.h>
extern int errno;

为了表明错误,stdio中很多函数都返回一个超出范围的值,如空指针或EOF。此时,错误由errno指出。许多函数都可能改变errno的值。它的值只有在函数调用失败的时候才有意义。因此需要在函数表明失败后立即检查errno。

另一种方式是通过检查文件流的状态来确认是否发生错误。

  • int ferror(FILE *stream); 测试文件流的错误标志,如果标志被设置,则会返回非0值。

  • int feof(FILE *stream); 测试文件流的文件尾标志,如果标志被设置,则会返回非0值。

  • void clearerr(FILE *stream); 用来清除文件流的上述两种标志

文件流和文件描述符

每个文件流都和一个底层的文件描述符相关联。

  • int fileno(FILE *stream); 返回文件流对应的底层文件描述符

  • FILE *fdopen(int fildes, const char *mode); 为已打开的文件描述符创建新的文件流。

如果用open创建了一个文件,又想用文件流来操作,就可以使用fdopen。

3.7 文件和目录的维护

chmod

#include<sys/stat.h>
int chmod(const char *path, mode_t mode);

参数mode同open中的一样

chown

#include<sys/types.h>
#include<unistd.h>
int chown(const char *path, uid_t owner, git_t group);

三者头文件#include<unistd.h>

  • int unlink(const char *path); 减少文件的链接数。如果文件的链接数为0,且没有进程打开它,这个文件就会被删除。

  • int link(const char *path1, const char *path2); 创建一个指向path1的新链接,新链接的路径就是path2

  • symlink(const char *path1, const char *path2); 参数意义同上,不过创建的是软链接。注意软链接不会count在文件的链接数里。

mkdir rmdir

#include<sys/types.h>
#include<sys/stat.h>
int mkdir(const char *path, mode_t mode);

mkdir用来创建一个目录,mode参数和open一样。

#include<unistd.h>
int rmdir(const char *path);

rmdir用来删除目录。只有目录为空时,才能直接删除。

chdir getcwd

#include<unistd.h>
int chdir(const char *path);

同shell中的cd一样,用来切换目录

#include<unistd.h>
char *getcwd(char *buf, size_t size);

把当前目录的名字写到buf中。如果长度超过了size,则返回NULL。如果成功,返回指针buf。

3.8 扫描目录

与目录操作相关的函数在头文件dirent.h中,与FILE对应,有一个目录流DIR。结构struct dirent存储了目录下每个目录项的信息。dirent一般包括以下部分:

  • ino_t d_ino 文件的inode节点号

  • char d_name[] 文件名

opendir

#include<sys/types.h>
#include<dirent.h>
DIR *opendir(const char *name);

opendir打开一个目录并建立一个目录流。注意目录流使用一个底层文件描述符来访问目录本身,如果打开的文件过多,opendir可能会失败。

readdir

#include<sys/types.h>
#include<dirent.h>
struct dirent *readdir(DIR *dirp);

可以通过while来读取DIR中的每一个目录项。具体使用见下面的例子。

closedir

#include<sys/types.h>
#include<dirent.h>
int closedir(DIR *dirp);

关闭目录流并释放与之关联的资源。

一个实现tree功能的例子

#include<unistd.h>
#include<stdio.h>
#include<dirent.h>
#include<string.h>
#include<sys/stat.h>
#include<stdlib.h>
void printdir(char *dir, int depth)
{
DIR *dp;
struct dirent *entry;
struct stat statbuf;
if ((dp = opendir(dir)) == NULL) {
fprintf(stderr, "cannot open directory: %s\n", dir);
return;
}
chdir(dir);
while ((entry = readdir(dp)) != NULL) {
lstat(entry->d_name, &statbuf);
if (S_ISDIR(statbuf.st_mode)) {
/* Found a directory, but ignore . and .. */
if (strcmp(".", entry->d_name) == 0 || strcmp("..", entry->d_name) == 0)
continue;
printf("%*s%s/\n", depth, "", entry->d_name);
printdir(entry->d_name, depth+4);
}
else
printf("%*s%s\n", depth, "", entry->d_name);
}
chdir("..");
closedir(dp);
}
int main(int argc, char *argv[])
{
char *topdir = ".";
if (argc >= 2) {
topdir = argv[1];
}
printf("Directory scan of %s\n", topdir);
printdir(topdir, 0);
printf("done. \n");
exit(0);
}

3.9 错误处理

许多系统调用和函数在失败时都会设置外部变量errno来指明失败的原因。要注意,errno必须在函数出错之后立即检查,因为下一个函数调用不管有没有出错,都会重写这个变量。

下面是一些错误代码的取值,它们都存在errno.h

  • EPERM 操作不允许

  • ENOENT 文件或目录不存在

  • EINTR 系统调用被中断

  • EIO IO错误

strerror

#include<string.h>
char *strerror(int errnum);

这个函数把错误代码映射为一个说明字符串

perror

#include<stdio.h>
void perror(const char *s);

这个函数也把当前errno存储的错误代码映射到一个说明字符串,并且把s和冒号加到说明字符串之前,然后输出到标准输出。

像下面这样调用函数

perror("program_name");

会输出下面的内容

program_name: Too many open files