一、系统文件IO
操作文件除了使用C语言、C++以及其他的语言的接口之外,还可以使用操作系统提供的接口
1.1 open
1.1.1 open的第一个参数
open函数的第一个参数是pathname,表示要打开或创建的目标文件
若pathname以路径的方式给出,则需要创建该文件时,在pathname路径下进行创建
若pathname以文件名的方式给出,则需要创建该文件时,默认在当前路径下进行创建
1.1.2 open的第二个参数
open函数的第二个参数是flags,表示打开文件的方式
上面提供的参数选项仅仅是较为常用的,具体可以man 2 open命令进行查看
打开文件时可以传入多个参数选项,当有多个选项传入时,将这些选项用"按位或"隔开
传参原理:
系统接口open的第二个参数flags为整型类型,在32位平台上有32个bit位。若将一个bit位作为一个标志位,则理论上flags可以传递32种不同的标志位。而实际上flags的参数选项都是宏
在oen函数内部就可以通过使用一系列的位运算来判断是否设置了某一选项。
1.1.3 open的第三个参数
open函数的第三个参数是mode,表示创建文件的默认权限
传入0666参数进行文件创建,按理应得到-rw-rw-rw-权限的文件,但却得到了如下图的文件
这是因为权限掩码的存在(默认为0002),可以在代码中设置权限掩码来避免上述情况发生。
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { umask(0000);//设置文件掩码 int fd = open("./log.txt",O_RDWR | O_CREAT | O_TRUNC,0666); close(fd); return 0; }
注意: 当不需创建文件时,可以不用设置第三个参数
1.1.4 open的返回值
open函数成功调用后返回打开文件的文件描述符,若调用失败则返回-1。文件描述符具体在后面进行讲解。
1.2 close
使用close函数时传入需要关闭文件的文件描述符(即调用open函数的返回值)即可。若关闭文件成功则返回0;若关闭文件失败则返回-1。
1.3 write
Linux系统接口中使用write函数向文件写入信息
将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。
若数据写入成功,则返回实际写入数据的字节数。
若数据写入失败,则返回-1。
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { int fd = open("./log.txt",O_RDWR | O_CREAT | O_TRUNC,0666); if(fd < 0){ perror("open:"); exit(1); } const char* buf = "hello Linux!\n"; write(fd, buf, strlen(buf)); close(fd); return 0; }
1.4 read
Linux系统接口中使用write函数向文件写入信息
从文件描述符为fd的文件读取count字节的数据到buf位置当中
若数据读取成功,则返回实际读取数据的字节数
若数据读取失败,则返回-1
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { int fd = open("./log.txt",O_RDONLY); if(fd < 0){ perror("open:"); exit(1); } char buf[64] = {0}; while(read(fd, buf, 63)){ printf("%s",buf); memset(buf,0x00,64); } close(fd); return 0; }
二、文件描述符
2.1 进程与文件描述符
文件被访问的前提是被加载到内存中打开,一个进程可以打开多个文件,而系统当中又存在大量进程。即在系统中任何时刻都可能存在大量已经打开的文件,所以操作系统内部提供了struct file结构体用于描述文件(先描述,再组织),再使用双链表进行管理。
而为了区分已经打开的文件哪些属于特定的某一个进程,我们就还需要建立进程和文件之间的对应关系。
进程与文件之间的对应关系是如何确定的呢?
task_struct(PCB)中有一个指针,该指针指向一个名为files_struct的结构体。在该结构体中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。
当一个进程打开其第一个文件时,先将该文件从磁盘当中加载到内存,形成对应的struct file变量,将该struct file变量链入文件双链表中,并将该结构体的首地址填入到fd_array数组中下标为3的位置,最后返回该文件的文件描述符给调用进程。
因此只需要有某一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作。
内存文件 VS 磁盘文件
当文件存储在磁盘当中时,被称为磁盘文件;当磁盘文件被加载到内存中后,此时的文件被称为内存文件。磁盘文件和内存文件之间的关系就像程序和进程的关系一样,当程序加载到内存运行起来后便成了进程,而当磁盘文件加载到内存后便成了内存文件。
磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名、文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息。
文件加载到内存时,一般先加载文件的属性信息,当需要对文件内容进行读取、输入或输出等操作时,再延后式的加载文件数据。
2.2 文件描述符的分配规则
Linux进程会默认打开3个文件描述符,分别是标准输入0、标准输出1、标准错误2
分别对应的物理设备一般为:键盘、显示器、显示器
键盘和显示器都属于硬件,且操作系统能够识别。当某一进程创建时,操作系统就会根据键盘、显示器、显示器形成各自的struct file变量,将这3个struct file变量连入文件双链表当中,并将这3个struct file变量的地址分别填入fd_array数组下标为0、1、2的位置,至此就默认打开了标准输入流、标准输出流和标准错误流。
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { int fd1 = open("./log1.txt",O_RDWR | O_CREAT | O_TRUNC,0666); int fd2 = open("./log2.txt",O_RDWR | O_CREAT | O_TRUNC,0666); int fd3 = open("./log3.txt",O_RDWR | O_CREAT | O_TRUNC,0666); int fd4 = open("./log4.txt",O_RDWR | O_CREAT | O_TRUNC,0666); int fd5 = open("./log5.txt",O_RDWR | O_CREAT | O_TRUNC,0666); printf("fd1: %d\n",fd1); printf("fd2: %d\n",fd2); printf("fd3: %d\n",fd3); printf("fd4: %d\n",fd4); printf("fd5: %d\n",fd5); close(fd1); close(fd2); close(fd3); close(fd4); close(fd5); return 0; }
则后续打开的文件的文件描述符,从3开始逐个增加1。
在打开文件之前将文件描述符为0和2的文件关闭又会发生什么呢?
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { close(0); close(2); int fd1 = open("./log1.txt",O_RDWR | O_CREAT | O_TRUNC,0666); int fd2 = open("./log2.txt",O_RDWR | O_CREAT | O_TRUNC,0666); int fd3 = open("./log3.txt",O_RDWR | O_CREAT | O_TRUNC,0666); int fd4 = open("./log4.txt",O_RDWR | O_CREAT | O_TRUNC,0666); int fd5 = open("./log5.txt",O_RDWR | O_CREAT | O_TRUNC,0666); printf("fd1: %d\n",fd1); printf("fd2: %d\n",fd2); printf("fd3: %d\n",fd3); printf("fd4: %d\n",fd4); printf("fd5: %d\n",fd5); close(fd1); close(fd2); close(fd3); close(fd4); close(fd5); return 0; }
结论: 文件描述符是从最小但是没有被使用的fd_array数组下标开始进行分配的
三、重定向
3.1 自实现重定向原理
3.1.1 输出重定向
输出重定向就是,将本应该输出到一个文件的数据重定向输出到另一个文件中
若想让本应该输出到"显示器文件"的数据输出到log.txt文件当中,可以在打开log.txt文件之前将文件描述符为1的文件关闭(即将“显示器文件”关闭)。当我们后续打开log.txt文件时所分配到的文件描述符就是1。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { close(1); int fd = open("./log.txt",O_RDWR | O_CREAT | O_TRUNC, 0666); if(fd < 0){ perror("opern error:"); return 1; } printf("hello world\n"); fflush(stdout);//为什么需要刷新?阅读后续章节《缓冲区》 close(fd); return 0; }
3.1.2 追加重定向
输出重定向是覆盖式输出数据,而追加重定向是追加式输出数据,并无其他区别
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { close(1); int fd = open("./log.txt",O_WRONLY | O_APPEND); if(fd < 0){ perror("opern error:"); return 1; } printf("hello world\n"); fflush(stdout); return 0; }
3.1.3 输入重定向
输入重定向,将本应该从一个文件读取数据,现在重定向为从另一个文件读取数据
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { close(0); int fd = open("./log.txt",O_RDONLY); if(fd < 0){ perror("opern error:"); return 1; } char buf[64] = {0}; while(scanf("%s",buf) != EOF){ printf("%s\n",buf); } return 0; }
3.2 dup2函数
在Linux环境下还可以使用dup2()系统调用来实现重定向。dup2()本质上是通过fd_array数组中地址元素的拷贝完成重定向的。
#include <unistd.h> int dup2(int oldfd, int newfd);
函数功能: 将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中
函数返回值: 若调用成功则返回newfd,否则返回-1
若oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
若oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作并直接返回newfd。
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); if (fd < 0){ perror("open"); return 1; } close(1); dup2(fd, 1); printf("hello printf\n"); fprintf(stdout, "hello fprintf\n"); return 0; }