[Linux]文件与 fd
一、文件
1.打开文件
文件存储在磁盘中。进程需要读写文件时需要先“打开”文件。
“打开”文件的操作其实就是把文件从磁盘加载到内存。因为进程是在CPU上运行的,而根据冯诺依曼体系,CPU无法直接读取到磁盘内容只能直接读取内存,所以读写文件前需要先“打开”文件。
进程运行时会默认打开三个输入输出流,在C语言中是stdin(标准输入)、stdout(标准输出)、stderr(标准错误)。
2.打印内容到显示器
打印内容到显示器,有以下几种方法:
#include <stdio.h>printf("%s", "aaa");
fputs("bbb", stdin);
fwrite("ccc", 1, 3, stdin);
fprintf(stdin, "ddd");
二、open
除了C语言提供的接口外,我们还可以直接调用系统接口来读、写、创建文件:
1.参数
参数 flags 是一个位图,传入以下的宏:
O_RDONLY、O_WRONLY、O_RDWR、O_CREAT、O_APPEND、O_TRUNC
分别是只读、只写、读写、若文件不存在则创建、追加写入、若文件已存在则清空内容
这些宏只有一个比特位为 1,所以在传参时,可以通过按位或的方式组合起来,用一个参数位置来传入多条信息:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{open("log.txt", O_WRONLY | O_CREAT);return 0;
}
我们运行后发现,log.txt 的权限位不正常,并不是按照一般的默认权限设置的。
这是因为在系统层面,创建文件和调整文件权限分属不同的模块,并没有耦合起来。系统在创建文件的时候,没有办法同时赋予这个文件权限位,所以会出现这种杂乱的权限位的情况。
在需要创建文件的时候,我们一般采用第二个 open 接口,也就是传入 3 个参数,最后一个参数就是在指定文件的权限位。而在只需要读写,不需要创建一个新文件时,采用第一个 open 接口。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{open("log.txt", O_WRONLY | O_CREAT, 0666);return 0;
}
我们再运行后发现 log.txt 的权限实际是 0664,这是因为系统还用权限掩码 umask 修正了它,导致最终权限是 0666 - 0002 = 0664(实际系统运算过程并非如此,只是等价于这样相减)
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{umask(0);open("log.txt", O_WRONLY | O_CREAT, 0666);return 0;
}
这样我们可以把权限掩码设为 0000,再创建出的 log.txt 的权限就是0666 了。
这里设置的 umask 是本进程的 umask,只会影响本进程,不会影响到整个系统以及其他进程创建的文件。
2.mytouch
我们可以用 open 接口自己实现一个 touch:
3.返回值(文件描述符 fd)
创建失败返回 -1 并立刻设置错误码,创建成功返回一个整型 int,被称为文件描述符。
我们发现我们自己打开的文件,描述符是从 3 开始依次递增的。
没有 0、1、2 是因为,进程启动时会默认启动三个标准输入输出流:stdin、stdout、stderr,他们分别占用了 0、1、2。
三、close、read、write、lseek
1.close
关闭文件描述符对应的文件。
关闭失败返回 -1 并设置错误码 errno,关闭成功返回 0。
2.read
从 fd 代表的文件中,读取 count 字节数据到 buf 指针指向的空间中。
读取失败返回 -1 并设置错误码,读取成功返回读取的字节数。
count 是我们期望读取到的字节数,说明最多会读取到 count 个字节,也可能读不到这么多,所以会有一个返回值,告诉我们实际读入了多少字节。
从标准输入中读取数据到 s 数组中并打印:
输入 abcd 并回车,我们发现果然打印了 abcd。
s[n-1] 中原本存放着读入的 \n,把它置为 \0 是为了补上字符串结束标志并且避免多打一个空行。
如果用 s[n] = '\0' 则 printf 函数中就不需要再加 \n 了,否则会打出两个空行。
3.write
从 buf 指向的数组中,读取 count 个字节,写入到 fd 代表的文件中(从读写偏移量指向的位置开始写入)。
写入失败返回 -1 并设置错误码,写入成功返回写入的字节数。
在 log.txt 中已存在 helloworld 内容的情况下,用 O_WRONLY 方式打开 log.txt,再用 write 写入字符串 aaa,结果 log.txt 内容是 aaaloworld。
这是因为 O_WRONLY 打开后文件的读写偏移量被设置在文件开头,导致了覆盖式的写入。
如果希望内容只有 aaa,则要在 open 接口中指定用 O_WRONLY | O_TRUNC 方式打开,让 open 接口先清空 log.txt 中的内容。
如果希望内容是 helloworldaaa,则要传入 O_WRONLY | O_APPEND,让 open 把读写偏移量设置到文件末尾。
语言层的 fopen 其实就是对系统调用 open 的封装,fopen 传入的 "w" 参数会转换到 open 中的 O_WRONLY | O_CREAT | O_TRUNC(文件不存在则创建,存在则清空内容)。
我们知道 strlen 函数只会统计字符串的有效字符数,不会计算字符串末尾的 \0 结束标志。那么当向文件中写入字符串时,需不需要用 strlen()+1,把 \0 也写入进去呢?
我们发现 log.txt 文件结尾出现了乱码。
这是因为字符串以 \0 结尾只是 C 语言的规定,和文件毫无关系,写入到文件就会出现不可显示字符,所以向文件中写入字符串的时候,如果使用 strlen 函数来统计字符串大小,是不需要再 +1 多计算 \0 的。
我们在上面 三、2. 中之所以给字符数组 s 补上 \0,是为了使字符串一直符合 C 语言的规范,而不是因为文件写入的要求。
4.lseek
修改文件的读写位置偏移量,fd 是文件描述符,offset 是偏移量(以字节为单位),whence是偏移量对应的参考值(填入宏:SEEK_SET、SEEK_CUR、SEEK_END)。
SEEK_SET:读写偏移量将指向,从文件开头开始,向后偏移 offset 个字节的位置。
SEEK_CUR:从当前位置开始,偏移 offset 个字节处,正值向后偏移,负值向前偏移。
SEEK_END:从文件末尾开始,偏移 offset 个字节处,正值向后偏移,负值向前偏移。
修改失败返回 -1 并设置错误码,修改成功返回从文件开头开始计算的文件偏移量。
四、理解文件描述符 fd
1.文件描述符联系了进程和文件
我们知道,进程在内存中对应着一个 struct task_struct 结构体,这是内核用来描述进程的,许多这样的结构体组成一张 task_list 链表,构成了内核的进程管理。
而文件在内核中也是这样组织的,每个被打开的文件对应一个 struct file 结构体,这个结构体描述了文件的属性等信息,所有这些结构体又被组织成一张 file_list 链表形成内核的文件管理。
这时进程和文件之间还毫无关联,而我们要在进程中向文件写入,就必须在两者之间建立起联系。
在 struct task_struct 中,有一个指针 struct files_struct *files,它指向一张表,叫做文件描述符表,在这张表里有还有一个指针 struct file *fd_array[N],里面存放的就是某个 struct file 的地址。
所以文件描述符 fd,其实就是文件描述符表中 fd_array[N] 的数组下标,进程通过这样一个下标,就可以拿到文件的地址,进而对文件进行操作。
在系统层面,文件描述符是进程访问文件的唯一方式。
2.FILE 和 FILE*
FILE 和 FILE* 都是 C 语言提供的,是语言级的。
FILE 是一个结构体 struct FILE,其中包含着文件描述符 fd。因为 fopen 是对 open 的封装,fopen 返回 FILE*,open 返回 int fd,而 fd 又是进程访问文件的唯一方式,所以 FILE 必定要包含 fd。
stdin、stdout、stderr 就是 FILE* 类型,里面包含了许多成员。其中 _fileno 就对应着文件描述符: