目录
C语言文件操作
fopen
fwrite
fread
fclose
系统文件描述符
预备知识
open
write
read
文件描述符的理解
文件描述符与stdin/stdout/stderr 的对应关系
文件描述符是什么
再C语言/C++中,默认会打开三个流:
标准输入(stdin)
标准输出(stdout)
标准错误(stderr)
NAME
stdin, stdout, stderr - standard I/O streams
SYNOPSIS
#include
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
其中标准输入对应的是键盘、标准输出对应的是显示器、标准错误也是显示器。
下面我们看看一下 C语言 的文件操作!
再从C语言中,如果想要打开一个文件,那么使用一个函数 fopen:
NAME
fopen, fdopen, freopen - stream open functions
SYNOPSIS
#include
FILE *fopen(const char *path, const char *mode);
该函数第一个参数是 path 表示想要打开的文件的路径。
而第二个参数是打开的模式:
打开的模式有六种:
r:表示以读方式打开,且从头开始读
r+:表示以读写的方式打开,也是从头开始
w:表示以写的方式打开,如果没有对应的文件,那么就会创建一个,而且每一次打开都会清空文件内容,从头开始写。
w+:表示以读写的方式打开,如果没有对应文件,就会被创建,同时每一次也会清空文件,从头开始。
a:表示以写的方式打开。但是写是以追加的方式写,如果没有对应的文件,就会被创建。
a+:表示以读写的方式打开,但是写是追加的写,如果没有对应的文件,那么就会被创建,如果该文件是读的话,那么就 是以起始位置开始读,如果是写的话,那么就是以结尾位置开始写。
返回值,如果打开失败,那么就会返回空
下面测试一下 fopen:
void test1()
{
FILE* fd = fopen("log.txt", "w");
if(fd == NULL)
{
perror("fopen");
}
else
{
printf("文件打开成功\n");
}
}
上面我们以 w 的方式打开,那么如果没有的话,就会创建一个。
那么 log.txt 该文件会打开哪里的 log.txt 文件呢?
当前目录下:可是什么是当前目录呢?当前目录就是该程序运行的时候的目录,就叫当前目录。
如果这里写的是绝对路径的话,那么就按照绝对路径的方式打开。
结果:
[lxy@hecs-165234 linux103]$ ./myfile
文件打开成功
[lxy@hecs-165234 linux103]$ ll
total 20
-rw-rw-r-- 1 lxy lxy 0 Oct 6 16:08 log.txt
-rw-rw-r-- 1 lxy lxy 76 Oct 6 15:40 makefile
-rwxrwxr-x 1 lxy lxy 8488 Oct 6 16:04 myfile
-rw-rw-r-- 1 lxy lxy 215 Oct 6 16:04 myfile.c
我们之前是没有这个文件的。
但是当程序运行后,显示打开成功,而我们的当前目录下确实多了了一个 log.txt 文件。
下面我们以 r 的方式打开,但是i打开之前我们先删除掉 log.txt。
void test1()
{
FILE* fd = fopen("log.txt", "r");
if(fd == NULL)
{
perror("fopen");
}
else
{
printf("文件打开成功\n");
}
}
结果:
[lxy@hecs-165234 linux103]$ ./myfile
fopen: No such file or directory
这里我们以 r 的方式打开失败了。
因为 r 方式打开的话,没有对应的文件并不会创建对应的文件。
下面我们再看一下 write 接口:
NAME
fread, fwrite - binary stream input/output
SYNOPSIS
#include
size_t fwrite(const void *ptr, size_t size, size_t nmemb,
FILE *stream);
ptr 表示想要写入的数据。
size 表示想要写入多少数据。
nmemb 表示每一个数据的大小,也就是字节。(假设现在是 int 类型,当前有 3 个 int 类型,每个int 是4 个字节,那么就是 size = 3, nmemb = 4);
stream 表示是打开的 FILE 指针。
下面我们以 w 的方式大开文件,然后写入文件:
void test2()
{
FILE* fd = fopen("log.txt", "w");
if(fd == NULL)
{
perror("fopen");
}
// 打开成功
const char* str = "hello write\n";
fwrite(str, strlen(str), 1, fd);
}
上面我们写了一个字符串,写到 log.txt 文件当中。
但是我们写的字符串多加了一个 \n ?为什么?
因为写入文件后,文件里面的内容入过没有分隔符分开的话,那么就粘再一起了。
而且将 /n 写进入的话,打印出来还会换行,所以看起来清晰一点。
那我们为什么不将/0写进去呢?实际上文件是不认识 /0 的,所以写入也是乱码!
结果:
[lxy@hecs-165234 linux103]$ cat log.txt
hello write
下面我们再看一下,我们多调用几次该函数,会多写入吗?
[lxy@hecs-165234 linux103]$ cat log.txt
hello write
//这里我们的 log.txt 里面只有一行数据,我们多调用几次
[lxy@hecs-165234 linux103]$ ./myfile
[lxy@hecs-165234 linux103]$ ./myfile
[lxy@hecs-165234 linux103]$ ./myfile
[lxy@hecs-165234 linux103]$ ./myfile
[lxy@hecs-165234 linux103]$ ./myfile
//查看 log.txt
[lxy@hecs-165234 linux103]$ cat log.txt
hello write
上面我们发现多调用了几次也同样只有一行数据,为什么?
因为 w 是每次打开都会清空文件按,然后从头开始写,那么我们要是不想清空呢?
a 方式打开
下面我们追加的写入数据:
void test2()
{
FILE* fd = fopen("log.txt", "a");
if(fd == NULL)
{
perror("fopen");
}
// 打开成功
const char* str = "hello write\n";
fwrite(str, strlen(str), 1, fd);
}
下面我们多调用几次,看一下是会不会被清空后重写呢?
结果:
[lxy@hecs-165234 linux103]$ ./myfile
[lxy@hecs-165234 linux103]$ ./myfile
[lxy@hecs-165234 linux103]$ ./myfile
[lxy@hecs-165234 linux103]$ ./myfile
[lxy@hecs-165234 linux103]$ cat log.txt
hello write
hello write
hello write
hello write
hello write
这里看到我们全部都写再后面了,并没有被清空。
NAME
fread, fwrite - binary stream input/output
SYNOPSIS
#include
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr 是表示读出来的数据放到哪里。
size 表示读取多少个数据。
nmemb 表示每个数据大小是多少。
stream 表示从那个 FILE 指针里面读。
但是这次我们并不想使用这个接口,我们使用一个其他的接口,这个接口和 fwrite 基本一样,所以换一个接口使用,C语言中还有很多不同类型的接口
fgets
NAME
fgetc, fgets, getc, getchar, gets, ungetc - input of characters and strings
SYNOPSIS
#include
char *fgets(char *s, int size, FILE *stream);
上面是一批接口,但是使用起来都是差不多的,所以这里也只是介绍一下。
fgets 表示每一次都是行读取。
s 表示读取到 s 的缓冲区。
size 表示读取多少字节。
下面打开我们刚才写入的 log.txt 文件,然后按行读取里面的数据:
void test3()
{
FILE* fd = fopen("log.txt", "r");
if(fd == NULL)
{
perror("fopen");
}
// 打开成功
char buffer[64] = {0};
while(1)
{
if(fgets(buffer, sizeof(buffer) - 1, fd) == NULL) break;
printf("%s", buffer);
}
}
结果:
[lxy@hecs-165234 linux103]$ ./myfile
hello write
hello write
hello write
hello write
hello write
那么我们下面如果将我们的 myfile.c 改一下。
我们通过命令行参数传入一个文件,然后打印这个文件会怎么样子?
int main(int argc, char* argv[])
{
if(argc != 2)
{
printf("Usage: %s + file_name\n", argv[0]);
exit(1);
}
FILE* fd = fopen(argv[1], "r");
char buffer[128] = {0};
while(1)
{
if(fgets(buffer, sizeof(buffer) - 1, fd) == NULL) break;
printf("%s", buffer);
}
return 0;
}
这样我们就可以通过命令行参数将我们的文件内容打印出来。
结果:
[lxy@hecs-165234 linux103]$ ./myfile log.txt
hello write
hello write
hello write
hello write
hello write
[lxy@hecs-165234 linux103]$ ./myfile makefile
myfile:myfile.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -rf myfile
这不仅可以打印 log.txt 还可以打印其他的文件,和 cat 命令很像,所以这就实现了一个我们自己的 cat命令。
C语言的文件惭怍就介绍到这里吗,下面我们看系统提供的函数。
同时我们再看一下C语言的文件操作与系统的文件操作有什么区别。
NAME
fclose - close a stream
SYNOPSIS
#include
int fclose(FILE *fp);
这个函数就是关闭打开的文件描述符的,所以不介绍了。
再介绍系统的文件描述符之前,我们先做一下预备知识!
再系统中创建一个文件,当该文件为空的时候,那么该文件有大小吗?
回答:有大小!
实际上,文件不仅只有文件的内容。文件 = 内容 + 属性。
既然我们现在知道,一个文件等于内容加属性,那么我们现在需要对文件操作,那么会有哪些操作?
回答:既然文件等于内容加属性,那么对文件的操作当然也就是对文件的内容和属性的操作。
文件时放在哪里的?回答:磁盘。
那么磁盘是什么?回答:外设。
那么访问外设普通人能访问吗?回答:不能。
那么谁才能访问外设?回答:操作系统。
既然只有操作系统可以访问外设,那么我们想要访问一个文件怎么办?回答当然需要系统为我们提供接口!
那么我们要访问文件的步骤是什么?回答:代码 -> 编译 -> .exe -> 运行 -> 访问文件。
那么最后是谁再访问文件?回答:进程再访问文件。
所以,访问文件实际上是进程再替我们访问文件,帮我们读写修改!
显示器是硬件吗?是的!
那么向显示器输入和向磁盘写入一样吗?一样!
所以我们发现向硬件写入都是一样的,本质上都是向文件里面写入!
所以这就是 Linux 中的一切皆文件。
NAME
open, creat - open and possibly create a file or device
SYNOPSIS
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
open 就是系统调用接口,用来打开一个文件。
首先我们看第一个 open 函数。
他的第一个参数是 pathname ,表示就是一个文件名/路径。
而第二个参数是一个 flag 也就是一格标志位,那么这个标志位是干什么的?
实际上这个标志位是一格位图!
可以通过一些标志来控制传入的值的各个比特位!
下面看一下常用的标志位!
再 Linux 中一般的标志位都是宏定义!
O_WRONLY: 表示以读写方式打开
O_RDONLY: 表示以读方式打开
O_APPEND: 表示以追加的方式来打开
O_CREAT: 表示如果没有对应的文件,就创建
O_TRUNC: 表示每次打开都清空文件
返回值是一个 int 类型,这里先不说,先使用!如果返回值小于0,表示打开失败!
下面我们先用第一个函数测试一下:
void test4()
{
int fd = open("log.txt", O_WRONLY);
if(fd < 0)
{
perror("open");
exit(1);
}
// 打开成功
printf("fd: %d\n", fd);
}
当前我们目录下有 log.txt 文件,然后我们以 读写方式打开。
如果成功的话,那么我们再打印一下 fd 也就是返回值。
结果:
[lxy@hecs-165234 linux103]$ ./myfile
fd: 3
上面打开成功了,看到返回值是 3
下面我们删掉 log.txt 文件后再试一下:
[lxy@hecs-165234 linux103]$ ./myfile
open: No such file or directory
这里就打开失败了,但是为什么呢?因为我们只是以读写方式打开。
如果没有对应的文件就会报错!那么我们想要没有文件就创建呢?
我们 open 的时候再将 O_CREAT标志位设置到 flag 中。
void test4()
{
//int fd = open("log.txt", O_WRONLY);
int fd = open("log.txt", O_WRONLY|O_CREAT);
if(fd < 0)
{
perror("open");
exit(1);
}
// 打开成功
printf("fd: %d\n", fd);
}
下面我们继续打开以读写方式打开 log.txt 文件,入锅没有文件的话,就创建:
[lxy@hecs-165234 linux103]$ ./myfile
fd: 3
[lxy@hecs-165234 linux103]$ ll
total 24
---sr-s--T 1 lxy lxy 0 Oct 6 17:42 log.txt
-rw-rw-r-- 1 lxy lxy 76 Oct 6 15:40 makefile
-rw-rw-r-- 1 lxy lxy 358 Oct 6 17:27 mycat.c
-rwxrwxr-x 1 lxy lxy 8872 Oct 6 17:40 myfile
-rw-rw-r-- 1 lxy lxy 1010 Oct 6 17:40 myfile.c
这里看到确实,没有 log.txt 文件就创建了,但是为什么,创建的 log.txt 文件那么奇怪呢?
log.txt 文件的权限和下面的普通文件的权限不一样呢?
这是因为入锅我们直接这样创建出来的话,这个文件的权限就是随机的,所以我们需要使用第二个 open
int open(const char *pathname, int flags, mode_t mode);
第二个 open 比第一个多了一格参数,而前面两个参数是一样的!
那么最后一个参数是干什么的呢?
最后一格参数表示权限!
下面我们试一下使用第二个函数打开文件:
void test5()
{
int fd = open("log.txt", O_WRONLY|O_CREAT,0666);
if(fd < 0)
{
// 打开失败
perror("open");
exit(1);
}
// 打开成功
}
这里打开 log.txt 但是没有 log.txt 文件,所以会创建。
我们传入的权限是 666 - rw-rw-rw-,这里的 0 表示八进制
那么我们看一下是否为能创建好!
结果:
[lxy@hecs-165234 linux103]$ ll
total 24
-rw-rw-r-- 1 lxy lxy 0 Oct 6 18:06 log.txt
-rw-rw-r-- 1 lxy lxy 76 Oct 6 15:40 makefile
-rw-rw-r-- 1 lxy lxy 358 Oct 6 17:27 mycat.c
-rwxrwxr-x 1 lxy lxy 8936 Oct 6 18:05 myfile
-rw-rw-r-- 1 lxy lxy 1266 Oct 6 18:05 myfile.c
这里创建的是 rw-rw-rw- 为什么?
因为有权限掩码 0002,所以我们写入的权限也是受到权限掩码的影响。
那么要怎么办呢?
我们可以调用 umask 函数然后将当前进程的权限掩码修改为 0 ,这样我们设置的是多少,那么创建的文件的权限就是多少。
umask
NAME
umask - set file mode creation mask
SYNOPSIS
#include
#include
mode_t umask(mode_t mask);
下面看一下修改后的函数:
void test5()
{
umask(0);
int fd = open("log.txt", O_WRONLY|O_CREAT,0666);
if(fd < 0)
{
// 打开失败
perror("open");
exit(1);
}
// 打开成功
}
结果:
[lxy@hecs-165234 linux103]$ ll
total 24
-rw-rw-rw- 1 lxy lxy 0 Oct 6 18:16 log.txt
-rw-rw-r-- 1 lxy lxy 76 Oct 6 15:40 makefile
-rw-rw-r-- 1 lxy lxy 358 Oct 6 17:27 mycat.c
-rwxrwxr-x 1 lxy lxy 8984 Oct 6 18:15 myfile
-rw-rw-r-- 1 lxy lxy 1278 Oct 6 18:15 myfile.c
这里看到创建的文件的权限就是我们设置的了。
NAME
write - write to a file descriptor
SYNOPSIS
#include
ssize_t write(int fd, const void *buf, size_t count);
首先是第一个参数 fd 表示open 打开的文件描述符!
第二个参数想要写入的数据。
第三个表示想要写入多少字节。
返回值实际写入了多少字节。
下面我们再向文件里面写内容:
void test4()
{
//int fd = open("log.txt", O_WRONLY);
int fd = open("log.txt", O_WRONLY|O_CREAT);
if(fd < 0)
{
perror("open");
exit(1);
}
// 打开成功
printf("fd: %d\n", fd);
//写入数据
const char* str = "hello write\n";
write(fd, str, strlen(str));
}
这里将 str 写入到 log.txt 文件中。
结果:
[lxy@hecs-165234 linux103]$ ./myfile
fd: 3
[lxy@hecs-165234 linux103]$ cat log.txt
hello write
成功写入了,没问题!
下面我们将 str 改一下:
void test4()
{
//int fd = open("log.txt", O_WRONLY);
int fd = open("log.txt", O_WRONLY|O_CREAT);
if(fd < 0)
{
perror("open");
exit(1);
}
// 打开成功
printf("fd: %d\n", fd);
//写入数据
const char* str = "nihao";
write(fd, str, strlen(str));
}
这里将 str 改成 nihao 后我们再写入试一下:
结果:
[lxy@hecs-165234 linux103]$ ./myfile
fd: 3
[lxy@hecs-165234 linux103]$ cat log.txt
nihao write
这里看到把前面几个字符给覆盖了,后面的没有修改。
也就是写的时候重头开始写。
那么我们要是想要每一次打开都清空后开始写呢?我们需要将 O_TRUNC 设置进标志位!
void test4()
{
umask(0);
int fd = open("log.txt", O_WRONLY|O_TRUNC,0666 );
if(fd < 0)
{
perror("open");
exit(1);
}
// 打开成功
printf("fd: %d\n", fd);
//写入数据
const char* str = "nihao";
write(fd, str, strlen(str));
}
现在我们执行一下,看是否会清空后重写!
结果:
[lxy@hecs-165234 linux103]$ cat log.txt
nihao
那么我们想要追加呢?我们只需要添加 O_APPEND
void test4()
{
umask(0);
int fd = open("log.txt", O_WRONLY|O_APPEND,0666 );
if(fd < 0)
{
perror("open");
exit(1);
}
// 打开成功
printf("fd: %d\n", fd);
//写入数据
const char* str = "nihao\n";
write(fd, str, strlen(str));
}
结果:
[lxy@hecs-165234 linux103]$ ./myfile
fd: 3
[lxy@hecs-165234 linux103]$ ./myfile
fd: 3
[lxy@hecs-165234 linux103]$ ./myfile
fd: 3
[lxy@hecs-165234 linux103]$ ./myfile
fd: 3
结果
[lxy@hecs-165234 linux103]$ cat log.txt
nihaonihao
nihao
nihao
nihao
nihao
上面就可以解决追加的问题了。
NAME
read - read from a file descriptor
SYNOPSIS
#include
ssize_t read(int fd, void *buf, size_t count);
fd 表示打开的文件描述符
buf 表示读取到那个缓冲区
count 表示读取多少个字节
返回值表示实际读取到多少字节
下面读取 log.txt 文件,实际上入锅只是读取的话,那么可以使用 open 的第一个函数:
void test6()
{
int fd = open("log.txt", O_RDONLY);
if(fd < 0)
{
perror("open");
exit(1);
}
// 打开成功
char buffer[64] = {0};
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if(s > 0)
{
// 读取成功
buffer[s] = '\0';
}
printf("%s", buffer);
}
结果:
[lxy@hecs-165234 linux103]$ ./myfile
nihaonihao
nihao
nihao
nihao
nihao
nihao
这里看到读取成功了。
上面我们一直再 open 打开文件,然后回返回一个 int 类型的值,而这个值就叫做文件描述符!
那么文件描述符究竟是什么呢?
我们下面多打开一些文件,看一下返回值:
void test7()
{
int fd1 = open("log.txt1", O_WRONLY|O_CREAT, 0666);
printf("fd1: %d\n", fd1);
int fd2 = open("log.txt2", O_WRONLY|O_CREAT, 0666);
printf("fd2: %d\n", fd2);
int fd3 = open("log.txt3", O_WRONLY|O_CREAT, 0666);
printf("fd3: %d\n", fd3);
int fd4 = open("log.txt4", O_WRONLY|O_CREAT, 0666);
printf("fd4: %d\n", fd4);
}
结果:
[lxy@hecs-165234 linux103]$ ./myfile
fd1: 3
fd2: 4
fd3: 5
fd4: 6
这里从 3 开始打印,然后依次递增。
再系统中,我们打开对文件操作是什么来操作文件的?
回答:进程来操作!
进程操作文件要怎么操作,直接再磁盘中能操作吗?
回答:不可以,需要加载到内存中才能操作!
那么一个进程可以对多个文件操作吗?
回答:可以!
那么也就是内存中很多被打开的文件,既然有很多被打开的文件,那么操作系统要不要管理这些文件?
回答:需要管理!
怎么管理?
回答:先描述,再组织!(将文件用数据结构来描述,然后可以使用双链表链接起来)。
那么我们看到我们的文件描述符是从 3 开始增长的,不绝对奇怪吗?
为什么是从3开始呢?前面的文件描述符呢?
实际上,前面我们已经说过了,C语言/C++回默认打开三个流:标准输入、标准输出、标准错误。
标准输入就是 0 号文件描述符。
标准输出就是 1 号文件描述符。
标准错误就是 2 号文件描述符。
那么我们如何验证呢?
我们可以像stdout 和 2 号文件描述符里面写,看是否会打印到显示器上,然后我们向stdin 和 2 号文件描述符里读,看是否是键盘,实际上 stderr 也是显示器,所以这里就不测试了。
void test8()
{
char buffer[64] = {0};
//向标准输出中写
const char* str = "hello stdout\n";
fprintf(stdout,"%s", str);
write(2, str, strlen(str));
}
这里 fprintf 向 标准输出中写,也就是显示器。
wreit 向 2 号文件描述符中写。
结果:
[lxy@hecs-165234 linux103]$ ./myfile
hello stdout
hello stdout
这里看到都写到显示器上了,所以说明 stdout 就是 2 号文件描述符
void test8()
{
// // 向stdin中读
fgets(buffer, sizeof(buffer) - 1, stdin);
printf("%s", buffer);
int s = read(2, buffer, sizeof(buffer) - 1);
if(s > 0)
{
buffer[s] = '\0';
}
printf("%s", buffer);
}
结果:
[lxy@hecs-165234 linux103]$ ./myfile
hello fgets
hello fgets
hello read
hello read
下面的 stderr 就不验证了,但是我们已经知道他们是意义对应的!
现在有一个疑问,为什么是从 0 开始递增呢?
我们这个 0 开始的像什么呢?
实际上 有一个 struct file 结构体,里面存的是打开的文件的信息。
而PCB里面有一个struct file 的数组,里面放的就是struct file 这个与下标一一对应。
只要有下标就可以找到对应的文件信息,只要有信息,那么就可以对对应的文件进行操作。
所以实际上文件描述符就是数组的下标,那么每一次都是如何设置文件描述符的?
我们将 0 号文件描述符关闭后,我们再打开一个文件,看一下文件描述符是多少:
void test1()
{
int fd = open("log.txt", O_WRONLY|O_CREAT,0666 );
if(fd < 0)
{
perror("open");
exit(1);
}
// 打开成功
printf("fd: %d\n", fd);
}
结果:
[lxy@hecs-165234 linux104]$ ./myfile
fd: 3
首先是我们没有关闭任何一个文件描述符,那么下面我们关闭掉 0号文件描述符
上面打开的文件的文件描述符就是 3,因为 0、1、2 都已经有了
void test1()
{
// 关闭 0 号文件描述符
close(0);
int fd = open("log.txt", O_WRONLY|O_CREAT,0666 );
if(fd < 0)
{
perror("open");
exit(1);
}
// 打开成功
printf("fd: %d\n", fd);
}
结果:
[lxy@hecs-165234 linux104]$ ./myfile
fd: 0
这里看到的文件描述符是 0,而我们也关闭了 0 号文件描述符
那么我们下面关掉2号文件描述符,看一下打开的文件描述符是多少
void test1()
{
// 关闭 0 号文件描述符
close(2);
int fd = open("log.txt", O_WRONLY|O_CREAT,0666 );
if(fd < 0)
{
perror("open");
exit(1);
}
// 打开成功
printf("fd: %d\n", fd);
}
结果:
[lxy@hecs-165234 linux104]$ ./myfile
fd: 2
这里看到的文件描述符是 2
也是我们关掉的文件描述符。
结论:
我们每一次打开文件,那么该文件返回值的文件描述符就是最小没有被占用的文件描述符