1、回顾C文件接口
对于文件,我们有以下共识原理:
1、文件=内容+属性。
2、文件分为打开的文件和没打开的文件。
3、打开的文件:谁打开的呢?——进程,所以本质是研究进程和文件之间的关系。
4、没打开的文件:在磁盘上放着。我们最关注什么问题?——没有打开的文件非常多,文件如何被分门别类的防止好,也就是如何存储的问题。——我们要快速的对文件进行增删查改,快速找到文件。
文件被打开,必须先被加载到内存中,而且一个进程可能打开多个文件,所以进程:文件=1:n。
那么也就注定了操作系统可能存在大量的被打开的文件,OS要不要管理这些文件?要,如何管理?——先描述,再组织。所以内核中一定存在struct xxx结构体对象,里面描述了文件的很多信息。



fopen打开文件,返回一个FILE*的指针,第一个参数就是要打开的文件,第二个参数是打开的方式。fclose用来关闭文件。
然后我把文件打开的六个方式截了出来,大家可以看一下。r就是以只读的方式打开,r+就是以读写的方式打开。
下面演示以w方式打开:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("PID: %d\n", getpid());
FILE* fp = fopen("log.txt", "w");
fclose(fp);
sleep(1000);
return 0;
}

运行程序后,默认在源程序所在路径下创建了log.txt文件。以w方式打开,如果不存在目标文件就会直接创建。那么为什么在源程序所在路径下创建呢?——右边我们查看进程当前工作目录可以看到,当前工作路径cwd就是源程序所在的路径。所以w方式打开文件,如果文件不存在,默认在当前路径下创建文件。这个当前路径就是cwd。
那么如果我们更改了当前路径,是不是就会把文件创建到其他目录呢?下面我们使用chdir验证:

#include <stdio.h>
#include <unistd.h>
int main()
{
chdir("../");
printf("PID: %d\n", getpid());
FILE* fp = fopen("log.txt", "w");
fclose(fp);
sleep(1000);
return 0;
}

可以看到,使用chdir后,进程的当前工作路径cwd修改成上级路径,并且在上级路径下创建了log.txt文件。
下面我们使用fwrite接口对文件进行写入操作:

fwrite第一个参数就是要写入字符串的地址,第二个是写入的单个元素大小,第三个参数是写入的元素个数,第四个参数就是要写入的文件。返回值表示实际写入的元素个数。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
//printf("PID: %d\n", getpid());
FILE* fp = fopen("log.txt", "w");
const char* message = "hello linux";
fwrite(message, strlen(message), 1, fp);
fclose(fp);
return 0;
}

原来目录下不存在log.txt,以w方式打开创建了log.txt,并向文件中写入hello linux。但是当我们多次运行文件我们发现内容还是一样的。

我们把字符串内容修改,然后重新写入。写入前是hello linux,写入后变成了xxx。说明w方式打开每次都会清空文件内容,然后重新写入。

那么对于之前说的输出重定向,每次也会清空文件内容然后再写入。那么输出重定向底层肯定也是以w方式打开的文件,如果文件不存在就创建,然后清空文件内容重新写入。
另外说明一个问题:使用fwrite写入的时候,我们需要计算字符串长度传给第二个参数,strlen(message),这里要不要加1呢?如果加1就是算上字符串的结尾’\0’,这里是不需要加1的,因为字符串以’\0’结尾是C语言的规定,跟文件没有关系。
下面使用以a方式打开文件并写入:以a方式打开文件会在文件的末尾进行写入,也就是追加写入。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
//printf("PID: %d\n", getpid());
FILE* fp = fopen("log.txt", "a");
const char* message = "hello c++ linux\n";
fwrite(message, strlen(message), 1, fp);
fclose(fp);
return 0;
}

可以看到,使用a方式打开文件,每次写入都会在文件的末尾写入,也就是追加写入。

那么追加重定向底层也是使用a方式打开的,向文件末尾处追加新的内容。

>log.txt这样的方式就是相当于以w方式打开了一下文件。
所以就有以下两个结论:
1、w方式打开文件:如果文件不存在就创建,写入之前会对文件进行清空处理。
2、a方式打开文件:a在文件末尾写入,追加写。

C语言默认会给我们打开三个文件:stdin:标准输入——键盘文件,stdout:标准输出——显示器文件,stderr:标准错误——显示器文件,可以看到它们都是FILE*的指针。C++默认会打开:cin、cout、cerr。
下面使用fprintf向stdout文件中写入:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
//printf("PID: %d\n", getpid());
FILE* fp = fopen("log.txt", "a");
const char* message = "hello c++ linux\n";
//fwrite(message, strlen(message), 1, fp);
fprintf(stdout, "%s", message);
fclose(fp);
return 0;
}

可以看到成功的向显示器文件写入了hello c++ linux。在C语言看来,向显示器写入和向文件写入没有任何区别。
2、系统调用接口

文件是在磁盘上存储的,磁盘是外部设备,打开文件写入就是要访问磁盘上的文件,而访问磁盘文件其实就是访问硬件。而用户并不能直接访问底层硬件,操作系统是软硬件资源的管理者,并且操作系统不相信用户,所以注定了用户只能通过操作系统提供的系统调用接口去访问硬件。所以用户访问底层硬件必须贯穿操作系统。
所以:printf/fprintf/fscanf/fwrite/fread/fgets…这些库函数必定要封装系统调用。
下面介绍Linux系统接口:

open用于打开一个文件或设备。第一个参数表示打开的文件路径,第二个表示打开文件的方式(读/写等)。
那么第二个函数还有一个参数,第三个参数表示的是,如果文件不存在,创建文件并通过mode设置文件对应的权限。
所以一般第一个函数用于打开已存在的文件,第二个函数用于打开不存在的文件。返回值我们等下再谈

文件打开方式我们这里最常用的就是上面这六个:
O_RDONLY:只读
O_WRONLY:只写
O_RDWR:读写方式打开
O_CREAT:不存在就创建
O_TRUNC:每次打开清空文件内容
O_APPEND:追加写
而这么多打开方式是可以组合在一起传参的,通过按位或|即可将它们组合在一起。
下面介绍通过比特位的组合传参形式:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define ONE (1<<0) // 1
#define TWO (1<<1) // 2
#define THREE (1<<2) // 4
#define FOUR (1<<3) // 8
void func(int flags)
{
if (flags & ONE) printf("function 1\n");
if (flags & TWO) printf("function 2\n");
if (flags & THREE) printf("function 3\n");
if (flags & FOUR) printf("function 4\n");
}
int main()
{
printf("---------------------------\n");
func(ONE);
printf("---------------------------\n");
func(TWO);
printf("---------------------------\n");
func(ONE|FOUR);
printf("---------------------------\n");
func(ONE|TWO|THREE);
printf("---------------------------\n");
func(THREE|FOUR);
printf("---------------------------\n");
return 0;
}

open函数就是类似上面这种方式来传参的,所以本质上O_CREAT、O_WRONLY等都是宏。
使用open函数打开不存在的文件:

可以看到,该文件的权限是乱的,所以需要使用第二个函数打开,并设置对应的权限。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_CREAT|O_WRONLY, 0666);
return 0;
}

但是我们发现现在log.txt的权限还是不符合我们的预期,因为我们传入的权限是666,但是这里是664。这是因为有权限掩码,当前系统的权限掩码位0002,所以其他人的写入权限就被去除了。那么如果我就想让该文件的权限为666呢?可以使用如下的系统调用:

#include <stdio.h>
#include <string.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_CREAT|O_WRONLY, 0666);
return 0;
}

如图,我们在进程内将权限掩码设置为0,那么再次创建log.txt对应权限就被设置为了666。使用umask函数只在当前进程有效,并不会影响系统的。
使用write函数写入:

跟open配套的还有close函数,用于关闭文件,将open的返回值fd传入即可。

write的第一个参数就是open的返回值,第二个参数要写入的字符串,第三个参数是写入字符串的长度。
#include <stdio.h>
#include <string.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_CREAT|O_WRONLY, 0666);
const char* message = "hello linux";
write(fd, message, strlen(message));
close(fd);
return 0;
}

运行后向文件写入了hello linux,然后再多次运行程序,发现文件内容还是hello linux,并没有发生变化,现在我们将要写入的字符串修改为aaa,再次写入看看:

我们发现原来的文件内容是hello linux,然后再次写入变成了aaalo linux,说明O_WRONLY默认是从文件的起始位置覆盖写入的,而前面之所以运行了几次都没有发生变化是因为每次写入的内容都是一样的,从头覆盖后内容并不会发生变化。
所以,如果要每次打开文件都清空然后再写入我们需要加上O_TRUNC这个选项:
int fd = open("log.txt", O_CREAT|O_WRONLY|O_TRUNC, 0666);

这时候再写入bbbb时,由于携带了O_TRUNC,所以会把文件内容清空然后再写入。
那如果我想追加写入呢?需要携带O_APPEND:
int fd = open("log.txt", O_CREAT|O_WRONLY|O_APPEND, 0666);

所以:fopen以w/a方式打开文件,本质就是对下面open函数的封装。
FILE* fp = fopen("log.txt", "w");
int fd = open("log.txt", O_CREAT|O_WRONLY|O_TRUNC, 0666);
FILE* fp = fopen("log.txt", "a");
int fd = open("log.txt", O_CREAT|O_WRONLY|O_APPEND, 0666);
3、文件描述符fd

一个进程运行起来,可能打开很多个文件,那么其他进程也一样,所以操作系统中就会存在大量的被打开的文件。那么这些被打开的文件要不要管理起来呢?当然要,如何管理?——先描述,再组织。
所以Linux中,使用struct file来描述被打开文件的信息,里面直接或间接包含如下信息:
在磁盘的什么位置、基本属性、权限、大小、谁打开的、文件的内容缓冲区信息、引用计数等
每个文件还有一个文件的内核缓冲区信息,当我们向文件写入的时候,会先写入到这个缓冲区中,然后合适的时候再写入磁盘中。并且struct file对象之间通过指针连接起来,组织成双向链表的形式。那么操作系统对被打开文件的管理就转换成了对链表的增删查改。
那么进程是不是也必须跟打开的文件关联起来,所以进程的task_struct内有一个struct files_struct* files指针,它指向了一个struct files_struct的对象,而struct files_struct这个结构体对象中有一个struct file*的指针数组fd_array,这个数组里面就保存了struct file的地址,fd_array我们就称为文件描述符表。所以进程就可以找到自己打开的文件。

再回过头来看,open函数的返回值就是file descriptor——文件描述符fd,如果失败返回-1。这个fd本质上就是文件描述符表的下标,也就是数组fd_array的下标。
我们在程序中打印fd出来看看:

运行后我们惊奇的发现这个下标是3,那么前面的0、1、2是什么呢?
前面的0、1、2刚好就是三个,而C语言默认会打开三个文件:stdin、stdout、stderr,所以0、1、2对应的就是键盘文件、显示器文件、显示器文件。
C语言默认打开三个文件,这是C语言的特性吗?并不是,这是操作系统的特性,进程默认会打开键盘、显示器、显示器文件。
下面我们进行测试:
使用write向文件描述符1、2写入数据,我们期望在显示器上看到写入的数据:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
const char* message = "hello linux\n";
write(1, message, strlen(message));
write(2, message, strlen(message));
return 0;
}

下面使用系统调用从0号文件描述符读取数据,我们期望的是从键盘读入数据,然后将字符串再输出出来。

read的返回值是实际读取的字符个数,write的返回值是实际写入字符的个数。第一个参数fd表示文件描述符,第二个参数表示要读到的数组,第三个参数表示要读的字符个数。虽然你要读count个,但实际并不一定读count个字符,所以read有返回值,返回你实际读取的字符个数。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
char buff[1024];
ssize_t n = read(0, buff, sizeof(buff) - 1);
buff[n] = '\0';
printf("echo: %s\n", buff);
return 0;
}

注意,第三个参数传参要-1,因为还要在数组中加入\0。
下面我们把1号文件关了,向2号写入:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(1);
printf("hello C++\n");
printf("hello linux\n");
const char* msg = "hello C++ linux\n";
write(2, msg, strlen(msg));
return 0;
}

由于printf就是向文件描述符为1的文件写入,所以我们把fd=1的文件关了printf就不起作用了。而我们向fd=2的文件写入,也就是向标准错误写入还是没问题的。

我们前面说过,struct file里面还会有引用计数,所以1、2下标的指针都是指向显示器文件的,当我们close(1)的时候,其实就是找到struct file,然后将引用计数--,判断是否为0,如果为0就直接释放掉了,如果不为0什么也不做,然后将对应数组下标置空。
最后一个问题:C语言中的FILE结构和fd有什么关系呢?
我们说了,C语言上的fwrite等函数,底层都是要调用操作系统提供的系统调用的,而调用系统调用必定要传fd,所以FILE结构里面肯定要保存fd信息。
在Linux下,我们可以通过FILE*的指针,访问里面的_fileno对象,_fileno就是fd。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
printf("stdin: %d\n", stdin->_fileno);
printf("stdout: %d\n", stdout->_fileno);
printf("stderr: %d\n", stderr->_fileno);
return 0;
}


被折叠的 条评论
为什么被折叠?



